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
polyphonic/settings.py
env
text.*
static
test.*
static
teststore

View File

@ -24,4 +24,13 @@ static/fonts/Quicksand_Book.otf:
wget -O quicksand.zip https://dl.dafont.com/dl/?f=quicksand
mkdir -p static/fonts
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)
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:
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
from datetime import datetime
from urllib.parse import urlparse
import os.path
s3client = boto3.client('s3')
s3client = boto3.client('s3', **getattr(settings, 'S3_CREDENTIALS', {}))
BUCKET = settings.AWS_BUCKET
@ -102,15 +103,29 @@ class Submission(models.Model):
instrument = models.CharField(max_length=100, verbose_name="Instrument / Voice")
notes = models.TextField(blank=True)
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):
params = {'Bucket': BUCKET, 'Key': self.key}
@property
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)
@property
def download_name(self):
uri = urlparse(self.download_url)
_, name = os.path.split(uri.path)
return name or "<Unknown>"
def key_template(self):
return "{}_{}_{}_${{filename}}".format(
timezone.localtime(self.date).isoformat(timespec='seconds').replace(':', ''),
return "submissions/{}_{}_{}_${{filename}}".format(
timezone.localtime(self.date).isoformat(timespec='seconds').replace(':', '')[:17],
slugify(self.name),
slugify(self.instrument)
)

View File

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

View File

@ -5,7 +5,7 @@
<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">
<div class="fallback">
{% for field, value in upload.fields.items %}
@ -55,11 +55,11 @@ Dropzone.options.itemUpload = {
autoProcessQueue: false,
createImageThumbnails: false,
//acceptedFiles: acceptFiles,
timeout: 3600000,
timeout: 3600000,
maxFiles: 1,
addRemoveLinks: true,
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",
init: function() {
let dz = this;
@ -79,6 +79,9 @@ Dropzone.options.itemUpload = {
let s3_location = f.xhr.getResponseHeader("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>
<p>
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>
</div>
<div>
@ -13,6 +16,7 @@
{% csrf_token %}
{{ form }}
<div class="form-actions">
<a href="{% url 'project_detail' project.pk %}">Cancel</a>
<button type="submit" class="btn-primary">Continue</button>
</div>
</form>

View File

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

View File

@ -3,28 +3,51 @@ from django.test import TestCase, Client
from interface import models
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):
self.client = Client()
def test_submission(self):
ensemble = models.Ensemble.objects.create(name="The Be Sharps", passphrase="Homer")
project = ensemble.projects.create(name='Baby on Board')
response = self.client.post('/register', {'code': ensemble.code, 'passphrase': ensemble.passphrase})
def test_submission_upload(self):
response = self.client.post('/register', {'code': '12-34', 'passphrase': 'Homer'})
self.assertRedirects(response, '/')
response = self.client.post(f"/projects/{project.pk}/submission", {'name': 'Ned', 'instrument': 'God'})
#self.assertRedirects(response, '/projects/1/submission/1/upload')
self.skipTest("Need to mock S3")
response = self.client.post(f"/projects/1/submission", {'name': 'Ned', 'instrument': 'Harp', 'method': 'upload'})
self.assertRedirects(response, '/projects/1/submission/1/upload')
response = self.client.get(response.url)
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['url'], f"http://localhost:9000/{models.BUCKET}")
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(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)
self.assertRedirects(self.client.get(f"/projects/1/submission/1/cancel"), '/projects/1')
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/<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>/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>/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.detail import DetailView, SingleObjectMixin
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.http import HttpResponseRedirect
from django.core.exceptions import SuspiciousOperation
@ -11,6 +11,7 @@ from django.contrib import auth
from markdown2 import markdown
from datetime import datetime
from urllib.parse import urlparse, urlencode
import os.path
from . import models, forms
@ -84,8 +85,7 @@ class S3UploadMixin(ProjectMixin):
context['accept_files'] = self.accept_files
return context
class S3CompleteMixin(View):
always_set = False
class S3CompleteView(SingleObjectMixin, RedirectView):
def complete(self, key):
self.object.key = key
@ -94,14 +94,14 @@ class S3CompleteMixin(View):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.always_set or not self.object.key:
if 'location' in request.GET:
uri = urlparse(request.GET['location'])
self.complete(uri.path[1:])
elif 'key' in request.GET:
self.complete(request.GET['key'])
else:
raise KeyError("No key or location found")
if 'key' in request.GET:
self.complete(request.GET['key'])
elif 'location' in request.GET:
uri = urlparse(request.GET['location'])
_bucket, key = uri.path[1:].split('/', 1)
self.complete(key)
else:
raise KeyError("No key or location found")
return super().get(request, *args, **kwargs)
@ -209,9 +209,10 @@ class WikiEditView(ProjectMixin, UpdateView):
model = models.WikiPage
fields = ['title', 'markdown']
class SubmissionCreateView(ProjectMixin, CreateView):
model = models.Submission
fields = ['name', 'instrument', 'notes']
class SubmissionCreateView(ProjectMixin, FormView):
#model = models.Submission
#fields = ['name', 'instrument', 'url', 'notes']
form_class = forms.SubmissionForm
template_name = "interface/submission_create.html"
def form_valid(self, form):
@ -221,24 +222,33 @@ class SubmissionCreateView(ProjectMixin, CreateView):
self.request.session['name'] = self.object.name
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)
def get_initial(self):
return { k: self.request.session.get(k) for k in ('name', 'instrument') }
class SubmissionDetailView(ProjectMixin, S3CompleteMixin, DetailView):
class SubmissionCompleteView(ProjectMixin, S3CompleteView):
model = models.Submission
def complete(self, key):
self.object.url = key
self.object.private = False
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):
context = super().get_context_data(**kwargs)
if self.request.is_admin:
context['download'] = self.object.presigned_url()
context['can_download'] = self.request.is_admin
return context
class SubmissionUploadView(S3UploadMixin, DetailView):
@ -246,12 +256,35 @@ class SubmissionUploadView(S3UploadMixin, DetailView):
model = models.Submission
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):
return resolve_url('submission_detail', **self.kwargs)
def get_cancel_url(self):
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):
model = models.Submission
@ -294,9 +327,8 @@ class ResourceUploadView(S3UploadMixin, DetailView):
def get_cancel_url(self):
return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceCompleteView(S3CompleteMixin, SingleObjectMixin, RedirectView):
class ResourceCompleteView(ProjectMixin, S3CompleteView):
model = models.Resource
always_set = True
def get_redirect_url(self, **kwargs):
return resolve_url('resource_list', project=self.kwargs['project'])