Browse Source

Add member notes (#35)

* Linting.

* Clean up.

* Hope this makes installs better.

* Rework edit member form with notes and banned and suspended.

* Add slack notifications.

* Display new member statuses.

* Update docker run command

migrate, collectstatic, and rebuild_index

* Formatting and only push when master.

* Don’t need this for an edit page.

* More meaningful action.

* Add testing requirements.

* Run tests during build.

* Use prod settings for testing.

* Nail down the order of those tests.
feature/python-error-tracking
Drew Larson 8 years ago
committed by GitHub
parent
commit
49f37ed492
  1. 16
      .travis.yml
  2. 4
      Dockerfile
  3. 1
      Dockerfile-webpack
  4. 16
      bikeshop_project/assets/js/components/SignIn.jsx
  5. 59
      bikeshop_project/assets/js/components/SignedInList.jsx
  6. 6
      bikeshop_project/bikeshop/settings/production.py
  7. 8
      bikeshop_project/registration/forms.py
  8. 30
      bikeshop_project/registration/migrations/0003_auto_20170215_0308.py
  9. 3
      bikeshop_project/registration/models.py
  10. 2
      bikeshop_project/registration/serializers.py
  11. 316
      bikeshop_project/registration/templates/edit_member_form.html
  12. 6
      bikeshop_project/registration/tests/test_views.py
  13. 7
      bikeshop_project/registration/views.py
  14. 8
      docker-compose.dev.yml
  15. 2
      docker-compose.prod.yml
  16. 1
      requirements/production.txt

16
.travis.yml

@ -4,10 +4,12 @@ services:
before_install: before_install:
- docker build -t bcbc/workstand:production . - docker build -t bcbc/workstand:production .
after_success: after_success:
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
- export REPO=bcbc/workstand after_success:
- export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "production"; else echo $TRAVIS_BRANCH ; fi` - if [ "$TRAVIS_BRANCH" == "master" ]; then
- docker build -f Dockerfile -t $REPO:$COMMIT . docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
- docker tag $REPO:$COMMIT $REPO:$TAG docker push bcbc/workstand:production;
- docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER fi
- docker push $REPO notifications:
slack:
secure: n2tF/UW/3cnLgqxU0iq6M6mXoYitXgv4x8Iqv9uFuO6AXJktKADVbSeHVmeDVJu7vlyexytpN7BfJ2W0rbMB5Q+yef3TmWhc5P0MWRR3J+QwbLXdqBIP2tku69OK2aUEcxPvoJZLtDKdfNTt9vtQurGC9kbJ+MKmU0Sm+MFrrxrM4D7/74rynbhppMnwcwouVgzdfmJOt0gc0ySZlWqOacUSOVZxhpJ6aqABkFHWbIzO9LHXBwFLJU3oa8poFg51AYx2cuy7PjHfMtEt2i5bHXPR0NkVm4UMbtmkLvvmHLErS1tAHUgWnLAKAL8i7j8hsKWjBc/+0Pwz7t4j5M79+XcF78dS7VQOzmrP/TdyWfsN4rZeUvl+b+owZKLv1uLG6kQhRI332fzCwtK+VejtBtjdoC9nq1KTOIHa6y10aswqGgfsPL4+6UOke/LZsgb9fqfiB44TYkn0dgfh1J/3P+hNtLOsm1Q2IZQvDU97CqdavfAlye4uKC/WlnY3fXeISEMeE57ZAu8Tz4IKrAIbg7mt7qFTmNqxUIvzfWO5ZaMQZhncRfIMajAUigXr9XjRkewo9cxF2cuDwllTVeY5MTrq8hTqlr0g/N5njS72KkLAzaXKUmzyTpfndrGKH5K2XiSGlpgOZ6ao/FOKr3TmzNV5fiOsLEr9Y2+Q3bgknOE=

4
Dockerfile

@ -7,6 +7,7 @@ WORKDIR /code
RUN mkdir requirements RUN mkdir requirements
ADD bikeshop_project /code ADD bikeshop_project /code
ADD requirements/base.txt /code/requirements/base.txt ADD requirements/base.txt /code/requirements/base.txt
ADD requirements/testing.txt /code/requirements/testing.txt
ADD requirements/production.txt /code/requirements/production.txt ADD requirements/production.txt /code/requirements/production.txt
RUN pip install -r requirements/production.txt RUN pip install -r requirements/production.txt
RUN npm cache clean RUN npm cache clean
@ -16,5 +17,6 @@ RUN bower install --allow-root
ADD ./bikeshop_project/package.json package.json ADD ./bikeshop_project/package.json package.json
RUN npm install --unsafe-perm RUN npm install --unsafe-perm
RUN npm run build-production RUN npm run build-production
RUN DJANGO_SETTINGS_MODULE=bikeshop.settings.production python manage.py collectstatic --no-input RUN DJANGO_SETTINGS_MODULE=bikeshop.settings.production python manage.py test
CMD 'bash -c "PYTHONUNBUFFERED=TRUE python manage.py migrate --no-input && python manage.py collectstatic --no-input && python manage.py rebuild_index --noinput && gunicorn --log-file=- -b 0.0.0.0:8000 bikeshop.wsgi:application"'
EXPOSE 8000 EXPOSE 8000

1
Dockerfile-webpack

@ -4,6 +4,7 @@ WORKDIR /code
ADD ./bikeshop_project/package.json package.json ADD ./bikeshop_project/package.json package.json
RUN npm install RUN npm install
RUN npm install -g bower RUN npm install -g bower
ADD ./bikeshop_project/.bowerrc .bowerrc
ADD ./bikeshop_project/bower.json bower.json ADD ./bikeshop_project/bower.json bower.json
RUN bower install --allow-root RUN bower install --allow-root
EXPOSE 3000:3000 EXPOSE 3000:3000

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

@ -29,11 +29,14 @@ export default class SignIn extends React.Component {
componentDidMount() { componentDidMount() {
fetch('/members/signin/') fetch('/members/signin/')
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then((visits) => {
const visits = JSON.parse(data);
this.setState({ signedIn: visits.map(visit => ({ this.setState({ signedIn: visits.map(visit => ({
id: visit.member.id, id: visit.member.id,
banned: visit.member.banned,
suspended: visit.member.suspended,
purpose: visit.purpose, purpose: visit.purpose,
first_name: visit.member.first_name,
last_name: visit.member.last_name,
text: `${visit.member.first_name} ${visit.member.last_name}`, text: `${visit.member.first_name} ${visit.member.last_name}`,
value: `${visit.member.first_name} ${visit.member.last_name} <${visit.member.email}>`, value: `${visit.member.first_name} ${visit.member.last_name} <${visit.member.email}>`,
at: moment(visit.created_at), at: moment(visit.created_at),
@ -68,14 +71,11 @@ export default class SignIn extends React.Component {
return response.json(); return response.json();
} }
}) })
.then((data) => { .then((parsedData) => {
const signedIn = this.state.signedIn;
const parsedData = JSON.parse(data);
signedIn.push({ ...member, purpose, at: moment(parsedData.results.created_at) });
this.setState({ this.setState({
...this.state, ...this.state,
signedIn, signedIn: [...this.state.signedIn,
{ ...parsedData.results, purpose, at: moment(parsedData.results.created_at) }],
signOn: { purpose: 'FIX', member: undefined }, signOn: { purpose: 'FIX', member: undefined },
searchText: '', searchText: '',
members: [], members: [],

59
bikeshop_project/assets/js/components/SignedInList.jsx

@ -5,6 +5,33 @@ import React, { PropTypes } from 'react';
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table';
import moment from 'moment'; import moment from 'moment';
const styles = {
suspended: {
display: 'block',
padding: '10px',
background: 'yellow',
borderRadius: '5px',
textAlign: 'center',
color: 'black',
},
good: {
display: 'block',
padding: '10px',
background: 'green',
borderRadius: '5px',
textAlign: 'center',
color: 'white',
},
banned: {
display: 'block',
padding: '10px',
background: 'red',
borderRadius: '5px',
textAlign: 'center',
color: 'white',
},
};
export default class SignedInList extends React.Component { export default class SignedInList extends React.Component {
static propTypes = { static propTypes = {
members: PropTypes.arrayOf(PropTypes.shape({ members: PropTypes.arrayOf(PropTypes.shape({
@ -40,14 +67,25 @@ export default class SignedInList extends React.Component {
render() { render() {
const memberRows = this.sortMembers(this.props.members) const memberRows = this.sortMembers(this.props.members)
.map(member => ( .map((member) => {
<TableRow selectable={false} key={member.id}> let memberStatus = <span style={styles.good} className="good">good</span>;
<TableRowColumn>{member.text}</TableRowColumn>
<TableRowColumn>{member.purpose}</TableRowColumn> if (member.banned) {
<TableRowColumn>{member.at.fromNow()}</TableRowColumn> memberStatus = <span style={styles.banned} className="banned">banned</span>;
<TableRowColumn><a href={`/members/edit/${member.id}/`}>Profile</a></TableRowColumn> } else if (member.suspended) {
</TableRow> memberStatus = <span style={styles.suspended} className="suspended">suspended</span>;
)); }
return (
<TableRow selectable={false} key={member.id}>
<TableRowColumn>{member.first_name} {member.last_name}</TableRowColumn>
<TableRowColumn>{member.purpose}</TableRowColumn>
<TableRowColumn>{member.at.fromNow()}</TableRowColumn>
<TableRowColumn>{memberStatus}</TableRowColumn>
<TableRowColumn><a href={`/members/edit/${member.id}/`}>Profile</a></TableRowColumn>
</TableRow>
);
});
return ( return (
<div className="mdl-cell mdl-cell--12-col"> <div className="mdl-cell mdl-cell--12-col">
@ -57,11 +95,12 @@ export default class SignedInList extends React.Component {
</FloatingActionButton> </FloatingActionButton>
<Table selectable={false}> <Table selectable={false}>
<TableHeader adjustForCheckbox={false} displaySelectAll={false}> <TableHeader adjustForCheckbox={false} displaySelectAll={false}>
<TableRow> <TableRow key="blah">
<TableHeaderColumn>Name</TableHeaderColumn> <TableHeaderColumn>Name</TableHeaderColumn>
<TableHeaderColumn>Purpose</TableHeaderColumn> <TableHeaderColumn>Purpose</TableHeaderColumn>
<TableHeaderColumn>Signed-in At</TableHeaderColumn> <TableHeaderColumn>Signed-in At</TableHeaderColumn>
<TableHeaderColumn/> <TableHeaderColumn>Member Status</TableHeaderColumn>
<TableHeaderColumn />
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody displayRowCheckbox={false}> <TableBody displayRowCheckbox={false}>

6
bikeshop_project/bikeshop/settings/production.py

@ -1,4 +1,4 @@
import os import sys
from .base import * # noqa from .base import * # noqa
@ -47,3 +47,7 @@ WEBPACK_LOADER = {
'IGNORE': ['.+\.hot-update.js', '.+\.map'] 'IGNORE': ['.+\.hot-update.js', '.+\.map']
} }
} }
# Covers regular testing and django-coverage
if 'test' in sys.argv or 'test_coverage' in sys.argv:
DATABASES['default']['ENGINE'] = 'django.db.backends.sqlite3' # noqa

8
bikeshop_project/registration/forms.py

@ -1,4 +1,4 @@
from django.forms import ModelForm, EmailInput, TextInput, DateInput, CheckboxInput, BooleanField from django.forms import ModelForm, EmailInput, TextInput, DateInput, CheckboxInput, BooleanField, Textarea
from django.utils import timezone from django.utils import timezone
from registration.models import Member from registration.models import Member
@ -12,7 +12,8 @@ class MemberForm(ModelForm):
exclude = ('waiver',) exclude = ('waiver',)
fields = ['email', 'email_consent', 'first_name', 'last_name', 'preferred_name', 'date_of_birth', fields = ['email', 'email_consent', 'first_name', 'last_name', 'preferred_name', 'date_of_birth',
'guardian_name', 'phone', 'street', 'city', 'province', 'country', 'post_code', 'waiver'] 'guardian_name', 'phone', 'street', 'city', 'province', 'country', 'post_code', 'waiver',
'banned', 'suspended', 'notes']
widgets = { widgets = {
'email': EmailInput(attrs={'class': 'mdl-textfield__input'}), 'email': EmailInput(attrs={'class': 'mdl-textfield__input'}),
'email_consent': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}), 'email_consent': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}),
@ -28,6 +29,9 @@ class MemberForm(ModelForm):
'country': TextInput(attrs={'class': 'mdl-textfield__input'}), 'country': TextInput(attrs={'class': 'mdl-textfield__input'}),
'post_code': TextInput(attrs={'class': 'mdl-textfield__input', 'post_code': TextInput(attrs={'class': 'mdl-textfield__input',
'pattern': '[A-Za-z][0-9][A-Za-z] [0-9][A-Za-z][0-9]'}), 'pattern': '[A-Za-z][0-9][A-Za-z] [0-9][A-Za-z][0-9]'}),
'notes': Textarea(attrs={'class': 'mdl-textfield__input'}),
'suspended': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}),
'banned': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}),
} }
labels = { labels = {

30
bikeshop_project/registration/migrations/0003_auto_20170215_0308.py

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-15 03:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registration', '0002_auto_20161130_0157'),
]
operations = [
migrations.AddField(
model_name='member',
name='banned',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='member',
name='notes',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='member',
name='suspended',
field=models.BooleanField(default=False),
),
]

3
bikeshop_project/registration/models.py

@ -93,6 +93,9 @@ class Member(models.Model):
post_code = models.CharField(max_length=20, null=True, blank=False) post_code = models.CharField(max_length=20, null=True, blank=False)
waiver = models.DateTimeField(null=True, blank=True) waiver = models.DateTimeField(null=True, blank=True)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
notes = models.TextField(null=True, blank=True)
suspended = models.BooleanField(default=False)
banned = models.BooleanField(default=False)
def get_full_name(self): def get_full_name(self):
# The user is identified by their email address # The user is identified by their email address

2
bikeshop_project/registration/serializers.py

@ -10,4 +10,4 @@ class MemberSerializer(ModelSerializer):
class Meta: class Meta:
model = Member model = Member
fields = ('first_name', 'last_name', 'email', 'id') fields = ('first_name', 'last_name', 'email', 'id', 'banned', 'suspended')

316
bikeshop_project/registration/templates/edit_member_form.html

@ -2,7 +2,7 @@
{% load staticfiles %} {% load staticfiles %}
{% block scripts %} {% block scripts %}
<script src="{% static 'vendor/moment/min/moment.min.js' %}"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js"></script>
<script> <script>
dateOfBirthInput = document.getElementById('{{ form.date_of_birth.id_for_label }}'); dateOfBirthInput = document.getElementById('{{ form.date_of_birth.id_for_label }}');
@ -23,41 +23,14 @@
} }
}); });
var waiverCheckBox = document.getElementById('{{ form.waiver_substitute.id_for_label }}');
var submitButton = document.getElementById('submit');
var requiredCheckboxes = function() {
return waiverCheckBox.checked;
};
waiverCheckBox.addEventListener('change', function() {
if (requiredCheckboxes()) {
submitButton.disabled = false;
}
else {
submitButton.disabled = true;
}
});
// On page load check on the check boxes
if (requiredCheckboxes()) {
submitButton.disabled = false;
}
</script> </script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="mdl-cell mdl-cell--8-col"> <div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col">
<h1>{{ form.instance.first_name }} {{ form.instance.last_name }}</h1> <h1>{{ form.instance.first_name }} {{ form.instance.last_name }}</h1>
<p>
The Bridge City Bicycle Co-operative (herein referred to as The BCBC and The Community) is a nonprofit,
community bicycle repair education and resource co-operative. We offer our members nonjudgmental repair
space, tools and instruction during business hours (hours on website) by donation, and educational
workshops. We also offer reconditioned/recycled low cost bikes and parts for sale.
The BCBC is operated by volunteers; a medley of professionals, students, bike enthusiasts, activists,
and other community members who share a love for cycling in Saskatoon. Membership is open to all
individuals and costs $20 per year. A receipt will be issued to you once your membership fee has been paid.
</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@ -66,118 +39,197 @@
<span class="error">{{ form.errors }}</span> <span class="error">{{ form.errors }}</span>
</div> </div>
{% endif %} {% endif %}
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.email.errors %}is-invalid{% endif %}"> <div class="mdl-grid">
{{ form.email }} <div class="mdl-cell mdl-cell--12-col">
<label class="mdl-textfield__label" for="{{ form.email.id_for_label }}">{{ form.email.label }}</label> <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.email.errors %}is-invalid{% endif %}">
{% if form.email.errors %} {{ form.email }}
<span class="mdl-textfield__error">{{ form.email.errors }}</span> <label class="mdl-textfield__label" for="{{ form.email.id_for_label }}">{{ form.email.label }}</label>
{% else %} {% if form.email.errors %}
<span class="mdl-textfield__error">Invalid email.</span> <span class="mdl-textfield__error">{{ form.email.errors }}</span>
{% endif %} {% else %}
</div> <span class="mdl-textfield__error">Invalid email.</span>
<div> {% endif %}
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.email_consent.id_for_label }}"> </div>
{{ form.email_consent }} <div>
<span class="mdl-checkbox__label">{{ form.email_consent.label }}</span> <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.email_consent.id_for_label }}">
</label> {{ form.email_consent }}
</div> <span class="mdl-checkbox__label">{{ form.email_consent.label }}</span>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.first_name.errors %}is-invalid{% endif %}"> </label>
{{ form.first_name }} </div>
<label class="mdl-textfield__label" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label> </div>
{% if form.first_name.errors %}
<span class="mdl-textfield__error">{{ form.first_name.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Name too long.</span>
{% endif %}
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.last_name.errors %}is-invalid{% endif %}">
{{ form.last_name }}
<label class="mdl-textfield__label" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label>
{% if form.last_name.errors %}
<span class="mdl-textfield__error">{{ form.last_name.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Name too long.</span>
{% endif %}
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.preferred_name.errors %}is-invalid{% endif %}">
{{ form.preferred_name }}
<label class="mdl-textfield__label" for="{{ form.preferred_name.id_for_label }}">{{ form.preferred_name.label }}</label>
{% if form.preferred_name.errors %}
<span class="mdl-textfield__error">{{ form.preferred_name.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Name too long.</span>
{% endif %}
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.date_of_birth.errors %}is-invalid{% endif %}">
{{ form.date_of_birth }} <div class="mdl-grid">
<label class="mdl-textfield__label" for="{{ form.date_of_birth.id_for_label }}">{{ form.date_of_birth.label }} (e.g. 1991-08-25)</label> <div class="mdl-cell">
{% if form.date_of_birth.errors %} <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.first_name.errors %}is-invalid{% endif %}">
<span class="mdl-textfield__error">{{ form.date_of_birth.errors }}</span> {{ form.first_name }}
{% else %} <label class="mdl-textfield__label" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label>
<span class="mdl-textfield__error">Incorrect date.</span> {% if form.first_name.errors %}
{% endif %} <span class="mdl-textfield__error">{{ form.first_name.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Name too long.</span>
{% endif %}
</div>
</div>
<div class="mdl-cell">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.last_name.errors %}is-invalid{% endif %}">
{{ form.last_name }}
<label class="mdl-textfield__label" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label>
{% if form.last_name.errors %}
<span class="mdl-textfield__error">{{ form.last_name.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Name too long.</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.guardian_name.errors %}is-invalid{% endif %}">
{{ form.guardian_name }} <div class="mdl-grid">
<label class="mdl-textfield__label" for="{{ form.guardian_name.id_for_label }}">{{ form.guardian_name.label }}</label> <div class="mdl-cell">
{% if form.guardian_name.errors %} <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.preferred_name.errors %}is-invalid{% endif %}">
<span class="mdl-textfield__error">{{ form.guardian_name.errors }}</span> {{ form.preferred_name }}
{% else %} <label class="mdl-textfield__label" for="{{ form.preferred_name.id_for_label }}">{{ form.preferred_name.label }}</label>
<span class="mdl-textfield__error">Name too long.</span> {% if form.preferred_name.errors %}
{% endif %} <span class="mdl-textfield__error">{{ form.preferred_name.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Name too long.</span>
{% endif %}
</div>
</div>
<div class="mdl-cell">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.phone.errors %}is-invalid{% endif %}">
{{ form.phone }}
<label class="mdl-textfield__label" for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label>
{% if form.phone.errors %}
<span class="mdl-textfield__error">{{ form.phone.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Digits only.</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.phone.errors %}is-invalid{% endif %}">
{{ form.phone }} <div class="mdl-grid">
<label class="mdl-textfield__label" for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label> <div class="mdl-cell">
{% if form.phone.errors %} <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.date_of_birth.errors %}is-invalid{% endif %}">
<span class="mdl-textfield__error">{{ form.phone.errors }}</span> {{ form.date_of_birth }}
{% else %} <label class="mdl-textfield__label" for="{{ form.date_of_birth.id_for_label }}">{{ form.date_of_birth.label }} (e.g. 1991-08-25)</label>
<span class="mdl-textfield__error">Digits only.</span> {% if form.date_of_birth.errors %}
{% endif %} <span class="mdl-textfield__error">{{ form.date_of_birth.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Incorrect date.</span>
{% endif %}
</div>
</div>
<div class="mdl-cell">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.guardian_name.errors %}is-invalid{% endif %}">
{{ form.guardian_name }}
<label class="mdl-textfield__label" for="{{ form.guardian_name.id_for_label }}">{{ form.guardian_name.label }}</label>
{% if form.guardian_name.errors %}
<span class="mdl-textfield__error">{{ form.guardian_name.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Name too long.</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.post_code.errors %}is-invalid{% endif %}">
{{ form.post_code }} <div class="mdl-grid">
<label class="mdl-textfield__label" for="{{ form.post_code.id_for_label }}">{{ form.post_code.label }}</label> <div class="mdl-cell">
{% if form.post_code.errors %} <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.post_code.errors %}is-invalid{% endif %}">
<span class="mdl-textfield__error">{{ form.post_code.errors }}</span> {{ form.post_code }}
{% else %} <label class="mdl-textfield__label" for="{{ form.post_code.id_for_label }}">{{ form.post_code.label }}</label>
<span class="mdl-textfield__error">Format: A0A 0A0</span> {% if form.post_code.errors %}
{% endif %} <span class="mdl-textfield__error">{{ form.post_code.errors }}</span>
{% else %}
<span class="mdl-textfield__error">Format: A0A 0A0</span>
{% endif %}
</div>
</div>
<div class="mdl-cell">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.street.errors %}is-invalid{% endif %}">
{{ form.street }}
<label class="mdl-textfield__label" for="{{ form.street.id_for_label }}">{{ form.street.label }}</label>
{% if form.street.errors %}
<span class="mdl-textfield__error">{{ form.street.errors }}</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.street.errors %}is-invalid{% endif %}"> <div class="mdl-grid">
{{ form.street }} <div class="mdl-cell">
<label class="mdl-textfield__label" for="{{ form.street.id_for_label }}">{{ form.street.label }}</label> <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.city.errors %}is-invalid{% endif %}">
{% if form.street.errors %} {{ form.city }}
<span class="mdl-textfield__error">{{ form.street.errors }}</span> <label class="mdl-textfield__label" for="{{ form.city.id_for_label }}">{{ form.city.label }}</label>
{% endif %} {% if form.city.errors %}
<span class="mdl-textfield__error">{{ form.city.errors }}</span>
{% endif %}
</div>
</div>
<div class="mdl-cell">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.province.errors %}is-invalid{% endif %}">
{{ form.province }}
<label class="mdl-textfield__label" for="{{ form.province.id_for_label }}">{{ form.province.label }}</label>
{% if form.province.errors %}
<span class="mdl-textfield__error">{{ form.province.errors }}</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.city.errors %}is-invalid{% endif %}"> <div class="mdl-grid">
{{ form.city }} <div class="mdl-cell">
<label class="mdl-textfield__label" for="{{ form.city.id_for_label }}">{{ form.city.label }}</label> <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.country.errors %}is-invalid{% endif %}">
{% if form.city.errors %} {{ form.country }}
<span class="mdl-textfield__error">{{ form.city.errors }}</span> <label class="mdl-textfield__label" for="{{ form.country.id_for_label }}">{{ form.country.label }}</label>
{% endif %} {% if form.country.errors %}
<span class="mdl-textfield__error">{{ form.country.errors }}</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.province.errors %}is-invalid{% endif %}">
{{ form.province }} <div class="mdl-grid">
<label class="mdl-textfield__label" for="{{ form.province.id_for_label }}">{{ form.province.label }}</label> <div class="mdl-cell mdl-cell--12-col">
{% if form.province.errors %} <h2>Member Info</h2>
<span class="mdl-textfield__error">{{ form.province.errors }}</span> <h3>Notes</h3>
{% endif %} </div>
<div class="mdl-cell mdl-cell--12-col">
<div class="mdl-textfield mdl-js-textfield mdl-cell--12-col">
{{ form.notes }}
<label class="mdl-textfield__label" for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
{% if form.notes.errors %}
<span class="mdl-textfield__error">{{ form.notes.errors }}</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.country.errors %}is-invalid{% endif %}">
{{ form.country }} <div class="mdl-grid">
<label class="mdl-textfield__label" for="{{ form.country.id_for_label }}">{{ form.country.label }}</label> <div class="mdl-cell mdl-cell--3-col">
{% if form.country.errors %} <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.suspended.id_for_label }}">
<span class="mdl-textfield__error">{{ form.country.errors }}</span> {{ form.suspended }}
{% endif %} <span class="mdl-checkbox__label">{{ form.suspended.label }}</span>
</label>
</div>
<div class="mdl-cell mdl-cell--3-col">
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.banned.id_for_label }}">
{{ form.banned }}
<span class="mdl-checkbox__label">{{ form.banned.label }}</span>
</label>
</div>
</div> </div>
<div> <div class="mdl-grid">
<button id="submit" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">Submit</button> <div class="mdl-cell">
<button id="submit" type="submit"
class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">
Save</button>
</div>
</div> </div>
</form> </form>
</div> </div>
<div class="mdl-cell mdl-cell--8-col"> </div>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--8-col">
{% if form.instance.memberships %} {% if form.instance.memberships %}
<table class="mdl-data-table mdl-js-data-table"> <table class="mdl-data-table mdl-js-data-table">
<thead> <thead>
@ -205,5 +257,7 @@
{% endif %} {% endif %}
<a class="mdl-button mdl-js-button mdl-button--flat mdl-js-ripple-effect mdl-button--colored" href="{% url 'new_membership' member_id=member.id %}">Add membership</a> <a class="mdl-button mdl-js-button mdl-button--flat mdl-js-ripple-effect mdl-button--colored" href="{% url 'new_membership' member_id=member.id %}">Add membership</a>
</div> </div>
</div>
{% endblock %} {% endblock %}

6
bikeshop_project/registration/tests/test_views.py

@ -104,6 +104,10 @@ class TestMemberSignIn(TestCase):
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertIsInstance(response, JsonResponse) self.assertIsInstance(response, JsonResponse)
data = json.loads(response.content.decode())
results = data['results']
self.assertTrue('banned' in results)
self.assertTrue('suspended' in results)
self.assertTrue(visit) self.assertTrue(visit)
def test_post_no_member(self): def test_post_no_member(self):
@ -137,3 +141,5 @@ class TestMemberSignIn(TestCase):
data = json.loads(data_string) data = json.loads(data_string)
self.assertTrue(len(data), 3) self.assertTrue(len(data), 3)
self.assertTrue('banned' in data[0]['member'])
self.assertTrue('suspended' in data[0]['member'])

7
bikeshop_project/registration/views.py

@ -83,16 +83,17 @@ class MemberSignIn(View):
def post(self, request): def post(self, request):
member = get_object_or_404(Member, id=request.POST.get('id')) member = get_object_or_404(Member, id=request.POST.get('id'))
visit = signin_member(member, request.POST.get('purpose')) visit = signin_member(member, request.POST.get('purpose'))
data = json.dumps(dict(results=dict(id=member.id, created_at=visit.created_at.isoformat()))) data = dict(results=dict(id=member.id, first_name=member.first_name, last_name=member.last_name,
suspended=member.suspended, banned=member.banned,
created_at=visit.created_at.isoformat()))
return JsonResponse(data=data, safe=False, status=201) return JsonResponse(data=data, safe=False, status=201)
def get(self, request): def get(self, request):
visits = get_signed_in_members().prefetch_related() visits = get_signed_in_members().prefetch_related()
serializer = VisitSerializer(visits, many=True) serializer = VisitSerializer(visits, many=True)
results_json = JSONRenderer().render(serializer.data)
return JsonResponse(data=results_json.decode(), safe=False, status=200) return JsonResponse(data=serializer.data, safe=False, status=200)
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')

8
docker-compose.dev.yml

@ -10,6 +10,8 @@ services:
- "62260:62260" - "62260:62260"
volumes: volumes:
- ./bikeshop_project:/code:rw - ./bikeshop_project:/code:rw
volumes_from:
- webpack
redis: redis:
restart: always restart: always
db: db:
@ -22,5 +24,7 @@ services:
ports: ports:
- "3000:3000" - "3000:3000"
restart: always restart: always
volumes_from: volumes:
- workstand - /code/node_modules
- /code/vendor
- ./bikeshop_project:/code:rw

2
docker-compose.prod.yml

@ -23,7 +23,7 @@ services:
image: bcbc/workstand:production image: bcbc/workstand:production
env_file: env_file:
- workstand.env - workstand.env
command: gunicorn --log-file=- -b 0.0.0.0:8000 bikeshop.wsgi:application command: 'bash -c "PYTHONUNBUFFERED=TRUE python manage.py migrate --no-input && python manage.py collectstatic --no-input && python manage.py rebuild_index --noinput && gunicorn --log-file=- -b 0.0.0.0:8000 bikeshop.wsgi:application"'
environment: environment:
- DJANGO_SETTINGS_MODULE=bikeshop.settings.production - DJANGO_SETTINGS_MODULE=bikeshop.settings.production
volumes: volumes:

1
requirements/production.txt

@ -1,2 +1,3 @@
-r base.txt -r base.txt
-r testing.txt
gunicorn==19.4.5 gunicorn==19.4.5

Loading…
Cancel
Save