Browse Source

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.
feature/python-error-tracking
Drew Larson 8 years ago
parent
commit
560141298a
  1. 10
      bikeshop_project/assets/js/components/SignIn.jsx
  2. 155
      bikeshop_project/assets/js/members/components/MemberTable/index.jsx
  3. 22
      bikeshop_project/assets/js/members/index.jsx
  4. 14
      bikeshop_project/bikeshop/urls.py
  5. 4
      bikeshop_project/core/templates/dashboard.html
  6. 13
      bikeshop_project/registration/serializers.py
  7. 21
      bikeshop_project/registration/templates/members.html
  8. 7
      bikeshop_project/registration/urls.py
  9. 17
      bikeshop_project/registration/views.py
  10. 30
      bikeshop_project/webpack.config.js
  11. 9
      bikeshop_project/webpack.dev.config.js
  12. 1
      requirements/base.txt

10
bikeshop_project/assets/js/components/SignIn.jsx

@ -1,13 +1,16 @@
import ContentAdd from 'material-ui/svg-icons/content/add';
import fetch from 'isomorphic-fetch'; import fetch from 'isomorphic-fetch';
import FloatingActionButton from 'material-ui/FloatingActionButton';
import moment from 'moment'; import moment from 'moment';
import { polyFill } from 'es6-promise'; import { polyFill } from 'es6-promise';
import RaisedButton from 'material-ui/RaisedButton';
import React from 'react'; import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';
import Member from './Member'; import Member from './Member';
import Purpose from './Purpose'; import Purpose from './Purpose';
import SignedInList from './SignedInList'; import SignedInList from './SignedInList';
export default class SignIn extends React.Component { export default class SignIn extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -132,6 +135,7 @@ export default class SignIn extends React.Component {
searchText={this.state.searchText} searchText={this.state.searchText}
/> />
</div> </div>
<div className="mdl-cell mdl-cell--4-col"> <div className="mdl-cell mdl-cell--4-col">
<Purpose <Purpose
handleChange={this.handlePurposeChoice} handleChange={this.handlePurposeChoice}
@ -139,7 +143,6 @@ export default class SignIn extends React.Component {
/> />
</div> </div>
</div> </div>
<div className="mdl-grid"> <div className="mdl-grid">
<div className="mdl-cell mdl-cell--2-col mdl-cell--10-offset"> <div className="mdl-cell mdl-cell--2-col mdl-cell--10-offset">
<RaisedButton onClick={this.signIn} label="Sign-in" /> <RaisedButton onClick={this.signIn} label="Sign-in" />
@ -147,6 +150,9 @@ export default class SignIn extends React.Component {
</div> </div>
<div className="mdl-grid"> <div className="mdl-grid">
<SignedInList members={this.state.signedIn} /> <SignedInList members={this.state.signedIn} />
<FloatingActionButton href="/members/new/">
<ContentAdd />
</FloatingActionButton>
</div> </div>
</div> </div>
); );

155
bikeshop_project/assets/js/members/components/MemberTable/index.jsx

@ -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

@ -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'));

14
bikeshop_project/bikeshop/urls.py

@ -18,16 +18,30 @@ from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.views import login, logout_then_login from django.contrib.auth.views import login, logout_then_login
from django.contrib.staticfiles.urls import staticfiles_urlpatterns 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 core import urls as core_urls
from registration import urls as member_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 = [ urlpatterns = [
url(r'^', include(core_urls)), url(r'^', include(core_urls)),
url(r'^login/', login, {'template_name': 'login.html'}, name='login'), url(r'^login/', login, {'template_name': 'login.html'}, name='login'),
url(r'^logout/', logout_then_login, name='logout'), url(r'^logout/', logout_then_login, name='logout'),
url(r'^members/', include(member_urls)), url(r'^members/', include(member_urls)),
url(r'^admin/', admin.site.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'): if getattr(settings, 'DEBUG'):

4
bikeshop_project/core/templates/dashboard.html

@ -53,7 +53,9 @@
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="root"></div> <div id="root"></div>
{% render_bundle 'main' %} {% render_bundle 'signin' %}
{% endblock %} {% endblock %}
{% render_bundle 'webpack' %}

13
bikeshop_project/registration/serializers.py

@ -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')

21
bikeshop_project/registration/templates/members.html

@ -1,22 +1,7 @@
{% extends 'dashboard.html' %} {% extends 'dashboard.html' %}
{% load render_bundle from webpack_loader %}
{% block content %} {% block content %}
<div class="mdl-cell mdl-cell--6-col"> <div id="root"></div>
<ul class="demo-list-three mdl-list"> {% render_bundle 'members' %}
{% 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>
{% endblock %} {% endblock %}

7
bikeshop_project/registration/urls.py

@ -1,6 +1,11 @@
from django.conf.urls import url 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 = [ urlpatterns = [
url(r'^new/$', MemberFormView.as_view(), name='member_new'), url(r'^new/$', MemberFormView.as_view(), name='member_new'),
url(r'^search/(?P<query>[\w@\.\+]+)/$', MemberSearchView.as_view(), name='member_search'), url(r'^search/(?P<query>[\w@\.\+]+)/$', MemberSearchView.as_view(), name='member_search'),

17
bikeshop_project/registration/views.py

@ -9,12 +9,13 @@ from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from rest_framework import serializers from rest_framework import viewsets
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from core.models import Visit from core.models import Visit
from registration.utils import signin_member, get_signed_in_members from registration.utils import signin_member, get_signed_in_members
from .serializers import MemberSerializer
from .forms import MemberForm from .forms import MemberForm
from .models import Member from .models import Member
@ -64,15 +65,6 @@ class MemberSearchView(View):
return HttpResponse(data, content_type='application/json') 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): class VisitSerializer(ModelSerializer):
member = MemberSerializer() member = MemberSerializer()
@ -109,3 +101,8 @@ class Members(TemplateView):
def get(self, request): def get(self, request):
members = Member.objects.all() members = Member.objects.all()
return self.render_to_response(dict(members=members)) return self.render_to_response(dict(members=members))
class MemberViewSet(viewsets.ModelViewSet):
queryset = Member.objects.all()
serializer_class = MemberSerializer

30
bikeshop_project/webpack.config.js

@ -1,4 +1,4 @@
const path = require("path"); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const BundleTracker = require('webpack-bundle-tracker'); const BundleTracker = require('webpack-bundle-tracker');
const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin');
@ -7,17 +7,19 @@ const autoprefixer = require('autoprefixer');
module.exports = { module.exports = {
context: __dirname, context: __dirname,
devtool: 'inline-source-map', 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: { output: {
path: path.resolve('./assets/bundles/'), path: path.resolve('./assets/bundles/'),
filename: "[name]-[hash].js" filename: '[name]-[hash].js',
}, },
plugins: [ plugins: [
new BundleTracker({ filename: './webpack-stats.json' }), new BundleTracker({ filename: './webpack-stats.json' }),
new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), new ExtractTextPlugin('react-toolbox.css', { allChunks: true }),
new webpack.NoErrorsPlugin() new webpack.NoErrorsPlugin(),
], ],
module: { module: {
@ -27,22 +29,22 @@ module.exports = {
exclude: /node_modules/, exclude: /node_modules/,
loader: 'babel-loader', loader: 'babel-loader',
query: { query: {
presets: ['es2015', 'stage-0', 'react'] presets: ['es2015', 'stage-0', 'react'],
} },
}, },
{ {
test: /(\.scss|\.css)$/, 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: { resolve: {
modulesDirectories: [ modulesDirectories: [
'node_modules', 'node_modules',
'bower_components', 'bower_components',
path.resolve(__dirname, './node_modules') path.resolve(__dirname, './node_modules'),
], ],
extensions: ['', '.js', '.jsx', '.scss'] extensions: ['', '.js', '.jsx', '.scss'],
}, },
postcss: [autoprefixer] postcss: [autoprefixer],
} };

9
bikeshop_project/webpack.dev.config.js

@ -6,11 +6,14 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
var config = require('./webpack.base.config.js') var config = require('./webpack.base.config.js')
// Use webpack dev server // Use webpack dev server
config.entry = [ config.entry = {
webpack: [
'webpack-dev-server/client?http://webpack.docker:3000', 'webpack-dev-server/client?http://webpack.docker:3000',
'webpack/hot/only-dev-server', '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 // override django's STATIC_URL for webpack bundles
config.output.publicPath = 'http://webpack.docker:3000/assets/bundles/' config.output.publicPath = 'http://webpack.docker:3000/assets/bundles/'

1
requirements/base.txt

@ -11,3 +11,4 @@ djangorestframework
django-webpack-loader django-webpack-loader
requests requests
PyYAML PyYAML
djangorestframework-jwt==1.9.0
Loading…
Cancel
Save