Switched to generic views

This commit is contained in:
Tris 2020-09-09 13:12:26 +10:00
parent 43700aa66b
commit 3e9c2c7ca0
20 changed files with 365 additions and 178 deletions

View File

@ -16,6 +16,10 @@ class SubmissionAdmin(admin.ModelAdmin):
list_display = ['name', 'instrument', 'date', 'complete']
list_filter = ['project', 'complete']
class ResourceAdmin(admin.ModelAdmin):
list_display = ['name', 'media_type', 'project']
list_filter = ['project']
class WikiPageAdmin(admin.ModelAdmin):
list_display = ['title', 'project']
list_filter = ['project']
@ -23,5 +27,5 @@ class WikiPageAdmin(admin.ModelAdmin):
admin.site.register(models.Ensemble, EnsembleAdmin)
admin.site.register(models.Project, ProjectAdmin)
admin.site.register(models.Submission, SubmissionAdmin)
admin.site.register(models.Resource)
admin.site.register(models.Resource, ResourceAdmin)
admin.site.register(models.WikiPage, WikiPageAdmin)

View File

@ -1,29 +0,0 @@
from django.http import HttpResponseRedirect
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:
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

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.1 on 2020-09-09 00:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0013_auto_20200907_1455'),
]
operations = [
migrations.AddField(
model_name='resource',
name='description',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='resource',
name='key',
field=models.CharField(blank=True, max_length=255),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.1 on 2020-09-09 01:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0014_auto_20200909_1016'),
]
operations = [
migrations.AddField(
model_name='resource',
name='media_type',
field=models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('*', 'General')], default='*', max_length=10),
),
]

View File

