Added private links

This commit is contained in:
Tris 2020-10-04 17:22:01 +11:00
parent ad0db64f3c
commit f0a942dfc6
14 changed files with 187 additions and 52 deletions

5
.gitignore vendored
View File

@ -4,5 +4,6 @@ db.sqlite3
credentials credentials
polyphonic/settings.py polyphonic/settings.py
env env
text.* test.*
static static
teststore

View File

@ -24,4 +24,13 @@ static/fonts/Quicksand_Book.otf:
wget -O quicksand.zip https://dl.dafont.com/dl/?f=quicksand wget -O quicksand.zip https://dl.dafont.com/dl/?f=quicksand
mkdir -p static/fonts mkdir -p static/fonts
unzip -d static/fonts quicksand.zip unzip -d static/fonts quicksand.zip
rm quicksand.zip rm quicksand.zip
start_s3_storage:
test ! -f teststore/pid
mkdir -p teststore
MINIO_ACCESS_KEY=polyphonic_test_key MINIO_SECRET_KEY=polyphonic_secret minio server teststore & echo "$$!" > teststore/pid
cat teststore/pid
stop_s3_storage:
kill `cat teststore/pid` && rm teststore/pid

View File

@ -7,6 +7,11 @@ class CodeForm(forms.Form):
passphrase = forms.CharField(max_length=32) passphrase = forms.CharField(max_length=32)
class SubmissionForm(forms.ModelForm): class SubmissionForm(forms.ModelForm):
method = forms.ChoiceField(choices=(
('upload', 'I need to upload a file'),
('link', 'I have a link from my own cloud storage provider')
), initial='upload')
class Meta: class Meta:
model = Submission model = Submission
fields = ['name', 'instrument', 'notes'] fields = ['name', 'instrument', 'method', 'notes']

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.1 on 2020-10-03 11:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0019_project_owner'),
]
operations = [
migrations.RenameField(
model_name='submission',
old_name='key',
new_name='url',
),
migrations.AddField(
model_name='submission',
name='private',
field=models.BooleanField(default=False),
),
]

View File

@ -9,10 +9,11 @@ import random
import boto3 import boto3
from datetime import datetime from datetime import datetime
from urllib.parse import urlparse
import os.path import os.path
s3client = boto3.client('s3') s3client = boto3.client('s3', **getattr(settings, 'S3_CREDENTIALS', {}))
BUCKET = settings.AWS_BUCKET BUCKET = settings.AWS_BUCKET
@ -102,15 +103,29 @@ class Submission(models.Model):
instrument = models.CharField(max_length=100, verbose_name="Instrument / Voice") instrument = models.CharField(max_length=100, verbose_name="Instrument / Voice")
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
complete = models.BooleanField(default=False) complete = models.BooleanField(default=False)
key = models.CharField(max_length=512, blank=True) url = models.CharField(max_length=512, blank=True)
private = models.BooleanField(default=False)
def presigned_url(self): @property
params = {'Bucket': BUCKET, 'Key': self.key} def download_url(self):
if not self.complete:
raise RuntimeError("Submission not complete")
if self.private:
return self.url
params = {'Bucket': BUCKET, 'Key': self.url}
return s3client.generate_presigned_url('get_object', Params=params, ExpiresIn=3600) return s3client.generate_presigned_url('get_object', Params=params, ExpiresIn=3600)
@property
def download_name(self):
uri = urlparse(self.download_url)
_, name = os.path.split(uri.path)
return name or "<Unknown>"
def key_template(self): def key_template(self):
return "{}_{}_{}_${{filename}}".format( return "submissions/{}_{}_{}_${{filename}}".format(
timezone.localtime(self.date).isoformat(timespec='seconds').replace(':', ''), timezone.localtime(self.date).isoformat(timespec='seconds').replace(':', '')[:17],
slugify(self.name), slugify(self.name),
slugify(self.instrument) slugify(self.instrument)
) )

View File

@ -24,7 +24,7 @@
<h3> <h3>
{{ resource.name }} {{ resource.name }}
{% if download %} {% if download %}
<small><a href="{{ download }}"> <small><a href="{{ download }}" target="_blank" rel="noopener noreferrer">
<i class="fas fa-download"></i> Download <i class="fas fa-download"></i> Download
</a></small> </a></small>
{% endif %} {% endif %}

View File

