diff --git a/.travis.yml b/.travis.yml
index 5dc368a..f38373f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,10 +4,12 @@ services:
before_install:
- docker build -t bcbc/workstand:production .
after_success:
- - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
- - export REPO=bcbc/workstand
- - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "production"; else echo $TRAVIS_BRANCH ; fi`
- - docker build -f Dockerfile -t $REPO:$COMMIT .
- - docker tag $REPO:$COMMIT $REPO:$TAG
- - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER
- - docker push $REPO
\ No newline at end of file
+
+after_success:
+ - if [ "$TRAVIS_BRANCH" == "master" ]; then
+ docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
+ docker push bcbc/workstand:production;
+ fi
+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=
diff --git a/Dockerfile b/Dockerfile
index 5769a56..1d3a352 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,6 +7,7 @@ WORKDIR /code
RUN mkdir requirements
ADD bikeshop_project /code
ADD requirements/base.txt /code/requirements/base.txt
+ADD requirements/testing.txt /code/requirements/testing.txt
ADD requirements/production.txt /code/requirements/production.txt
RUN pip install -r requirements/production.txt
RUN npm cache clean
@@ -16,5 +17,6 @@ RUN bower install --allow-root
ADD ./bikeshop_project/package.json package.json
RUN npm install --unsafe-perm
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
diff --git a/Dockerfile-webpack b/Dockerfile-webpack
index f6ef5fb..2ee7464 100644
--- a/Dockerfile-webpack
+++ b/Dockerfile-webpack
@@ -4,6 +4,7 @@ WORKDIR /code
ADD ./bikeshop_project/package.json package.json
RUN npm install
RUN npm install -g bower
+ADD ./bikeshop_project/.bowerrc .bowerrc
ADD ./bikeshop_project/bower.json bower.json
RUN bower install --allow-root
EXPOSE 3000:3000
diff --git a/bikeshop_project/assets/js/components/SignIn.jsx b/bikeshop_project/assets/js/components/SignIn.jsx
index 7f8bafe..34bea38 100644
--- a/bikeshop_project/assets/js/components/SignIn.jsx
+++ b/bikeshop_project/assets/js/components/SignIn.jsx
@@ -29,11 +29,14 @@ export default class SignIn extends React.Component {
componentDidMount() {
fetch('/members/signin/')
.then(response => response.json())
- .then((data) => {
- const visits = JSON.parse(data);
+ .then((visits) => {
this.setState({ signedIn: visits.map(visit => ({
id: visit.member.id,
+ banned: visit.member.banned,
+ suspended: visit.member.suspended,
purpose: visit.purpose,
+ first_name: visit.member.first_name,
+ last_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}>`,
at: moment(visit.created_at),
@@ -68,14 +71,11 @@ export default class SignIn extends React.Component {
return response.json();
}
})
- .then((data) => {
- const signedIn = this.state.signedIn;
- const parsedData = JSON.parse(data);
-
- signedIn.push({ ...member, purpose, at: moment(parsedData.results.created_at) });
+ .then((parsedData) => {
this.setState({
...this.state,
- signedIn,
+ signedIn: [...this.state.signedIn,
+ { ...parsedData.results, purpose, at: moment(parsedData.results.created_at) }],
signOn: { purpose: 'FIX', member: undefined },
searchText: '',
members: [],
diff --git a/bikeshop_project/assets/js/components/SignedInList.jsx b/bikeshop_project/assets/js/components/SignedInList.jsx
index a8a9775..16d9ca3 100644
--- a/bikeshop_project/assets/js/components/SignedInList.jsx
+++ b/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 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 {
static propTypes = {
members: PropTypes.arrayOf(PropTypes.shape({
@@ -40,14 +67,25 @@ export default class SignedInList extends React.Component {
render() {
const memberRows = this.sortMembers(this.props.members)
- .map(member => (
-
- {member.text}
- {member.purpose}
- {member.at.fromNow()}
- Profile
-
- ));
+ .map((member) => {
+ let memberStatus = good ;
+
+ if (member.banned) {
+ memberStatus = banned ;
+ } else if (member.suspended) {
+ memberStatus = suspended ;
+ }
+
+ return (
+
+ {member.first_name} {member.last_name}
+ {member.purpose}
+ {member.at.fromNow()}
+ {memberStatus}
+ Profile
+
+ );
+ });
return (
@@ -57,11 +95,12 @@ export default class SignedInList extends React.Component {
-
+
Name
Purpose
Signed-in At
-
+ Member Status
+
diff --git a/bikeshop_project/bikeshop/settings/production.py b/bikeshop_project/bikeshop/settings/production.py
index e9d6db3..8f82509 100644
--- a/bikeshop_project/bikeshop/settings/production.py
+++ b/bikeshop_project/bikeshop/settings/production.py
@@ -1,4 +1,4 @@
-import os
+import sys
from .base import * # noqa
@@ -47,3 +47,7 @@ WEBPACK_LOADER = {
'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
diff --git a/bikeshop_project/registration/forms.py b/bikeshop_project/registration/forms.py
index 369a024..0ba295a 100644
--- a/bikeshop_project/registration/forms.py
+++ b/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 registration.models import Member
@@ -12,7 +12,8 @@ class MemberForm(ModelForm):
exclude = ('waiver',)
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 = {
'email': EmailInput(attrs={'class': 'mdl-textfield__input'}),
'email_consent': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}),
@@ -28,6 +29,9 @@ class MemberForm(ModelForm):
'country': 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]'}),
+ 'notes': Textarea(attrs={'class': 'mdl-textfield__input'}),
+ 'suspended': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}),
+ 'banned': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}),
}
labels = {
diff --git a/bikeshop_project/registration/migrations/0003_auto_20170215_0308.py b/bikeshop_project/registration/migrations/0003_auto_20170215_0308.py
new file mode 100644
index 0000000..9d0a032
--- /dev/null
+++ b/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),
+ ),
+ ]
diff --git a/bikeshop_project/registration/models.py b/bikeshop_project/registration/models.py
index 73b6ca4..b301876 100644
--- a/bikeshop_project/registration/models.py
+++ b/bikeshop_project/registration/models.py
@@ -93,6 +93,9 @@ class Member(models.Model):
post_code = models.CharField(max_length=20, null=True, blank=False)
waiver = models.DateTimeField(null=True, blank=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):
# The user is identified by their email address
diff --git a/bikeshop_project/registration/serializers.py b/bikeshop_project/registration/serializers.py
index 9b67b32..6f96420 100644
--- a/bikeshop_project/registration/serializers.py
+++ b/bikeshop_project/registration/serializers.py
@@ -10,4 +10,4 @@ class MemberSerializer(ModelSerializer):
class Meta:
model = Member
- fields = ('first_name', 'last_name', 'email', 'id')
+ fields = ('first_name', 'last_name', 'email', 'id', 'banned', 'suspended')
diff --git a/bikeshop_project/registration/templates/edit_member_form.html b/bikeshop_project/registration/templates/edit_member_form.html
index d93c37c..61064b1 100644
--- a/bikeshop_project/registration/templates/edit_member_form.html
+++ b/bikeshop_project/registration/templates/edit_member_form.html
@@ -2,7 +2,7 @@
{% load staticfiles %}
{% block scripts %}
-
+
{% endblock %}
{% block content %}
-
+
+
{{ form.instance.first_name }} {{ form.instance.last_name }}
-
- 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.
-
+
{% endif %}
-
- {{ form.email }}
- {{ form.email.label }}
- {% if form.email.errors %}
- {{ form.email.errors }}
- {% else %}
- Invalid email.
- {% endif %}
-
-
-
- {{ form.email_consent }}
- {{ form.email_consent.label }}
-
-
-
- {{ form.first_name }}
- {{ form.first_name.label }}
- {% if form.first_name.errors %}
- {{ form.first_name.errors }}
- {% else %}
- Name too long.
- {% endif %}
-
-
- {{ form.last_name }}
- {{ form.last_name.label }}
- {% if form.last_name.errors %}
- {{ form.last_name.errors }}
- {% else %}
- Name too long.
- {% endif %}
-
-
- {{ form.preferred_name }}
-
{{ form.preferred_name.label }}
- {% if form.preferred_name.errors %}
-
{{ form.preferred_name.errors }}
- {% else %}
-
Name too long.
- {% endif %}
+
+
+
+ {{ form.email }}
+ {{ form.email.label }}
+ {% if form.email.errors %}
+ {{ form.email.errors }}
+ {% else %}
+ Invalid email.
+ {% endif %}
+
+
+
+ {{ form.email_consent }}
+ {{ form.email_consent.label }}
+
+
+
-
- {{ form.date_of_birth }}
-
{{ form.date_of_birth.label }} (e.g. 1991-08-25)
- {% if form.date_of_birth.errors %}
-
{{ form.date_of_birth.errors }}
- {% else %}
-
Incorrect date.
- {% endif %}
+
+
+
+
+ {{ form.first_name }}
+ {{ form.first_name.label }}
+ {% if form.first_name.errors %}
+ {{ form.first_name.errors }}
+ {% else %}
+ Name too long.
+ {% endif %}
+
+
+
+
+ {{ form.last_name }}
+ {{ form.last_name.label }}
+ {% if form.last_name.errors %}
+ {{ form.last_name.errors }}
+ {% else %}
+ Name too long.
+ {% endif %}
+
+
-
- {{ form.guardian_name }}
-
{{ form.guardian_name.label }}
- {% if form.guardian_name.errors %}
-
{{ form.guardian_name.errors }}
- {% else %}
-
Name too long.
- {% endif %}
+
+
+
+
+ {{ form.preferred_name }}
+ {{ form.preferred_name.label }}
+ {% if form.preferred_name.errors %}
+ {{ form.preferred_name.errors }}
+ {% else %}
+ Name too long.
+ {% endif %}
+
+
+
+
+ {{ form.phone }}
+ {{ form.phone.label }}
+ {% if form.phone.errors %}
+ {{ form.phone.errors }}
+ {% else %}
+ Digits only.
+ {% endif %}
+
+
-
- {{ form.phone }}
-
{{ form.phone.label }}
- {% if form.phone.errors %}
-
{{ form.phone.errors }}
- {% else %}
-
Digits only.
- {% endif %}
+
+
+
+
+ {{ form.date_of_birth }}
+ {{ form.date_of_birth.label }} (e.g. 1991-08-25)
+ {% if form.date_of_birth.errors %}
+ {{ form.date_of_birth.errors }}
+ {% else %}
+ Incorrect date.
+ {% endif %}
+
+
+
+
+ {{ form.guardian_name }}
+ {{ form.guardian_name.label }}
+ {% if form.guardian_name.errors %}
+ {{ form.guardian_name.errors }}
+ {% else %}
+ Name too long.
+ {% endif %}
+
+
-
- {{ form.post_code }}
-
{{ form.post_code.label }}
- {% if form.post_code.errors %}
-
{{ form.post_code.errors }}
- {% else %}
-
Format: A0A 0A0
- {% endif %}
+
+
+
+
+ {{ form.post_code }}
+ {{ form.post_code.label }}
+ {% if form.post_code.errors %}
+ {{ form.post_code.errors }}
+ {% else %}
+ Format: A0A 0A0
+ {% endif %}
+
+
+
+
+ {{ form.street }}
+ {{ form.street.label }}
+ {% if form.street.errors %}
+ {{ form.street.errors }}
+ {% endif %}
+
+
-
- {{ form.street }}
-
{{ form.street.label }}
- {% if form.street.errors %}
-
{{ form.street.errors }}
- {% endif %}
+
+
+
+ {{ form.city }}
+ {{ form.city.label }}
+ {% if form.city.errors %}
+ {{ form.city.errors }}
+ {% endif %}
+
+
+
+
+ {{ form.province }}
+ {{ form.province.label }}
+ {% if form.province.errors %}
+ {{ form.province.errors }}
+ {% endif %}
+
+
-
- {{ form.city }}
-
{{ form.city.label }}
- {% if form.city.errors %}
-
{{ form.city.errors }}
- {% endif %}
+
+
+
+ {{ form.country }}
+ {{ form.country.label }}
+ {% if form.country.errors %}
+ {{ form.country.errors }}
+ {% endif %}
+
+
-
- {{ form.province }}
-
{{ form.province.label }}
- {% if form.province.errors %}
-
{{ form.province.errors }}
- {% endif %}
+
+
+
+
Member Info
+ Notes
+
+
+
+ {{ form.notes }}
+ {{ form.notes.label }}
+ {% if form.notes.errors %}
+ {{ form.notes.errors }}
+ {% endif %}
+
+
-
- {{ form.country }}
-
{{ form.country.label }}
- {% if form.country.errors %}
-
{{ form.country.errors }}
- {% endif %}
+
+
+
+
+ {{ form.suspended }}
+ {{ form.suspended.label }}
+
+
+
+
+ {{ form.banned }}
+ {{ form.banned.label }}
+
+
-
-
+
+
+
{% if form.instance.memberships %}
@@ -205,5 +257,7 @@
{% endif %}
Add membership
+
+
{% endblock %}
diff --git a/bikeshop_project/registration/tests/test_views.py b/bikeshop_project/registration/tests/test_views.py
index 31b630d..be2678c 100644
--- a/bikeshop_project/registration/tests/test_views.py
+++ b/bikeshop_project/registration/tests/test_views.py
@@ -104,6 +104,10 @@ class TestMemberSignIn(TestCase):
self.assertEqual(response.status_code, 201)
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)
def test_post_no_member(self):
@@ -137,3 +141,5 @@ class TestMemberSignIn(TestCase):
data = json.loads(data_string)
self.assertTrue(len(data), 3)
+ self.assertTrue('banned' in data[0]['member'])
+ self.assertTrue('suspended' in data[0]['member'])
diff --git a/bikeshop_project/registration/views.py b/bikeshop_project/registration/views.py
index cee46dc..0ea34a3 100644
--- a/bikeshop_project/registration/views.py
+++ b/bikeshop_project/registration/views.py
@@ -83,16 +83,17 @@ class MemberSignIn(View):
def post(self, request):
member = get_object_or_404(Member, id=request.POST.get('id'))
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)
def get(self, request):
visits = get_signed_in_members().prefetch_related()
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')
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 5222490..aa5c443 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -10,6 +10,8 @@ services:
- "62260:62260"
volumes:
- ./bikeshop_project:/code:rw
+ volumes_from:
+ - webpack
redis:
restart: always
db:
@@ -22,5 +24,7 @@ services:
ports:
- "3000:3000"
restart: always
- volumes_from:
- - workstand
+ volumes:
+ - /code/node_modules
+ - /code/vendor
+ - ./bikeshop_project:/code:rw
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index e2009ea..da5fe66 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -23,7 +23,7 @@ services:
image: bcbc/workstand:production
env_file:
- 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:
- DJANGO_SETTINGS_MODULE=bikeshop.settings.production
volumes:
diff --git a/requirements/production.txt b/requirements/production.txt
index a945c48..e658cfd 100644
--- a/requirements/production.txt
+++ b/requirements/production.txt
@@ -1,2 +1,3 @@
-r base.txt
+-r testing.txt
gunicorn==19.4.5