@ -15,6 +15,11 @@ s3client = boto3.client('s3')
BUCKET = settings.AWS_BUCKET
MEDIA_TYPES = [
('audio', "Audio"),
('video', "Video"),
('*', "General"),
]
def generate_code(length=9):
return "".join([ random.choice('0123456789') for _ in range(length) ])
@ -53,11 +58,21 @@ class Project(models.Model):
class Resource(models.Model):
project = models.ForeignKey(Project, related_name='resources', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
key = models.CharField(max_length=255)
description = models.TextField(blank=True)
key = models.CharField(max_length=255, blank=True)
media_type = models.CharField(max_length=10, choices=MEDIA_TYPES, default='*')
def key_template(self):
return "{}_${{filename}}".format(slugify(self.name))
def presigned_url(self):
if not self.key:
return ""
params = {'Bucket': BUCKET, 'Key': self.key}
return s3client.generate_presigned_url('get_object', Params=params, ExpiresIn=3600)
return s3client.generate_presigned_url('get_object', Params=params, ExpiresIn=3600*24)
def __str__(self):
return self.name
class WikiPage(models.Model):
project = models.ForeignKey(Project, related_name='wiki_pages', on_delete=models.CASCADE)

View File

@ -95,6 +95,11 @@ UL.nav-buttons > LI {
margin: 2px 10px;
}
.admin-actions {
text-align: right;
margin: 10px;
}
/* FORMS */
FORM {
@ -157,14 +162,14 @@ TEXTAREA {
flex-direction: column;
}
.list-group A {
.list-group > * {
border: 1px solid var(--gray-blue);
border-radius: 10px;
padding: 2px 20px;
margin-top: 20px;
}
.list-group A:hover {
.list-group > A:hover {
background-color: var(--light-blue);
text-decoration: none;
}
@ -211,4 +216,9 @@ TABLE.horizontal TH {
TABLE.horizontal TD,
TABLE.horizontal TH {
padding: 5px;
}
.resource-player {
width: 100%;
border-radius: 10px;
}

View File

@ -21,7 +21,7 @@
<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> <span class="">My
<a class="nav-link" href="{% url 'ensemble_detail' %}"><i class="fas fa-music"></i> <span class="">My
Projects</span></a>
</li>
<li class="nav-item">

View File

@ -5,7 +5,7 @@
<h1>Projects for {{ ensemble.name }}</h1>
<div class="list-group narrow">
{% for project in ensemble.active_projects %}
<a class="" href="{% url 'project' project_id=project.id %}">
<a class="" href="{% url 'project_detail' pk=project.id %}">
<h3>{{ project.name }}</h3>
<p><small>Due in {{ project.deadline|timeuntil }}</small></p>
</a>

View File

@ -1,41 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>{{ project.name }}</h1>
{% 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>
<table>
<tbody>
{% for submission in project.submissions %}
<tr>
<td>{{ submission.date|timesince }} ago</td>
<td>{{ submission.name }} ({{ submission.instrument }})</td>
{% if request.user.is_authenticated %}
<td>
<a href="{% url 'submission' project_id=project.pk submission_id=submission.pk %}"><i class="fas fa-info-circle"></i></a>
&nbsp;
<a href="{{ submission.presigned_url }}"><i class="fas fa-download"></i></a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
<div class="project-links">
<div class="pills" role="tablist">
<a role="tab" href="{% url 'project' project_id=project.id %}">Project info</a>
<a role="tab" href="{% url 'resources' project_id=project.pk %}">Resources</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 'create_submission' project_id=project.id %}">Send a file</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<h1>{{ project.name }}</h1>
{% block page %}
No content
{% endblock %}
<div class="project-links">
<div class="pills" role="tablist">
<a role="tab" href="{% url 'project_detail' pk=project.id %}">Project info</a>
<a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a>
{% for page in project.wiki_pages.all %}
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a>
{% endfor %}
<!--a role="tab" href="">Record a submission</a-->
<a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "interface/project_base.html" %}
{% 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>
<table>
<tbody>
{% for submission in project.submissions %}
<tr>
<td>{{ submission.date|timesince }} ago</td>
<td>{{ submission.name }} ({{ submission.instrument }})</td>
{% if request.user.is_authenticated %}
<td>
<a href="{% url 'submission_detail' project=project.pk pk=submission.pk %}"><i class="fas fa-info-circle"></i></a>
&nbsp;
<a href="{{ submission.presigned_url }}"><i class="fas fa-download"></i></a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>{{ title }}</h3>
<p>{{ instructions }}</p>
<form method="POST">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button>Create</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>Resources</h3>
<div class="list-group narrow">
{% for resource in object_list %}
{% with download=resource.presigned_url %}
<div>
<h3>
{{ resource.name }}
{% if download %}
<a href="{{ download }}">
<i class="fas fa-download"></i>
</a>
{% endif %}
{% if request.user.is_authenticated %}
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}">
<i class="fas fa-upload"></i>
</a>
{% endif %}
</h3>
<p><small>{{ resource.description }}</small></p>
{% if download and resource.media_type == 'audio' %}
<audio class="resource-player" controls src="{{ download }}"></audio>
{% endif %}
</div>
{% endwith %}
{% endfor %}
</div>
{% if request.user.is_authenticated %}
<div class="admin-actions">
<a href="{% url 'resource_create' project=project.pk %}"><i class="fas fa-plus-circle"></i> Add new</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "interface/project.html" %}
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">

View File

@ -1,4 +1,4 @@
{% extends "interface/project.html" %}
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">

View File

@ -19,7 +19,7 @@
<div class="form-actions text-right">
<a class=""
href="{% url 'cancel_submission' project_id=project.pk submission_id=submission.pk %}">Cancel</a>
href="{{ cancel_url }}">Cancel</a>
&nbsp;
<button type="submit" id="upload">Upload</button>
</div>
@ -53,8 +53,8 @@ function startUpload(e) {
//$('input[name="file"]').attr('disabled', true);
// patch form to use our ajax policy
$('input[name="policy"]').val("{{ ajax_post.fields.policy }}");
$('input[name="signature"]').val("{{ ajax_post.fields.signature }}");
$('input[name="policy"]').val("{{ ajax_upload.fields.policy }}");
$('input[name="signature"]').val("{{ ajax_upload.fields.signature }}");
$('input[name="success_action_redirect"]').remove();
@ -81,7 +81,7 @@ function startUpload(e) {
if (xhr.status == 204) {
status.html("Finished upload");
let s3_location = xhr.getResponseHeader("Location");
location.href += "/complete?location=" + s3_location;
location.href = "{{ success_url }}?location=" + s3_location;
} else {
status.html("Something went wrong");
console.log(xhr.responseText);

View File

@ -1,4 +1,4 @@
{% extends "interface/project.html" %}
{% extends "interface/project_base.html" %}
{% block page %}
<div class="wiki-page">

View File

@ -3,12 +3,18 @@ from django.urls import path
from . import views
urlpatterns = [
path('', views.my_projects, name='my_projects'),
path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
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.create_submission, name="create_submission"),
path('projects/<int:project_id>/submission/<int:submission_id>', 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"),
path('projects/<int:pk>', views.ProjectDetailView.as_view(), name="project_detail"),
path('projects/<int:project>/page/<int:pk>', views.WikiView.as_view(), name="wiki"),
path('projects/<int:project>/submission', views.SubmissionCreateView.as_view(), name="submission_create"),
path('projects/<int:project>/submission/<int:pk>', views.SubmissionDetailView.as_view(), name="submission_detail"),
path('projects/<int:project>/submission/<int:pk>/upload', views.SubmissionUploadView.as_view(), name="submission_upload"),
path('projects/<int:project>/submission/<int:pk>/cancel', views.SubmissionCancelView.as_view(), name="submission_cancel"),
path('projects/<int:project>/resources', views.ResourceListView.as_view(), name="resource_list"),
path('projects/<int:project>/resources/add', views.ResourceCreateView.as_view(), name="resource_create"),
path('projects/<int:project>/resources/<int:pk>', views.ResourceUploadView.as_view(), name="resource_upload"),
path('projects/<int:project>/resources/<int:pk>/complete', views.ResourceCompleteView.as_view(), name="resource_complete"),
]

View File

@ -1,23 +1,91 @@
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
from django.views.generic import TemplateView, View, RedirectView
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from django.views.generic.base import ContextMixin
from django.http import HttpResponseRedirect
from django.core.exceptions import SuspiciousOperation
from markdown2 import markdown
from datetime import datetime
from urllib.parse import urlparse
from . import models, forms
from .decorators import check_allowed
from base64 import b64decode
import logging
logger = logging.getLogger(__name__)
class EnsembleMixin(object):
def dispatch(self, request, *args, **kwargs):
request.ensemble_id = request.session.get('ensemble')
if not request.ensemble_id:
return redirect('register')
return super().dispatch(request, *args, **kwargs)
class ProjectMixin(EnsembleMixin):
def get_project(self):
if not hasattr(self, '_project'):
self._project = get_object_or_404(models.Project,
pk=self.kwargs['project'], ensemble=self.request.ensemble_id)
return self._project
def get_queryset(self):
return super().get_queryset().filter(project=self.get_project())
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['project'] = self.get_project()
return context
class S3UploadMixin(ProjectMixin):
def get_cancel_url(self):
return self.cancel_url
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
success_url = self.request.build_absolute_uri(self.get_success_url())
key_template = self.object.key_template()
project = self.get_project()
context['upload'] = project.presigned_post(key_template,
fields={'success_action_redirect': success_url},
conditions=[["starts-with", "$success_action_redirect", ""]])
context['ajax_upload'] = project.presigned_post(key_template)
context['success_url'] = success_url
context['cancel_url'] = self.get_cancel_url()
return context
class S3CompleteMixin(View):
always_set = False
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.always_set or not self.object.key:
uri = urlparse(request.GET['location'])
self.object.key = uri.path[1:]
self.object.save()
return super().get(request, *args, **kwargs)
def forbidden(request):
return render(request, 'interface/forbidden.html', {})
def register(request):
if 'clear' in request.GET:
request.session.clear()
request.ensemble_id = request.session.get('ensemble')
registered = request.session.setdefault('registered', {})
@ -32,7 +100,7 @@ def register(request):
if ensemble.passphrase == data['passphrase']:
request.session['ensemble'] = ensemble.pk
registered[ensemble.code] = ensemble.pk
return redirect('my_projects')
return redirect('ensemble_detail')
except models.Ensemble.DoesNotExist:
form.add_error(None, "Incorrect code or passphrase")
@ -46,89 +114,104 @@ def register(request):
return render(request, 'interface/register.html', {'form': form, 'current': current})
@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, 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, 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 create_submission(request, project_id):
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()
# cache details for next time
request.session['name'] = s.name
request.session['instrument'] = s.instrument
return redirect('submission', project_id=project_id, submission_id=s.pk)
else:
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_create.html', context)
@check_allowed
def submission(request, project_id, submission_id):
project = get_object_or_404(models.Project, pk=project_id, ensemble=request.ensemble_id)
submission = project.all_submissions.get(pk=submission_id)
if submission.complete:
context = {'project': project, 'submission': submission}
if request.user.is_authenticated:
context['download'] = submission.presigned_url()
return render(request, 'interface/submission_detail.html', context)
class EnsembleDetailView(EnsembleMixin, DetailView):
# Need to do an upload
redirect = request.build_absolute_uri(resolve_url('complete_submission', project_id=project.pk, submission_id=submission.pk))
def get_object(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
key = submission.key_template()
upload = project.presigned_post(key,
fields={'success_action_redirect': redirect},
conditions=[["starts-with", "$success_action_redirect", ""]])
class ProjectDetailView(EnsembleMixin, DetailView):
def get_queryset(self):
return models.Project.objects.filter(ensemble=self.request.ensemble_id)
class WikiView(ProjectMixin, DetailView):
template_name = 'interface/wiki.html'
model = models.WikiPage
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data['wiki_html'] = markdown(self.object.markdown)
return data
class SubmissionCreateView(ProjectMixin, CreateView):
model = models.Submission
fields = ['name', 'instrument', 'notes']
template_name = "interface/submission_create.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.project = self.get_project()
self.object.save()
self.request.session['name'] = self.object.name
self.request.session['instrument'] = self.object.instrument
return redirect('submission_upload', project=self.object.project.pk, pk=self.object.pk)
def get_initial(self):
return { k: self.request.session.get(k) for k in ('name', 'instrument') }
class SubmissionDetailView(ProjectMixin, S3CompleteMixin, DetailView):
model = models.Submission
# need an additional presigned without the redirect for ajax submission
ajax_post = project.presigned_post(key)
context = {'upload': upload, 'ajax_post': ajax_post, 'project': project, 'submission': submission}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
context['download'] = self.object.presigned_url()
return context
return render(request, 'interface/submission_upload.html', context)
class SubmissionUploadView(S3UploadMixin, DetailView):
template_name = 'interface/submission_upload.html'
model = models.Submission
@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.all_submissions.get(pk=submission_id)
submission.delete()
return redirect('project', project_id=project_id)
def get_success_url(self):
return resolve_url('submission_detail', **self.kwargs)
@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.all_submissions.get(pk=submission_id)
s.complete = True
uri = urlparse(request.GET['location'])
s.key = uri.path[1:]
s.save()
return redirect('submission', project_id=project_id, submission_id=submission_id)
def get_cancel_url(self):
return resolve_url('submission_cancel', **self.kwargs)
class SubmissionCancelView(ProjectMixin, SingleObjectMixin, View):
model = models.Submission
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
return redirect('project_detail', pk=kwargs['project'])
class ResourceCreateView(ProjectMixin, CreateView):
model = models.Resource
fields = ['name', 'media_type', 'description']
template_name = 'interface/project_form.html'
title = "Add a new resource"
def form_valid(self, form):
if not self.request.user.is_authenticated:
raise SuspiciousOperation("Must be logged in to create resources")
self.object = form.save(commit=False)
self.object.project = self.get_project()
self.object.save()
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk)
class ResourceUploadView(S3UploadMixin, DetailView):
model = models.Resource
template_name = 'interface/submission_upload.html'
def get_success_url(self):
return resolve_url('resource_complete', **self.kwargs)
def get_cancel_url(self):
return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceCompleteView(S3CompleteMixin, SingleObjectMixin, RedirectView):
model = models.Resource
always_set = True
def get_redirect_url(self, **kwargs):
return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceListView(ProjectMixin, ListView):
model = models.Resource

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 = ['localhost', '192.168.100.123']
ALLOWED_HOSTS = ['localhost', '192.168.100.130']
# Application definition