@ -5,7 +5,7 @@
<div class="narrow"> <div class="narrow">
<h3 id="status">Select a file for upload</h3> <h3 id="status">File upload</h3>
<form method="POST" action="{{ upload.url }}" enctype="multipart/form-data" id="item-upload" class="dropzone"> <form method="POST" action="{{ upload.url }}" enctype="multipart/form-data" id="item-upload" class="dropzone">
<div class="fallback"> <div class="fallback">
{% for field, value in upload.fields.items %} {% for field, value in upload.fields.items %}
@ -55,11 +55,11 @@ Dropzone.options.itemUpload = {
autoProcessQueue: false, autoProcessQueue: false,
createImageThumbnails: false, createImageThumbnails: false,
//acceptedFiles: acceptFiles, //acceptedFiles: acceptFiles,
timeout: 3600000, timeout: 3600000,
maxFiles: 1, maxFiles: 1,
addRemoveLinks: true, addRemoveLinks: true,
maxFilesize: 500, maxFilesize: 500,
dictDefaultMessage: "Select a file to upload (max 500Mb)", dictDefaultMessage: "Click to select a file to upload (max 500Mb)",
dictFallbackMessage: "Your browser is old!, using alternative method", dictFallbackMessage: "Your browser is old!, using alternative method",
init: function() { init: function() {
let dz = this; let dz = this;
@ -79,6 +79,9 @@ Dropzone.options.itemUpload = {
let s3_location = f.xhr.getResponseHeader("Location"); let s3_location = f.xhr.getResponseHeader("Location");
location.href = "{{ success_url }}?location=" + s3_location; location.href = "{{ success_url }}?location=" + s3_location;
}); });
this.on('error', function(file, msg) {
status.html('There was an issue - please see troubleshooting');
});
} }
}; };

View File

@ -5,7 +5,10 @@
<h3>Excellent, you are ready to make a submission!</h3> <h3>Excellent, you are ready to make a submission!</h3>
<p> <p>
Please enter some basic information so we can identify your submission and Please enter some basic information so we can identify your submission and
note anything that might be relevant. note anything that might be relevant.<br/>
Most people will want to upload their
file directly but if you have your own cloud storage provider you can send a
public link to your submission instead.
</p> </p>
</div> </div>
<div> <div>
@ -13,6 +16,7 @@
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
<div class="form-actions"> <div class="form-actions">
<a href="{% url 'project_detail' project.pk %}">Cancel</a>
<button type="submit" class="btn-primary">Continue</button> <button type="submit" class="btn-primary">Continue</button>
</div> </div>
</form> </form>

View File

