diff --git a/bikeshop_project/assets/js/components/SignIn.jsx b/bikeshop_project/assets/js/components/SignIn.jsx index 69fc7b4..3fef0c2 100644 --- a/bikeshop_project/assets/js/components/SignIn.jsx +++ b/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 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} /> +
-
@@ -147,6 +150,9 @@ export default class SignIn extends React.Component {
+ + +
); diff --git a/bikeshop_project/assets/js/members/components/MemberTable/index.jsx b/bikeshop_project/assets/js/members/components/MemberTable/index.jsx new file mode 100644 index 0000000..44eb40c --- /dev/null +++ b/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 => ( + + {member.first_name} + {member.last_name} + {member.email} + + + )); + + const filteredMemberRows = this.state.filteredMembers.map(member => ( + + {member.first_name} + {member.last_name} + {member.email} + + + )); + + + return ( +
+
+

Members

+ + + + + + + + + + + First name + Last Name + Email + + + + + {filteredMemberRows.length ? + filteredMemberRows : undefined + } + {memberRows.length && !this.state.searchText ? + memberRows : + + {'Members loading.'} + + } + +
+
+
+ ); + } +} diff --git a/bikeshop_project/assets/js/members/index.jsx b/bikeshop_project/assets/js/members/index.jsx new file mode 100644 index 0000000..c67abc5 --- /dev/null +++ b/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 = () => ( + +
+

Members

+ +
+
+); + +ReactDOM.render(, document.getElementById('root')); diff --git a/bikeshop_project/bikeshop/urls.py b/bikeshop_project/bikeshop/urls.py index f45ed72..26264fe 100644 --- a/bikeshop_project/bikeshop/urls.py +++ b/bikeshop_project/bikeshop/urls.py @@ -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'): diff --git a/bikeshop_project/core/templates/dashboard.html b/bikeshop_project/core/templates/dashboard.html index 996eb1e..aa938bd 100644 --- a/bikeshop_project/core/templates/dashboard.html +++ b/bikeshop_project/core/templates/dashboard.html @@ -53,7 +53,9 @@ {% endblock %} + {% block content %}
- {% render_bundle 'main' %} + {% render_bundle 'signin' %} {% endblock %} +{% render_bundle 'webpack' %} diff --git a/bikeshop_project/registration/serializers.py b/bikeshop_project/registration/serializers.py new file mode 100644 index 0000000..f060691 --- /dev/null +++ b/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') \ No newline at end of file diff --git a/bikeshop_project/registration/templates/members.html b/bikeshop_project/registration/templates/members.html index 26c105a..5743a1e 100644 --- a/bikeshop_project/registration/templates/members.html +++ b/bikeshop_project/registration/templates/members.html @@ -1,22 +1,7 @@ {% extends 'dashboard.html' %} +{% load render_bundle from webpack_loader %} {% block content %} -
- -
+
+ {% render_bundle 'members' %} {% endblock %} \ No newline at end of file diff --git a/bikeshop_project/registration/urls.py b/bikeshop_project/registration/urls.py index 181c90b..e861aa9 100644 --- a/bikeshop_project/registration/urls.py +++ b/bikeshop_project/registration/urls.py @@ -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[\w@\.\+]+)/$', MemberSearchView.as_view(), name='member_search'), diff --git a/bikeshop_project/registration/views.py b/bikeshop_project/registration/views.py index 7630be5..a0cd442 100644 --- a/bikeshop_project/registration/views.py +++ b/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.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 diff --git a/bikeshop_project/webpack.config.js b/bikeshop_project/webpack.config.js index d5e5bca..b2594cb 100644 --- a/bikeshop_project/webpack.config.js +++ b/bikeshop_project/webpack.config.js @@ -1,48 +1,50 @@ -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'); -const autoprefixer = require('autoprefixer'); +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 + context: __dirname, + devtool: 'inline-source-map', + entry: { + signin: './assets/js/index', + members: './assets/js/members/index', + }, + output: { + path: path.resolve('./assets/bundles/'), + filename: '[name]-[hash].js', + }, - output: { - path: path.resolve('./assets/bundles/'), - filename: "[name]-[hash].js" - }, + plugins: [ + new BundleTracker({ filename: './webpack-stats.json' }), + new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), + new webpack.NoErrorsPlugin(), + ], - plugins: [ - new BundleTracker({filename: './webpack-stats.json'}), - new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), - new webpack.NoErrorsPlugin() + module: { + loaders: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + 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'), + }, ], - - module: { - loaders: [ - { - test: /\.jsx?$/, - exclude: /node_modules/, - loader: 'babel-loader', - query: { - 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') - } - ] - }, - resolve: { - modulesDirectories: [ - 'node_modules', - 'bower_components', - path.resolve(__dirname, './node_modules') - ], - extensions: ['', '.js', '.jsx', '.scss'] - }, - postcss: [autoprefixer] -} \ No newline at end of file + }, + resolve: { + modulesDirectories: [ + 'node_modules', + 'bower_components', + path.resolve(__dirname, './node_modules'), + ], + extensions: ['', '.js', '.jsx', '.scss'], + }, + postcss: [autoprefixer], +}; diff --git a/bikeshop_project/webpack.dev.config.js b/bikeshop_project/webpack.dev.config.js index 4826c0b..0f4ada6 100644 --- a/bikeshop_project/webpack.dev.config.js +++ b/bikeshop_project/webpack.dev.config.js @@ -6,11 +6,14 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin'); var config = require('./webpack.base.config.js') // Use webpack dev server -config.entry = [ - 'webpack-dev-server/client?http://webpack.docker:3000', - 'webpack/hot/only-dev-server', - './assets/js/index' -] +config.entry = { + webpack: [ + 'webpack-dev-server/client?http://webpack.docker:3000', + 'webpack/hot/only-dev-server', + ], + 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/' diff --git a/requirements/base.txt b/requirements/base.txt index e871554..791a167 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -11,3 +11,4 @@ djangorestframework django-webpack-loader requests PyYAML +djangorestframework-jwt==1.9.0 \ No newline at end of file