mirror of
https://github.com/fspc/workstand.git
synced 2025-04-04 10:03:22 -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 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>
|
||||||
);
|
);
|
||||||
|
@ -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 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'):
|
||||||
|
@ -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
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' %}
|
{% 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 %}
|
@ -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'),
|
||||||
|
@ -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
|
||||||
|
@ -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],
|
||||||
}
|
};
|
||||||
|
@ -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/'
|
||||||
|
@ -11,3 +11,4 @@ djangorestframework
|
|||||||
django-webpack-loader
|
django-webpack-loader
|
||||||
requests
|
requests
|
||||||
PyYAML
|
PyYAML
|
||||||
|
djangorestframework-jwt==1.9.0
|
Loading…
x
Reference in New Issue
Block a user