@ -8,8 +8,8 @@
<tr><th>From:</th><td>{{ submission.name }}</td></tr> <tr><th>From:</th><td>{{ submission.name }}</td></tr>
<tr><th>Instrument:</th><td>{{ submission.instrument }}</td></tr> <tr><th>Instrument:</th><td>{{ submission.instrument }}</td></tr>
<tr><th>Notes:</th><td>{{ submission.notes }}</td></tr> <tr><th>Notes:</th><td>{{ submission.notes }}</td></tr>
{% if download %} {% if can_download %}
<tr><th>Download:</th><td><a href="{{ download }}">{{ submission.key }}</a></td></tr> <tr><th>Download:</th><td><a href="{{ submission.download_url }}" target="_blank" rel="noopener noreferrer">{{ submission.download_name }}</a></td></tr>
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,18 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>Link to cloud storage</h3>
<p>Please paste the full link from your storage provider</p>
</div>
<div>
<form class="vertical" action="" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<a href="{{ cancel_url }}">Cancel</a>
<button type="submit" class="btn-primary">Continue</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -16,7 +16,7 @@
<td>{{ submission.instrument }}</td> <td>{{ submission.instrument }}</td>
<td> <td>
<a href="{% url 'submission_detail' project=project.pk pk=submission.pk %}"><i class="fas fa-info-circle"></i></a> <a href="{% url 'submission_detail' project=project.pk pk=submission.pk %}"><i class="fas fa-info-circle"></i></a>
<a href="{{ submission.presigned_url }}"><i class="fas fa-download"></i></a> <a href="{{ submission.download_url }}" target="_blank" rel="noopener noreferrer"><i class="fas fa-download"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -3,28 +3,51 @@ from django.test import TestCase, Client
from interface import models from interface import models
class SubmissionTestCase(TestCase): class SubmissionTestCase(TestCase):
@staticmethod
def setUpTestData():
e1 = models.Ensemble.objects.create(name='The Be Sharps', code="1234", passphrase='Homer')
e1.projects.create(name='Baby on Board')
e2 = models.Ensemble.objects.create(name='Lisa and the Bleeding Gums', code="2345", passphrase="Maggie")
e2.projects.create(name='Baker St')
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
def test_submission(self): def test_submission_upload(self):
ensemble = models.Ensemble.objects.create(name="The Be Sharps", passphrase="Homer") response = self.client.post('/register', {'code': '12-34', 'passphrase': 'Homer'})
project = ensemble.projects.create(name='Baby on Board')
response = self.client.post('/register', {'code': ensemble.code, 'passphrase': ensemble.passphrase})
self.assertRedirects(response, '/') self.assertRedirects(response, '/')
response = self.client.post(f"/projects/{project.pk}/submission", {'name': 'Ned', 'instrument': 'God'}) response = self.client.post(f"/projects/1/submission", {'name': 'Ned', 'instrument': 'Harp', 'method': 'upload'})
#self.assertRedirects(response, '/projects/1/submission/1/upload') self.assertRedirects(response, '/projects/1/submission/1/upload')
self.skipTest("Need to mock S3")
response = self.client.get(response.url) response = self.client.get(response.url)
upload = response.context['upload'] upload = response.context['upload']
self.assertEqual(upload['url'], f"https://{ensemble.bucket}.s3.amazonaws.com/") self.assertEqual(upload['url'], f"http://localhost:9000/{models.BUCKET}")
self.assertRegex(upload['fields']['key'], r'^baby-on-board\/[0-9T\-]+_ned_god_\$\{filename\}$') self.assertRegex(upload['fields']['key'], r'^baby-on-board\/submissions\/[0-9T\-]+_ned_harp_\$\{filename\}$')
self.assertEqual(upload['fields']['success_action_redirect'], 'http://testserver/projects/1/submission/1/complete') self.assertEqual(upload['fields']['success_action_redirect'], 'http://testserver/projects/1/submission/1/complete')
self.assertEqual(models.Submission.objects.count(), 1) self.assertEqual(models.Submission.objects.count(), 1)
self.assertRedirects(self.client.get(f"/projects/{project.pk}/submission/1/cancel"), '/projects/1') self.assertRedirects(self.client.get(f"/projects/1/submission/1/cancel"), '/projects/1')
self.assertEqual(models.Submission.objects.count(), 0) self.assertEqual(models.Submission.objects.count(), 0)
def test_submission_link(self):
response = self.client.post('/register', {'code': '12-34', 'passphrase': 'Homer'})
self.assertRedirects(response, '/')
response = self.client.post(f"/projects/1/submission", {'name': 'Ned', 'instrument': 'Harp', 'method': 'link'})
self.assertRedirects(response, '/projects/1/submission/1/link')
url = 'https://drive.google.com/a/path/to/a/video.mp4#g6e6e4a23'
response = self.client.post(f"/projects/1/submission/1/link", {'url': url})
self.assertRedirects(response, '/projects/1/submission/1')
response = self.client.get('/projects/1/submission/1')
self.assertContains(response, "Thankyou for your submission")
response = self.client.get('/projects/1')
self.assertContains(response, 'Ned')
s = models.Submission.objects.get(pk=1)
self.assertEqual(s.download_url, url)

View File

