Removing old files

This commit is contained in:
Tris Forster 2022-11-19 21:30:59 +11:00
parent 598ee5ad7e
commit 8f18b9ab9d
77 changed files with 1734 additions and 2493 deletions

View File

@ -1 +1,2 @@
pylint==2.6.0
django-debug-toolbar

View File

@ -5,14 +5,15 @@ from . import models
class EnsembleAdmin(admin.ModelAdmin):
list_display = ['name', 'ensemble_code', 'slug']
class ModuleInline(admin.StackedInline):
model = models.Module
extra = 0
class ProjectAdmin(admin.ModelAdmin):
list_display = ['name', 'ensemble', 'event_date', 'active', 'slug']
list_display = ['name', 'ensemble', 'event_date', 'active']
list_filter = ['ensemble', 'active']
class SubmissionAdmin(admin.ModelAdmin):
list_display = ['name', 'instrument', 'date', 'complete']
list_filter = ['project', 'complete']
inlines = [ModuleInline]
class ResourceAdmin(admin.ModelAdmin):
list_display = ['name', 'media_type', 'project']
@ -24,6 +25,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, ResourceAdmin)
admin.site.register(models.WikiPage, WikiPageAdmin)

View File

@ -1,17 +1,46 @@
from django import forms
from .models import Submission
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class CodeForm(forms.Form):
from . import models, fields
class BaseForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = self.get_form_helper()
def get_form_helper(self):
helper = FormHelper(self)
helper.add_input(Submit('submit', 'Submit', css_class='button is-link'))
return helper
class ProjectForm(forms.ModelForm, BaseForm):
class Meta:
model = models.Project
fields = ['name', 'description', 'event_date']
widgets = {
'event_date': forms.DateTimeInput(attrs={'type': 'date'})
}
class ResourceForm(forms.ModelForm, BaseForm):
class Meta:
model = models.Resource
fields = ['name', 'media_type', 'description', 'file']
def get_form_helper(self):
helper = super().get_form_helper()
helper[3].wrap(fields.BulmaFileUpload)
return helper
class CodeForm(BaseForm):
code = forms.CharField(max_length=14,
widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx', 'inputmode': 'numeric'}))
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', 'method', 'notes']
class ResourceUploadForm(forms.Form):
pass
# file = S3UploadField()

View File

@ -1,7 +1,10 @@
# Generated by Django 3.1.1 on 2020-09-04 09:59
# Generated by Django 3.2.7 on 2022-11-18 09:54
import byostorage.user
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import interface.models
class Migration(migrations.Migration):
@ -9,32 +12,69 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('byostorage', '0004_alter_userstorage_storage'),
]
operations = [
migrations.CreateModel(
name='Ensemble',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Display name', max_length=100)),
('slug', models.SlugField(editable=False, help_text='Short name for the ensemble - used for folders', max_length=100)),
('code', models.CharField(default=interface.models.generate_code, help_text='Ensemble registration code', max_length=9)),
('passphrase', models.CharField(help_text='Used to register ensembles', max_length=100)),
('details', models.TextField(blank=True, help_text='Description of the ensemble (markdown)')),
('admins', models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL)),
('storage', models.ForeignKey(help_text='Default storage for this ensemble', null=True, on_delete=django.db.models.deletion.SET_NULL, to='byostorage.userstorage')),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True, help_text='Markdown format')),
('active', models.BooleanField(default=True)),
('event_date', models.DateField(blank=True, null=True)),
('owner', models.CharField(blank=True, max_length=255)),
('ensemble', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='interface.ensemble')),
],
options={
'ordering': ['active', '-pk'],
},
),
migrations.CreateModel(
name='WikiPage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('markdown', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiki_pages', to='interface.project')),
],
),
migrations.CreateModel(
name='Submission',
name='Resource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('instrument', models.CharField(max_length=100)),
('notes', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='interface.project')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True)),
('file', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=interface.models.resource_key)),
('media_type', models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10)),
('visible', models.BooleanField(default=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
],
options={
'ordering': ['-visible', '-pk'],
},
),
migrations.CreateModel(
name='Module',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(max_length=20)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='interface.project')),
],
),
]

View File

@ -1,93 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-21 23:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import interface.models
class Migration(migrations.Migration):
replaces = [('interface', '0001_initial'), ('interface', '0002_auto_20200904_1004'), ('interface', '0003_auto_20200905_0118'), ('interface', '0004_auto_20200905_0127'), ('interface', '0005_auto_20200905_0638'), ('interface', '0006_submission_key'), ('interface', '0007_auto_20200906_1009'), ('interface', '0008_auto_20200906_1122'), ('interface', '0009_auto_20200907_0103'), ('interface', '0010_auto_20200907_0148'), ('interface', '0011_auto_20200907_0234'), ('interface', '0012_remove_ensemble_bucket'), ('interface', '0013_auto_20200907_1455'), ('interface', '0014_auto_20200909_1016'), ('interface', '0015_resource_media_type'), ('interface', '0016_auto_20200910_2025'), ('interface', '0017_auto_20200914_0943'), ('interface', '0018_auto_20200914_1009'), ('interface', '0019_project_owner'), ('interface', '0020_auto_20201003_2103'), ('interface', '0021_project_description')]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Project',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='WikiPage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('markdown', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiki_pages', to='interface.project')),
('title', models.CharField(default='', max_length=255)),
],
),
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(default=interface.models.generate_code, max_length=9)),
('passphrase', models.CharField(max_length=100)),
('admins', models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL)),
],
),
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'),
),
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)),
('key', models.CharField(blank=True, max_length=255)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
('description', models.TextField(blank=True)),
('media_type', models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10)),
('visible', models.BooleanField(default=True)),
],
),
migrations.AddField(
model_name='project',
name='owner',
field=models.CharField(blank=True, max_length=255),
),
migrations.CreateModel(
name='Submission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('instrument', models.CharField(max_length=100, verbose_name='Instrument / Voice')),
('notes', models.TextField(blank=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_submissions', to='interface.project')),
('date', models.DateTimeField(auto_now_add=True)),
('complete', models.BooleanField(default=False)),
('url', models.CharField(blank=True, max_length=512)),
('private', models.BooleanField(default=False)),
],
),
migrations.AddField(
model_name='project',
name='description',
field=models.TextField(blank=True),
),
]

View File

@ -1,31 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-04 10:04
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('interface', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='submission',
name='date',
field=models.DateField(auto_created=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='wikipage',
name='title',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='submission',
name='notes',
field=models.TextField(blank=True),
),
]

View File

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

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

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

