From 8a8bccd850f52de64da60552debd4b15ab98e833 Mon Sep 17 00:00:00 2001 From: Tris Forster Date: Sun, 6 Sep 2020 22:22:28 +1000 Subject: [PATCH] Ditched bootstrap --- interface/admin.py | 4 +- interface/decorators.py | 10 + interface/forms.py | 10 +- .../migrations/0007_auto_20200906_1009.py | 35 ++++ .../migrations/0008_auto_20200906_1122.py | 24 +++ interface/models.py | 17 +- interface/static/interface/css/polyphonic.css | 181 +++++++++++++++++- interface/templates/base.html | 52 +++-- .../templates/interface/bootstrap-form.html | 29 --- interface/templates/interface/project.html | 43 ++--- .../templates/interface/project_list.html | 10 +- interface/templates/interface/register.html | 27 ++- interface/templates/interface/submission.html | 8 +- interface/templates/interface/upload.html | 48 ++--- interface/tests.py | 3 - interface/tests/__init__.py | 0 interface/tests/test_submission.py | 26 +++ interface/views.py | 43 +++-- polyphonic/settings.py | 2 +- 19 files changed, 403 insertions(+), 169 deletions(-) create mode 100644 interface/migrations/0007_auto_20200906_1009.py create mode 100644 interface/migrations/0008_auto_20200906_1122.py delete mode 100644 interface/templates/interface/bootstrap-form.html delete mode 100644 interface/tests.py create mode 100644 interface/tests/__init__.py create mode 100644 interface/tests/test_submission.py diff --git a/interface/admin.py b/interface/admin.py index 4f28ede..c65d0a9 100644 --- a/interface/admin.py +++ b/interface/admin.py @@ -4,6 +4,8 @@ from django.contrib import admin from . import models +class EnsembleAdmin(admin.ModelAdmin): + list_display = ['name', 'ensemble_code'] class ProjectAdmin(admin.ModelAdmin): @@ -18,7 +20,7 @@ class WikiPageAdmin(admin.ModelAdmin): list_display = ['title', 'project'] list_filter = ['project'] -admin.site.register(models.Ensemble) +admin.site.register(models.Ensemble, EnsembleAdmin) admin.site.register(models.Project, ProjectAdmin) admin.site.register(models.Submission, SubmissionAdmin) admin.site.register(models.Resource) diff --git a/interface/decorators.py b/interface/decorators.py index 32f7374..764f608 100644 --- a/interface/decorators.py +++ b/interface/decorators.py @@ -4,6 +4,16 @@ def check_allowed(view_func): def _view(request, *args, **kwargs): + code = request.GET.get('code') + if code: + # just change if we can + try: + ensemble = request.session.get('registered', {})[code.replace('-', '')] + request.session['ensemble'] = ensemble + except KeyError: + # need to register this code + return HttpResponseRedirect('/register?code=' + code) + request.ensemble_id = request.session.get('ensemble') if request.ensemble_id is None: diff --git a/interface/forms.py b/interface/forms.py index a403abc..4f6b578 100644 --- a/interface/forms.py +++ b/interface/forms.py @@ -1,8 +1,12 @@ -from django.forms import ModelForm - +from django import forms from .models import Submission -class SubmissionForm(ModelForm): +class CodeForm(forms.Form): + code = forms.CharField(max_length=14, + widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx'})) + passphrase = forms.CharField(max_length=32) + +class SubmissionForm(forms.ModelForm): class Meta: model = Submission fields = ['name', 'instrument', 'notes'] \ No newline at end of file diff --git a/interface/migrations/0007_auto_20200906_1009.py b/interface/migrations/0007_auto_20200906_1009.py new file mode 100644 index 0000000..87f9acd --- /dev/null +++ b/interface/migrations/0007_auto_20200906_1009.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.1 on 2020-09-06 10:09 + +from django.db import migrations, models +import django.db.models.deletion +import interface.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('interface', '0006_submission_key'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='bucket', + ), + migrations.AddField( + model_name='ensemble', + name='bucket', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AlterField( + model_name='ensemble', + name='code', + field=models.CharField(default=interface.models.generate_code, max_length=12), + ), + migrations.AlterField( + model_name='submission', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_submissions', to='interface.project'), + ), + ] diff --git a/interface/migrations/0008_auto_20200906_1122.py b/interface/migrations/0008_auto_20200906_1122.py new file mode 100644 index 0000000..e9f6a97 --- /dev/null +++ b/interface/migrations/0008_auto_20200906_1122.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.1 on 2020-09-06 11:22 + +from django.db import migrations, models +import interface.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('interface', '0007_auto_20200906_1009'), + ] + + operations = [ + migrations.RenameField( + model_name='ensemble', + old_name='password', + new_name='passphrase', + ), + migrations.AlterField( + model_name='ensemble', + name='code', + field=models.CharField(default=interface.models.generate_code, max_length=9), + ), + ] diff --git a/interface/models.py b/interface/models.py index 2ca8b33..d948497 100644 --- a/interface/models.py +++ b/interface/models.py @@ -1,6 +1,8 @@ from django.db import models from django.utils.text import slugify +import random + import boto3 from datetime import datetime @@ -9,14 +11,22 @@ import os.path s3client = boto3.client('s3') +def generate_code(length=9): + return "".join([ random.choice('0123456789') for _ in range(length) ]) + class Ensemble(models.Model): name = models.CharField(max_length=100) - code = models.CharField(max_length=12) - password = models.CharField(max_length=100) + code = models.CharField(max_length=9, default=generate_code) + passphrase = models.CharField(max_length=100) + bucket = models.CharField(max_length=100) def active_projects(self): return self.projects.filter(active=True) + def ensemble_code(self): + code = str(self.code) + return "{}-{}-{}".format(code[:3], code[3:6], code[6:]) + def __str__(self): return self.name @@ -25,14 +35,13 @@ class Project(models.Model): ensemble = models.ForeignKey(Ensemble, related_name='projects', on_delete=models.CASCADE, null=True) active = models.BooleanField(default=True) deadline =models.DateField(null=True, blank=True) - bucket = models.CharField(max_length=100) def submissions(self): return self.all_submissions.filter(complete=True) def presigned_post(self, object_name, fields={}, conditions=[], expires=3600): key = os.path.join(slugify(self.name), object_name) - return s3client.generate_presigned_post(self.bucket, key, Fields=fields, Conditions=conditions, ExpiresIn=expires) + return s3client.generate_presigned_post(self.ensemble.bucket, key, Fields=fields, Conditions=conditions, ExpiresIn=expires) def __str__(self): return self.name diff --git a/interface/static/interface/css/polyphonic.css b/interface/static/interface/css/polyphonic.css index 2d6910b..38e6c5c 100644 --- a/interface/static/interface/css/polyphonic.css +++ b/interface/static/interface/css/polyphonic.css @@ -1,12 +1,183 @@ -.navbar { - margin-bottom: 50px; - background-color: #69C; + +:root { + --border-color: #292929; + --gray-blue: #667788; + --light-blue: #c5eff7; } -.form-actions { +@font-face { + font-family: 'DreamOrphans'; + src: url('../../fonts/dream orphans.ttf') format('truetype'); +} + +@font-face { + font-family: 'Quicksand'; + src: url('../../fonts/Quicksand_Book.otf'); +} + +@font-face { + font-family: 'QuicksandBold'; + src: url('../../fonts/Quicksand_Bold_Oblique.otf'); +} + +.debug DIV { + border: 1px dashed #DDD; +} + +BODY { +} + +.main { + max-width: 1000px; + margin: 10px auto; + border: 1px solid var(--border-color); + border-radius: 5px; + font-family: 'Quicksand', Arial, Helvetica, sans-serif; +} + +.content { + margin: 20px; +} + +.narrow { + max-width: 500px; + margin: 0px auto; +} + +@media all and (max-width: 900px) { + .mdhide { + display: none; + } +} + +@media all and (max-width: 700px) { + .smhide { + display: none; + } + UL.nav-buttons { + flex-direction: column; + } +} + +/* HEADER BAR */ + +.navigation { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 50px; + background-color: var(--gray-blue); + color: var(--light-blue) !important; +} + +.navigation > * { + white-space: nowrap; +} + +.navigation A, +.navigation A:visited { + color: var(--light-blue); + text-decoration: none; +} + +.navigation .brand { + font-family: 'QuicksandBold', 'Quicksand', Arial, Helvetica, sans-serif; + font-size: 1.5rem; + margin: auto 20px; +} + +UL.nav-buttons { + display: flex; + list-style: none; +} +UL.nav-buttons > LI { + margin: 2px 10px; +} + +/* FORMS */ + +FORM { + display: flex; + flex-direction: column; + max-width: 400px; + margin: 20px auto; +} + +LABEL { margin-top: 20px; } -#project H1 { +TEXTAREA { + height: 50px; +} + +.form-actions { + text-align: right; + margin-top: 20px; +} + +.btn { + background-color: var(--gray-blue); + display: inline-block; + border: none; + color: var(--light-blue); + text-decoration: none; + border-radius: 1em; + font-size: 1em; +} + +.btn:hover { + cursor: pointer; + color: white; +} + +.pills { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 40px; +} + +.pills A { + border: 1px solid var(--gray-blue); + padding: 4px 10px 2px 10px; + margin: 10px 5px; + border-radius: 10px; + white-space: nowrap; +} + +.pills A:hover { + background-color: var(--light-blue); + text-decoration: none +} + +.list-group { + display: flex; + flex-direction: column; +} + +.list-group A { + border: 1px solid var(--gray-blue); + border-radius: 10px; + padding: 2px 20px; + margin-top: 20px; +} + +.list-group A:hover { + background-color: var(--light-blue); + text-decoration: none; +} + +A, A:visited { + text-decoration: none; + color: var(--gray-blue); + font-weight: bold; +} + +A:hover { + text-decoration: underline; +} + +H1 { text-align: center; } \ No newline at end of file diff --git a/interface/templates/base.html b/interface/templates/base.html index 32530c2..9f9bc0a 100644 --- a/interface/templates/base.html +++ b/interface/templates/base.html @@ -6,49 +6,45 @@ - - - - {% block title %}Polyphonic{% endblock %} - {% block header %} - {% endblock %} - - - - {% block content %} -

