Ditched bootstrap

This commit is contained in:
Tris Forster 2020-09-06 22:22:28 +10:00
parent a1d10ea30a
commit 8a8bccd850
19 changed files with 403 additions and 169 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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']

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

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

View File

@ -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;
}

View File

@ -6,49 +6,45 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
<script src="https://kit.fontawesome.com/c837098e5b.js" crossorigin="anonymous"></script>
<title>{% block title %}Polyphonic{% endblock %}</title>
</head>
<body>
{% block header %}
<nav class="navbar navbar-expand-lg navbar-dark">
<a class="navbar-brand" href="/"><i class="fas fa-random"></i> Polyphonic</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<span class="navbar-text">Virtual Ensemble Manager</span>
<div class="collapse navbar-collapse" id="navbarSupportedContent"></div>
<ul class="navbar-nav ml-auto">
<div class="main">
{% block navigation %}
<nav class="navigation">
<div>
<a class="brand" href="/"><i class="fas fa-random smhide"></i> Polyphonic</a>
<span class="mdhide">Virtual Ensemble Manager</span>
</div>
<ul class="nav-buttons">
{% if request.ensemble_id %}
<li class="nav-item">
<a class="nav-link" href="{% url 'my_projects' %}"><i class="fas fa-music"></i> My Projects</a>
<a class="nav-link" href="{% url 'my_projects' %}"><i class="fas fa-music"></i> <span class="">My
Projects</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'register' %}"><i class="fas fa-users"></i> Change Ensemble</a>
<a class="nav-link" href="{% url 'register' %}"><i class="fas fa-users"></i> <span class="">Change
Ensemble</span></a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href=""><i class="fas fa-question"></i> About</a>
<a class="nav-link" href=""><i class="fas fa-question"></i> <span class="">About</span></a>
</li>
</ul>
</div>
</ul>
</nav>
{% endblock %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
{% block content %}
<h1>No content!</h1>
{% endblock %}
<div class="content">
{% block content %}
<h1>No content!</h1>
{% endblock %}
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
<!-- late load scripts -->
<script src="https://kit.fontawesome.com/c837098e5b.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -1,29 +0,0 @@
<form class="form-horizontal" method="post">{% csrf_token %}
<fieldset>
<legend>{{ title }}</legend>
{% for field in form %}
{% if field.errors %}
<div class="control-group error">
<label class="control-label">{{ field.label }}</label>
<div class="controls">{{ field }}
<span class="help-inline">
{% for error in field.errors %}{{ error }}{% endfor %}
</span>
</div>
</div>
{% else %}
<div class="control-group">
<label class="control-label">{{ field.label }}</label>
<div class="controls">{{ field }}
{% if field.help_text %}
<p class="help-inline"><small>{{ field.help_text }}</small></p>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</fieldset>
<div class="form-actions text-right">
<button type="submit" class="btn btn-primary" >Submit</button>
</div>
</form>

View File

@ -1,31 +1,24 @@
{% extends "base.html" %}
{% block content %}
<div class="container" id="project">
<h1>{{ project.name }}</h1>
<div class="row">
<div class="col-9">
{% block page %}
<h3 class="text-center">Due in {{ project.deadline|timeuntil }}</h3>
<p>There have been {{ project.submissions.count }} submissions so far...</p>
{% endblock %}
</div>
<div class="col-3">
<div class="nav flex-column nav-pills" role="tablist">
<a class="nav-link" role=tab"
href="{% url 'project' project_id=project.id %}">Project info</a>
{% for page in project.wiki_pages.all %}
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
href="{% url 'wiki' project_id=project.id wiki_id=page.id %}"
role="tab">{{ page.title }}</a>
{% endfor %}
<a class="nav-link" role="tab"
href="">Record a submission</a>
<a class="nav-link" role="tab"
href="{% url 'submission' project_id=project.id %}">Send a file</a>
</div>
</div>
<h1>{{ project.name }}</h1>
<div>
{% block page %}
<div class="narrow">
<h3 class="text-center">Due in {{ project.deadline|timeuntil }}!</h3>
<p>There have been {{ project.submissions.count }} submissions so far...</p>
</div>
{% endblock %}
</div>
<div class="project-links">
<div class="pills" role="tablist">
<a role=tab" href="{% url 'project' project_id=project.id %}">Project info</a>
{% for page in project.wiki_pages.all %}
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
href="{% url 'wiki' project_id=project.id wiki_id=page.id %}">{{ page.title }}</a>
{% endfor %}
<a role="tab" href="">Record a submission</a>
<a role="tab" href="{% url 'submission' project_id=project.id %}">Send a file</a>
</div>
</div>
{% endblock %}

View File

@ -2,17 +2,13 @@
{% block content %}
<div class="container">
<h1>Projects for {{ ensemble.name }}</h1>
<div class="col-md-8 offset-md-2">
<div class="list-group">
<div class="list-group narrow">
{% for project in ensemble.active_projects %}
<a class="list-group-item list-group-item-action" href="{% url 'project' project_id=project.id %}">
<h4>{{ project.name }}</h4>
<a class="" href="{% url 'project' project_id=project.id %}">
<h3>{{ project.name }}</h3>
<p><small>Due in {{ project.deadline|timeuntil }}</small></p>
</a>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,25 +1,24 @@
{% extends "base.html" %}
{% block content %}
<div class="col-md-4 offset-md-4">
<div class="card">
<div class="card-header">Enter the ensemble details given to you</div>
<div class="card-body">
<div class="narrow">
<div>
<h3>My Ensembles</h3>
<ul>
{% for ensemble in current %}
<li><a href="/?code={{ ensemble.ensemble_code}}">{{ ensemble.name }}</a></li>
{% endfor %}
</ul>
</div>
<div>
<form action="" method="POST">
<h3>Add a new ensemble</h3>
{% csrf_token %}
<div class="form-group">
<label for="code">Ensemble Code</label>
<input type="text" class="form-control" placeholder="xxxx-xxxx-xxxx" name="code" value="{{ code }}"/>
</div>
<div class="form-group">
<label for="passphrase">Passphrase</label>
<input type="password" class="form-control" name="passphrase"/>
</div>
<div class="text-right">
{{ form }}
<div class="form-actions">
<button class="btn btn-primary">Enter</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -10,7 +10,13 @@
</p>
<div class="">
{% include "interface/bootstrap-form.html" %}
<form action="" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button type="submit" class="btn-primary">Continue</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -4,44 +4,24 @@
{% block content %}
<div class="container">
<div class="col-md-4 offset-md-4">
<div class="card" id="legacy-upload">
<div class="card-header">Ready to upload file</div>
<div class="card-body">
<form method="POST" action="{{ upload.url }}" enctype="multipart/form-data" id="video-upload">
{% for field, value in upload.fields.items %}
<input type="hidden" name="{{ field }}" value="{{ value }}" />
{% endfor %}
<input name="file" type="file" accept="video/*"/>
<div class="narrow">
<h3>Ready to upload file</h3>
<form method="POST" action="{{ upload.url }}" enctype="multipart/form-data" id="video-upload">
{% for field, value in upload.fields.items %}
<input type="hidden" name="{{ field }}" value="{{ value }}" />
{% endfor %}
<div class="form-actions text-right">
<a class="btn btn-secondary" href="{% url 'cancel_submission' project_id=project.pk submission_id=submission.pk %}">Cancel</a>
<button class="btn btn-primary">Upload</button>
</div>
</form>
</div>
<input name="file" type="file" accept="video/*" />
<div class="form-actions text-right">
<a class=""
href="{% url 'cancel_submission' project_id=project.pk submission_id=submission.pk %}">Cancel</a>
&nbsp;
<button class="">Upload</button>
</div>
</div>
</form>
</div>
<script src="{% static 'dropzone/dropzone.js' %}"></script>
<script>
(function() {
</div>
//document.getElementById('fallback-file').remove();
Dropzone.options.videoUpload = {
paramName: "file", // The name that will be used to transfer the file
maxFilesize: 500, // MB
accept: function(file, done) {
if (file.name == "justinbieber.jpg") {
done("Naha, you don't.");
} else { done(); }
}
}
})();
</script>
{% endblock %}

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@ -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)

View File

@ -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)

View File

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