@ -18,8 +18,10 @@ urlpatterns = [
path('projects/<int:project>/submission', views.SubmissionCreateView.as_view(), name="submission_create"), 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>', views.SubmissionDetailView.as_view(), name="submission_detail"),
path('projects/<int:project>/submission/<int:pk>/link', views.SubmissionLinkView.as_view(), name="submission_link"),
path('projects/<int:project>/submission/<int:pk>/upload', views.SubmissionUploadView.as_view(), name="submission_upload"), 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>/submission/<int:pk>/cancel', views.SubmissionCancelView.as_view(), name="submission_cancel"),
path('projects/<int:project>/submission/<int:pk>/complete', views.SubmissionCompleteView.as_view(), name="submission_complete"),
path('projects/<int:project>/submissions', views.SubmissionListView.as_view(), name="submission_list"), path('projects/<int:project>/submissions', views.SubmissionListView.as_view(), name="submission_list"),
path('projects/<int:project>/resources', views.ResourceListView.as_view(), name="resource_list"), path('projects/<int:project>/resources', views.ResourceListView.as_view(), name="resource_list"),

View File

@ -2,7 +2,7 @@ from django.shortcuts import render, get_object_or_404, redirect, resolve_url
from django.views.generic import TemplateView, View, RedirectView from django.views.generic import TemplateView, View, RedirectView
from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView, FormView
from django.views.generic.base import ContextMixin from django.views.generic.base import ContextMixin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
@ -11,6 +11,7 @@ from django.contrib import auth
from markdown2 import markdown from markdown2 import markdown
from datetime import datetime from datetime import datetime
from urllib.parse import urlparse, urlencode from urllib.parse import urlparse, urlencode
import os.path
from . import models, forms from . import models, forms
@ -84,8 +85,7 @@ class S3UploadMixin(ProjectMixin):
context['accept_files'] = self.accept_files context['accept_files'] = self.accept_files
return context return context
class S3CompleteMixin(View): class S3CompleteView(SingleObjectMixin, RedirectView):
always_set = False
def complete(self, key): def complete(self, key):
self.object.key = key self.object.key = key
@ -94,14 +94,14 @@ class S3CompleteMixin(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
if self.always_set or not self.object.key: if 'key' in request.GET:
if 'location' in request.GET: self.complete(request.GET['key'])
uri = urlparse(request.GET['location']) elif 'location' in request.GET:
self.complete(uri.path[1:]) uri = urlparse(request.GET['location'])
elif 'key' in request.GET: _bucket, key = uri.path[1:].split('/', 1)
self.complete(request.GET['key']) self.complete(key)
else: else:
raise KeyError("No key or location found") raise KeyError("No key or location found")
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -209,9 +209,10 @@ class WikiEditView(ProjectMixin, UpdateView):
model = models.WikiPage model = models.WikiPage
fields = ['title', 'markdown'] fields = ['title', 'markdown']
class SubmissionCreateView(ProjectMixin, CreateView): class SubmissionCreateView(ProjectMixin, FormView):
model = models.Submission #model = models.Submission
fields = ['name', 'instrument', 'notes'] #fields = ['name', 'instrument', 'url', 'notes']
form_class = forms.SubmissionForm
template_name = "interface/submission_create.html" template_name = "interface/submission_create.html"
def form_valid(self, form): def form_valid(self, form):
@ -221,24 +222,33 @@ class SubmissionCreateView(ProjectMixin, CreateView):
self.request.session['name'] = self.object.name self.request.session['name'] = self.object.name
self.request.session['instrument'] = self.object.instrument self.request.session['instrument'] = self.object.instrument
if form.cleaned_data['method'] == 'link':
return redirect('submission_link', project=self.object.project.pk, pk=self.object.pk)
return redirect('submission_upload', project=self.object.project.pk, pk=self.object.pk) return redirect('submission_upload', project=self.object.project.pk, pk=self.object.pk)
def get_initial(self): def get_initial(self):
return { k: self.request.session.get(k) for k in ('name', 'instrument') } return { k: self.request.session.get(k) for k in ('name', 'instrument') }
class SubmissionCompleteView(ProjectMixin, S3CompleteView):
class SubmissionDetailView(ProjectMixin, S3CompleteMixin, DetailView):
model = models.Submission model = models.Submission
def complete(self, key): def complete(self, key):
self.object.url = key
self.object.private = False
self.object.complete = True self.object.complete = True
super().complete(key) self.object.save()
def get_redirect_url(self, **kwargs):
return resolve_url('submission_detail', **self.kwargs)
class SubmissionDetailView(ProjectMixin, DetailView):
model = models.Submission
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if self.request.is_admin: context['can_download'] = self.request.is_admin
context['download'] = self.object.presigned_url()
return context return context
class SubmissionUploadView(S3UploadMixin, DetailView): class SubmissionUploadView(S3UploadMixin, DetailView):
@ -246,12 +256,35 @@ class SubmissionUploadView(S3UploadMixin, DetailView):
model = models.Submission model = models.Submission
accept_files = "video/*" accept_files = "video/*"
def get_success_url(self):
return resolve_url('submission_complete', **self.kwargs)
def get_cancel_url(self):
return resolve_url('submission_cancel', **self.kwargs)
class SubmissionLinkView(ProjectMixin, UpdateView):
model = models.Submission
template_name = 'interface/submission_link.html'
fields = ['url']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = self.get_cancel_url()
return context
def get_success_url(self): def get_success_url(self):
return resolve_url('submission_detail', **self.kwargs) return resolve_url('submission_detail', **self.kwargs)
def get_cancel_url(self): def get_cancel_url(self):
return resolve_url('submission_cancel', **self.kwargs) return resolve_url('submission_cancel', **self.kwargs)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.complete = True
self.object.private = True
self.object.save()
return redirect(self.get_success_url())
class SubmissionCancelView(ProjectMixin, SingleObjectMixin, View): class SubmissionCancelView(ProjectMixin, SingleObjectMixin, View):
model = models.Submission model = models.Submission
@ -294,9 +327,8 @@ class ResourceUploadView(S3UploadMixin, DetailView):
def get_cancel_url(self): def get_cancel_url(self):
return resolve_url('resource_list', project=self.kwargs['project']) return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceCompleteView(S3CompleteMixin, SingleObjectMixin, RedirectView): class ResourceCompleteView(ProjectMixin, S3CompleteView):
model = models.Resource model = models.Resource
always_set = True
def get_redirect_url(self, **kwargs): def get_redirect_url(self, **kwargs):
return resolve_url('resource_list', project=self.kwargs['project']) return resolve_url('resource_list', project=self.kwargs['project'])