S3 upload working

This commit is contained in:
Tris Forster 2020-09-05 19:47:32 +10:00
parent 9ace9c3146
commit 83c011e32f
19 changed files with 386 additions and 43 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
__pycache__
*.pyc
db.sqlite3
db.sqlite3
credentials

View File

@ -4,6 +4,22 @@ from django.contrib import admin
from . import models
admin.site.register(models.Project)
admin.site.register(models.Submission)
admin.site.register(models.WikiPage)
class ProjectAdmin(admin.ModelAdmin):
list_display = ['name', 'ensemble', 'deadline', 'active']
list_filter = ['ensemble', 'active']
class SubmissionAdmin(admin.ModelAdmin):
list_display = ['name', 'instrument', 'date', 'complete']
list_filter = ['project', 'complete']
class WikiPageAdmin(admin.ModelAdmin):
list_display = ['title', 'project']
list_filter = ['project']
admin.site.register(models.Ensemble)
admin.site.register(models.Project, ProjectAdmin)
admin.site.register(models.Submission, SubmissionAdmin)
admin.site.register(models.Resource)
admin.site.register(models.WikiPage, WikiPageAdmin)

19
interface/decorators.py Normal file
View File

@ -0,0 +1,19 @@
from django.http import HttpResponseRedirect
def check_allowed(view_func):
def _view(request, *args, **kwargs):
request.ensemble_id = request.session.get('ensemble')
if request.ensemble_id is None:
return HttpResponseRedirect('/register')
return view_func(request, *args, **kwargs)
_view.__name__ = view_func.__name__
_view.__dict__ = view_func.__dict__
_view.__doc__ = view_func.__doc__
return _view

8
interface/forms.py Normal file
View File

@ -0,0 +1,8 @@
from django.forms import ModelForm
from .models import Submission
class SubmissionForm(ModelForm):
class Meta:
model = Submission
fields = ['name', 'instrument', 'notes']

View File

