Added private links
This commit is contained in:
parent
ad0db64f3c
commit
f0a942dfc6
5
.gitignore
vendored
5
.gitignore
vendored
@ -4,5 +4,6 @@ db.sqlite3
|
||||
credentials
|
||||
polyphonic/settings.py
|
||||
env
|
||||
text.*
|
||||
static
|
||||
test.*
|
||||
static
|
||||
teststore
|
||||
11
Makefile
11
Makefile
@ -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
|
||||
@ -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']
|
||||
23
interface/migrations/0020_auto_20201003_2103.py
Normal file
23
interface/migrations/0020_auto_20201003_2103.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
18
interface/templates/interface/submission_link.html
Normal file
18
interface/templates/interface/submission_link.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
|
||||
@ -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)
|
||||
@ -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"),
|
||||
|
||||
@ -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'])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user