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

+
{% csrf_token %} @@ -66,118 +39,197 @@ {{ form.errors }}
{% endif %} -
- {{ form.email }} - - {% if form.email.errors %} - {{ form.email.errors }} - {% else %} - Invalid email. - {% endif %} -
-
- -
-
- {{ form.first_name }} - - {% if form.first_name.errors %} - {{ form.first_name.errors }} - {% else %} - Name too long. - {% endif %} -
-
- {{ form.last_name }} - - {% if form.last_name.errors %} - {{ form.last_name.errors }} - {% else %} - Name too long. - {% endif %} -
-
- {{ form.preferred_name }} - - {% if form.preferred_name.errors %} - {{ form.preferred_name.errors }} - {% else %} - Name too long. - {% endif %} +
+
+
+ {{ form.email }} + + {% if form.email.errors %} + {{ form.email.errors }} + {% else %} + Invalid email. + {% endif %} +
+
+ +
+
-
- {{ form.date_of_birth }} - - {% if form.date_of_birth.errors %} - {{ form.date_of_birth.errors }} - {% else %} - Incorrect date. - {% endif %} + +
+
+
+ {{ form.first_name }} + + {% if form.first_name.errors %} + {{ form.first_name.errors }} + {% else %} + Name too long. + {% endif %} +
+
+
+
+ {{ form.last_name }} + + {% if form.last_name.errors %} + {{ form.last_name.errors }} + {% else %} + Name too long. + {% endif %} +
+
-
- {{ form.guardian_name }} - - {% if form.guardian_name.errors %} - {{ form.guardian_name.errors }} - {% else %} - Name too long. - {% endif %} + +
+
+
+ {{ form.preferred_name }} + + {% if form.preferred_name.errors %} + {{ form.preferred_name.errors }} + {% else %} + Name too long. + {% endif %} +
+
+
+
+ {{ form.phone }} + + {% if form.phone.errors %} + {{ form.phone.errors }} + {% else %} + Digits only. + {% endif %} +
+
-
- {{ form.phone }} - - {% if form.phone.errors %} - {{ form.phone.errors }} - {% else %} - Digits only. - {% endif %} + +
+
+
+ {{ form.date_of_birth }} + + {% if form.date_of_birth.errors %} + {{ form.date_of_birth.errors }} + {% else %} + Incorrect date. + {% endif %} +
+
+
+
+ {{ form.guardian_name }} + + {% if form.guardian_name.errors %} + {{ form.guardian_name.errors }} + {% else %} + Name too long. + {% endif %} +
+
-
- {{ form.post_code }} - - {% if form.post_code.errors %} - {{ form.post_code.errors }} - {% else %} - Format: A0A 0A0 - {% endif %} + +
+
+
+ {{ form.post_code }} + + {% if form.post_code.errors %} + {{ form.post_code.errors }} + {% else %} + Format: A0A 0A0 + {% endif %} +
+
+
+
+ {{ form.street }} + + {% if form.street.errors %} + {{ form.street.errors }} + {% endif %} +
+
-
- {{ form.street }} - - {% if form.street.errors %} - {{ form.street.errors }} - {% endif %} +
+
+
+ {{ form.city }} + + {% if form.city.errors %} + {{ form.city.errors }} + {% endif %} +
+
+
+
+ {{ form.province }} + + {% if form.province.errors %} + {{ form.province.errors }} + {% endif %} +
+
-
- {{ form.city }} - - {% if form.city.errors %} - {{ form.city.errors }} - {% endif %} +
+
+
+ {{ form.country }} + + {% if form.country.errors %} + {{ form.country.errors }} + {% endif %} +
+
-
- {{ form.province }} - - {% if form.province.errors %} - {{ form.province.errors }} - {% endif %} + +
+
+

Member Info

+

Notes

+
+
+
+ {{ form.notes }} + + {% if form.notes.errors %} + {{ form.notes.errors }} + {% endif %} +
+
-
- {{ form.country }} - - {% if form.country.errors %} - {{ form.country.errors }} - {% endif %} + +
+
+ +
+
+ +
-
- +
+
+ +
-
+
+
+
{% 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