@ -1,18 +0,0 @@
# 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,35 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-06 10:09
from django.db import migrations, models
import django.db.models.deletion
import interface.models
class Migration(migrations.Migration):
dependencies = [
('interface', '0006_submission_key'),
]
operations = [
migrations.RemoveField(
model_name='project',
name='bucket',
),
migrations.AddField(
model_name='ensemble',
name='bucket',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
migrations.AlterField(
model_name='ensemble',
name='code',
field=models.CharField(default=interface.models.generate_code, max_length=12),
),
migrations.AlterField(
model_name='submission',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_submissions', to='interface.project'),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-06 11:22
from django.db import migrations, models
import interface.models
class Migration(migrations.Migration):
dependencies = [
('interface', '0007_auto_20200906_1009'),
]
operations = [
migrations.RenameField(
model_name='ensemble',
old_name='password',
new_name='passphrase',
),
migrations.AlterField(
model_name='ensemble',
name='code',
field=models.CharField(default=interface.models.generate_code, max_length=9),
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 01:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0008_auto_20200906_1122'),
]
operations = [
migrations.RemoveField(
model_name='submission',
name='key',
),
migrations.AddField(
model_name='submission',
name='location',
field=models.CharField(blank=True, max_length=512),
),
migrations.AlterField(
model_name='ensemble',
name='bucket',
field=models.CharField(max_length=255),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 01:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0009_auto_20200907_0103'),
]
operations = [
migrations.RenameField(
model_name='submission',
old_name='location',
new_name='key',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 02:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0010_auto_20200907_0148'),
]
operations = [
migrations.AlterField(
model_name='submission',
name='date',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 04:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0011_auto_20200907_0234'),
]
operations = [
migrations.RemoveField(
model_name='ensemble',
name='bucket',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 04:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0012_remove_ensemble_bucket'),
]
operations = [
migrations.RenameField(
model_name='resource',
old_name='uri',
new_name='key',
),
]

View File

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

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

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

View File

@ -1,25 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-13 23:43
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('interface', '0016_auto_20200910_2025'),
]
operations = [
migrations.AddField(
model_name='ensemble',
name='admins',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='submission',
name='instrument',
field=models.CharField(max_length=100, verbose_name='Instrument / Voice'),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-14 00:09
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('interface', '0017_auto_20200914_0943'),
]
operations = [
migrations.AddField(
model_name='resource',
name='visible',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='ensemble',
name='admins',
field=models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-10-03 09:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0018_auto_20200914_1009'),
]
operations = [
migrations.AddField(
model_name='project',
name='owner',
field=models.CharField(blank=True, max_length=255),
),
]

View File

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

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-10-05 03:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0020_auto_20201003_2103'),
]
operations = [
migrations.AddField(
model_name='project',
name='description',
field=models.TextField(blank=True),
),
]

View File

@ -1,34 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-03 09:43
from django.db import migrations, models
from django.utils.text import slugify
def create_slugs(apps, schema_editor):
for model in ('Ensemble', 'Project'):
M = apps.get_model('interface', model)
for instance in M.objects.all():
if instance.slug == '':
instance.slug = slugify(instance.name)
instance.save()
class Migration(migrations.Migration):
dependencies = [
('interface', '0021_project_description'),
]
operations = [
migrations.AddField(
model_name='ensemble',
name='slug',
field=models.SlugField(default='', editable=False, max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='project',
name='slug',
field=models.SlugField(default='', editable=False, max_length=100),
preserve_default=False,
),
migrations.RunPython(create_slugs)
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-12 02:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('interface', '0022_auto_20210303_2043'),
]
operations = [
migrations.AddField(
model_name='ensemble',
name='storage',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='byostorage.userstorage'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-12 06:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0023_ensemble_storage'),
]
operations = [
migrations.AddField(
model_name='project',
name='accept_submissions',
field=models.BooleanField(default=False, help_text='Allow media submissions from participants'),
),
migrations.AddField(
model_name='project',
name='has_items',
field=models.BooleanField(default=True, help_text='Enable items to be added from the library'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-12 06:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0024_auto_20210312_1712'),
]
operations = [
migrations.RenameField(
model_name='project',
old_name='has_items',
new_name='enable_library',
),
migrations.RenameField(
model_name='project',
old_name='accept_submissions',
new_name='enable_submissions',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-12 22:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0025_auto_20210312_1713'),
]
operations = [
migrations.RenameField(
model_name='project',
old_name='deadline',
new_name='event_date',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-22 00:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0026_auto_20210313_0926'),
]
operations = [
migrations.AlterField(
model_name='wikipage',
name='title',
field=models.CharField(max_length=255),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2021-05-05 05:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0027_auto_20210322_1154'),
]
operations = [
migrations.AddField(
model_name='ensemble',
name='details',
field=models.TextField(blank=True),
),
]

View File

@ -4,19 +4,16 @@ from django.utils import timezone
from django.conf import settings
from django.shortcuts import resolve_url
from byostorage.user import BYOStorage
import random
import boto3
from datetime import datetime
from urllib.parse import urlparse
import os.path
s3client = boto3.client('s3', **getattr(settings, 'S3_CREDENTIALS', {}))
BUCKET = settings.AWS_BUCKET
MEDIA_TYPES = [
('audio', "Audio"),
('video', "Video"),
@ -24,14 +21,14 @@ MEDIA_TYPES = [
]
def rough_date(d):
days = (self.event_date - timezone.now().date()).days
days = (d - timezone.now()).days
in_past = days < 0
if in_past:
days = abs(days)
if days ==0:
return "today!"
if days >= 7:
return in_past, "{0:d} weeks, {1:d} days".format(days / 7, days % 7)
return in_past, "{0:d} weeks, {1:d} days".format(int(days / 7), int(days % 7))
return in_past, f"{days} days"
@ -39,16 +36,25 @@ def generate_code(length=9):
return "".join([ random.choice('0123456789') for _ in range(length) ])
class Ensemble(models.Model):
name = models.CharField(max_length=100)
code = models.CharField(max_length=9, default=generate_code)
passphrase = models.CharField(max_length=100)
''' A group that plays together
'''
name = models.CharField(max_length=100,
help_text="Display name")
slug = models.SlugField(max_length=100, editable=False, unique=True,
help_text="Short name for the ensemble - used for folders")
code = models.CharField(max_length=9, default=generate_code,
help_text="Ensemble registration code")
passphrase = models.CharField(max_length=100,
help_text="Used to register ensembles")
admins = models.ManyToManyField('auth.User', related_name='ensembles')
slug = models.SlugField(max_length=100, editable=False)
details = models.TextField(blank=True)
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL)
details = models.TextField(blank=True,
help_text="Description of the ensemble (markdown)")
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL,
help_text="Default storage for this ensemble")
def active_projects(self):
return self.projects.filter(active=True)
return self.projects.filter(active=True).order_by('event_date')
def ensemble_code(self):
code = str(self.code)
@ -62,24 +68,24 @@ class Ensemble(models.Model):
def __str__(self):
return self.name
class Project(models.Model):
''' A Project linked to an ensemble
'''
name = models.CharField(max_length=100)
ensemble = models.ForeignKey(Ensemble, related_name='projects', on_delete=models.CASCADE, null=True)
description = models.TextField(blank=True)
description = models.TextField(blank=True,
help_text="Markdown format")
active = models.BooleanField(default=True)
event_date =models.DateField(null=True, blank=True)
event_date =models.DateTimeField(null=True, blank=True)
owner = models.CharField(max_length=255, blank=True)
slug = models.SlugField(max_length=100, editable=False)
enable_library = models.BooleanField(default=True, help_text="Enable items to be added from the library")
enable_submissions = models.BooleanField(default=False, help_text="Allow media submissions from participants")
class Meta:
ordering = ['active', '-pk']
@property
def submissions(self):
return self.all_submissions.filter(complete=True).order_by('-pk')
def presigned_post(self, object_name, fields=None, conditions=None, expires=3600):
key = os.path.join(self.slug, object_name)
return s3client.generate_presigned_post(BUCKET, key, Fields=fields or {}, Conditions=conditions or [], ExpiresIn=expires)
return self.all_submissions.order_by('-pk')
@property
def days(self):
@ -87,42 +93,59 @@ class Project(models.Model):
@property
def has_happened(self):
return self.event_date < timezone.now().date()
return self.event_date < timezone.now()
def save(self):
if not self.slug:
self.slug = slugify(self.name)
super(Project, self).save()
@property
def rough_date(self):
in_past, s = rough_date(self.event_date)
if in_past:
return f"{s} ago"
return f"In {s}"
@property
def folder(self):
print(f"{self.ensemble.storage_id}:{self.ensemble.slug}/{self.slug}")
return f"{self.ensemble.storage_id}:{self.ensemble.slug}/{self.slug}"
def __str__(self):
return self.name
class Module(models.Model):
''' Enable modules on a oriject
'''
name = models.SlugField(max_length=20, choices=[ (x, x.title()) for x in settings.POLYPHONIC_MODULES ])
project = models.ForeignKey(Project, related_name="modules", on_delete=models.CASCADE)
def resource_key(resource, filename):
return f'{resource.project.folder}/resources/{filename}'
class Resource(models.Model):
''' A viewable file resource attached to a project
e.g PDF instructions, MP3 backing track
'''
project = models.ForeignKey(Project, related_name='resources', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
key = models.CharField(max_length=255, blank=True)
file = models.FileField(storage=BYOStorage(), upload_to=resource_key)
media_type = models.CharField(max_length=10, choices=MEDIA_TYPES, default='*')
visible = models.BooleanField(default=True)
def key_template(self):
return "{}/${{filename}}".format(slugify(self.name))
class Meta:
ordering = ['-visible', '-pk']
def accept(self):
if self.media_type == 'general':
return ".*"
return f"{self.media_type}/*"
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*24)
def __str__(self):
return self.name
class WikiPage(models.Model):
''' An editable wiki page for the project in markdown format
'''
project = models.ForeignKey(Project, related_name='wiki_pages', on_delete=models.CASCADE)
title = models.CharField(max_length=255)
markdown = models.TextField()
@ -132,9 +155,9 @@ class WikiPage(models.Model):
def __str__(self):
return self.title
"""
class Submission(models.Model):
project = models.ForeignKey(Project, related_name='all_submissions', on_delete=models.CASCADE)
project = models.ForeignKey(Project, related_name='old_submissions', on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True, )
name = models.CharField(max_length=255)
instrument = models.CharField(max_length=100, verbose_name="Instrument / Voice")
@ -180,3 +203,4 @@ class Submission(models.Model):
def __str__(self):
return f"{self.name}: {self.date}"
"""

View File

@ -1,366 +1,63 @@
@font-face {
font-family: MartinHand;
src: url('/static/fonts/Martinhand3.ttf');
}
:root {
--border-color: #292929;
--gray-blue: #667788;
--light-blue: #c5eff7;
--light-grey: #EEEEEE;
--primary: #485fc7;
}
@font-face {
font-family: 'Quicksand';
src: url('../../fonts/Quicksand_Book.otf');
.fancy {
font-family: MartinHand;
color: var(--primary);
}
@font-face {
font-family: 'QuicksandBold';
src: url('../../fonts/Quicksand_Bold_Oblique.otf');
.has-text-shadow {
text-shadow: 1px 1px 2px #000;
}
.debug DIV {
border: 1px dashed #DDD;
.is-form-group {
max-width: 600px;
align-self: center;
}
HTML {
height: 100%;
.is-centered {
margin: auto;
}
BODY {
background-image: url('../background.png');
background-position: center top;
background-size: 100%;
background-repeat: no-repeat;
margin: 0px;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
.is-action {
cursor: pointer;
}
.main {
max-width: 1280px;
margin: 10px auto;
border: 1px solid var(--border-color);
border-radius: 5px;
font-family: 'Quicksand', Arial, Helvetica, sans-serif;
font-size: 14pt;
background-color: white;
.menu-label {
color: var(--primary);
}
.content {
margin: 20px;
flex-direction: column;
.button.is-primary, .button.is-primary:hover {
background-color: var(--primary);
}
.narrow {
max-width: 500px;
margin: 0px auto;
A.admin-link:after {
content: "*";
}
.wide {
width: 1200px;
TEXTAREA.input {
height: 400px;
}
.collapse {
display: flex;
flex-direction: row;
justify-content: space-around;
}
@media all and (max-width: 1200px) {
.wide {
width: 900px;
}
}
@media all and (max-width: 900px) {
.mdhide {
display: none;
}
.wide {
width: auto;
}
}
@media all and (max-width: 700px) {
.smhide {
display: none;
}
.collapse {
flex-direction: column;
}
.wide {
width: auto;
}
}
/* HEADER BAR */
.navigation {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 50px;
background-color: var(--gray-blue);
color: var(--light-blue) !important;
}
.navigation > * {
white-space: nowrap;
}
.navigation A,
.navigation A:visited {
color: var(--light-blue);
text-decoration: none;
}
.navigation .brand {
font-family: 'QuicksandBold', 'Quicksand', Arial, Helvetica, sans-serif;
font-size: 1.5rem;
margin: auto 20px;
}
UL.nav-buttons {
display: flex;
list-style: none;
}
UL.nav-buttons > LI {
margin: 2px 10px;
}
/* FORMS */
FORM.vertical {
display: flex;
flex-direction: column;
max-width: 400px;
margin: 0px auto;
}
LABEL {
margin-top: 20px;
}
TEXTAREA {
height: 50px;
}
INPUT[type=checkbox] {
margin-right: auto;
}
.form-actions {
text-align: right;
margin-top: 20px;
}
.badge {
display: inline-block;
border: 1px solid var(--gray-blue);
font-weight: bold;
font-size: 0.9em;
border-radius: 10px;
padding: 4px 10px 2px;
margin: 2px;
}
.btn {
background-color: var(--gray-blue);
display: inline-block;
.control INPUT[type='file'] {
border: none;
color: var(--light-blue);
text-decoration: none;
border-radius: 1em;
font-size: 1em;
}
.btn:hover {
cursor: pointer;
color: white;
}
.clickable {
cursor: pointer;
}
.action {
margin: 0px 10px 0px 5px;
}
.pills {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 40px;
}
.pills A {
border: 1px solid var(--gray-blue);
padding: 4px 10px 2px 10px;
margin: 10px 5px;
border-radius: 10px;
white-space: nowrap;
}
.pills A:hover {
background-color: var(--light-blue);
text-decoration: none
}
.list-group {
display: flex;
flex-direction: column;
}
.list-group > * {
border: 1px solid var(--gray-blue);
border-radius: 10px;
padding: 2px 20px;
margin-top: 20px;
}
.list-group > A:hover {
background-color: var(--light-blue);
text-decoration: none;
}
/* PROGRESS BAR */
.progress {
display: relative;
border: 1px solid var(--border-color);
border-radius: 5px;
margin: 20px 10px;
}
.progress-bar {
width: 0%;
height: 1.5em;
background-color: var(--light-blue);
border-radius: 5px;
}
.text-center {
width: 80%;
text-align: center;
margin: auto 10%;
}
A, A:visited {
text-decoration: none;
color: var(--gray-blue);
font-weight: bold;
}
A:hover {
text-decoration: underline;
}
H1 {
text-align: center;
}
TABLE {
border-spacing: 0px;
}
TD, TH {
padding: 3px 6px;
text-align: left;
}
TABLE THEAD TR {
background-color: var(--gray-blue);
color: var(--light-blue);
font-weight: bolder;
}
TABLE THEAD TH {
padding: 5px 6px;
}
TABLE.zebra TR:nth-child(even) {
background-color: var(--light-grey);
}
TABLE.horizontal TH {
text-align: right;
}
TABLE.horizontal TD,
TABLE.horizontal TH {
padding: 5px;
}
TABLE SELECT {
width: 100%;
}
.resource-player {
width: 100%;
border-radius: 10px;
max-width: 640px;
max-height: 640px;
margin: 5px;
border: 1px solid var(--border-color);
}
.dz-clickable {
text-align: center;
}
.scrollable {
max-height: 200px;
overflow: auto;
.project-footer {
position: fixed;
bottom: 0px;
right: 0px;
background-color: #EEE;
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 5px;
}
.admin-tools {
float: right;
padding: 10pt;
}
.disabled {
background-color: #DDD;
}
.dz-image {
width: 240px !important;
}
.dz-progress {
width: 200px !important;
margin-left: -100px !important;
margin-top: 24px !important;
}
.item-table {
border-collapse: collapse;
}
.item-table TD {
border: 1px solid #999;
}
TD.select-cell {
padding: 0px !important;
}
.select-cell SELECT {
border: none;
background-color: transparent;
height: 30px;
font-family: inherit;
font-size: inherit;
}
#tag-list DIV {
padding: 5px;
}
DIV.selected {
background-color: var(--gray-blue);
color: white;
border: 1px solid #999;
border-radius: 5px;
padding: 10px 10px;
margin-top: 30px;
border-top-left-radius: 6px;
}

View File

@ -1,50 +1,50 @@
{% load static %}
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
<script src="{% static 'interface/js/interface.js' %}"></script>
<title>{% block title %}Polyphonic{% endblock %}</title>
{% block media %}{% endblock %}
</head>
<body>
<div class="main">
{% block navigation %}
<nav class="navigation">
<div>
<a class="brand" href="/"><i class="fas fa-random smhide"></i> Polyphonic</a>
<span class="mdhide">Ensemble Manager</span>
</div>
<ul class="nav-buttons">
{% if request.ensemble_id %}
<li class="nav-item">
<a class="nav-link" href="{% url 'ensemble_detail' %}"><i class="fas fa-music" title="Projects"></i> <span class="smhide">My
Projects</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'register' %}"><i class="fas fa-users" title="Ensembles"></i> <span class="mdhide">My
Ensembles</span></a>
</li>
{% endif %}
{% if request.is_admin %}
<li class="nav-item">
<a href="{% url 'manage' %}"><i class="fas fa-user-lock" title="Admin"></i></a>
</li>
{% endif %}
</ul>
</nav>
{% endblock %}
<div class="content">
{% block content %}
<h1>No content!</h1>
{% endblock %}
{% block navigation %}
<nav class="navbar" role="navigation">
<div class="navbar-brand has-text-primary">
<a class="navbar-item" href="/">
<span class="icon fancy is-size-2 mx-4"><i class="fas fa-random"></i></span>
<span class="fancy is-size-2">Polyphonic</span>
</a>
<span class="navbar-item is-hidden-mobile fancy is-size-5">Music Ensemble Manager</span>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="projectMenu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</div>
<div id="navbarMain" class="navbar-menu">
<div class="navbar-end">
<span class="navbar-item is-size-4">{% firstof ensemble project.ensemble %}</span>
</div>
</div>
</nav>
{% endblock %}
{% block content %}
<h1>No content!</h1>
{% endblock %}
<!-- late load scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

View File

@ -1,14 +1,15 @@
{% extends "interface/project_base.html" %}
{% load crispy_forms_tags %}
{% block media %}
{{ form.media }}
{% endblock %}
{% block page %}
<div>
<h3>{% firstof title view.title %}</h3>
<form class="vertical" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div>
</form>
<h3 class="subtitle">{% firstof title view.title %}</h3>
<div class="columns is-centered">
<div class="column is-form-group">
{% crispy form %}
</div>
</div>
{% endblock %}

View File

@ -1,22 +1,73 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% block admin %}
<a href="{% url 'project_create' %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Add new</span>
</a>
{% endblock %}
{% block page %}
<div style="flex-grow: 1">
<h1>Projects for {{ ensemble.name }}</h1>
<div class="list-group narrow">
{% for project in ensemble.active_projects %}
<a class="" href="{% url 'project_detail' project=project.id %}">
<h3>{{ project.name }}</h3>
<p><small>
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
{% if project.works.count %}{{ project.works.count }} works<br/>{% endif %}
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
</small></p>
</a>
{% endfor %}
<h3 class="title">Projects for {{ ensemble.name }}</h3>
<div class="columns is-multiline">
{% for project in ensemble.active_projects %}
<div class="column is-half">
<div class="card">
<a class="" href="{% url 'project_detail' project=project.id %}">
<header class="card-header">
<p class="card-header-title">{{ project.name }}</p>
<p class="card-header-icon" style="color: black;">{{ project.rough_date }}</p>
</header>
</a>
<div class="card-content">
<div class="content">
{{ project.description | markdown }}
</div>
<p><small>
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
{% if project.works.count %}
<a href="{% url 'item_list' project=project.pk %}">
{{ project.works.count }} works
</a>
<br/>
{% endif %}
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
</small></p>
</div>
</div>
</div>
<div style="float: right; margin-top: 10px; color: #999;">
<small>{{ ensemble.ensemble_code }}</small>
{% empty %}
<div class="hero">
<div class="hero-body">
<p class="title">No projects currently planned</p>
<p class="subtitle">Go put your feet up!</p>
</div>
</div>
{% endfor %}
</div>
{% if request.is_admin %}
<div class="">
<div class="card">
<header class="card header">
<p class="card-header-title">Admin Details</p>
</header>
<div class="card-content">
<p>
Joining instructions for participants<br/><br/>
URL: <a href="{{ ensemble_url }}">{{ ensemble_url }}</a><br/>
Code: {{ ensemble.ensemble_code }}<br/>
Passphrase: {{ ensemble.passphrase }}
</p>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -2,40 +2,109 @@
{% block content %}
{% if request.is_admin %}
<div class="admin-tools">
{% block admin %}
{% endblock %}
</div>
{% endif %}
<h1>{{ project.name }}</h1>
{% block page %}
No content
{% endblock %}
<div class="columns">
<div class="column is-narrow is-hidden-touch" id="projectMenu">
<div style="margin: auto 2em;">
<aside class="menu">
<p class="menu-label">My Things</p>
<ul class="menu-list">
<li><a href="{% url 'ensemble_list' %}">Ensembles</a></li>
<li><a href="{% url 'ensemble_detail' %}">Projects</a></li>
<li><a href="{% url 'work_list' %}">Library</a></li>
</ul>
<div class="project-links">
<div class="pills" role="tablist">
{% if project %}
<p class="menu-label">This Project</p>
<ul class="menu-list">
<li><a role="tab" href="{% url 'project_detail' project=project.id %}">Project Info</a></li>
{% for page in project.wiki_pages.all %}
<li><a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a></li>
{% endfor %}
<li><a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
{% if 'library' in modules %}
<li><a href="{% url 'item_list' project=project.id %}">My Music</a></li>
{% endif %}
{% if 'submission' in modules %}
<!--a role="tab" href="">Record a submission</a-->
{% if request.is_admin %}
<li><a role="tab" class="admin-link" href="{% url 'submission_list' project=project.id %}">Submissions</a></li>
{% endif %}
<li><a role="tab" href="{% url 'submission_create' project=project.id %}">Send a File</a></li>
{% endif %}
</ul>
{% endif %}
<p class="menu-label">Admin</p>
{% if request.is_admin %}
<ul class="menu-list">
<li><a class="admin-link" href="{% url 'collection_list' %}">Collections</a></li>
{% if request.user.is_superuser %}
<li><a class="admin-link" href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
{% endif %}
</ul>
{% endif %}
<ul class="menu-list">
{% if request.user.is_authenticated %}
<li><a href="{% url 'logout' %}">Logout</a></li>
{% else %}
<li><a href="{% url 'login' %}">Login</a></li>
{% endif %}
</ul>
</aside>
</div>
</div>
<div class="column">
{% if project %}
<a role="tab" href="{% url 'project_detail' project=project.id %}">Project info</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 %}
{% endif %}
<div class="tabs is-centered is-hidden-desktop">
<ul>
<li><a href="">Info</a></li>
{% if project.wiki_pages.count %}
<li><a href="">Pages</a></li>
{% endif %}
{% if project.resources.count %}
<li><a href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
{% endif %}
{% if project.enable_library %}
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
{% endif %}
{% if project.enable_submissions %}
<li><a href="{% url 'submission_create' project=project.pk %}">Send File</a></li>
{% endif %}
</ul>
</div>
{% if project and project.enable_submissions%}
<a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a>
<!--a role="tab" href="">Record a submission</a-->
{% if request.is_admin %}
<a role="tab" href="{% url 'submission_list' project=project.id %}">Submissions</a>
{% endif %}
<a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a>
{% endif %}
{% include "library/project_menu.html" %}
<section class="section">
{% if request.is_admin %}
<div class="admin-tools is-pulled-right">
{% block admin %}
{% endblock %}
</div>
{% endif %}
{% if project %}<h3 class="title">{{ project.name }}</h3>{% endif %}
{% block page %}
No content
{% endblock %}
</section>
</div>
</div>
{% if ensemble %}
<div class="project-footer">
{{ ensemble.name }}
</div>
{% endif %}
{% endblock %}

View File

@ -1,32 +1,62 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% load polyphonic %}
{% block admin %}
<a href="{% url 'wiki_create' project=project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-file"></i></span>
<span>Add Page</span>
</a>
<a href="{% url 'project_edit' project=project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span>
<span>Edit</span>
</a>
{% endblock %}
{% block page %}
<div class="narrow">
<div class="box">
{% if project.event_date %}
<h3 class="text-center">
<h3 class="subtitle is-centered">
<b>{{ project.event_date|date:"l jS F Y, g:i A" }}</b>
{% if project.has_happened %}
{{ project.event_date|timesince }} ago.
({{ project.event_date|roughtimesince }} ago)
{% else %}
In {{ project.event_date|timeuntil }}.
(in {{ project.event_date|roughtimeuntil }}...)
{% endif %}
</h3>
{% endif %}
<p>{{ project.description|markdown }}</p>
{% if project.owner %}
<p>Project email: <a href="mailto:{{ project.owner }}">{{ project.owner }}</a></p>
{% endif %}
{% if project.enable_submissions %}
{% include 'submissions/project_detail.html' %}
{% endif %}
{% if project.enable_library %}
{% include 'library/project_detail.html' %}
{% endif %}
<div class="block">
{% if project.description %}
<p class="content">{{ project.description|markdown }}</p>
{% else %}
<p>No description</p>
{% endif %}
</div>
{% if 'library' in modules %}
<div class="block">
{% include 'library/project_detail.html' %}
</div>
{% endif %}
{% if 'submission' in modules %}
<div class="block">
{% include 'submissions/project_detail.html' %}
</div>
{% endif %}
{% if project.owner %}
<div class="block">
{% if project.owner.email %}
The project owner is <a href="mailto:{{ project.owner.email }}">{{ project.owner }}</a>
{% else %}
The project owner is {{ project.owner }}.
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,15 +1,18 @@
{% extends "interface/project_base.html" %}
{% load crispy_forms_tags %}
{% block media %}
{{ form.media }}
{% endblock %}
{% block page %}
<div class="narrow">
<h3>{{ title }}</h3>
<p>{{ instructions }}</p>
<form class="vertical" method="POST">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div>
</form>
<h3 class="subtitle">{% firstof title view.title %}</h3>
<div class="columns is-centered">
<div class="column is-form-group">
{% if instructions %}
<p>{{ instructions }}</p>
{% endif %}
{% crispy form %}
</div>
</div>
{% endblock %}

View File

@ -1,11 +0,0 @@
ALL = {{ targets|join:" " }}
-include "local.mk"
all: ${ALL}
{% for s in submissions %}
{{ s.name }}:
curl -o $@ -L {{ s.url }}
{% endfor %}

View File

@ -1,29 +1,24 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<section class="section">
{% if not request.user.is_authenticated %}
<a href="{% url 'login' %}" style="float: right"><i class="fa fa-key"></i></a>
{% endif %}
<div class="collapse">
{% if current %}
<div>
<h3>My Ensembles</h3>
<ul>
{% for ensemble in current %}
<li><a href="/?code={{ ensemble.ensemble_code}}">{{ ensemble.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<div>
<form action="" class="vertical" method="POST">
<h3>Join an ensemble</h3>
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button class="btn btn-primary">Enter</button>
<div class="columns is-centered">
<div class="box is-half">
<h3 class="title">Join an ensemble</h3>
<form action="" method="POST">
{% csrf_token %}
{{ form | crispy }}
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Register</button>
</div>
</form>
</div>
</div>
</form>
</div>
</div>
</section>
{% endblock %}

View File

@ -2,34 +2,40 @@
{% load md2 %}
{% block admin %}
<a href="{% url 'resource_create' project=project.pk %}"><i class="fas fa-plus-circle"></i> Add new</a>
<a class="button is-link" href="{% url 'resource_create' project=project.pk %}">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Add new</span>
</a>
{% endblock %}
{% block page %}
<div class="narrow">
<h3>Resources</h3>
<div class="list-group narrow">
{% for resource in object_list %}
{% with download=resource.presigned_url %}
<div>
{% if request.is_admin %}
<div class="admin-tools">
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}">
<i class="fas fa-upload"></i>
</a>
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}">
<i class="fas fa-edit"></i>
</a>
{% block page %}
<div class="columns is-multiline">
{% for resource in object_list %}
{% with download=resource.file.url %}
<div class="column is-half">
<div class="card {% if not object.visible %}disabled{% endif %}">
<div class="card-header">
<div class="card-header-title">
{% if download %}
<a href="{{ download }}">{{ resource.name }}</a>
{% else %}
{{ resource.name }}
{% endif %}
</div>
<div class="card-header-icon">
{% if request.is_admin %}
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}" class="icon" title="Upload">
<i class="fas fa-upload"></i>
</a>
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}" class="icon" title="Edit">
<i class="fas fa-edit"></i>
</a>
{% endif %}
</div>
</div>
{% endif %}
<h3>
{{ resource.name }}
{% if download %}
<small><a href="{{ download }}" target="_blank" rel="noopener noreferrer">
<i class="fas fa-download"></i> Download
</a></small>
{% endif %}
</h3>
<div class="card-content">
<p>
<small>{{ resource.description|markdown }}</small>
{% if not resource.visible %}
@ -37,11 +43,16 @@
{% endif %}
</p>
{% if download and resource.media_type == 'audio' %}
<audio class="resource-player" controls src="{{ download }}"></audio>
<audio class="resource-player" controls src="{{ download }}" style="width: 100%;"></audio>
{% endif %}
</div>
</div>
{% endwith %}
{% endfor %}
</div>
{% endwith %}
{% empty %}
<div class="column">
<p>There are resources for this project</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -1,24 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<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.<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>
<form class="vertical" action="" method="POST" enctype="multipart/form-data">
{% 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>
</div>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>Thankyou for your submission!</h3>
<table class="horizontal">
<tbody>
<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 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>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% 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

@ -1,36 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="admin-tools">
<a href="{{ signed_url }}">
<i class="fas fa-list"></i>
</a>
</div>
<table style="max-width: 800px; margin: 10pt auto;">
<thead>
<tr>
<th>Date</th><th>Time</th><th>Name</th><th>Instrument</th><th></th></tr>
</tr>
</thead>
<tbody>
{% for submission in object_list %}
<tr>
<td>{{ submission.date.date }}</td>
<td>{{ submission.date.time }}</td>
<td>{{ submission.name }}</td>
<td>{{ submission.instrument }}</td>
<td>
<a href="{% url 'submission_detail' project=project.pk pk=submission.pk %}"><i class="fas fa-info-circle" title="Info"></i></a>
{% if submission.private %}
<i style="color: #999" class="fas fa-video" title="No preview available"></i>
{% else %}
<a href="{% url 'submission_preview' project=project.pk pk=submission.pk %}"><i class="fas fa-video" title="Preview"></i></a>
{% endif %}
<a href="{{ submission.download_url }}" target="_blank" rel="noopener noreferrer"><i class="fas fa-save" title="Download"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="text-center">
<div style="text-align: right">
<a href="{% url 'submission_list' project=project.pk %}"><i class="fas fa-arrow-left"></i> Back</a>
</div>
{% with object.download_url as url %}
<video class="resource-player" src="{{ url }}" controls></video>
<p style="text-align: right">
<b>{{ object.name }}</b> ({{ object.instrument }})
<small>{{ object.date }}</small>
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" download><i class="fas fa-save"></i> Download</a>
</p>
{% endwith %}
</div>
{% endblock %}

View File

@ -1,11 +1,15 @@
{% extends "interface/project_base.html" %}
{% block admin %}
<a href="{% url 'wiki_edit' project=project.pk pk=object.pk %}" class="admin-tool"><i class="fas fa-edit"></i></a>
<a href="{% url 'wiki_edit' project=project.pk pk=wikipage.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span>
<span>Edit</span>
</a>
{% endblock %}
{% block page %}
<div class="wiki-page">
<h3 class="subtitle">{{ wikipage.title }}</h3>
<div class="box content wiki-page">
{{ wiki_html|safe }}
</div>
{% endblock %}

View File

@ -1,4 +1,5 @@
{% extends "interface/project_base.html" %}
{% load crispy_forms_tags %}
{% block page %}
<style>
@ -14,10 +15,7 @@ FORM.vertical {
<p>{{ instructions }}</p>
<form class="vertical" method="POST">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div>
{{ form | crispy }}
</form>
</div>
{% endblock %}

View File

@ -1,17 +1,25 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="narrow">
<p style="text-align: center">
<section class="section">
<div class="columns is-centered">
<div class="box is-half">
<p class="block">
Login is only required to administer a project.<br/>
If you have an ensemble code <a href="{% url 'register' %}">enter it here</a> instead.
</p>
<form method="POST" class="vertical">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button type="submit">Login</button>
{{ form | crispy }}
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Login</button>
<a href="{% url 'ensemble_detail' %}" class="button is-light">Cancel</a>
</div>
</div>
</form>
</div>
</div>
</section>
{% endblock %}

View File

@ -8,30 +8,23 @@ urlpatterns = [
path('login', auth_views.LoginView.as_view(), name='login'),
path('logout', views.logout, name='logout'),
path('register', views.register, name="register"),
path('manage', views.ManageView.as_view(), name="manage"),
path('', views.EnsembleProjectListView.as_view(), name='ensemble_detail'),
path('ensembles', views.EnsembleListView.as_view(), name="ensemble_list"),
path('ensembles/<int:pk>', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
path('ensembles/<int:pk>/forget', views.EnsembleForgetView.as_view(), name='ensemble_forget'),
path('projects/create', views.ProjectCreateView.as_view(), name="project_create"),
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
path('projects/<int:project>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
path('projects/<int:project>/edit', views.ProjectUpdateView.as_view(), name="project_edit"),
#path('projects/<int:project>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
path('projects/<int:project>/page/create', views.WikiCreateView.as_view(), name="wiki_create"),
path('projects/<int:project>/page/<int:pk>', views.WikiView.as_view(), name="wiki"),
path('projects/<int:project>/page/<int:pk>/edit', views.WikiEditView.as_view(), name="wiki_edit"),
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>/preview', views.SubmissionPreview.as_view(), name="submission_preview"),
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>/submission/<int:pk>/download', views.SubmissionDownloadView.as_view(), name="submission_download"),
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/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>/upload', views.ResourceUploadView.as_view(), name="resource_upload"),
path('projects/<int:project>/resources/<int:pk>/edit', views.ResourceEditView.as_view(), name="resource_edit"),
path('projects/<int:project>/resources/<int:pk>/complete', views.ResourceCompleteView.as_view(), name="resource_complete"),
]

View File

@ -1,23 +1,16 @@
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 import TemplateView, RedirectView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView, FormView
from django.views.generic.base import ContextMixin
from django.http import HttpResponseRedirect
from django.views.generic.edit import CreateView, UpdateView
from django.core.exceptions import SuspiciousOperation
from django.core.signing import Signer
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
from base64 import b64decode
import logging
logger = logging.getLogger(__name__)
@ -36,6 +29,11 @@ class EnsembleMixin(object):
request.ensemble_id = request.session.get('ensemble')
request.is_admin = request.user.is_superuser
if request.is_admin:
if request.ensemble_id is None:
return redirect('ensemble_list')
return super().dispatch(request, *args, **kwargs)
if 'auth' in request.GET:
sig = signer.sign(request.path)
if sig[len(request.path)+1:] == request.GET['auth']:
@ -45,15 +43,15 @@ class EnsembleMixin(object):
else:
raise SuspiciousOperation("Bad auth code")
if not request.ensemble_id:
return redirect('register')
if not request.is_admin and request.user.is_authenticated:
if request.user.is_authenticated:
try:
request.user.ensembles.get(pk=request.ensemble_id)
request.is_admin = True
except models.Ensemble.DoesNotExist:
pass
if not request.ensemble_id:
return redirect('register')
if self.admin_required and not request.is_admin:
return redirect('login')
@ -64,6 +62,11 @@ class EnsembleMixin(object):
def ensemble(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
#def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# context['ensemble'] = self.ensemble
# return context
class ProjectMixin(EnsembleMixin):
def get_project(self):
@ -81,57 +84,9 @@ class ProjectMixin(EnsembleMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['project'] = self.get_project()
context['modules'] = context['project'].modules.values_list('name', flat=True)
return context
class S3UploadMixin(ProjectMixin):
accept_files = ''
def get_accept_files(self):
return self.accept_files
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()
context['accept_files'] = self.accept_files
return context
class S3CompleteView(SingleObjectMixin, RedirectView):
def complete(self, key):
self.object.key = key
self.object.save()
def get(self, request, *args, **kwargs):
self.object = self.get_object()
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)
if bucket != models.BUCKET:
key = uri.path[1:]
self.complete(key)
else:
raise KeyError("No key or location found")
return super().get(request, *args, **kwargs)
def register(request):
if 'clear' in request.GET:
@ -139,16 +94,13 @@ def register(request):
request.ensemble_id = request.session.get('ensemble')
registered = request.session.setdefault('registered', {})
registered = request.session.setdefault('registered', [])
code = request.GET.get('code', '').replace('-', '')
print("Registering with code %s", code)
# check if already joined
if code in registered:
request.session['ensemble'] = registered[code]
return redirect('ensemble_detail')
if request.user.is_superuser and code:
if code in registered or request.user.is_superuser:
request.session['ensemble'] = models.Ensemble.objects.get(code=code).pk
return redirect('ensemble_detail')
@ -162,7 +114,7 @@ def register(request):
ensemble = models.Ensemble.objects.get(code=data['code'].replace('-', ''))
if ensemble.passphrase.lower() == data['passphrase'].lower():
request.session['ensemble'] = ensemble.pk
registered[ensemble.code] = ensemble.pk
registered.append(ensemble.code)
return redirect('ensemble_detail')
except models.Ensemble.DoesNotExist:
form.add_error(None, "Incorrect code or passphrase")
@ -175,18 +127,17 @@ def register(request):
if request.user.is_superuser:
current = models.Ensemble.objects.all()
else:
current = models.Ensemble.objects.filter(pk__in=registered.values())
current = models.Ensemble.objects.filter(pk__in=registered)
return render(request, 'interface/register.html', {'form': form, 'current': current})
def on_login(sender, **kwargs):
user = kwargs['user']
request = kwargs['request']
registered = request.session.get('registered', {})
registered = request.session.get('registered', [])
for e in user.ensembles.all():
if not e.code in registered:
registered[e.code] = e.pk
registered.append(e.code)
request.session['registered'] = registered
auth.signals.user_logged_in.connect(on_login)
@ -198,6 +149,39 @@ def logout(request):
request.session['registered'] = registered
return redirect('/')
class EnsembleForgetView(EnsembleMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs):
registered = self.request.session.setdefault('registered', [])
ensemble = models.Ensemble.objects.get(pk=self.kwargs['pk'])
try:
registered.remove(ensemble.code)
self.request.session['registered'] = registered
except KeyError:
pass
if self.request.ensemble_id == ensemble.pk:
del(self.request.session['ensemble'])
return resolve_url('ensemble_detail')
class EnsembleListView(ListView):
def get_queryset(self):
registered = self.request.session.get('registered', [])
if self.request.user.is_superuser:
current = models.Ensemble.objects.all()
else:
current = models.Ensemble.objects.filter(code__in=registered)
return current
#def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# context['ensemble_id'] = self.request.session.get('ensemble')
# return context
class EnsembleProjectListView(EnsembleMixin, DetailView):
template_name = 'interface/ensemble_project_list.html'
@ -212,6 +196,12 @@ class EnsembleProjectListView(EnsembleMixin, DetailView):
def get_object(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
if self.request.is_admin:
data['ensemble_url'] = self.request.build_absolute_uri('/?code={0}'.format(self.ensemble.ensemble_code()))
return data
class EnsembleDetailView(DetailView):
model = models.Ensemble
@ -220,30 +210,54 @@ class ProjectDetailView(ProjectMixin, DetailView):
def get_object(self):
return self.get_project()
class ProjectMakefileView(EnsembleMixin, DetailView):
template_name = 'interface/project_submissions.mk'
content_type = 'text/plain'
def get_queryset(self):
if self.request.is_admin:
return models.Project.objects.all()
class ProjectCreateView(EnsembleMixin, CreateView):
model = models.Project
template_name = "interface/default_form.html"
title = "Add a new project"
form_class = forms.ProjectForm
return models.Project.objects.filter(ensemble=self.request.ensemble_id)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.ensemble_id = self.request.ensemble_id
self.object.owner = self.request.user
self.object.save()
return redirect('project_detail', project=self.object.pk)
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
class ProjectUpdateView(EnsembleMixin, UpdateView):
model = models.Project
template_name = "interface/default_form.html"
#fields = ['name', 'description', 'event_date', 'enable_library', 'enable_submissions', 'active']
pk_url_kwarg = 'project'
form_class = forms.ProjectForm
data['submissions'] = []
data['targets'] = []
for s in self.object.submissions:
name = s.short_name
data['targets'].append(name)
data['submissions'].append({
'url': self.request.build_absolute_uri(signed_url('submission_download', project=self.kwargs['pk'], pk=s.pk)),
'name': name,
})
def get_success_url(self):
return resolve_url('project_detail', project=self.kwargs['project'])
return data
#class ProjectMakefileView(EnsembleMixin, DetailView):
# template_name = 'interface/project_submissions.mk'
# content_type = 'text/plain'
#
# def get_queryset(self):
# if self.request.is_admin:
# return models.Project.objects.all()
#
# return models.Project.objects.filter(ensemble=self.request.ensemble_id)
#
# def get_context_data(self, **kwargs):
# data = super().get_context_data(**kwargs)
#
# data['submissions'] = []
# data['targets'] = []
# for s in self.object.submissions:
# name = s.short_name
# data['targets'].append(name)
# data['submissions'].append({
# 'url': self.request.build_absolute_uri(signed_url('submission_download', project=self.kwargs['pk'], pk=s.pk)),
# 'name': name,
# })
#
# return data
class WikiView(ProjectMixin, DetailView):
template_name = 'interface/wiki.html'
@ -258,123 +272,22 @@ class WikiCreateView(ProjectMixin, CreateView):
admin_required = True
model = models.WikiPage
fields = ['title', 'markdown']
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.project = self.get_project()
self.object.save()
return redirect('wiki', project=self.object.project_id, pk=self.object.pk)
class WikiEditView(ProjectMixin, UpdateView):
admin_required = True
model = models.WikiPage
fields = ['title', 'markdown']
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):
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
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 SubmissionCompleteView(ProjectMixin, S3CompleteView):
model = models.Submission
def complete(self, key):
self.object.url = key
self.object.private = False
self.object.complete = True
self.object.save()
def get_redirect_url(self, **kwargs):
return resolve_url('submission_detail', **self.kwargs)
class SubmissionDownloadView(ProjectMixin, SingleObjectMixin, RedirectView):
model = models.Submission
admin_required = True
def get_redirect_url(self, **kwargs):
return self.get_object().download_url
class SubmissionDetailView(ProjectMixin, DetailView):
model = models.Submission
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['can_download'] = self.request.is_admin
return context
class SubmissionPreview(ProjectMixin, DetailView):
model = models.Submission
template_name = 'interface/submission_preview.html'
admin_required = True
class SubmissionUploadView(S3UploadMixin, DetailView):
template_name = 'interface/s3_upload.html'
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
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
return redirect('project_detail', pk=kwargs['project'])
class SubmissionListView(ProjectMixin, ListView):
model = models.Submission
admin_required = True
def get_queryset(self):
return super().get_queryset().filter(complete=True).order_by('-pk')
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data['signed_url'] = self.request.build_absolute_uri(signed_url('project_makefile', pk=self.kwargs['project']))
return data
class ResourceCreateView(ProjectMixin, CreateView):
model = models.Resource
fields = ['name', 'media_type', 'description']
form_class = forms.ResourceForm
template_name = 'interface/project_form.html'
title = "Add a new resource"
admin_required = True
@ -385,26 +298,15 @@ class ResourceCreateView(ProjectMixin, CreateView):
self.object.save()
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk)
class ResourceUploadView(S3UploadMixin, DetailView):
class ResourceUploadView(ProjectMixin, UpdateView):
admin_required = True
model = models.Resource
template_name = 'interface/s3_upload.html'
def get_accept_files(self):
return self.object.accept()
fields = ['file']
template_name = 'interface/default_form.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(ProjectMixin, S3CompleteView):
model = models.Resource
def get_redirect_url(self, **kwargs):
return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceListView(ProjectMixin, ListView):
model = models.Resource

View File

@ -2,38 +2,43 @@ from django.contrib import admin
from . import models
#class OrchestrationAdmin(admin.ModelAdmin):
# list_display = ['name', 'ensemble']
# list_filter = ['ensemble']
#admin.site.register(models.Orchestration, OrchestrationAdmin)
class EnsembleAccessInline(admin.StackedInline):
model = models.EnsembleAccess
extra = 0
class CollectionAdmin(admin.ModelAdmin):
list_display = ['name', 'location']
list_display = ['name', 'location', 'storage', 'prefix']
inlines = [EnsembleAccessInline]
admin.site.register(models.Collection, CollectionAdmin)
class ItemInline(admin.TabularInline):
model = models.Item
model = models.ProjectItem
extra = 0
class DocInline(admin.TabularInline):
model = models.Document
extra = 0
class MetaInline(admin.TabularInline):
model = models.WorkMeta
extra = 0
class WorkAdmin(admin.ModelAdmin):
list_display = ['name', 'edition', 'composer', 'running_time']
list_display = ['name', 'composer', 'edition', 'identifier', 'running_time']
list_filter = ['collection']
inlines = [DocInline, ItemInline]
inlines = [MetaInline, DocInline, ItemInline]
admin.site.register(models.Work, WorkAdmin)
class PartInline(admin.TabularInline):
model = models.Part
class SectionInline(admin.TabularInline):
model = models.Section
fields = ['tag', 'start', 'end']
class DocumentAdmin(admin.ModelAdmin):
list_display = ['work', '__str__']
list_filter = ['work__collection']
inlines = [PartInline]
inlines = [SectionInline]
admin.site.register(models.Document, DocumentAdmin)
@ -41,7 +46,7 @@ class ItemAdmin(admin.ModelAdmin):
list_display = ['project', 'work', 'order']
list_filter = ['project']
admin.site.register(models.Item, ItemAdmin)
admin.site.register(models.ProjectItem, ItemAdmin)
class EnsembleAccessAdmin(admin.ModelAdmin):
list_display = ['ensemble', 'collection', 'access_type']

View File

@ -1,15 +1,17 @@
from django import forms
from .models import Work
from interface.models import Project
from interface.widgets import DatePickerInput
from django.db.models import Q
from interface.forms import BaseForm
class WorkCreateForm(forms.ModelForm):
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False)
class WorkCreateForm(forms.ModelForm, BaseForm):
#uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False)
class Meta:
model = Work
fields = ['uploads', 'name', 'composer', 'edition', 'collection', 'code', 'running_time', 'notes']
fields = ['name', 'code', 'running_time', 'notes']
class PlaylistAddForm(forms.Form):
work = forms.ModelChoiceField(queryset=Work.objects.all())

View File

@ -1,6 +1,6 @@
# Generated by Django 3.1.1 on 2021-04-28 03:53
# Generated by Django 3.2.7 on 2022-11-18 09:54
import byostorage.cached
import byostorage.user
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
@ -12,9 +12,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('interface', '0027_auto_20210322_1154'),
('byostorage', '0003_auto_20210323_1047'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('byostorage', '0004_alter_userstorage_storage'),
('interface', '0001_initial'),
]
operations = [
@ -22,11 +22,11 @@ class Migration(migrations.Migration):
name='Collection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('location', models.CharField(help_text='Physical location', max_length=100)),
('notes', models.TextField(blank=True)),
('name', models.CharField(help_text='Name of the collection', max_length=255)),
('prefix', models.SlugField(default='default', max_length=30)),
('location', models.CharField(help_text='Physical location (institution, town...)', max_length=100)),
('notes', models.TextField(blank=True, help_text='Publicly visible notes about collection and loans policy')),
('administrators', models.ManyToManyField(help_text='Administrators for this collection', related_name='collections', to=settings.AUTH_USER_MODEL)),
('ensembles', models.ManyToManyField(related_name='collections', to='interface.Ensemble')),
('storage', models.ForeignKey(blank=True, help_text='Storage for documents', null=True, on_delete=django.db.models.deletion.CASCADE, to='byostorage.userstorage')),
],
),
@ -35,65 +35,71 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('doctype', models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source')], default=1)),
('upload', models.FileField(storage=byostorage.cached.CachedStorage(), upload_to=library.models.doc_upload_filename)),
('upload', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=library.models.doc_upload_filename)),
('created', models.DateTimeField(auto_now_add=True)),
('version', models.CharField(blank=True, max_length=30)),
],
),
migrations.CreateModel(
name='Item',
name='ProjectItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('checkout', models.DateTimeField()),
('due', models.DateTimeField(blank=True, null=True)),
('returned', models.DateTimeField(blank=True, null=True)),
('order', models.SmallIntegerField(default=0)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='interface.project')),
('approved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='interface.project')),
],
options={
'ordering': ['order', 'work'],
},
),
migrations.CreateModel(
name='Orchestration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('instruments', models.TextField()),
('ensemble', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orchestrations', to='interface.ensemble')),
],
),
migrations.CreateModel(
name='Work',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(editable=False, max_length=100)),
('name', models.CharField(max_length=255)),
('version', models.CharField(blank=True, help_text='Version or edition details', max_length=100)),
('composer', models.CharField(blank=True, help_text='Use Composer / Arranger format', max_length=255)),
('edition', models.CharField(blank=True, help_text='Edition details', max_length=255)),
('composer', models.CharField(blank=True, max_length=255)),
('orchestration', models.CharField(blank=True, help_text='IMDB format instrumentation', max_length=255)),
('original_parts', models.JSONField(blank=True, help_text='Original printed parts', null=True)),
('code', models.CharField(blank=True, help_text='Collection specific code or number', max_length=100)),
('licence', models.PositiveSmallIntegerField(choices=[(2, 'Public Domain'), (4, 'Copyright Expired'), (6, 'Copyrighted'), (10, 'Internal use only')], default=6, help_text='Copyright status')),
('max_loans', models.BooleanField(default=1, help_text='How many projects can this work be attached to')),
('running_time', models.IntegerField(blank=True, help_text='Running time in seconds', null=True)),
('max_projects', models.IntegerField(default=1, help_text='How many projects can this work be attached to')),
('running_time', models.DurationField(blank=True, help_text='Running time in seconds', null=True)),
('notes', models.TextField(blank=True)),
('tag_list', models.CharField(blank=True, help_text='Multiple tags for the work', max_length=255)),
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.collection')),
('orchestration', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.orchestration')),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='library.collection')),
('parent', models.ForeignKey(blank=True, help_text='Arrangement of another work or part of an anthology', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='related_works', to='library.work')),
('projects', models.ManyToManyField(help_text='Current usage', related_name='works', through='library.Item', to='interface.Project')),
('projects', models.ManyToManyField(help_text='Current usage', related_name='works', through='library.ProjectItem', to='interface.Project')),
],
),
migrations.CreateModel(
name='Part',
name='WorkMeta',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(choices=[('tag', 'Tag'), ('arr', 'Arranger'), ('lyrics', 'Lyracist'), ('genre', 'Genre'), ('style', 'Style')], max_length=20)),
('value', models.CharField(max_length=255)),
('work', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_info', to='library.work')),
],
),
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tag', models.SlugField(max_length=20)),
('start', models.SmallIntegerField(blank=True, null=True)),
('end', models.SmallIntegerField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('doc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='library.document')),
('doc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sections', to='library.document')),
],
options={
'ordering': ['doc', 'start', 'pk'],
},
),
migrations.AddField(
model_name='item',
model_name='projectitem',
name='work',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_items', to='library.work'),
),
@ -102,9 +108,12 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access_type', models.PositiveSmallIntegerField(choices=[(1, 'Unlimited'), (2, 'Approval required')], default=2)),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.collection')),
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='interface.ensemble')),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_ensembles', to='library.collection')),
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_collections', to='interface.ensemble')),
],
options={
'verbose_name_plural': 'Ensemble access',
},
),
migrations.AddField(
model_name='document',

View File

@ -1,34 +0,0 @@
# Generated by Django 3.1.1 on 2021-05-04 08:30
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('library', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='item',
name='approved_by',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='auth.user'),
preserve_default=False,
),
migrations.AddField(
model_name='item',
name='checkin',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='item',
name='checkout',
field=models.DateTimeField(default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 3.1.1 on 2021-05-04 08:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('library', '0002_auto_20210504_1830'),
]
operations = [
migrations.AlterField(
model_name='work',
name='max_loans',
field=models.IntegerField(default=1, help_text='How many projects can this work be attached to'),
),
migrations.AlterField(
model_name='work',
name='orchestration',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.orchestration'),
),
]

View File

@ -1,44 +0,0 @@
# Generated by Django 3.1.1 on 2021-05-04 23:27
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('library', '0003_auto_20210504_1845'),
]
operations = [
migrations.RenameField(
model_name='item',
old_name='checkin',
new_name='due',
),
migrations.AddField(
model_name='item',
name='returned',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='collection',
name='location',
field=models.CharField(help_text='Physical location (institution, town...)', max_length=100),
),
migrations.AlterField(
model_name='collection',
name='name',
field=models.CharField(help_text='Often just the name of the owning ensemble', max_length=255),
),
migrations.AlterField(
model_name='collection',
name='notes',
field=models.TextField(blank=True, help_text='Publicly visible notes about collection and loans policy'),
),
migrations.AlterField(
model_name='work',
name='collection',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='library.collection'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2021-05-06 02:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0004_auto_20210505_0927'),
]
operations = [
migrations.AddField(
model_name='work',
name='parts',
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -1,48 +0,0 @@
# Generated by Django 3.2.7 on 2021-09-02 03:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('interface', '0028_ensemble_details'),
('library', '0005_work_parts'),
]
operations = [
migrations.AlterModelOptions(
name='ensembleaccess',
options={'verbose_name_plural': 'Ensemble access'},
),
migrations.RemoveField(
model_name='collection',
name='ensembles',
),
migrations.AddField(
model_name='document',
name='version',
field=models.CharField(blank=True, max_length=30),
),
migrations.AddField(
model_name='item',
name='version',
field=models.CharField(blank=True, help_text='Limited to specific version tag', max_length=30),
),
migrations.AlterField(
model_name='collection',
name='name',
field=models.CharField(help_text='Name of the collection', max_length=255),
),
migrations.AlterField(
model_name='ensembleaccess',
name='collection',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_ensembles', to='library.collection'),
),
migrations.AlterField(
model_name='ensembleaccess',
name='ensemble',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_collections', to='interface.ensemble'),
),
]

View File

@ -1,33 +0,0 @@
# Generated by Django 3.2.7 on 2021-09-02 04:07
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('library', '0006_auto_20210902_1355'),
]
operations = [
migrations.RemoveField(
model_name='work',
name='orchestration',
),
migrations.RemoveField(
model_name='work',
name='version',
),
migrations.AddField(
model_name='document',
name='created',
field=models.DateTimeField(auto_created=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='work',
name='edition',
field=models.CharField(blank=True, help_text='Edition details', max_length=100),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.7 on 2021-09-02 04:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0007_auto_20210902_1407'),
]
operations = [
migrations.AddField(
model_name='work',
name='orchestration',
field=models.CharField(blank=True, help_text='IMDB format instrumentation', max_length=255),
),
]

View File

@ -5,162 +5,23 @@ from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.functional import cached_property
from django.core.files.storage import get_storage_class
from django.db.models import Q
from django.db.models import Q, Count, Min, Max
import re
from byostorage.user import BYOStorage
from .imslp import Instrument
import logging
#from polyphonic.settings import LIBRARY_STORAGE
logger = logging.getLogger(__name__)
try:
library_storage = get_storage_class(settings.LIBRARY_STORAGE)()
except (ImportError, AttributeError):
library_storage = get_storage_class()()
logger.info("Library storage: %s", library_storage.__class__.__name__)
#try:
# library_storage = get_storage_class(settings.LIBRARY_STORAGE)()
#except (ImportError, AttributeError):
# logger.exception("Failed to load library storage")
# library_storage = get_storage_class()()
#logger.info("Library storage: %s", library_storage.__class__.__name__)
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_Instruments
ABBREVIATIONS = """
score Score
acc Accordion
afl Alto flute
alt Alto (voice) (contralto)
arp Arpeggione
bag Bagpipe
bar Baritone (voice)
bass Bass (voice)
bbar Bass baritone (voice)
bc Continuo (Basso continuo)
bcl Bass clarinet
bell Bell (Chimes)
bfl Bass flute
bgtr Bass guitar
bjo Banjo
bn Bassoon
bob Bass oboe (Baritone oboe)
br Brass instruments
bryt Baryton
bstcl Basset clarinet
bsthn Basset horn
bug Bugle
cbcl Contrabass clarinet
cbn Contrabassoon
cch Children's chorus
cel Celesta
ch Mixed chorus
cimb Cimbalom
cit Cittern
cl Clarinet
clvd Clavichord
cm Chalumeau
conc Concertina
crh Crumhorn
crt Cornet
crtt Cornett (Zink)
cv Child's voice
db Double Bass
dlcn Dulcian
dom Domra
dulc Dulcimer
egtr Electric guitar
eh English horn (Cor anglais)
elec Electronic Instruments
epf Electric piano
eq Equal voices
erhu Erhu
euph Euphonium
fch Female chorus
fda Flute d'amore (Tenor flute)
fgh Flugelhorn
fife Fife
fl Flute
flag Flageolet
ghca Glass harmonica (Bowl organ)
gl Glockenspiel
gtr Guitar
harm Harmonium
hca Harmonica (Mouth Organ)
heck Heckelphone
hn Horn
hp Harp
hpd Harpsichord
kbd Keyboard instrument
lute Lute
lyre Lyre
mand Mandolin
mar Marimba
mch Male chorus
mez Mezzo-soprano
mus Musette
nar Narrator (Reciter)
ob Oboe
oca Ocarina
oda Oboe d'amore
om Ondes Martenot
oph Ophicleide
orch Orchestra
org Organ
oud Oud
pan Pan flute (Pan-pipes)
perc Percussion
pf Piano
pf3h Piano 3 hands
pf4h Piano 4 hands
pf5h Piano 5 hands
pf6h Piano 6 hands
pflh Piano left hand
pfped Pedal piano
pfrh Piano right hand
picc Piccolo
pipa Pipa
pk Timpani
ptpt Piccolo trumpet
reb Rebec
rec Recorder
sar Sarrusophone
sax Saxophone
sheng Sheng
shw Shawm
sit Sitar
skbt Sackbut
sop Soprano (voice)
srp Serpent
stpt Slide trumpet
str String instruments
sxh Saxhorn
syn Synthesizer
tba Tuba
tbn Trombone
ten Tenor
thrm Theremin
timp Timpani
tpt Trumpet
uch Unison chorus
uke Ukelele (Ukulele)
v Voice (solo)
va Viola
vap Viola pomposa
vc Cello
vda Viola d'amore
vib Vibraphone
vie Vielle (Hurdy-Gurdy)
viol Viol (Viola da gamba)
vlne Violone
vn Violin
vuv Vuvuzela
vv Voices (multiple soloists)
wag Wagner tuba
ww Woodwind instruments
xiao Xiao
xyl Xylophone
zith Zither
"""
INSTRUMENTS = []
for line in ABBREVIATIONS.split('\n'):
parts = line.strip().split(maxsplit=1)
if len(parts) < 2: continue
name, _, _ = parts[1].partition('(')
INSTRUMENTS.append((parts[0], name))
'''
ORCHESTRATIONS = {
@ -194,12 +55,6 @@ ACCESS_TYPES = [
(2, 'Approval required'),
]
def tag_to_instrument(tag):
m = re.match(r'([A-Za-z]+)(\d*)', tag)
if not m:
return tag
l = m.groups()
return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip()
'''
class Orchestration(models.Model):
@ -223,37 +78,44 @@ class Orchestration(models.Model):
return self.name
'''
class Item(models.Model):
class ProjectItem(models.Model):
"""
Item represents a specic version of a Work in a Project e.g. item in set list or programme
ProjectItem represents a Work attached to a Project e.g. item in set list or programme
It also allows works to be shared from one ensemble to another on a per-project basis.
"""
project = models.ForeignKey('interface.Project', on_delete=models.CASCADE)
project = models.ForeignKey('interface.Project', on_delete=models.CASCADE, related_name='items')
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name='project_items')
checkout = models.DateTimeField()
due = models.DateTimeField(null=True, blank=True)
returned = models.DateTimeField(null=True, blank=True)
approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
order = models.SmallIntegerField(default=0)
version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag")
section = models.SlugField
#version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag")
class Meta:
ordering = ['order', 'work']
def __str__(self):
return f"<{self.project.slug}:{self.work.slug}>"
return f"<{self.project_id}:{self.work.slug}>"
class Collection(models.Model):
"""
Storage location for works (physical or virtual)
A logical collection of works, typically owned by an organisation or person (physical or virtual)
"""
name = models.CharField(max_length=255, help_text="Name of the collection")
administrators = models.ManyToManyField('auth.User', related_name="collections", help_text="Administrators for this collection")
location = models.CharField(max_length=100, help_text="Physical location (institution, town...)")
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True, help_text="Storage for documents")
notes = models.TextField(blank=True, help_text="Publicly visible notes about collection and loans policy")
#ensembles = models.ManyToManyField('interface.Ensemble', related_name="collections", through='EnsembleAccess')
name = models.CharField(max_length=255,
help_text="Name of the collection")
prefix = models.SlugField(max_length=30, default="default",
help_text="Folder to store works in")
administrators = models.ManyToManyField('auth.User', related_name="collections",
help_text="Administrators for this collection")
location = models.CharField(max_length=100,
help_text="Physical location (institution, town...)", blank=True)
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True,
help_text="User storage for documents")
notes = models.TextField(blank=True,
help_text="Publicly visible notes about collection and loans policy (markdown format)")
def __str__(self):
return self.name
@ -268,61 +130,69 @@ class EnsembleAccess(models.Model):
class Meta:
verbose_name_plural = "Ensemble access"
META_TAGS = (
('tag', 'Tag'),
('arr', 'Arranger'),
('lyrics', 'Lyracist'),
('genre', 'Genre'),
('style', 'Style'),
('orchestration', 'Orchestration'),
)
class Work(models.Model):
"""
A musical work 'owned' by a collection from a licencing perspective.
"""
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works", help_text="Owner")
slug = models.SlugField(max_length=100, editable=False)
name = models.CharField(max_length=255)
edition = models.CharField(max_length=100, blank=True, help_text="Edition details")
slug = models.SlugField(max_length=100, editable=False,
help_text="Used as folder name")
name = models.CharField(max_length=255, help_text="Original name of the work")
edition = models.CharField(max_length=255, blank=True,
help_text="Edition details to distinguish multiple versions")
parent = models.ForeignKey('Work', null=True, blank=True, on_delete=models.SET_NULL, related_name="related_works",
help_text="Arrangement of another work or part of an anthology")
composer = models.CharField(max_length=255, blank=True, help_text="Use Composer / Arranger format")
#orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works', blank=True)
orchestration = models.CharField(max_length=255, blank=True, help_text="IMDB format instrumentation")
parts = models.JSONField(null=True, blank=True)
composer = models.CharField(max_length=255, blank=True,
help_text="Surname, First Name/Initials")
original_parts = models.JSONField(default=dict, blank=True, help_text="Original printed parts (IMSLP format)")
# Collection details
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works")
code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number")
licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status")
max_loans = models.IntegerField(default=1, help_text="How many projects can this work be attached to")
max_projects = models.IntegerField(default=1, help_text="How many active projects can this work be attached to")
# Extra info
running_time = models.IntegerField(null=True, blank=True, help_text="Running time in seconds")
running_time = models.DurationField(null=True, blank=True, help_text="Running time in seconds")
notes = models.TextField(blank=True)
tag_list = models.CharField(max_length=255, blank=True, help_text="Multiple tags for the work")
projects = models.ManyToManyField('interface.Project', through='Item', related_name="works", help_text="Current usage")
@property
def duration(self):
if self.running_time is None:
return "-:--"
return "{0:d}:{1:02d}".format(int(self.running_time / 60), self.running_time % 60)
# Allocation to projects
projects = models.ManyToManyField('interface.Project', through='ProjectItem', related_name="works", help_text="Current usage")
@property
def tags(self):
return self.tag_list.split(';') if self.tag_list else []
def extract(self, *tags):
@tags.setter
def set_tags(self, tags):
self.tag_list = ";".join(tags)
qs = self.docs.filter(sections__tag__in=tags)
qs = qs.annotate(Count('sections'), end=Min('sections__end'), start=Max('sections__start')) \
.filter(sections__count=len(tags))
return list(qs.values_list('upload', 'start', 'end'))
@property
def digital_parts(self):
return Part.objects.filter(doc__work=self.pk)
return Section.objects.filter(doc__work=self.pk)
@property
def physical_parts(self):
if not self.parts:
if not self.original_parts:
return []
return [ (tag_to_instrument(k), v) for (k, v) in self.parts.items() ]
return [ (Instrument.from_tag(k), v) for (k, v) in self.original_parts.items() ]
#@property
#def instruments(self):
# return self.orchestration.as_list()
@property
def tags(self):
return self.meta_info.filter(name='tag').values_list('value', flat=True)
@property
def meta(self):
return self.meta_info.exclude(name='tag')
def save(self, *args, **kwargs):
if not self.slug:
@ -346,30 +216,47 @@ class Work(models.Model):
@property
def is_available(self):
if self.max_loans < 0:
if self.max_projects < 0:
return True
return self.max_loans > self.loans
return self.max_projects > self.loans
@property
def available(self):
if self.max_loans < 0:
if self.max_projects < 0:
return 'Unlimited'
a = self.max_loans - self.loans
return '{0} of {1}'.format(max(a, 0), self.max_loans)
a = self.max_projects - self.loans
return '{0} of {1}'.format(max(a, 0), self.max_projects)
@property
def identifier(self):
return f"{self.collection.pk:03d}-{self.pk:03d}"
if self.code:
return self.code;
composer = self.composer or "Anon"
words = self.name.split()
if len(words) > 2:
work = ''.join([ x[0] for x in self.name.split() ])
else:
work = words[0][:3]
return f"{composer[:4]}-{work}-{self.pk:03d}".upper()
def __str__(self):
return f"{self.name} ({self.composer})"
class WorkMeta(models.Model):
work = models.ForeignKey(Work, on_delete=models.CASCADE, related_name='meta_info')
name = models.SlugField(max_length=20, choices=META_TAGS)
value = models.CharField(max_length=255)
def doc_upload_filename(doc, filename):
storage = doc.work.collection.storage
collection = doc.work.collection
storage = collection.storage
if not storage:
raise RuntimeError("Collection has no storage attached")
return f'{storage}:works/{doc.work.slug}-{doc.work.pk}/{filename}'
return f'{storage}:library/{collection.prefix}/{doc.work.slug}-{doc.work.pk}/{filename}'
class Document(models.Model):
"""
@ -377,29 +264,28 @@ class Document(models.Model):
"""
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1)
upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage)
upload = models.FileField(upload_to=doc_upload_filename, storage=BYOStorage())
created = models.DateTimeField(auto_now_add=True)
version = models.CharField(max_length=30, blank=True)
def __str__(self):
return self.upload.name
class Part(models.Model):
class Section(models.Model):
"""
Part is a tagged portion of a Document
Section is a tagged portion of a Document
"""
doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="parts")
tag = models.SlugField(max_length=20)
doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="sections")
tag = models.CharField(max_length=50)
start = models.SmallIntegerField(null=True, blank=True)
end = models.SmallIntegerField(null=True, blank=True)
notes = models.TextField(blank=True)
class Meta:
ordering = ['doc', 'start', 'pk']
@property
def instrument(self):
return tag_to_instrument(self.tag)
return Instrument.from_tag(self.tag)
@property
def filename(self):

View File

@ -1,57 +1,131 @@
{% extends "interface/project_base.html" %}
{% block admin %}
<a href="#" onclick="saveTags()"><i class="fas fa-save"></i> Save</a>&nbsp;
<a href="#" onclick="saveTags()" class="button is-link">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save</span>
</a>
<a href="{% url 'work_detail' pk=object.work.pk %}" class="button is-link is-light">
<span>Cancel</span>
</a>
{% endblock %}
{% block media %}
<style>
.tag-grid {
display: flex;
}
.grid-column {
margin-left: 1em;
margin-right: 1em;
}
.grid-page {
height: 25px;
margin-bottom: 10px;
border: 1px solid #999;
border-radius: 5px;
text-align: center;
width: 50px;
cursor: pointer;
}
.grid-page.is-active {
background-color: var(--primary);
color: white;
}
.grid-tag {
border: 2px solid var(--primary);
background-color: rgba(240, 240, 240, 0.5);
border-radius: 5px;
padding-left: 1em;
padding-right: 1em;
margin-bottom: 5px;
min-width: 200px;
position: absolute;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
#tag-area {
min-width: 220px;
}
</style>
{% endblock %}
{% block page %}
<h2><a href="{% url 'work_list' %}">Works</a> / <a href="{% url 'work_detail' document.work.pk %}">{{ document.work.name }}</a></h2>
<div id="annotation-area">
<div style="display: flex">
<canvas id="inline-viewer" style="width: 600px;"></canvas>
<div style="margin: 0px 20px">
<div>
<div style="text-align: center;">
<p>Page: <span id="page_num"></span> / <span id="page_count"></span></p>
</div>
<button id="prev"><i class="fas fa-backward"></i></button>
<button id="next"><i class="fas fa-forward"></i></button>
</div>
<div id="tag-list" style="cursor: pointer; margin-top: 20px;">
<div id="tag-None" data-tag="None">No tag</div>
<div id="tag-Score" data-tag="Score">Score</div>
{% for instrument in document.work.instruments %}
<div id="tag-{{ instrument.0 }}" data-tag="{{ instrument.0 }}">{{ instrument.1 }}</div>
{% endfor %}
</div>
<h3 class="subtitle"><a href="{% url 'work_detail' document.work.pk %}">{{ document.work.name }}</a></h3>
<div id="annotation-area" class="columns is-centered">
<div class="column is-narrow">
<div class="has-text-centered">
<div class="level">
<div class="level-left">
<div class="level-item">
<p>Page <span id="page-num">-</span> / <span id="page-count">-</span></p>
</div>
</div>
<div class="level-right">
<div class="level-item">
<div class="field has-addons">
<span class="control">
<input type="text" class="input" list="instrument-list" id="add-instrument-name"/>
<datalist id="instrument-list">
{% for inst in json_data.instruments.values %}
<option value="{{inst}}"/>
{% endfor %}
</datalist>
</span>
<span class="control">
<input type="number" class="input" max="4" min="1" size="3" id="add-instrument-variant"/>
</span>
<span class="control">
<button class="button is-primary" onclick="addInstrument()">Add</button>
</span>
</div>
</div>
</div>
</div>
<div class="box" style="display: inline-block;">
<canvas id="inline-viewer" style="width: 500px;"></canvas>
</div>
</div>
</div>
<div class="column is-narrow">
<div class="tag-grid">
<div class="grid-column" id="page-list">
</div>
<div class="grid-column" id="tag-area">
</div>
</div>
</div>
</div>
<p>{{ document.upload.name }}</p>
{% endblock %}
{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.min.js" integrity="sha512-g4FwCPWM/fZB1Eie86ZwKjOP+yBIxSBM/b2gQAiSVqCgkyvZ0XxYPDEcN2qqaKKEvK6a05+IPL1raO96RrhYDQ==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.min.js"
integrity="sha512-g4FwCPWM/fZB1Eie86ZwKjOP+yBIxSBM/b2gQAiSVqCgkyvZ0XxYPDEcN2qqaKKEvK6a05+IPL1raO96RrhYDQ=="
crossorigin="anonymous"></script>
{{ json_data|json_script:"data" }}
<script type="text/javascript">
let url = "{{ document.upload.url|safe }}";
let url = "{{ document.upload.url|safe }}";
// Loaded via <script> tag, create shortcut to access PDF.js exports.
let pdfjsLib = window['pdfjs-dist/build/pdf'];
// Loaded via <script> tag, create shortcut to access PDF.js exports.
let pdfjsLib = window['pdfjs-dist/build/pdf'];
// The workerSrc property shall be specified.
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js';
// The workerSrc property shall be specified.
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js';
// get current page tags
let data = JSON.parse(document.getElementById('data').textContent);
let currentTags = document.getElementById('current-tags');
var selectedTag = document.getElementById('tag-None');
var dirty = false;
// get current page tags
let data = JSON.parse(document.getElementById('data').textContent);
let tagArea = document.getElementById('tag-area');
var dirty = false;
document.getElementById('tag-list').onclick = (e) => setTag(e.target.dataset.tag);
//document.getElementById('tag-list').onclick = (e) => setTag(e.target.dataset.tag);
var pdfDoc = null,
var pdfDoc = null,
pageNum = 1,
pageRendering = false,
pageNumPending = null,
@ -59,135 +133,257 @@ var pdfDoc = null,
canvas = document.getElementById('inline-viewer'),
ctx = canvas.getContext('2d');
/**
* Get page info from document, resize canvas accordingly, and render page.
* @param num Page number.
*/
function renderPage(num) {
pageRendering = true;
// Using promise to fetch the page
pdfDoc.getPage(num).then(function(page) {
var viewport = page.getViewport({scale: scale});
canvas.height = viewport.height;
canvas.width = viewport.width;
/**
* Get page info from document, resize canvas accordingly, and render page.
* @param num Page number.
*/
function renderPage(num) {
pageRendering = true;
// Using promise to fetch the page
pdfDoc.getPage(num).then(function (page) {
var viewport = page.getViewport({ scale: scale });
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render PDF page into canvas context
var renderContext = {
canvasContext: ctx,
viewport: viewport
};
var renderTask = page.render(renderContext);
// Render PDF page into canvas context
var renderContext = {
canvasContext: ctx,
viewport: viewport
};
var renderTask = page.render(renderContext);
// Wait for rendering to finish
renderTask.promise.then(function() {
pageRendering = false;
if (pageNumPending !== null) {
// New page rendering is pending
renderPage(pageNumPending);
pageNumPending = null;
}
try {
document.getElementById('grid-page-' + pageNum).classList.remove('is-active');
} catch(e) {}
pageNum = num;
document.getElementById('grid-page-' + pageNum).classList.add('is-active');
// Wait for rendering to finish
renderTask.promise.then(function () {
pageRendering = false;
if (pageNumPending !== null) {
// New page rendering is pending
renderPage(pageNumPending);
pageNumPending = null;
}
});
});
// Update page counters
document.getElementById('page-num').textContent = num;
}
/**
* If another page rendering in progress, waits until the rendering is
* finised. Otherwise, executes rendering immediately.
*/
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
}
/**
* Displays previous page.
*/
function onPrevPage() {
if (pageNum <= 1) {
return;
}
pageNum--;
queueRenderPage(pageNum);
}
//document.getElementById('prev').addEventListener('click', onPrevPage);
/**
* Displays next page.
*/
function onNextPage() {
if (pageNum >= pdfDoc.numPages) {
return;
}
pageNum++;
queueRenderPage(pageNum);
}
//document.getElementById('next').addEventListener('click', onNextPage);
/**
* Asynchronously downloads PDF.
*/
pdfjsLib.getDocument(url).promise.then(function (pdfDoc_) {
pdfDoc = pdfDoc_;
const pageList = document.getElementById('page-list');
pageList.innerHTML = '';
for (var i=0; i<pdfDoc.numPages; i++) {
let page = i+1;
let el = document.createElement('div');
el.className = 'grid-page';
el.id = 'grid-page-' + page;
el.innerHTML = page;
el.addEventListener('click', (evt) => {
queueRenderPage(page);
});
pageList.appendChild(el);
}
document.getElementById('page-count').textContent = pdfDoc.numPages;
// Initial/first page rendering
renderPage(pageNum);
tagArea.innerHTML = '';
for (let pageTag of data.pageTags) {
addTag(pageTag[0], pageTag[1], pageTag[2]);
}
dirty = false;
});
// Update page counters
document.getElementById('page_num').textContent = num;
function addInstrument() {
let name = document.getElementById('add-instrument-name');
let variant = document.getElementById('add-instrument-variant');
// get the page tags
let tag = data.pageTags[num];
if(tag) {
selectTag(tag);
} else {
if (selectedTag) {
setTag(selectedTag.dataset.tag);
let inst_name = name.value;
var tag = null;
for (let key in data.instruments) {
if (data.instruments[key] == inst_name) {
tag = key;
break;
}
}
if (!tag) {
alert("Unknown tag: " + name);
return;
}
if (variant.value) {
tag += "-" + variant.value;
}
variant.value = '';
name.value = '';
addTag(tag, pageNum, pageNum);
}
}
/**
* If another page rendering in progress, waits until the rendering is
* finised. Otherwise, executes rendering immediately.
*/
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
function addTag(tag, start, end) {
console.log("addTag", tag, start, end);
const el = document.createElement('div');
el.className = 'grid-tag';
el.dataset.tag = tag;
el.dataset.start = start;
el.dataset.end = end;
let setStart = document.createElement('span');
setStart.className = "icon is-action";
setStart.innerHTML = '<i class="fas fa-sort-amount-down" title="Set start page"></i>';
setStart.addEventListener('click', () => setTagStart(el));
el.appendChild(setStart);
let label = document.createElement('span');
let name = document.createElement('b');
name.innerHTML = get_instrument(tag);
label.appendChild(name);
let del = document.createElement('span');
del.className = "icon is-action";
del.innerHTML = '<i class="fas fa-trash-alt" title="Remove this tag"></i>';
del.addEventListener('click', () => {el.remove(), dirty=true});
label.appendChild(del)
el.appendChild(label);
let setEnd = document.createElement('span');
setEnd.className = "icon is-action";
setEnd.innerHTML = '<i class="fas fa-sort-amount-up" title="Set end page"></i>';
setEnd.addEventListener('click', () => setTagEnd(el));
el.appendChild(setEnd);
updateTag(el);
tagArea.appendChild(el);
}
}
/**
* Displays previous page.
*/
function onPrevPage() {
if (pageNum <= 1) {
return;
}
pageNum--;
queueRenderPage(pageNum);
}
document.getElementById('prev').addEventListener('click', onPrevPage);
/**
* Displays next page.
*/
function onNextPage() {
if (pageNum >= pdfDoc.numPages) {
return;
}
pageNum++;
queueRenderPage(pageNum);
}
document.getElementById('next').addEventListener('click', onNextPage);
/**
* Asynchronously downloads PDF.
*/
pdfjsLib.getDocument(url).promise.then(function(pdfDoc_) {
pdfDoc = pdfDoc_;
document.getElementById('page_count').textContent = pdfDoc.numPages;
// Initial/first page rendering
renderPage(pageNum);
});
function setTag(tag) {
data.pageTags[pageNum] = tag;
function updateTag(tag) {
let start = tag.dataset.start;
let end = tag.dataset.end;
let span = end-start+1;
let height = span * 25 + (span-1) * 10;
let top = (start-1) * 35;
tag.style.height = height + 'px';
tag.style.marginTop = top + 'px';
dirty = true;
selectTag(tag);
}
}
function selectTag(tag) {
if( selectedTag ) {
selectedTag.classList.remove('selected');
function setTagStart(el) {
if (pageNum > el.dataset.end) {
alert("Select the first page and click extend");
return;
}
selectedTag = document.getElementById('tag-' + tag);
if( selectedTag) {
selectedTag.classList.add('selected');
el.dataset.start = pageNum;
updateTag(el);
}
function setTagEnd(el) {
if (pageNum < el.dataset.start) {
alert("Select the last page and click extend");
return;
}
}
el.dataset.end = pageNum;
updateTag(el);
}
function saveTags() {
console.log(data.pageTags);
function saveTags() {
const pageTags = [];
for (let pageTag of tagArea.children ) {
let start = pageTag.dataset.start;
let end = pageTag.dataset.end;
if (start == 1 && end == pdfDoc.numPages) {
start = null;
end = null;
}
pageTags.push([pageTag.dataset.tag, start, end])
}
console.log(pageTags);
fetch("", {
method: "POST",
headers: {"Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}"},
body: JSON.stringify(data.pageTags)}
method: "POST",
headers: { "Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}" },
body: JSON.stringify(pageTags)
}
).then((response) => {
if(response.ok) {
window.location = "{% url 'work_detail' document.work.pk %}"
} else {
alert("Failed: " + response.statusText)
}
if (response.ok) {
window.location = "{% url 'work_detail' document.work.pk %}"
} else {
alert("Failed: " + response.statusText)
}
});
dirty = false;
}
function checkSaved(e) {
if (dirty) {
e.preventDefault();
}
}
window.addEventListener('beforeunload', checkSaved);
function get_instrument(s) {
let parts = s.split('-');
let instrument = data.instruments[parts[0]];
if (parts.length == 2) {
return instrument + " " + parts[1];
}
return instrument;
}
function checkSaved(e) {
if (dirty) {
e.preventDefault();
}
}
window.addEventListener('beforeunload', checkSaved);
</script>
{% endblock %}

View File

@ -1,34 +1,60 @@
{% extends "interface/project_base.html" %}
{% block admin %}
<a href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin"> Change items</span></a>
<a class="button is-link" href="{% url 'item_list_manage' project.pk %}">
<span class="icon"><i class="fas fa-list"></i></span>
<span>Change items</span>
</a>
{% endblock %}
{% block page %}
<p>
This page lets you download a complete set of music for your part by selecting your instrument
and optionally a part (e.g. Flute 1 or 2).<br/>
You can also tweak which parts you get for each piece, or click on a piece for more download options.
</p>
<form action="" method="post" target="_blank" style="display: flex;">
<form action="" method="post" target="_blank">
{% csrf_token %}
<div style="margin: 0px 20px; text-align: right;">
<label for="instrument-select">Pick your instrument</label>
<select id="instrument-select" name="instrument" onchange="updateParts()">
{% for tag, instrument in instruments %}
<option value="{{ tag }}">{{ instrument }}</option>
{% endfor %}
</select><br/><br/>
<label for="part-preference">Part preference</label>
<input id="part-preference" name="part" type="number" value="0" min="0" max="4" onchange="updateParts()" size="1"/><br/><br/>
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button>
<div>
<div class="field is-grouped is-grouped-centered">
<div class="field has-addons control">
<span class="control">
<a class="button is-static">Your Instrument</a>
</span>
<span class="control">
<input type="text" class="input" list="instrument-list" id="instrument-name" onchange="updateParts()"/>
<datalist id="instrument-list">
{% for inst in instruments %}
<option value="{{inst.1}}"/>
{% endfor %}
</datalist>
</span>
<span class="control">
<span class="select" onchange="updateParts()">
<select id="part-preference">
<option value="0">Any</option>
<option value="1">1st</option>
<option value="2">2nd</option>
<option value="3">3rd</option>
<option value="4">4th</option>
</select>
</span>
</span>
</div>
<span class="control">
<button type="submit" class="button is-primary">
<span class="icon"><i class="fas fa-copy"></i></span>
<span>Get My Parts!</span>
</button>
</span>
</div>
</div>
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th/>
<th>Piece</th>
<th>Running time</th>
<th>Part</th>
<th/>
</tr>
</thead>
<tbody>
@ -36,33 +62,79 @@
<tr>
<td>{{ forloop.counter }}.</td>
<td>
{% if request.is_admin %}
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
{% else %}
{{ item.work.name }}
{% endif %}
</td>
<td>{% firstof item.work.running_time "------" %}</td>
<td class="select-cell">
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
<span class="select is-small">
<select name="instruments">
<option value='-'>None</option>
{% for part in item.work.parts %}
{% for part in item.work.digital_parts %}
<option value='{{ part.tag }}'>{{ part.instrument }}</option>
{% endfor %}
</select>
</span>
</td>
<td>
<a href=""><i class="fas fa-download"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td/>
<td/>
<td>{{ running_time }}</td>
<td/>
<td>
<button class="button is-link is-small" type="submit"><span class="icon"><i class="fas fa-copy"></i></span><span>Single combined PDF</span></button>
<a class="button is-link is-small"><span class="icon"><i class="fas fa-archive"></i></span><span>Individual files (zipped)</span></a>
</td>
</tr>
</tfoot>
</table>
</form>
<p class="content">
This page lets you download a complete set of music for your part by selecting your instrument
and optionally a part (e.g. Flute 1 or 2).<br/>
You can also tweak which parts you get for each piece, or click on a piece for more download options.
</p>
{% endblock %}
{% block scripts %}
{{ instruments|json_script:'instruments' }}
<script type="text/javascript">
const INSTRUMENTS = JSON.parse(document.getElementById('instruments').innerText);
function updateParts() {
let inst = document.getElementById("instrument-select").value;
var inst = document.getElementById("instrument-name").value;
window.localStorage.setItem('instrument-name', inst);
for (let i of INSTRUMENTS) {
if (i[1] === inst) inst = i[0];
}
let part = document.getElementById("part-preference").value;
let prefix = inst + part;
window.localStorage.setItem('part-preference', part);
selectParts(inst, part);
}
function selectParts(inst, part) {
let prefix = inst + "-" + part;
let instruments = document.getElementsByName("instruments");
for(let i=0; i<instruments.length; i++) {
var result = "-"
@ -82,9 +154,10 @@ function updateParts() {
}
document.getElementById("instrument-select").value="{{instrument}}";
document.getElementById("part-preference").value="{{part}}";
document.getElementById("instrument-name").value = localStorage.getItem('instrument-name', 'All');
document.getElementById("part-preference").value = localStorage.getItem('part-preference', 'Any');
updateParts();
</script>
{% endblock %}

View File

@ -1,8 +1,14 @@
{% extends "interface/project_base.html" %}
{% block admin %}
<a href="#" onclick="save()"><i class="fas fa-save"></i><span class="smhide action">Save</span></a>
<a href="{% url 'item_list_append' project.pk %}"><i class="fas fa-plus-circle"></i><span class="smhide action">Add</a></a>
<a href="{% url 'item_list_append' project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Add</span>
</a>
<a href="#" onclick="save()" class="button is-link">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save</span>
</a>
{% endblock %}
{% block page %}

View File

@ -1 +1,12 @@
<p>Click the '<a href="{% url 'item_list' project=project.pk %}">My Music</a>' button below to access your parts...</p>
{% with project.items.count as items %}
{% if items %}
<p>
There {{ items|pluralize:"is,are" }} currently {{ items}} item{{ items|pluralize }} for this project.<br/>
Click the '<a href="{% url 'item_list' project=project.pk %}">My Music</a>' button below to access your parts...
</p>
{% else %}
<p>
There are no items listed yet for this project - please check back later...
</p>
{% endif %}
{% endwith %}

View File

@ -1,6 +1,9 @@
<p class='menu-label'>Library</p>
<ul class="menu-list">
{% if project %}
<a href="{% url 'item_list' project=project.pk %}">My Music</a>
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
{% endif %}
{% if request.is_admin %}
<a href="{% url 'work_list' %}">Library</a>
{% endif %}
<li><a href="{% url 'work_list' %}">Library</a></li>
{% endif %}
</ul>

View File

@ -1,93 +1,211 @@
{% extends 'interface/project_base.html' %}
{% load path_filters %}
{% block media %}
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" type="text/css" />
{% endblock %}
{% block admin %}
<a href="{% url 'document_add' work.pk %}"><i class="fas fa-plus-circle"></i> Upload file</a>
<a href="{% url 'work_edit' work.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span>
<span>Edit</span>
</a>
<a href="{% url 'work_add_to_project' work.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Add to project</span>
</a>
{% endblock %}
{% block page %}
<h2>{{ work.name }} {% if work.running_time %}({{ work.duration }}){% endif %} <small>[{{ work.identifier }}]</small></h2>
<h4>{{ work.composer }}{% if work.version %} - {{ work.version }}{% endif %}</h4>
<p>{{ work.notes }}</p>
{% if work.collection %}
<p>Location: {{ work.collection }} [{{ work.collection_index }}]</p>
{% endif %}
{% if work.parent %}
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a></p>
{% endif %}
{% if work.related_works.count %}
<h3>Related</h3>
<ul>
{% for related in work.related_works.all %}
<li><a href="{% url 'work_detail' related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
<h3 class="title">
{{ work.name }}
{% for tag in work.tags %}
<span class="tag is-success">{{ tag }}</span>
{% endfor %}
</ul>
{% endif %}
</h3>
<p class="subtitle">{% firstof work.composer "Unattributed" %}{% if work.edition %} - {{ work.edition }}{% endif %}</p>
<section class="block">
<p class="block">{{ work.notes }}</p>
<h4>Loans
<p class="block">
Location: <a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]<br/>
Running time: {{ work.duration }}<br/>
Licence: {{ work.get_licence_display }}<br/>
{% for meta in work.meta %}
{{ meta.get_name_display }}: {{ meta.value }}<br/>
{% endfor %}
</p>
{% if work.parent %}
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
</p>
{% endif %}
{% if work.related_works.count %}
<h3>Related</h3>
<ul>
{% for related in work.related_works.all %}
<li><a href="{% url 'work_detail' related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>
<section class="block">
<div class="columns">
<div class="column is-half">
<div class="box">
<h4 class="subtitle is-size-4">
<span class="icon"><i class="fas fa-book"></i></span>
Printed Parts
</h4>
<div class="tags">
{% for inst, c in work.physical_parts %}
<span class="tag is-warning">{{ inst }} ({{ c }})</span>
{% empty %}
<p class="is-italic">No printed parts listed</p>
{% endfor %}
</div>
</div>
</div>
<div class="column is-half">
<div class="box">
<h4 class="subtitle is-size-4">
<span class="icon"><i class="fas fa-print"></i></span>
Digital Parts
</h4>
<div class="tags">
{% if work.digital_parts %}
<a class="tag is-danger" href="{% url 'work_partset' pk=work.pk %}">Full Set</a>
{% endif %}
{% for part in work.digital_parts %}
<a class="tag is-info" href="{% url 'part_download' pk=part.pk filename=part.filename %}"
target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a>
{% empty %}
<p class="is-italic">No digital parts available</p>
{% endfor %}
</div>
</div>
</div>
</div>
</section>
<section class="block">
<div class="box">
<div class="level">
<h4 class="subtitle is-size-4">
<span class="icon"><i class="fas fa-file"></i></span>
Files
</h4>
</div>
<div class="columns">
<div class="column">
<table class="table is-fullwidth">
<thead>
<tr>
<th>Filename</th>
<th>Sections</th>
<th/>
</tr>
</thead>
<tbody id="doc-list">
{% for doc in work.docs.all %}
<tr>
<td><a href="{% url 'document_download' pk=doc.pk %}" target="_blank">
{{ doc.upload.name|basename }}</a></td>
<td>
{% for part in doc.sections.all %}
<a class="tag is-info" href="{% url 'part_download' pk=part.pk filename=part.filename %}">{{ part.instrument }}</a>
{% endfor %}
</td>
<td class="has-text-right">
{% if request.is_admin %}
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"
title="Manage Tags"></i></a>
<a href=""><i class="fas fa-trash-alt" title="Delete Document"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if request.is_admin %}
<div class="column is-one-quarter">
<h4 class="is-size-5">Upload files</h4>
<form action="{% url 'document_add' object.pk %}" class="dropzone" id="doc-upload">
{% csrf_token %}
</form>
</div>
{% endif %}
</div>
</div>
</section>
{% if request.is_admin %}
<a href="{% url 'work_add_to_project' work.pk %}"><i class="fas fa-plus-circle"></i></a>
<section class="block">
<div class="box">
<div class="level">
<h4 class="is-size-4">
<span class="icon"><i class="fas fa-book-reader"></i></span>
Loans
</h4>
<span class="level-right">
<a class="icon-text" href="{% url 'work_add_to_project' work.pk %}"><span class="icon"><i
class="fas fa-plus-circle"></i></span> Checkout</a>
</span>
</div>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Ensemble</th>
<th>Project</th>
<th>Checked Out</th>
<th>Due Back</th>
</tr>
</thead>
<tbody>
{% for item in work.current_loans %}
<tr>
<td><a href="{% url 'ensemble_detail' item.project.ensemble_id %}">
{{ item.project.ensemble.name }}
</a></td>
<td><a href="{% url 'project_detail' item.project.pk %}">{{ item.project.name }}</a></td>
<td>{{ item.checkout.date|date:"d/m/Y" }}</td>
<td>{{ item.due.date|date:"d/m/Y" }}</td>
</tr>
{% empty %}
<tr>
<td>No current loans</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endif %}
</h4>
<table style="margin: 10px auto">
<thead>
<tr>
<th>Ensemble</th>
<th>Project</th>
<th>Checked Out</th>
<th>Due Back</th>
</tr>
</thead>
<tbody>
{% for item in work.current_loans %}
<tr>
<td><a href="{% url 'ensemble_detail' item.project.ensemble_id %}">{{ item.project.ensemble.name }}</a></td>
<td><a href="{% url 'project_detail' item.project.pk %}">{{ item.project.name }}</a></td>
<td>{{ item.checkout.date|date:"d/m/Y" }}</td>
<td>{{ item.due.date|date:"d/m/Y" }}</td>
</tr>
{% empty %}
<tr><td>No current loans</td></tr>
{% endfor %}
</tbody>
</table>
<h4>Printed Parts</h4>
<p>
{% for inst, c in work.physical_parts %}
<span class="badge">{{ inst }} ({{ c }})</span>
{% empty %}
No physical parts available
{% endfor %}
</p>
{% block scripts %}
<script>
Dropzone.options.docUpload = { // camelized version of the `id`
paramName: "upload", // The name that will be used to transfer the file
maxFilesize: 12, // MB
createImageThumbnails: false,
thumbnailWidth: 60,
thumbnailHeight: 60,
init: function () {
this.on("complete", file => {
console.log(file);
let data = JSON.parse(file.xhr.response);
console.log(data);
let tbody = document.getElementById('doc-list');
let tr = document.createElement('tr');
tr.innerHTML = data.entry;
tbody.appendChild(tr);
this.removeFile(file);
});
}
};
</script>
{% endblock %}
<h4>Digital Parts
<a href="{% url 'work_partset' pk=work.pk %}" title="Print part set"><i class="fas fa-print"></i></a>
</h4>
<p>
{% for part in work.digital_parts %}
<a class="badge" href="{% url 'part_download' pk=part.pk filename=part.filename %}"
target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a>
{% empty %}
No digital parts available
{% endfor %}
</p>
<h4>Documents</h4>
<ul>
{% for doc in work.docs.all %}
<li>
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name|basename }}</a>
&nbsp;
{% with parts=doc.parts.count %}
{% if parts %}[{{ parts }} parts]{% endif %}
{% endwith %}
{% if request.is_admin %}
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"></i></a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -2,26 +2,37 @@
{% load url_tools %}
{% block admin %}
<a href="{% url 'work_add' %}"><i class="fas fa-plus-circle"></i> Add new</a>
{% if collection_id %}
<a href="{% url 'work_add' pk=collection_id %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Add a work</span>
</a>
{% endif %}
{% endblock %}
{% block page %}
<h2>Library for {{ view.ensemble }}</h2>
<div style="margin-bottom: 1em;">
<form method="GET">
<input name="filter" type="text" placeholder="Filter" value="{{ request.GET.filter }}"/>
<a href="?">Clear</a>
</form>
</div>
<table class="zebra wide">
<h3 class="title">{{ title }}</h3>
<form method="GET">
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" name="filter" type="text" placeholder="Filter" value="{{ request.GET.filter }}"/>
</div>
<div class="control">
<a class="button" href="?"><i class="fas fa-times"></i></a>
</div>
</div>
</form>
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Work</th>
<th>Composer</th>
<th class="smhide">Edition</th>
<th class="smhide">Orchestration</th>
<th class="smhide">Collection</th>
<th>Copies</th>
<th class="is-hidden-mobile">Edition</th>
{% if request.is_admin %}
<th class="is-hidden-touch">Collection</th>
<th class="is-hidden-mobile">Copies</th>
{% endif %}
</tr>
</thead>
<tbody>
@ -29,10 +40,11 @@
<tr>
<td><a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></td>
<td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td>
<td class="smhide" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
<td class="smhid" title="{{ work.orchestration }}">{{ work.orchestration|truncatewords:2}}</td>
<td class="smhide">{{ work.collection.name }}</td>
<td style="color: {{ work.is_available|yesno:'green,red' }};">{{ work.available }}</td>
<td class="is-hidden-mobile" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
{% if request.is_admin %}
<td class="is-hidden-touch">{{ work.collection.name }}</td>
<td class="is-hidden-mobile {{ work.is_available|yesno:'has-text-success,has-text-danger' }}">{{ work.available }}</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="4">No works found</td></tr>
@ -40,22 +52,20 @@
</tbody>
</table>
<div class="pagination" style="text-align: right;">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="{% url_update page=1 %}" title="First">&laquo;</a>
<a href="{% url_update page=page_obj.previous_page_number %}" title="Previous">&lsaquo;</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{% url_update page=page_obj.next_page_number %}" title="Next">&rsaquo;</a>
<a href="{% url_update page=page_obj.paginator.num_pages %}" title="Last">&raquo;</a>
{% endif %}
</span>
</div>
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if page_obj.has_previous %}
<a class="pagination-previous" href="{% url_update page=page_obj.prev_page_number %}">Previous</a>
{% endif %}
{% if page_obj.has_next %}
<a class="pagination-next" href="{% url_update page=page_obj.next_page_number %}">Next page</a>
{% endif %}
<ul class="pagination-list">
{% for page in page_obj.paginator.page_range %}
<li>
<a class="pagination-link {% if forloop.counter == page_obj.number %}is-current{% endif %}" href="{% url_update page=forloop.counter %}" aria-label="Goto page {{ forloop.counter }}">{{ forloop.counter }}</a>
</li>
{% endfor %}
</ul>
</nav>
{% endblock %}

View File

@ -1,31 +1,37 @@
{% extends "interface/project_base.html" %}
{% block page %}
<h2><a href="{% url 'work_list' %}">Works</a> / <a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></h2>
<form action="" method="post" target="_blank">
{% csrf_token %}
<table style="margin: 0px auto">
<thead>
<tr>
<th>Part</th>
<th>Copies</th>
</tr>
</thead>
<tbody>
{% for part in work.parts %}
<tr>
<td>{{ part.instrument }}</td>
<td>
<input name="parts" type="hidden" value="{{ part.tag }}">
<input name="copies" type="number" value="{% if part.tag == 'Score' %}0{% else %}1{% endif %}" size="1">
</td>
</tr>
{% endfor %}
<tr>
<td colspan="2" style="text-align: center;"><button type="submit"><i class="fas fa-print"></i> Print</button></td>
</tr>
</tbody>
</table>
<div class="admin-tools is-pulled-right">
<button type="submit" class="button is-link">
<span class="icon"><i class="fas fa-print"></i></span>
<span>Print Set</span>
</button>
<a class="button is-link is-light" href="{% url 'work_detail' pk=object.pk %}">
<span>Cancel</span>
</a>
</div>
<h3 class="subtitle"><a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></h3>
<p class="block">
You can generate a custom partset for printing - select the number of copies of each you want...
</p>
<div class="columns is-multiline is-mobile">
{% for part in work.digital_parts %}
<div class="column is-3 has-text-right">
<span style="white-space: nowrap">{{ part.instrument }}</span>
<input name="parts" type="hidden" value="{{ part.tag }}">
<input name="copies" type="number" value="{% if part.tag == 'Score' %}0{% else %}1{% endif %}" size="1">
</div>
{% endfor %}
</div>
</form>
{% endblock %}

View File

@ -20,3 +20,31 @@ class IntegrationTestCase(TestCase):
def test_integration(self):
pass
def test_movement_from_large_work(self):
'''
Will be common to store a work which has several movements, but the project is only going to play one.
This also should give us the ability to store an anthology as one Work have Project reference 'no:23'
'''
work = self.sel.works.create(name="Some Quartet", composer="Beethoven")
for g in ('vl1', 'vl2', 'vla', 'vc'):
doc = work.docs.create(upload=f'sel/beethoven/some_quartet/some_quartet_{g}.pdf')
doc.sections.create(tag='mvmt:1', start=1, end=3)
doc.sections.create(tag='mvmt:2', start=4, end=8)
doc.sections.create(tag='mvmt:3', start=9, end=12)
doc.sections.create(tag=f'inst:{g}')
# no tags - get nothing (should it be everything?)
self.assertEqual(work.extract(), [])
# single tag - should get just that range
self.assertEqual(work.extract('inst:vl1'), [('sel/beethoven/some_quartet/some_quartet_vl1.pdf', None, None)])
# single tag - returns all documents with that range
result = work.extract('mvmt:2')
self.assertEqual(len(result), 4)
# multiple tags - returns the overlapping portion of all documents that have all tags
self.assertEqual(work.extract('inst:vl1', 'mvmt:2'), [('sel/beethoven/some_quartet/some_quartet_vl1.pdf', 4, 8)])
self.assertEqual(work.extract('inst:vl1', 'inst:vl2'), [])

View File

@ -8,17 +8,22 @@ urlpatterns = [
path('projects/<int:project>/items', views.ProjectItemListView.as_view(), name="item_list"),
path('projects/<int:project>/items/manage', views.ProjectItemManageView.as_view(), name="item_list_manage"),
path('projects/<int:project>/items/append', views.ProjectItemAddView.as_view(), name="item_list_append"),
path('library/collections', views.CollectionListView.as_view(), name="collection_list"),
path('library/collections/<int:pk>', views.CollectionWorkListView.as_view(), name="collection_work_list"),
path('library/collection/<int:pk>/create', views.WorkAddView.as_view(), name="work_add"),
path('library/works', views.WorkListView.as_view(), name="work_list"),
path('library/works/create', views.WorkAddView.as_view(), name="work_add"),
path('library/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
path('library/works/<int:pk>/edit', views.WorkUpdateView.as_view(), name="work_edit"),
path('library/works/<int:pk>/partset', views.WorkPartSetView.as_view(), name="work_partset"),
path('library/works/<int:pk>/add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"),
path('library/documents/<int:pk>/upload', views.DocumentAddView.as_view(), name="document_add"),
path('library/works/<int:pk>/upload', views.WorkAddDocumentView.as_view(), name="document_add"),
path('library/documents/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
]
from django.views.static import serve
urlpatterns.append(path('localstorage/<path:path>', serve, {'document_root': 'local_storage'}))
urlpatterns.append(path('docs/<path:path>', serve, {'document_root': 'local_storage'}))

View File

@ -2,22 +2,25 @@ from django.shortcuts import render, redirect, resolve_url
from django.views.generic.detail import DetailView, SingleObjectMixin, View
from django.views.generic.list import ListView, MultipleObjectMixin
from django.views.generic.edit import CreateView, FormView, UpdateView
from django.http import FileResponse, HttpResponse
from django.http import FileResponse, HttpResponse, JsonResponse
from django.db import IntegrityError
from django.db.models import Q, Count
from django.db.models import Q, Count, Sum
from django.utils.timezone import now
from django.urls import reverse
import json
import os.path
from interface.views import EnsembleMixin, ProjectMixin
from interface.models import Project
from .models import Work, Document, Part, INSTRUMENTS
from .models import Collection, Work, Document, Section
from .imslp import INSTRUMENTS
from . import forms, models
from .pdf_utils import extract_pages, extract_and_concat
class ProjectItemListView(ProjectMixin, ListView):
template_name = "library/item_list.html"
model = models.Item
model = models.ProjectItem
def post(self, request, **kwargs):
@ -44,7 +47,7 @@ class ProjectItemListView(ProjectMixin, ListView):
if tag == '-':
continue
part = Part.objects.filter(tag=tag, doc__work=pk).select_related('doc').get()
part = Section.objects.filter(tag=tag, doc__work=pk).select_related('doc').get()
sections.append((part.doc.upload.path, part.doc.work.name, part.start, part.end, 1))
result = extract_and_concat(sections)
@ -64,11 +67,16 @@ class ProjectItemListView(ProjectMixin, ListView):
data['instruments'] = INSTRUMENTS
data['instrument'] = self.request.session.get('instrument', 'Score')
data['part'] = self.request.session.get('part', '0')
data['running_time'] = self.get_queryset().aggregate(Sum('work__running_time'))['work__running_time__sum']
#if running_time:
# data['running_time'] = "{0:d}:{1:02d}".format(int(running_time / 60), running_time % 60)
#else:
# data['running_time'] = "-:--"
return data
class ProjectItemManageView(ProjectMixin, ListView):
template_name = "library/item_list_manage.html"
model = models.Item
model = models.ProjectItem
def post(self, request, **kwargs):
self.request = request
@ -99,38 +107,61 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
def get_object(self):
return self.get_project()
class CollectionListView(EnsembleMixin, ListView):
def get_queryset(self):
return Collection.objects.filter(administrators=self.request.user)
class WorkListView(EnsembleMixin, ListView):
paginate_by = 20
def get_queryset(self):
#works = Work.objects.filter(collection__ensembles=self.request.ensemble_id).order_by('name').select_related('collection')
works = Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').select_related('collection')
def get_works(self):
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').distinct().select_related('collection')
def get_context_data(self, *args, **kwargs):
data = super(WorkListView, self).get_context_data(*args, **kwargs)
data['title'] = f'Music available to {self.ensemble.name}'
return data
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None))
works = works.annotate(loan_count=loan_count)
def get_queryset(self):
works = self.get_works().order_by('name')
q = self.request.GET.get('filter')
if q:
works = works.filter(Q(name__contains=q) | Q(composer__contains=q) | Q(tag_list__contains=q))
if ":" in q:
name, _, value = q.partition(":")
works = works.filter(meta_info__name=name, meta_info__value__contains=value)
else:
works = works.filter(Q(name__contains=q) | Q(composer__contains=q) | Q(meta_info__value__contains=q))
return works
class CollectionWorkListView(WorkListView):
def get_works(self):
works = Work.objects.filter(collection=self.kwargs['pk']).distinct()
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None))
works = works.annotate(loan_count=loan_count)
return works
def get_context_data(self, *args, **kwargs):
data = super(CollectionWorkListView, self).get_context_data(*args, **kwargs)
data['title'] = Collection.objects.get(pk=self.kwargs['pk']).name
data['collection_id'] = self.kwargs['pk']
return data
class WorkAddView(EnsembleMixin, FormView):
template_name = "interface/default_form.html"
form_class = forms.WorkCreateForm
def get_form(self):
f = super(WorkAddView, self).get_form()
#qs = f.fields['orchestration'].queryset
#f.fields['orchestration'].queryset = qs.filter(ensemble_id__isnull=True) | qs.filter(ensemble_id=self.request.ensemble_id)
qs = f.fields['collection'].queryset
qs = qs.filter(administrators=self.request.user)
f.fields['collection'].queryset = qs
return f
title = "Add a new work"
def form_valid(self, form):
work = form.save(commit=False)
work.ensemble_id = self.request.ensemble_id
work.collection_id = self.kwargs['pk']
work.save()
# handle the files
@ -144,12 +175,26 @@ class WorkAddView(EnsembleMixin, FormView):
else:
return redirect('work_detail', pk=work.pk)
class WorkDetailView(EnsembleMixin, DetailView):
class WorkMixin(object):
def get_queryset(self):
if self.request.is_admin:
return Work.objects.all()
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
class WorkDetailView(EnsembleMixin, WorkMixin, DetailView):
pass
class WorkUpdateView(EnsembleMixin, WorkMixin, UpdateView):
fields = ['name', 'composer', 'edition', 'code', 'licence', 'max_projects', 'running_time', 'notes']
template_name = 'interface/default_form.html'
def get_success_url(self):
return resolve_url('work_detail', self.kwargs['pk'])
class WorkAddToProject(EnsembleMixin, FormView):
admin_required = True
form_class = forms.ProjectSelectForm
template_name = "interface/default_form.html"
title = "Select project to add work to"
@ -188,7 +233,7 @@ class WorkPartSetView(EnsembleMixin, DetailView):
sections = []
for i, tag in enumerate(parts):
part = work.parts.select_related('doc').get(tag=tag)
part = work.digital_parts.select_related('doc').get(tag=tag)
sections.append((part.doc.upload.path, part.instrument, part.start, part.end, int(copies[i])))
result = extract_and_concat(sections)
@ -200,25 +245,59 @@ class WorkPartSetView(EnsembleMixin, DetailView):
return response
def get_queryset(self):
return Version.objects.filter(work__ensemble_id=self.request.ensemble_id)
works = Work.objects.all()
class DocumentDetailView(EnsembleMixin, DetailView):
if not self.request.is_admin:
works = works.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
return works
def get_queryset(self):
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
class DocumentAddView(EnsembleMixin, CreateView):
class WorkAddDocumentView(EnsembleMixin, CreateView):
template_name = "interface/default_form.html"
model = Document
fields = ['upload']
def title(self):
work = Work.objects.get(pk=self.kwargs['pk'])
return f"Add a document to {work.name}"
def form_invalid(self, form):
if self.request.headers['Accept'] == 'application/json':
return HttpResponse(status=400)
return super().form_invalid(form)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.work_id = self.kwargs['pk']
self.object.save()
return redirect('document_annotate', self.object.pk)
doc = form.save(commit=False)
doc.work_id = self.kwargs['pk']
doc.save()
if self.request.headers['Accept'] == 'application/json':
filename = os.path.basename(doc.upload.name)
return JsonResponse({
"message": "created",
"id": doc.pk,
"entry": f"""
<td><a href="{reverse('document_download', args=[doc.pk])}">{filename}</a></td>
<td/>
<td class="has-text-right">
<a href="{reverse('document_annotate', args=[doc.pk])}"><i class="fas fa-tags"
title="Manage Tags"></i></a>
<a href=""><i class="fas fa-trash-alt" title="Delete Document"></i></a>
</td>
"""
}, status=201)
class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View):
return redirect('document_annotate', doc.pk)
class DocumentMixin(object):
def get_queryset(self):
if self.request.is_admin:
return Document.objects.select_related('work')
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
class DocumentDetailView(EnsembleMixin, DocumentMixin, DetailView):
pass
class DocumentDownloadView(EnsembleMixin, DocumentMixin, SingleObjectMixin, View):
def get(self, request, **args):
self.request = request
@ -229,10 +308,7 @@ class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View):
#return response
return redirect(self.object.upload.url)
def get_queryset(self):
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id)
class DocumentAnnotateView(EnsembleMixin, DetailView):
class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView):
template_name = 'library/document_annotate.html'
def post(self, request, **args):
@ -242,36 +318,24 @@ class DocumentAnnotateView(EnsembleMixin, DetailView):
data = json.loads(request.body)
tags = {}
for page, tag in data.items():
tags.setdefault(tag, []).append(int(page))
try:
del(tags['None'])
except KeyError:
pass
self.object.parts.all().delete()
for tag, pages in tags.items():
pages.sort()
end = pages[-1] if len(pages) > 1 else None
o = self.object.parts.create(tag=tag, start=pages[0], end=end)
self.object.sections.all().delete()
for tag, start, end in data:
#pages.sort()
#end = pages[-1] if len(pages) > 1 else None
o = self.object.sections.create(tag=tag, start=start, end=end)
return HttpResponse(status=204)
def get_context_data(self, **kwargs):
data = super(DocumentAnnotateView, self).get_context_data(**kwargs)
pages = {}
for part in data['document'].parts.all():
for i in range(part.start, (part.end or part.start)+1):
pages[i] = part.tag
pages = []
for part in data['document'].sections.all():
pages.append((part.tag, part.start, part.end))
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.orchestration}
data['json_data'] = {'pageTags': pages, 'instruments': dict(INSTRUMENTS)}
return data
def get_queryset(self):
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('work')
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
@ -289,4 +353,9 @@ class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
return response
def get_queryset(self):
return Part.objects.filter(doc__work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work')
if self.request.is_admin:
parts = Section.objects.all()
else:
parts = Section.objects.filter(doc__work__collection__allowed_ensembles__ensemble=self.request.ensemble_id)
return parts.select_related('doc', 'doc__work')

View File

@ -23,13 +23,18 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = None
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = False
ALLOWED_HOSTS = ['localhost']
# Application definition
POLYPHONIC_MODULES = [
'library',
'submissions'
]
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
@ -38,10 +43,14 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django_markdown2',
'crispy_forms',
'crispy_bulma',
'byostorage',
'interface',
'library',
]
] + POLYPHONIC_MODULES
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
CRISPY_TEMPLATE_PACK = "bulma"
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',

View File

@ -14,11 +14,12 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.urls import path, re_path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('interface.urls')),
path('', include('submissions.urls')),
path('', include('library.urls')),
]