mirror of
				https://github.com/fspc/workstand.git
				synced 2025-10-31 08:25:35 -04:00 
			
		
		
		
	Refactor member list to be searchable (#27)
Trigger! * Added members API endpoint. * Stubbed out member table. Search is broken. * Better, faster member search! * Add add member button.
This commit is contained in:
		
							parent
							
								
									65e71becf4
								
							
						
					
					
						commit
						560141298a
					
				| @ -1,13 +1,16 @@ | ||||
| import ContentAdd from 'material-ui/svg-icons/content/add'; | ||||
| import fetch from 'isomorphic-fetch'; | ||||
| import FloatingActionButton from 'material-ui/FloatingActionButton'; | ||||
| import moment from 'moment'; | ||||
| import { polyFill } from 'es6-promise'; | ||||
| import RaisedButton from 'material-ui/RaisedButton'; | ||||
| import React from 'react'; | ||||
| import RaisedButton from 'material-ui/RaisedButton'; | ||||
| 
 | ||||
| import Member from './Member'; | ||||
| import Purpose from './Purpose'; | ||||
| import SignedInList from './SignedInList'; | ||||
| 
 | ||||
| 
 | ||||
| export default class SignIn extends React.Component { | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
| @ -132,6 +135,7 @@ export default class SignIn extends React.Component { | ||||
|               searchText={this.state.searchText} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="mdl-cell mdl-cell--4-col"> | ||||
|             <Purpose | ||||
|               handleChange={this.handlePurposeChoice} | ||||
| @ -139,7 +143,6 @@ export default class SignIn extends React.Component { | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="mdl-grid"> | ||||
|           <div className="mdl-cell mdl-cell--2-col mdl-cell--10-offset"> | ||||
|             <RaisedButton onClick={this.signIn} label="Sign-in" /> | ||||
| @ -147,6 +150,9 @@ export default class SignIn extends React.Component { | ||||
|         </div> | ||||
|         <div className="mdl-grid"> | ||||
|           <SignedInList members={this.state.signedIn} /> | ||||
|           <FloatingActionButton href="/members/new/"> | ||||
|             <ContentAdd /> | ||||
|           </FloatingActionButton> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|  | ||||
| @ -0,0 +1,155 @@ | ||||
| import React from 'react'; | ||||
| import { polyFill } from 'es6-promise'; | ||||
| import fetch from 'isomorphic-fetch'; | ||||
| import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; | ||||
| import FlatButton from 'material-ui/FlatButton'; | ||||
| import { Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle } from 'material-ui/Toolbar'; | ||||
| import TextField from 'material-ui/TextField'; | ||||
| import RaisedButton from 'material-ui/RaisedButton'; | ||||
| 
 | ||||
| function checkStatus(response) { | ||||
|   if (response.status >= 200 && response.status < 300) { | ||||
|     return response; | ||||
|   } | ||||
|   const error = new Error(response.statusText); | ||||
|   error.response = response; | ||||
|   throw error; | ||||
| } | ||||
| 
 | ||||
| function parseJSON(response) { | ||||
|   return response.json(); | ||||
| } | ||||
| 
 | ||||
| export default class MemberTable extends React.Component { | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
| 
 | ||||
|     this.state = { | ||||
|       members: [], | ||||
|       searchText: '', | ||||
|       filteredMembers: [], | ||||
|       error: undefined, | ||||
|     }; | ||||
| 
 | ||||
|     this.handleUpdate = this.handleUpdate.bind(this); | ||||
|     this.handleSearch = this.handleSearch.bind(this); | ||||
|     this.clearSearch = this.clearSearch.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount() { | ||||
|     fetch('/api/v1/members/') | ||||
|       .then(checkStatus) | ||||
|       .then(parseJSON) | ||||
|       .then((data) => { | ||||
|         this.setState({ members: data }); | ||||
|         console.log('request succeeded with JSON response', data); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.log('request failed', error); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   handleUpdate(event, value) { | ||||
|     this.setState({ ...this.state, searchText: value }); | ||||
|   } | ||||
| 
 | ||||
|   clearSearch() { | ||||
|     this.setState({ | ||||
|       ...this.state, | ||||
|       searchText: '', | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   handleSearch() { | ||||
|     const value = this.state.searchText.trim(); | ||||
|     const self = this; | ||||
| 
 | ||||
|     fetch(`/members/search/${value}/`) | ||||
|       .then((response) => { | ||||
|         if (response.status === 200) { | ||||
|           return response.json(); | ||||
|         } | ||||
|         throw new Error('Bad response from server'); | ||||
|       }) | ||||
|       .then((data) => { | ||||
|         if (data.results.length > 0) { | ||||
|           self.setState({ | ||||
|             ...this.state, | ||||
|             error: '', | ||||
|             filteredMembers: this.state.members.filter((member) => { | ||||
|               const ids = data.results.map(m => m.id); | ||||
|               console.log(ids); | ||||
| 
 | ||||
|               if (ids.indexOf(member.id) !== -1) { | ||||
|                 return member; | ||||
|               } | ||||
|             }), | ||||
|           }); | ||||
|         } else { | ||||
|           self.setState({ ...this.state, error: 'Member not found.' }); | ||||
|         } | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     const memberRows = this.state.members.map(member => ( | ||||
|       <TableRow selectable={false} key={member.id}> | ||||
|         <TableRowColumn>{member.first_name}</TableRowColumn> | ||||
|         <TableRowColumn>{member.last_name}</TableRowColumn> | ||||
|         <TableRowColumn>{member.email}</TableRowColumn> | ||||
|         <TableRowColumn><FlatButton label="Edit" href={`/members/edit/${member.id}`} primary /></TableRowColumn> | ||||
|       </TableRow> | ||||
|       )); | ||||
| 
 | ||||
|     const filteredMemberRows = this.state.filteredMembers.map(member => ( | ||||
|       <TableRow selectable={false} key={member.id}> | ||||
|         <TableRowColumn>{member.first_name}</TableRowColumn> | ||||
|         <TableRowColumn>{member.last_name}</TableRowColumn> | ||||
|         <TableRowColumn>{member.email}</TableRowColumn> | ||||
|         <TableRowColumn><FlatButton label="Edit" href={`/members/edit/${member.id}`} primary /></TableRowColumn> | ||||
|       </TableRow> | ||||
|       )); | ||||
| 
 | ||||
| 
 | ||||
|     return ( | ||||
|       <div className="mdl-grid"> | ||||
|         <div className="mdl-cell mdl-cell--12-col"> | ||||
|           <h3>Members</h3> | ||||
|           <Toolbar> | ||||
|             <ToolbarGroup> | ||||
|               <TextField | ||||
|                 hintText="ma@example.com OR name" | ||||
|                 floatingLabelText="Search for member" | ||||
|                 onChange={this.handleUpdate} | ||||
|                 value={this.state.searchText} | ||||
|               /> | ||||
|               <RaisedButton label="Search" primary onClick={this.handleSearch} /> | ||||
|               <RaisedButton label="Clear" onClick={this.clearSearch} secondary /> | ||||
|             </ToolbarGroup> | ||||
|           </Toolbar> | ||||
|           <Table selectable={false}> | ||||
|             <TableHeader adjustForCheckbox={false} displaySelectAll={false}> | ||||
|               <TableRow> | ||||
|                 <TableHeaderColumn>First name</TableHeaderColumn> | ||||
|                 <TableHeaderColumn>Last Name</TableHeaderColumn> | ||||
|                 <TableHeaderColumn>Email</TableHeaderColumn> | ||||
|                 <TableHeaderColumn /> | ||||
|               </TableRow> | ||||
|             </TableHeader> | ||||
|             <TableBody displayRowCheckbox={false}> | ||||
|               {filteredMemberRows.length ? | ||||
|                 filteredMemberRows : undefined | ||||
|               } | ||||
|               {memberRows.length && !this.state.searchText ? | ||||
|               memberRows : | ||||
|               <TableRow> | ||||
|                 <TableRowColumn>{'Members loading.'}</TableRowColumn> | ||||
|               </TableRow> | ||||
|             } | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										22
									
								
								bikeshop_project/assets/js/members/index.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								bikeshop_project/assets/js/members/index.jsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import injectTapEventPlugin from 'react-tap-event-plugin'; | ||||
| import getMuiTheme from 'material-ui/styles/getMuiTheme'; | ||||
| import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; | ||||
| import MemberTable from './components/MemberTable'; | ||||
| 
 | ||||
| // Needed for onTouchTap | ||||
| // http://stackoverflow.com/a/34015469/988941 | ||||
| injectTapEventPlugin(); | ||||
| 
 | ||||
| 
 | ||||
| const App = () => ( | ||||
|   <MuiThemeProvider muiTheme={getMuiTheme()}> | ||||
|     <div> | ||||
|       <h1>Members</h1> | ||||
|       <MemberTable /> | ||||
|     </div> | ||||
|   </MuiThemeProvider> | ||||
| ); | ||||
| 
 | ||||
| ReactDOM.render(<App />, document.getElementById('root')); | ||||
| @ -18,16 +18,30 @@ from django.conf.urls import include, url | ||||
| from django.contrib import admin | ||||
| from django.contrib.auth.views import login, logout_then_login | ||||
| from django.contrib.staticfiles.urls import staticfiles_urlpatterns | ||||
| from rest_framework import routers | ||||
| from rest_framework_jwt.views import obtain_jwt_token | ||||
| 
 | ||||
| import registration | ||||
| from core import urls as core_urls | ||||
| from registration import urls as member_urls | ||||
| 
 | ||||
| routeLists = [ | ||||
|     registration.urls.apiRoutes, | ||||
| ] | ||||
| 
 | ||||
| router = routers.DefaultRouter() | ||||
| for routeList in routeLists: | ||||
|     for route in routeList: | ||||
|         router.register(route[0], route[1]) | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     url(r'^', include(core_urls)), | ||||
|     url(r'^login/', login, {'template_name': 'login.html'}, name='login'), | ||||
|     url(r'^logout/', logout_then_login, name='logout'), | ||||
|     url(r'^members/', include(member_urls)), | ||||
|     url(r'^admin/', admin.site.urls), | ||||
|     url(r'^api/v1/', include(router.urls)), | ||||
|     url(r'^api/v1/token-auth/', obtain_jwt_token), | ||||
| ] | ||||
| 
 | ||||
| if getattr(settings, 'DEBUG'): | ||||
|  | ||||
| @ -53,7 +53,9 @@ | ||||
|       </div> | ||||
| {% endblock %} | ||||
| 
 | ||||
| 
 | ||||
| {% block content %} | ||||
|     <div id="root"></div> | ||||
|     {% render_bundle 'main' %} | ||||
|     {% render_bundle 'signin' %} | ||||
| {% endblock %} | ||||
| {% render_bundle 'webpack' %} | ||||
|  | ||||
							
								
								
									
										13
									
								
								bikeshop_project/registration/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								bikeshop_project/registration/serializers.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| from rest_framework import serializers | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| 
 | ||||
| from .models import Member | ||||
| 
 | ||||
| 
 | ||||
| class MemberSerializer(ModelSerializer): | ||||
|     first_name = serializers.CharField(allow_blank=True, required=False) | ||||
|     last_name = serializers.CharField(allow_blank=True, required=False) | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = Member | ||||
|         fields = ('first_name', 'last_name', 'email', 'id') | ||||
| @ -1,22 +1,7 @@ | ||||
| {% extends 'dashboard.html' %} | ||||
| {% load render_bundle from webpack_loader %} | ||||
| 
 | ||||
| {% block content %} | ||||
|     <div class="mdl-cell mdl-cell--6-col"> | ||||
|         <ul class="demo-list-three mdl-list"> | ||||
|             {% for member in members %} | ||||
|           <li class="mdl-list__item mdl-list__item--three-line"> | ||||
|             <span class="mdl-list__item-primary-content"> | ||||
|               <i class="material-icons mdl-list__item-avatar">person</i> | ||||
|               <span>{{ member.full_name }}</span> | ||||
|               <span class="mdl-list__item-text-body"> | ||||
|                 {{ member.email }} | ||||
|               </span> | ||||
|             </span> | ||||
|             <span class="mdl-list__item-secondary-content"> | ||||
|               <a class="mdl-list__item-secondary-action" href="{% url 'member_edit' member_id=member.id %}"><i class="material-icons">edit</i></a> | ||||
|             </span> | ||||
|           </li> | ||||
|             {% endfor %} | ||||
|         </ul> | ||||
|     </div> | ||||
|     <div id="root"></div> | ||||
|     {% render_bundle 'members' %} | ||||
| {% endblock %} | ||||
| @ -1,6 +1,11 @@ | ||||
| from django.conf.urls import url | ||||
| 
 | ||||
| from .views import MemberFormView, MemberSearchView, MemberSignIn, Members | ||||
| from .views import MemberFormView, MemberSearchView, MemberSignIn, Members, MemberViewSet | ||||
| 
 | ||||
| apiRoutes = ( | ||||
|     (r'members', MemberViewSet), | ||||
| ) | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     url(r'^new/$', MemberFormView.as_view(), name='member_new'), | ||||
|     url(r'^search/(?P<query>[\w@\.\+]+)/$', MemberSearchView.as_view(), name='member_search'), | ||||
|  | ||||
| @ -9,12 +9,13 @@ from django.utils.decorators import method_decorator | ||||
| from django.views.decorators.csrf import csrf_exempt | ||||
| from django.views.generic import TemplateView, View | ||||
| from haystack.query import SearchQuerySet | ||||
| from rest_framework import serializers | ||||
| from rest_framework import viewsets | ||||
| from rest_framework.renderers import JSONRenderer | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| 
 | ||||
| from core.models import Visit | ||||
| from registration.utils import signin_member, get_signed_in_members | ||||
| from .serializers import MemberSerializer | ||||
| from .forms import MemberForm | ||||
| from .models import Member | ||||
| 
 | ||||
| @ -64,15 +65,6 @@ class MemberSearchView(View): | ||||
|         return HttpResponse(data, content_type='application/json') | ||||
| 
 | ||||
| 
 | ||||
| class MemberSerializer(ModelSerializer): | ||||
|     first_name = serializers.CharField(allow_blank=True, required=False) | ||||
|     last_name = serializers.CharField(allow_blank=True, required=False) | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = Member | ||||
|         fields = ('first_name', 'last_name', 'email', 'id') | ||||
| 
 | ||||
| 
 | ||||
| class VisitSerializer(ModelSerializer): | ||||
|     member = MemberSerializer() | ||||
| 
 | ||||
| @ -109,3 +101,8 @@ class Members(TemplateView): | ||||
|     def get(self, request): | ||||
|         members = Member.objects.all() | ||||
|         return self.render_to_response(dict(members=members)) | ||||
| 
 | ||||
| 
 | ||||
| class MemberViewSet(viewsets.ModelViewSet): | ||||
|     queryset = Member.objects.all() | ||||
|     serializer_class = MemberSerializer | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| const path = require("path"); | ||||
| const path = require('path'); | ||||
| const webpack = require('webpack'); | ||||
| const BundleTracker = require('webpack-bundle-tracker'); | ||||
| const ExtractTextPlugin = require('extract-text-webpack-plugin'); | ||||
| @ -7,17 +7,19 @@ const autoprefixer =  require('autoprefixer'); | ||||
| module.exports = { | ||||
|   context: __dirname, | ||||
|   devtool: 'inline-source-map', | ||||
|     entry: './assets/js/index', // entry point of our app. assets/js/index.js should require other js modules and dependencies it needs
 | ||||
| 
 | ||||
|   entry: { | ||||
|     signin: './assets/js/index', | ||||
|     members: './assets/js/members/index', | ||||
|   }, | ||||
|   output: { | ||||
|     path: path.resolve('./assets/bundles/'), | ||||
|         filename: "[name]-[hash].js" | ||||
|     filename: '[name]-[hash].js', | ||||
|   }, | ||||
| 
 | ||||
|   plugins: [ | ||||
|         new BundleTracker({filename: './webpack-stats.json'}), | ||||
|     new BundleTracker({ filename: './webpack-stats.json' }), | ||||
|     new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), | ||||
|         new webpack.NoErrorsPlugin() | ||||
|     new webpack.NoErrorsPlugin(), | ||||
|   ], | ||||
| 
 | ||||
|   module: { | ||||
| @ -27,22 +29,22 @@ module.exports = { | ||||
|         exclude: /node_modules/, | ||||
|         loader: 'babel-loader', | ||||
|         query: { | ||||
|                     presets: ['es2015', 'stage-0', 'react'] | ||||
|                 } | ||||
|           presets: ['es2015', 'stage-0', 'react'], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         test: /(\.scss|\.css)$/, | ||||
|                 loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox') | ||||
|           } | ||||
|         ] | ||||
|         loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox'), | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   resolve: { | ||||
|     modulesDirectories: [ | ||||
|       'node_modules', | ||||
|       'bower_components', | ||||
|             path.resolve(__dirname, './node_modules') | ||||
|       path.resolve(__dirname, './node_modules'), | ||||
|     ], | ||||
|         extensions: ['', '.js', '.jsx', '.scss'] | ||||
|     extensions: ['', '.js', '.jsx', '.scss'], | ||||
|   }, | ||||
|     postcss: [autoprefixer] | ||||
| } | ||||
|   postcss: [autoprefixer], | ||||
| }; | ||||
|  | ||||
| @ -6,11 +6,14 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin'); | ||||
| var config = require('./webpack.base.config.js') | ||||
| 
 | ||||
| // Use webpack dev server
 | ||||
| config.entry = [ | ||||
| config.entry = { | ||||
|     webpack: [ | ||||
|         'webpack-dev-server/client?http://webpack.docker:3000', | ||||
|         'webpack/hot/only-dev-server', | ||||
|     './assets/js/index' | ||||
| ] | ||||
|     ], | ||||
|     signin: './assets/js/index', | ||||
|     members: './assets/js/members/index', | ||||
| } | ||||
| 
 | ||||
| // override django's STATIC_URL for webpack bundles
 | ||||
| config.output.publicPath = 'http://webpack.docker:3000/assets/bundles/' | ||||
|  | ||||
| @ -11,3 +11,4 @@ djangorestframework | ||||
| django-webpack-loader | ||||
| requests | ||||
| PyYAML | ||||
| djangorestframework-jwt==1.9.0 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user