No content!

- {% endblock %} + +
+ {% block content %} +

No content!

+ {% endblock %} +
+ - - - - + + + \ No newline at end of file diff --git a/interface/templates/interface/bootstrap-form.html b/interface/templates/interface/bootstrap-form.html deleted file mode 100644 index 15aa5ad..0000000 --- a/interface/templates/interface/bootstrap-form.html +++ /dev/null @@ -1,29 +0,0 @@ -
{% csrf_token %} -
- {{ title }} - {% for field in form %} - {% if field.errors %} -
- -
{{ field }} - - {% for error in field.errors %}{{ error }}{% endfor %} - -
-
- {% else %} -
- -
{{ field }} - {% if field.help_text %} -

{{ field.help_text }}

- {% endif %} -
-
- {% endif %} - {% endfor %} -
-
- -
-
\ No newline at end of file diff --git a/interface/templates/interface/project.html b/interface/templates/interface/project.html index 5c305af..ce2ef80 100644 --- a/interface/templates/interface/project.html +++ b/interface/templates/interface/project.html @@ -1,31 +1,24 @@ {% extends "base.html" %} {% block content %} -
- -

{{ project.name }}

-
-
- {% block page %} -

Due in {{ project.deadline|timeuntil }}

-

There have been {{ project.submissions.count }} submissions so far...