@ -0,0 +1,33 @@
# Generated by Django 3.1.1 on 2020-09-05 01:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('interface', '0002_auto_20200904_1004'),
]
operations = [
migrations.CreateModel(
name='Ensemble',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('code', models.CharField(max_length=12)),
('password', models.CharField(max_length=100)),
],
),
migrations.AddField(
model_name='project',
name='active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='project',
name='ensemble',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='interface.ensemble'),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 3.1.1 on 2020-09-05 01:27
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('interface', '0003_auto_20200905_0118'),
]
operations = [
migrations.AddField(
model_name='project',
name='bucket',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='project',
name='deadline',
field=models.DateField(blank=True, null=True),
),
migrations.CreateModel(
name='Resource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('uri', models.CharField(max_length=255)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
],
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.1 on 2020-09-05 06:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0004_auto_20200905_0127'),
]
operations = [
migrations.AddField(
model_name='submission',
name='complete',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='submission',
name='date',
field=models.DateField(auto_now_add=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.1 on 2020-09-05 09:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0005_auto_20200905_0638'),
]
operations = [
migrations.AddField(
model_name='submission',
name='key',
field=models.CharField(blank=True, max_length=255),
),
]

View File

@ -1,12 +1,44 @@
from django.db import models
from django.utils.text import slugify
# Create your models here.
class Project(models.Model):
import boto3
from datetime import datetime
import os.path
s3client = boto3.client('s3')
class Ensemble(models.Model):
name = models.CharField(max_length=100)
code = models.CharField(max_length=12)
password = models.CharField(max_length=100)
def active_projects(self):
return self.projects.filter(active=True)
def __str__(self):
return self.name
class Project(models.Model):
name = models.CharField(max_length=100)
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 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)
def __str__(self):
return self.name
class Resource(models.Model):
project = models.ForeignKey(Project, related_name='resources', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
uri = models.CharField(max_length=255)
class WikiPage(models.Model):
project = models.ForeignKey(Project, related_name='wiki_pages', on_delete=models.CASCADE)
title = models.CharField(max_length=255)
@ -17,10 +49,19 @@ class WikiPage(models.Model):
class Submission(models.Model):
project = models.ForeignKey(Project, related_name='submissions', on_delete=models.CASCADE)
date = models.DateField(auto_created=True)
date = models.DateField(auto_now_add=True)
name = models.CharField(max_length=255)
instrument = models.CharField(max_length=100)
notes = models.TextField(blank=True)
complete = models.BooleanField(default=False)
key = models.CharField(max_length=255, blank=True)
def generate_key(self):
return "{}_{}_{}_${{filename}}".format(
datetime.now().isoformat(timespec='seconds').replace(':', ''),
slugify(self.name),
slugify(self.instrument)
)
def __str__(self):
return f"{self.name}: {self.date}"

View File

@ -17,16 +17,21 @@
{% block header %}
<nav class="navbar navbar-expand-lg navbar-dark">
<a class="navbar-brand" href="#"><i class="fas fa-random"></i> Polyphonic</a>
<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">
{% if request.ensemble_id %}
<li class="nav-item">
<a class="nav-link" href="{% url 'myprojects' %}""><i class="fas fa-music"></i> My Projects</a>
<a class="nav-link" href="{% url 'my_projects' %}"><i class="fas fa-music"></i> My Projects</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'register' %}"><i class="fas fa-users"></i> Change Ensemble</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href=""><i class="fas fa-question"></i> About</a>
</li>

View File

@ -0,0 +1,29 @@
<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

@ -7,6 +7,7 @@
<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>

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<h1>Projects for {{ ensemble.name }}</h1>
<div class="col-md-8 offset-md-2">
<div class="list-group">
{% 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>
<p><small>Due in {{ project.deadline|timeuntil }}</small></p>
</a>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% 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">
<form action="" method="POST">
{% 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">
<button class="btn btn-primary">Enter</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,29 +3,11 @@
{% block page %}
<p>
Some instructions about how to submit the file
{{ url }}
</p>
<div class="col-md-6 offset-md-3">
<form>
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" class="form-control"/>
</div>
<div class="form-group">
<label for="instrument">Instrument</label>
<input type="text" id="instrument" class="form-control"/>
</div>
<div class="form-group">
<label for="video-file">Video File</label>
<input type="file" accept="video/*" class="" id="video-file" />
</div>
<div class="form-group">
<label for="notes">Notes</label>
<textarea class="form-control" id="notes"></textarea>
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
{% include "interface/bootstrap-form.html" %}
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="col-md-4 offset-md-4">
<form method="POST" action="{{ upload.url }}" enctype="multipart/form-data">
{% for field, value in upload.fields.items %}
<input type="hidden" name="{{ field }}" value="{{ value }}" />
{% endfor %}
File:
<input name="file" type="file" accept="video/*"/>
<div class="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>
</div>
{% endblock %}

View File

@ -3,8 +3,11 @@ from django.urls import path
from . import views
urlpatterns = [
path('my_projects', views.myprojects, name='myprojects'),
path('<int:project_id>', views.project_page, name="project"),
path('<int:project_id>/page/<int:wiki_id>', views.wiki_page, name="wiki"),
path('<int:project_id>/submission', views.submission, name="submission"),
path('', views.my_projects, name='my_projects'),
path('register', views.register, name="register"),
path('projects/<int:project_id>', views.project_page, name="project"),
path('projects/<int:project_id>/page/<int:wiki_id>', views.wiki_page, name="wiki"),
path('projects/<int:project_id>/submission', views.submission, name="submission"),
path('projects/<int:project_id>/submission/<int:submission_id>/complete', views.complete_submission, name="complete_submission"),
path('projects/<int:project_id>/submission/<int:submission_id>/cancel', views.cancel_submission, name="cancel_submission"),
]

View File

@ -1,23 +1,91 @@
from django.shortcuts import render, get_object_or_404
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
from markdown2 import markdown
from datetime import datetime
from . import models
from . import models, forms
from .decorators import check_allowed
import logging
logger = logging.getLogger(__name__)
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')
ensemble = models.Ensemble.objects.get(code=code)
if ensemble.password != passphrase:
raise ValueError("Incorrect passphase")
request.session['ensemble'] = ensemble.pk
return redirect('my_projects')
except:
logger.exception("Registration failed")
return render(request, 'interface/register.html', {'code': code})
@check_allowed
def my_projects(request):
ensemble = get_object_or_404(models.Ensemble, pk=request.ensemble_id)
context = {'ensemble': ensemble}
return render(request, 'interface/project_list.html', context)
@check_allowed
def project_page(request, project_id):
project = get_object_or_404(models.Project, pk=project_id)
project = get_object_or_404(models.Project, pk=project_id, ensemble_id=request.ensemble_id)
context = {'project': project}
return render(request, 'interface/project.html', context)
@check_allowed
def wiki_page(request, project_id, wiki_id):
wiki = get_object_or_404(models.WikiPage, pk=wiki_id, project=project_id)
wiki = get_object_or_404(models.WikiPage, pk=wiki_id, project=project_id, project__ensemble=request.ensemble_id)
context = {'project': wiki.project, 'wiki': wiki, 'wiki_html': markdown(wiki.markdown)}
return render(request, 'interface/wiki.html', context)
@check_allowed
def submission(request, project_id):
project = get_object_or_404(models.Project, pk=project_id)
context = {'project': project}
project = get_object_or_404(models.Project, pk=project_id, ensemble=request.ensemble_id)
if request.method == 'POST':
form = forms.SubmissionForm(request.POST)
if form.is_valid():
s = form.save(commit=False)
s.project_id = project_id
s.save()
redirect = request.build_absolute_uri(resolve_url('complete_submission', project_id=project.pk, submission_id=s.pk))
upload = project.presigned_post(s.generate_key(),
fields={'success_action_redirect': redirect},
conditions=[["starts-with", "$success_action_redirect", ""]])
context = {'upload': upload, 'project': project, 'submission': s}
return render(request, 'interface/upload.html', context)
else:
form = forms.SubmissionForm()
context = {'project': project, 'form': form}
return render(request, 'interface/submission.html', context)
def myprojects(request):
return render(request, 'interface/my_projects.html', {})
@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.delete()
return redirect('project', project_id=project_id)
@check_allowed
def complete_submission(request, project_id, submission_id):
project = get_object_or_404(models.Project, pk=project_id, ensemble=request.ensemble_id)
s = project.submissions.get(pk=submission_id)
s.complete = True
s.key = request.GET['key']
s.save()
return redirect('project', project_id=project_id)

View File

@ -18,5 +18,5 @@ from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('project/', include('interface.urls')),
path('', include('interface.urls')),
]