- {% endblock %} -
-
- -
+

{{ project.name }}

+
+ {% block page %} +
+

Due in {{ project.deadline|timeuntil }}!

+

There have been {{ project.submissions.count }} submissions so far...

+
+ {% endblock %} +
+ {% endblock %} \ No newline at end of file diff --git a/interface/templates/interface/project_list.html b/interface/templates/interface/project_list.html index 5474b91..7df96f0 100644 --- a/interface/templates/interface/project_list.html +++ b/interface/templates/interface/project_list.html @@ -2,17 +2,13 @@ {% block content %} -

Projects for {{ ensemble.name }}

-
-
+
{% for project in ensemble.active_projects %} - -

{{ project.name }}

+
+

{{ project.name }}

Due in {{ project.deadline|timeuntil }}

{% endfor %}
-
-
{% endblock %} \ No newline at end of file diff --git a/interface/templates/interface/register.html b/interface/templates/interface/register.html index 5e8c0ed..c05a00a 100644 --- a/interface/templates/interface/register.html +++ b/interface/templates/interface/register.html @@ -1,25 +1,24 @@ {% extends "base.html" %} {% block content %} -
-
-
Enter the ensemble details given to you
-
+
+
+

My Ensembles

+ +
+
+

Add a new ensemble

{% csrf_token %} -
- - -
-
- - -
-
+ {{ form }} +
-
{% endblock %} \ No newline at end of file diff --git a/interface/templates/interface/submission.html b/interface/templates/interface/submission.html index d9e1c8e..db7de8d 100644 --- a/interface/templates/interface/submission.html +++ b/interface/templates/interface/submission.html @@ -10,7 +10,13 @@

- {% include "interface/bootstrap-form.html" %} +
+ {% csrf_token %} + {{ form }} +
+ +
+
diff --git a/interface/templates/interface/upload.html b/interface/templates/interface/upload.html index a560fcb..b485256 100644 --- a/interface/templates/interface/upload.html +++ b/interface/templates/interface/upload.html @@ -4,44 +4,24 @@ {% block content %} -
-
-
-
Ready to upload file
-
-
- {% for field, value in upload.fields.items %} - - {% endfor %} - - +
+

Ready to upload file

+ + {% for field, value in upload.fields.items %} + + {% endfor %} -
- Cancel - -
- -
+ +
+ Cancel +   +
-
+
- - {% endblock %} \ No newline at end of file diff --git a/interface/tests.py b/interface/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/interface/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/interface/tests/__init__.py b/interface/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/interface/tests/test_submission.py b/interface/tests/test_submission.py new file mode 100644 index 0000000..1d3880f --- /dev/null +++ b/interface/tests/test_submission.py @@ -0,0 +1,26 @@ +from django.test import TestCase, Client + +from interface import models + +class SubmissionTestCase(TestCase): + + def setUp(self): + self.client = Client() + + def test_submission(self): + ensemble = models.Ensemble.objects.create(name="The Be Sharps", passphrase="Homer", bucket="virtual-orchestra") + project = ensemble.projects.create(name='Baby on Board') + + response = self.client.post('/register', {'code': ensemble.code, 'passphrase': ensemble.passphrase}) + self.assertRedirects(response, '/') + + response = self.client.post(f"/projects/{project.pk}/submission", {'name': 'Ned', 'instrument': 'God'}) + + upload = response.context['upload'] + self.assertEqual(upload['url'], f"https://{ensemble.bucket}.s3.amazonaws.com/") + self.assertRegex(upload['fields']['key'], r'^baby-on-board\/[0-9T\-]+_ned_god_\$\{filename\}$') + self.assertEqual(upload['fields']['success_action_redirect'], 'http://testserver/projects/1/submission/1/complete') + + self.assertEqual(models.Submission.objects.count(), 1) + self.assertRedirects(self.client.get(f"/projects/{project.pk}/submission/1/cancel"), '/projects/1') + self.assertEqual(models.Submission.objects.count(), 0) \ No newline at end of file diff --git a/interface/views.py b/interface/views.py index 1f43184..ade8e97 100644 --- a/interface/views.py +++ b/interface/views.py @@ -14,20 +14,30 @@ def forbidden(request): return render(request, 'interface/forbidden.html', {}) def register(request): - code = '' - try: - code = request.POST.get('code', request.GET['code']) - passphrase = request.POST.get('passphrase') + + request.ensemble_id = request.session.get('ensemble') + registered = request.session.setdefault('registered', {}) - ensemble = models.Ensemble.objects.get(code=code) - if ensemble.password != passphrase: - raise ValueError("Incorrect passphase") + if request.method == "POST": + form = forms.CodeForm(request.POST) - request.session['ensemble'] = ensemble.pk - return redirect('my_projects') - except: - logger.exception("Registration failed") - return render(request, 'interface/register.html', {'code': code}) + if form.is_valid(): + + data = form.cleaned_data; + ensemble = models.Ensemble.objects.get(code=data['code'].replace('-', '')) + + + if ensemble.passphrase == data['passphrase']: + request.session['ensemble'] = ensemble.pk + registered[ensemble.code] = ensemble.pk + return redirect('my_projects') + + else: + form = forms.CodeForm(initial=request.GET) + + current = models.Ensemble.objects.filter(pk__in=registered.values()) + + return render(request, 'interface/register.html', {'form': form, 'current': current}) @check_allowed @@ -60,6 +70,10 @@ def submission(request, project_id): s.project_id = project_id s.save() + data = form.cleaned_data + request.session['name'] = data['name'] + request.session['instrument'] = data['instrument'] + redirect = request.build_absolute_uri(resolve_url('complete_submission', project_id=project.pk, submission_id=s.pk)) upload = project.presigned_post(s.generate_key(), @@ -69,7 +83,8 @@ def submission(request, project_id): return render(request, 'interface/upload.html', context) else: - form = forms.SubmissionForm() + initial = { k: request.session.get(k) for k in ('name', 'instrument') } + form = forms.SubmissionForm(initial=initial) context = {'project': project, 'form': form} return render(request, 'interface/submission.html', context) @@ -77,7 +92,7 @@ def submission(request, project_id): @check_allowed def cancel_submission(request, project_id, submission_id): project = get_object_or_404(models.Project, pk=project_id, ensemble=request.ensemble_id) - submission = project.submissions.get(pk=submission_id) + submission = project.all_submissions.get(pk=submission_id) submission.delete() return redirect('project', project_id=project_id) diff --git a/polyphonic/settings.py b/polyphonic/settings.py index 145a7ed..c262604 100644 --- a/polyphonic/settings.py +++ b/polyphonic/settings.py @@ -25,7 +25,7 @@ SECRET_KEY = '6y#33930^6@c762u(@6+&#_qx8eu^e8q+4t-(@m60vnjw37k26' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost', '192.168.100.123'] # Application definition