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 pylint==2.6.0
django-debug-toolbar

View File

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

View File

@ -1,17 +1,46 @@
from django import forms 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, code = forms.CharField(max_length=14,
widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx', 'inputmode': 'numeric'})) widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx', 'inputmode': 'numeric'}))
passphrase = forms.CharField(max_length=32) passphrase = forms.CharField(max_length=32)
class SubmissionForm(forms.ModelForm): class ResourceUploadForm(forms.Form):
method = forms.ChoiceField(choices=( pass
('upload', 'I need to upload a file'), # file = S3UploadField()
('link', 'I have a link from my own cloud storage provider')
), initial='upload')
class Meta:
model = Submission
fields = ['name', 'instrument', 'method', 'notes']

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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import interface.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -9,32 +12,69 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('byostorage', '0004_alter_userstorage_storage'),
] ]
operations = [ 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( migrations.CreateModel(
name='Project', name='Project',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)), ('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( migrations.CreateModel(
name='WikiPage', name='WikiPage',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('markdown', models.TextField()), ('markdown', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiki_pages', to='interface.project')), ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiki_pages', to='interface.project')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Submission', name='Resource',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=100)),
('instrument', models.CharField(max_length=100)), ('description', models.TextField(blank=True)),
('notes', models.TextField()), ('file', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=interface.models.resource_key)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='interface.project')), ('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.conf import settings
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from byostorage.user import BYOStorage
import random import random
import boto3
from datetime import datetime from datetime import datetime
from urllib.parse import urlparse from urllib.parse import urlparse
import os.path import os.path
s3client = boto3.client('s3', **getattr(settings, 'S3_CREDENTIALS', {}))
BUCKET = settings.AWS_BUCKET
MEDIA_TYPES = [ MEDIA_TYPES = [
('audio', "Audio"), ('audio', "Audio"),
('video', "Video"), ('video', "Video"),
@ -24,14 +21,14 @@ MEDIA_TYPES = [
] ]
def rough_date(d): def rough_date(d):
days = (self.event_date - timezone.now().date()).days days = (d - timezone.now()).days
in_past = days < 0 in_past = days < 0
if in_past: if in_past:
days = abs(days) days = abs(days)
if days ==0: if days ==0:
return "today!" return "today!"
if days >= 7: 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" return in_past, f"{days} days"
@ -39,16 +36,25 @@ def generate_code(length=9):
return "".join([ random.choice('0123456789') for _ in range(length) ]) return "".join([ random.choice('0123456789') for _ in range(length) ])
class Ensemble(models.Model): class Ensemble(models.Model):
name = models.CharField(max_length=100) ''' A group that plays together
code = models.CharField(max_length=9, default=generate_code)
passphrase = models.CharField(max_length=100) '''
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') admins = models.ManyToManyField('auth.User', related_name='ensembles')
slug = models.SlugField(max_length=100, editable=False) details = models.TextField(blank=True,
details = models.TextField(blank=True) help_text="Description of the ensemble (markdown)")
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL) storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL,
help_text="Default storage for this ensemble")
def active_projects(self): def active_projects(self):
return self.projects.filter(active=True) return self.projects.filter(active=True).order_by('event_date')
def ensemble_code(self): def ensemble_code(self):
code = str(self.code) code = str(self.code)
@ -62,24 +68,24 @@ class Ensemble(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Project(models.Model): class Project(models.Model):
''' A Project linked to an ensemble
'''
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
ensemble = models.ForeignKey(Ensemble, related_name='projects', on_delete=models.CASCADE, null=True) 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) 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) 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") class Meta:
enable_submissions = models.BooleanField(default=False, help_text="Allow media submissions from participants") ordering = ['active', '-pk']
@property @property
def submissions(self): def submissions(self):
return self.all_submissions.filter(complete=True).order_by('-pk') return self.all_submissions.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)
@property @property
def days(self): def days(self):
@ -87,42 +93,59 @@ class Project(models.Model):
@property @property
def has_happened(self): def has_happened(self):
return self.event_date < timezone.now().date() return self.event_date < timezone.now()
def save(self): @property
if not self.slug: def rough_date(self):
self.slug = slugify(self.name) in_past, s = rough_date(self.event_date)
super(Project, self).save() 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): def __str__(self):
return self.name 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): 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) project = models.ForeignKey(Project, related_name='resources', on_delete=models.CASCADE)
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
description = models.TextField(blank=True) 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='*') media_type = models.CharField(max_length=10, choices=MEDIA_TYPES, default='*')
visible = models.BooleanField(default=True) visible = models.BooleanField(default=True)
def key_template(self): class Meta:
return "{}/${{filename}}".format(slugify(self.name)) ordering = ['-visible', '-pk']
def accept(self): def accept(self):
if self.media_type == 'general': if self.media_type == 'general':
return ".*" return ".*"
return f"{self.media_type}/*" 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): def __str__(self):
return self.name return self.name
class WikiPage(models.Model): 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) project = models.ForeignKey(Project, related_name='wiki_pages', on_delete=models.CASCADE)
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
markdown = models.TextField() markdown = models.TextField()
@ -132,9 +155,9 @@ class WikiPage(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
"""
class Submission(models.Model): 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, ) date = models.DateTimeField(auto_now_add=True, )
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
instrument = models.CharField(max_length=100, verbose_name="Instrument / Voice") instrument = models.CharField(max_length=100, verbose_name="Instrument / Voice")
@ -180,3 +203,4 @@ class Submission(models.Model):
def __str__(self): def __str__(self):
return f"{self.name}: {self.date}" return f"{self.name}: {self.date}"
"""

View File

@ -1,366 +1,63 @@
@font-face {
font-family: MartinHand;
src: url('/static/fonts/Martinhand3.ttf');
}
:root { :root {
--border-color: #292929; --primary: #485fc7;
--gray-blue: #667788;
--light-blue: #c5eff7;
--light-grey: #EEEEEE;
} }
@font-face { .fancy {
font-family: 'Quicksand'; font-family: MartinHand;
src: url('../../fonts/Quicksand_Book.otf'); color: var(--primary);
} }
@font-face { .has-text-shadow {
font-family: 'QuicksandBold'; text-shadow: 1px 1px 2px #000;
src: url('../../fonts/Quicksand_Bold_Oblique.otf');
} }
.debug DIV { .is-form-group {
border: 1px dashed #DDD; max-width: 600px;
align-self: center;
} }
HTML { .is-centered {
height: 100%; margin: auto;
} }
BODY { .is-action {
background-image: url('../background.png'); cursor: pointer;
background-position: center top;
background-size: 100%;
background-repeat: no-repeat;
margin: 0px;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
} }
.main { .menu-label {
max-width: 1280px; color: var(--primary);
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;
} }
.content { .button.is-primary, .button.is-primary:hover {
margin: 20px; background-color: var(--primary);
flex-direction: column;
} }
.narrow { A.admin-link:after {
max-width: 500px; content: "*";
margin: 0px auto;
} }
.wide { TEXTAREA.input {
width: 1200px; height: 400px;
} }
.collapse { .control INPUT[type='file'] {
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;
border: none; border: none;
color: var(--light-blue); width: 80%;
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 {
text-align: center; text-align: center;
margin: auto 10%;
} }
A, A:visited { .project-footer {
text-decoration: none; position: fixed;
color: var(--gray-blue); bottom: 0px;
font-weight: bold; right: 0px;
}
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;
background-color: #EEE; background-color: #EEE;
border: 1px solid var(--border-color); padding: 10px 10px;
border-radius: 10px; margin-top: 30px;
padding: 5px; border-top-left-radius: 6px;
}
.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;
} }

View File

@ -1,50 +1,50 @@
{% load static %} {% load static %}
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <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="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> <link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
<script src="{% static 'interface/js/interface.js' %}"></script>
<title>{% block title %}Polyphonic{% endblock %}</title> <title>{% block title %}Polyphonic{% endblock %}</title>
{% block media %}{% endblock %}
</head> </head>
<body> <body>
<div class="main">
{% block navigation %} {% block navigation %}
<nav class="navigation"> <nav class="navbar" role="navigation">
<div> <div class="navbar-brand has-text-primary">
<a class="brand" href="/"><i class="fas fa-random smhide"></i> Polyphonic</a> <a class="navbar-item" href="/">
<span class="mdhide">Ensemble Manager</span> <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>
<ul class="nav-buttons">
{% if request.ensemble_id %} <div id="navbarMain" class="navbar-menu">
<li class="nav-item">
<a class="nav-link" href="{% url 'ensemble_detail' %}"><i class="fas fa-music" title="Projects"></i> <span class="smhide">My <div class="navbar-end">
Projects</span></a> <span class="navbar-item is-size-4">{% firstof ensemble project.ensemble %}</span>
</li> </div>
<li class="nav-item">
<a class="nav-link" href="{% url 'register' %}"><i class="fas fa-users" title="Ensembles"></i> <span class="mdhide">My </div>
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> </nav>
{% endblock %} {% endblock %}
<div class="content"> {% block content %}
{% block content %} <h1>No content!</h1>
<h1>No content!</h1> {% endblock %}
{% endblock %}
</div>
</div>
<!-- late load scripts --> <!-- late load scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <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" %} {% extends "interface/project_base.html" %}
{% load crispy_forms_tags %}
{% block media %}
{{ form.media }}
{% endblock %}
{% block page %} {% block page %}
<div> <h3 class="subtitle">{% firstof title view.title %}</h3>
<h3>{% firstof title view.title %}</h3> <div class="columns is-centered">
<form class="vertical" method="POST" enctype="multipart/form-data"> <div class="column is-form-group">
{% csrf_token %} {% crispy form %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div> </div>
</form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,22 +1,73 @@
{% extends "interface/project_base.html" %} {% 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 %} {% block page %}
<div style="flex-grow: 1"> <h3 class="title">Projects for {{ ensemble.name }}</h3>
<h1>Projects for {{ ensemble.name }}</h1>
<div class="list-group narrow"> <div class="columns is-multiline">
{% for project in ensemble.active_projects %} {% for project in ensemble.active_projects %}
<div class="column is-half">
<div class="card">
<a class="" href="{% url 'project_detail' project=project.id %}"> <a class="" href="{% url 'project_detail' project=project.id %}">
<h3>{{ project.name }}</h3> <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> <p><small>
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %} {% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
{% if project.works.count %}{{ project.works.count }} works<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 %} {% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
</small></p> </small></p>
</a>
{% endfor %}
</div> </div>
<div style="float: right; margin-top: 10px; color: #999;"> </div>
<small>{{ ensemble.ensemble_code }}</small> </div>
{% 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>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -2,40 +2,109 @@
{% block content %} {% block content %}
{% if request.is_admin %}
<div class="admin-tools">
{% block admin %}
{% endblock %}
</div>
{% endif %}
<h1>{{ project.name }}</h1>
{% block page %} <div class="columns">
No content <div class="column is-narrow is-hidden-touch" id="projectMenu">
{% endblock %} <div style="margin: auto 2em;">
<aside class="menu">
<div class="project-links"> <p class="menu-label">My Things</p>
<div class="pills" role="tablist"> <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>
{% if project %} {% if project %}
<a role="tab" href="{% url 'project_detail' project=project.id %}">Project info</a> <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 %} {% for page in project.wiki_pages.all %}
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}" <li><a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a> href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a></li>
{% endfor %} {% 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 %} {% endif %}
{% if project and project.enable_submissions%} {% if 'submission' in modules %}
<a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a>
<!--a role="tab" href="">Record a submission</a--> <!--a role="tab" href="">Record a submission</a-->
{% if request.is_admin %} {% if request.is_admin %}
<a role="tab" href="{% url 'submission_list' project=project.id %}">Submissions</a> <li><a role="tab" class="admin-link" href="{% url 'submission_list' project=project.id %}">Submissions</a></li>
{% endif %}
<a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a>
{% endif %} {% endif %}
{% include "library/project_menu.html" %} <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 %}
<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>
{% endif %}
<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>
</div> </div>
{% if ensemble %}
<div class="project-footer">
{{ ensemble.name }}
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,32 +1,62 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load md2 %} {% 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 %} {% block page %}
<div class="narrow"> <div class="box">
{% if project.event_date %} {% 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 %} {% if project.has_happened %}
{{ project.event_date|timesince }} ago. ({{ project.event_date|roughtimesince }} ago)
{% else %} {% else %}
In {{ project.event_date|timeuntil }}. (in {{ project.event_date|roughtimeuntil }}...)
{% endif %} {% endif %}
</h3> </h3>
{% endif %} {% endif %}
<p>{{ project.description|markdown }}</p> <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 %} {% if project.owner %}
<p>Project email: <a href="mailto:{{ project.owner }}">{{ project.owner }}</a></p> <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 %} {% endif %}
{% if project.enable_submissions %}
{% include 'submissions/project_detail.html' %}
{% endif %}
{% if project.enable_library %}
{% include 'library/project_detail.html' %}
{% endif %}
</div> </div>
{% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,18 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load crispy_forms_tags %}
{% block media %}
{{ form.media }}
{% endblock %}
{% block page %} {% block page %}
<div class="narrow"> <h3 class="subtitle">{% firstof title view.title %}</h3>
<h3>{{ title }}</h3> <div class="columns is-centered">
<div class="column is-form-group">
{% if instructions %}
<p>{{ instructions }}</p> <p>{{ instructions }}</p>
<form class="vertical" method="POST"> {% endif %}
{% csrf_token %} {% crispy form %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div> </div>
</form>
</div> </div>
{% endblock %} {% 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" %} {% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %} {% block content %}
<section class="section">
{% if not request.user.is_authenticated %} {% if not request.user.is_authenticated %}
<a href="{% url 'login' %}" style="float: right"><i class="fa fa-key"></i></a> <a href="{% url 'login' %}" style="float: right"><i class="fa fa-key"></i></a>
{% endif %} {% endif %}
<div class="collapse"> <div class="columns is-centered">
{% if current %} <div class="box is-half">
<div> <h3 class="title">Join an ensemble</h3>
<h3>My Ensembles</h3> <form action="" method="POST">
<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 %} {% csrf_token %}
{{ form }} {{ form | crispy }}
<div class="form-actions"> <div class="field is-grouped">
<button class="btn btn-primary">Enter</button> <div class="control">
<button class="button is-link">Register</button>
</div>
</div> </div>
</form> </form>
</div>
</div> </div>
</div>
</section>
{% endblock %} {% endblock %}

View File

@ -2,34 +2,40 @@
{% load md2 %} {% load md2 %}
{% block admin %} {% 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 %} {% endblock %}
{% block page %} {% block page %}
<div class="narrow"> <div class="columns is-multiline">
<h3>Resources</h3> {% for resource in object_list %}
<div class="list-group narrow"> {% with download=resource.file.url %}
{% for resource in object_list %} <div class="column is-half">
{% with download=resource.presigned_url %} <div class="card {% if not object.visible %}disabled{% endif %}">
<div> <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 %} {% if request.is_admin %}
<div class="admin-tools"> <a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}" class="icon" title="Upload">
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
</a> </a>
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}"> <a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}" class="icon" title="Edit">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
{% endif %}
</div> </div>
{% endif %} </div>
<h3> <div class="card-content">
{{ resource.name }}
{% if download %}
<small><a href="{{ download }}" target="_blank" rel="noopener noreferrer">
<i class="fas fa-download"></i> Download
</a></small>
{% endif %}
</h3>
<p> <p>
<small>{{ resource.description|markdown }}</small> <small>{{ resource.description|markdown }}</small>
{% if not resource.visible %} {% if not resource.visible %}
@ -37,11 +43,16 @@
{% endif %} {% endif %}
</p> </p>
{% if download and resource.media_type == 'audio' %} {% 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 %} {% endif %}
</div> </div>
{% endwith %}
{% endfor %}
</div> </div>
</div>
{% endwith %}
{% empty %}
<div class="column">
<p>There are resources for this project</p>
</div>
{% endfor %}
</div> </div>
{% endblock %} {% 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" %} {% extends "interface/project_base.html" %}
{% block admin %} {% 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 %} {% endblock %}
{% block page %} {% block page %}
<div class="wiki-page"> <h3 class="subtitle">{{ wikipage.title }}</h3>
<div class="box content wiki-page">
{{ wiki_html|safe }} {{ wiki_html|safe }}
</div> </div>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

@ -8,30 +8,23 @@ urlpatterns = [
path('login', auth_views.LoginView.as_view(), name='login'), path('login', auth_views.LoginView.as_view(), name='login'),
path('logout', views.logout, name='logout'), path('logout', views.logout, name='logout'),
path('register', views.register, name="register"), path('register', views.register, name="register"),
path('manage', views.ManageView.as_view(), name="manage"),
path('', views.EnsembleProjectListView.as_view(), name='ensemble_detail'), 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>', 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>', 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>', 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>/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', 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/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>/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.shortcuts import render, get_object_or_404, redirect, resolve_url
from django.views.generic import TemplateView, View, RedirectView from django.views.generic import TemplateView, RedirectView
from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.detail import DetailView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView, FormView from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.base import ContextMixin
from django.http import HttpResponseRedirect
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.signing import Signer from django.core.signing import Signer
from django.contrib import auth from django.contrib import auth
from markdown2 import markdown from markdown2 import markdown
from datetime import datetime
from urllib.parse import urlparse, urlencode
import os.path
from . import models, forms from . import models, forms
from base64 import b64decode
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,6 +29,11 @@ class EnsembleMixin(object):
request.ensemble_id = request.session.get('ensemble') request.ensemble_id = request.session.get('ensemble')
request.is_admin = request.user.is_superuser 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: if 'auth' in request.GET:
sig = signer.sign(request.path) sig = signer.sign(request.path)
if sig[len(request.path)+1:] == request.GET['auth']: if sig[len(request.path)+1:] == request.GET['auth']:
@ -45,16 +43,16 @@ class EnsembleMixin(object):
else: else:
raise SuspiciousOperation("Bad auth code") raise SuspiciousOperation("Bad auth code")
if not request.ensemble_id: if request.user.is_authenticated:
return redirect('register')
if not request.is_admin and request.user.is_authenticated:
try: try:
request.user.ensembles.get(pk=request.ensemble_id) request.user.ensembles.get(pk=request.ensemble_id)
request.is_admin = True request.is_admin = True
except models.Ensemble.DoesNotExist: except models.Ensemble.DoesNotExist:
pass pass
if not request.ensemble_id:
return redirect('register')
if self.admin_required and not request.is_admin: if self.admin_required and not request.is_admin:
return redirect('login') return redirect('login')
@ -64,6 +62,11 @@ class EnsembleMixin(object):
def ensemble(self): def ensemble(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id) 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): class ProjectMixin(EnsembleMixin):
def get_project(self): def get_project(self):
@ -81,57 +84,9 @@ class ProjectMixin(EnsembleMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['project'] = self.get_project() context['project'] = self.get_project()
context['modules'] = context['project'].modules.values_list('name', flat=True)
return context 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): def register(request):
if 'clear' in request.GET: if 'clear' in request.GET:
@ -139,16 +94,13 @@ def register(request):
request.ensemble_id = request.session.get('ensemble') request.ensemble_id = request.session.get('ensemble')
registered = request.session.setdefault('registered', {}) registered = request.session.setdefault('registered', [])
code = request.GET.get('code', '').replace('-', '') code = request.GET.get('code', '').replace('-', '')
print("Registering with code %s", code)
# check if already joined # check if already joined
if code in registered: if code in registered or request.user.is_superuser:
request.session['ensemble'] = registered[code]
return redirect('ensemble_detail')
if request.user.is_superuser and code:
request.session['ensemble'] = models.Ensemble.objects.get(code=code).pk request.session['ensemble'] = models.Ensemble.objects.get(code=code).pk
return redirect('ensemble_detail') return redirect('ensemble_detail')
@ -162,7 +114,7 @@ def register(request):
ensemble = models.Ensemble.objects.get(code=data['code'].replace('-', '')) ensemble = models.Ensemble.objects.get(code=data['code'].replace('-', ''))
if ensemble.passphrase.lower() == data['passphrase'].lower(): if ensemble.passphrase.lower() == data['passphrase'].lower():
request.session['ensemble'] = ensemble.pk request.session['ensemble'] = ensemble.pk
registered[ensemble.code] = ensemble.pk registered.append(ensemble.code)
return redirect('ensemble_detail') return redirect('ensemble_detail')
except models.Ensemble.DoesNotExist: except models.Ensemble.DoesNotExist:
form.add_error(None, "Incorrect code or passphrase") form.add_error(None, "Incorrect code or passphrase")
@ -175,18 +127,17 @@ def register(request):
if request.user.is_superuser: if request.user.is_superuser:
current = models.Ensemble.objects.all() current = models.Ensemble.objects.all()
else: 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}) return render(request, 'interface/register.html', {'form': form, 'current': current})
def on_login(sender, **kwargs): def on_login(sender, **kwargs):
user = kwargs['user'] user = kwargs['user']
request = kwargs['request'] request = kwargs['request']
registered = request.session.get('registered', {}) registered = request.session.get('registered', [])
for e in user.ensembles.all(): for e in user.ensembles.all():
if not e.code in registered: if not e.code in registered:
registered[e.code] = e.pk registered.append(e.code)
request.session['registered'] = registered request.session['registered'] = registered
auth.signals.user_logged_in.connect(on_login) auth.signals.user_logged_in.connect(on_login)
@ -198,6 +149,39 @@ def logout(request):
request.session['registered'] = registered request.session['registered'] = registered
return redirect('/') 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): class EnsembleProjectListView(EnsembleMixin, DetailView):
template_name = 'interface/ensemble_project_list.html' template_name = 'interface/ensemble_project_list.html'
@ -212,6 +196,12 @@ class EnsembleProjectListView(EnsembleMixin, DetailView):
def get_object(self): def get_object(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id) 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): class EnsembleDetailView(DetailView):
model = models.Ensemble model = models.Ensemble
@ -220,30 +210,54 @@ class ProjectDetailView(ProjectMixin, DetailView):
def get_object(self): def get_object(self):
return self.get_project() return self.get_project()
class ProjectMakefileView(EnsembleMixin, DetailView):
template_name = 'interface/project_submissions.mk'
content_type = 'text/plain'
def get_queryset(self): class ProjectCreateView(EnsembleMixin, CreateView):
if self.request.is_admin: model = models.Project
return models.Project.objects.all() 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): class ProjectUpdateView(EnsembleMixin, UpdateView):
data = super().get_context_data(**kwargs) 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'] = [] def get_success_url(self):
data['targets'] = [] return resolve_url('project_detail', project=self.kwargs['project'])
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 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): class WikiView(ProjectMixin, DetailView):
template_name = 'interface/wiki.html' template_name = 'interface/wiki.html'
@ -259,122 +273,21 @@ class WikiCreateView(ProjectMixin, CreateView):
model = models.WikiPage model = models.WikiPage
fields = ['title', 'markdown'] 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): class WikiEditView(ProjectMixin, UpdateView):
admin_required = True admin_required = True
model = models.WikiPage model = models.WikiPage
fields = ['title', 'markdown'] 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): class ResourceCreateView(ProjectMixin, CreateView):
model = models.Resource model = models.Resource
fields = ['name', 'media_type', 'description'] form_class = forms.ResourceForm
template_name = 'interface/project_form.html' template_name = 'interface/project_form.html'
title = "Add a new resource" title = "Add a new resource"
admin_required = True admin_required = True
@ -385,26 +298,15 @@ class ResourceCreateView(ProjectMixin, CreateView):
self.object.save() self.object.save()
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk) 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 model = models.Resource
template_name = 'interface/s3_upload.html' fields = ['file']
template_name = 'interface/default_form.html'
def get_accept_files(self):
return self.object.accept()
def get_success_url(self): 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']) 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): class ResourceListView(ProjectMixin, ListView):
model = models.Resource model = models.Resource

View File

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

View File

@ -1,15 +1,17 @@
from django import forms from django import forms
from .models import Work from .models import Work
from interface.models import Project from interface.models import Project
from interface.widgets import DatePickerInput
from django.db.models import Q from django.db.models import Q
from interface.forms import BaseForm
class WorkCreateForm(forms.ModelForm): class WorkCreateForm(forms.ModelForm, BaseForm):
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False) #uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False)
class Meta: class Meta:
model = Work model = Work
fields = ['uploads', 'name', 'composer', 'edition', 'collection', 'code', 'running_time', 'notes'] fields = ['name', 'code', 'running_time', 'notes']
class PlaylistAddForm(forms.Form): class PlaylistAddForm(forms.Form):
work = forms.ModelChoiceField(queryset=Work.objects.all()) 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.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -12,9 +12,9 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('interface', '0027_auto_20210322_1154'),
('byostorage', '0003_auto_20210323_1047'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('byostorage', '0004_alter_userstorage_storage'),
('interface', '0001_initial'),
] ]
operations = [ operations = [
@ -22,11 +22,11 @@ class Migration(migrations.Migration):
name='Collection', name='Collection',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)), ('name', models.CharField(help_text='Name of the collection', max_length=255)),
('location', models.CharField(help_text='Physical location', max_length=100)), ('prefix', models.SlugField(default='default', max_length=30)),
('notes', models.TextField(blank=True)), ('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)), ('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')), ('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=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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)), ('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( migrations.CreateModel(
name='Item', name='ProjectItem',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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)), ('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={ options={
'ordering': ['order', 'work'], '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( migrations.CreateModel(
name='Work', name='Work',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(editable=False, max_length=100)), ('slug', models.SlugField(editable=False, max_length=100)),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('version', models.CharField(blank=True, help_text='Version or edition details', max_length=100)), ('edition', models.CharField(blank=True, help_text='Edition details', max_length=255)),
('composer', models.CharField(blank=True, help_text='Use Composer / Arranger format', 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)), ('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')), ('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')), ('max_projects', models.IntegerField(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)), ('running_time', models.DurationField(blank=True, help_text='Running time in seconds', null=True)),
('notes', models.TextField(blank=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(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='library.collection')),
('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')),
('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')), ('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( 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=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tag', models.SlugField(max_length=20)), ('tag', models.SlugField(max_length=20)),
('start', models.SmallIntegerField(blank=True, null=True)), ('start', models.SmallIntegerField(blank=True, null=True)),
('end', models.SmallIntegerField(blank=True, null=True)), ('end', models.SmallIntegerField(blank=True, null=True)),
('notes', models.TextField(blank=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={ options={
'ordering': ['doc', 'start', 'pk'], 'ordering': ['doc', 'start', 'pk'],
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name='projectitem',
name='work', name='work',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_items', to='library.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=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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)), ('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')), ('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, to='interface.ensemble')), ('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( migrations.AddField(
model_name='document', 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.timezone import now
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.core.files.storage import get_storage_class 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 import logging
#from polyphonic.settings import LIBRARY_STORAGE
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: #try:
library_storage = get_storage_class(settings.LIBRARY_STORAGE)() # library_storage = get_storage_class(settings.LIBRARY_STORAGE)()
except (ImportError, AttributeError): #except (ImportError, AttributeError):
library_storage = get_storage_class()() # logger.exception("Failed to load library storage")
logger.info("Library storage: %s", library_storage.__class__.__name__) # 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 = { ORCHESTRATIONS = {
@ -194,12 +55,6 @@ ACCESS_TYPES = [
(2, 'Approval required'), (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): class Orchestration(models.Model):
@ -223,36 +78,43 @@ class Orchestration(models.Model):
return self.name 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. 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') work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name='project_items')
checkout = models.DateTimeField() checkout = models.DateTimeField()
due = models.DateTimeField(null=True, blank=True) due = models.DateTimeField(null=True, blank=True)
returned = models.DateTimeField(null=True, blank=True) returned = models.DateTimeField(null=True, blank=True)
approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE) approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
order = models.SmallIntegerField(default=0) 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: class Meta:
ordering = ['order', 'work'] ordering = ['order', 'work']
def __str__(self): def __str__(self):
return f"<{self.project.slug}:{self.work.slug}>" return f"<{self.project_id}:{self.work.slug}>"
class Collection(models.Model): 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") name = models.CharField(max_length=255,
administrators = models.ManyToManyField('auth.User', related_name="collections", help_text="Administrators for this collection") help_text="Name of the collection")
location = models.CharField(max_length=100, help_text="Physical location (institution, town...)") prefix = models.SlugField(max_length=30, default="default",
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True, help_text="Storage for documents") help_text="Folder to store works in")
notes = models.TextField(blank=True, help_text="Publicly visible notes about collection and loans policy") administrators = models.ManyToManyField('auth.User', related_name="collections",
#ensembles = models.ManyToManyField('interface.Ensemble', related_name="collections", through='EnsembleAccess') 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): def __str__(self):
return self.name return self.name
@ -268,61 +130,69 @@ class EnsembleAccess(models.Model):
class Meta: class Meta:
verbose_name_plural = "Ensemble access" verbose_name_plural = "Ensemble access"
META_TAGS = (
('tag', 'Tag'),
('arr', 'Arranger'),
('lyrics', 'Lyracist'),
('genre', 'Genre'),
('style', 'Style'),
('orchestration', 'Orchestration'),
)
class Work(models.Model): class Work(models.Model):
""" """
A musical work 'owned' by a collection from a licencing perspective. 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,
slug = models.SlugField(max_length=100, editable=False) help_text="Used as folder name")
name = models.CharField(max_length=255) name = models.CharField(max_length=255, help_text="Original name of the work")
edition = models.CharField(max_length=100, blank=True, help_text="Edition details") 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", 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") 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") composer = models.CharField(max_length=255, blank=True,
#orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works', blank=True) help_text="Surname, First Name/Initials")
orchestration = models.CharField(max_length=255, blank=True, help_text="IMDB format instrumentation")
parts = models.JSONField(null=True, blank=True) original_parts = models.JSONField(default=dict, blank=True, help_text="Original printed parts (IMSLP format)")
# Collection details # Collection details
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works") 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") 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") 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 # 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) 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") # Allocation to projects
projects = models.ManyToManyField('interface.Project', through='ProjectItem', related_name="works", help_text="Current usage")
@property def extract(self, *tags):
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)
@property qs = self.docs.filter(sections__tag__in=tags)
def tags(self): qs = qs.annotate(Count('sections'), end=Min('sections__end'), start=Max('sections__start')) \
return self.tag_list.split(';') if self.tag_list else [] .filter(sections__count=len(tags))
@tags.setter return list(qs.values_list('upload', 'start', 'end'))
def set_tags(self, tags):
self.tag_list = ";".join(tags)
@property @property
def digital_parts(self): def digital_parts(self):
return Part.objects.filter(doc__work=self.pk) return Section.objects.filter(doc__work=self.pk)
@property @property
def physical_parts(self): def physical_parts(self):
if not self.parts: if not self.original_parts:
return [] 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 @property
#def instruments(self): def tags(self):
# return self.orchestration.as_list() 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): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:
@ -346,30 +216,47 @@ class Work(models.Model):
@property @property
def is_available(self): def is_available(self):
if self.max_loans < 0: if self.max_projects < 0:
return True return True
return self.max_loans > self.loans return self.max_projects > self.loans
@property @property
def available(self): def available(self):
if self.max_loans < 0: if self.max_projects < 0:
return 'Unlimited' return 'Unlimited'
a = self.max_loans - self.loans a = self.max_projects - self.loans
return '{0} of {1}'.format(max(a, 0), self.max_loans) return '{0} of {1}'.format(max(a, 0), self.max_projects)
@property @property
def identifier(self): 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): def __str__(self):
return f"{self.name} ({self.composer})" 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): def doc_upload_filename(doc, filename):
storage = doc.work.collection.storage collection = doc.work.collection
storage = collection.storage
if not storage: if not storage:
raise RuntimeError("Collection has no storage attached") 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): class Document(models.Model):
""" """
@ -377,29 +264,28 @@ class Document(models.Model):
""" """
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs") work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1) 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) created = models.DateTimeField(auto_now_add=True)
version = models.CharField(max_length=30, blank=True) version = models.CharField(max_length=30, blank=True)
def __str__(self): def __str__(self):
return self.upload.name 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") doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="sections")
tag = models.SlugField(max_length=20) tag = models.CharField(max_length=50)
start = models.SmallIntegerField(null=True, blank=True) start = models.SmallIntegerField(null=True, blank=True)
end = models.SmallIntegerField(null=True, blank=True) end = models.SmallIntegerField(null=True, blank=True)
notes = models.TextField(blank=True)
class Meta: class Meta:
ordering = ['doc', 'start', 'pk'] ordering = ['doc', 'start', 'pk']
@property @property
def instrument(self): def instrument(self):
return tag_to_instrument(self.tag) return Instrument.from_tag(self.tag)
@property @property
def filename(self): def filename(self):

View File

@ -1,57 +1,131 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% block admin %} {% 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 %} {% endblock %}
{% block page %} {% block page %}
<h2><a href="{% url 'work_list' %}">Works</a> / <a href="{% url 'work_detail' document.work.pk %}">{{ document.work.name }}</a></h2> <h3 class="subtitle"><a href="{% url 'work_detail' document.work.pk %}">{{ document.work.name }}</a></h3>
<div id="annotation-area"> <div id="annotation-area" class="columns is-centered">
<div style="display: flex"> <div class="column is-narrow">
<canvas id="inline-viewer" style="width: 600px;"></canvas> <div class="has-text-centered">
<div style="margin: 0px 20px"> <div class="level">
<div> <div class="level-left">
<div style="text-align: center;"> <div class="level-item">
<p>Page: <span id="page_num"></span> / <span id="page_count"></span></p> <p>Page <span id="page-num">-</span> / <span id="page-count">-</span></p>
</div> </div>
<button id="prev"><i class="fas fa-backward"></i></button>
<button id="next"><i class="fas fa-forward"></i></button>
</div> </div>
<div id="tag-list" style="cursor: pointer; margin-top: 20px;"> <div class="level-right">
<div id="tag-None" data-tag="None">No tag</div> <div class="level-item">
<div id="tag-Score" data-tag="Score">Score</div> <div class="field has-addons">
{% for instrument in document.work.instruments %} <span class="control">
<div id="tag-{{ instrument.0 }}" data-tag="{{ instrument.0 }}">{{ instrument.1 }}</div> <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 %} {% 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> </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> </div>
<p>{{ document.upload.name }}</p> <p>{{ document.upload.name }}</p>
{% endblock %} {% endblock %}
{% block scripts %} {% 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" }} {{ json_data|json_script:"data" }}
<script type="text/javascript"> <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. // Loaded via <script> tag, create shortcut to access PDF.js exports.
let pdfjsLib = window['pdfjs-dist/build/pdf']; let pdfjsLib = window['pdfjs-dist/build/pdf'];
// The workerSrc property shall be specified. // The workerSrc property shall be specified.
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js';
// get current page tags // get current page tags
let data = JSON.parse(document.getElementById('data').textContent); let data = JSON.parse(document.getElementById('data').textContent);
let currentTags = document.getElementById('current-tags'); let tagArea = document.getElementById('tag-area');
var selectedTag = document.getElementById('tag-None'); var dirty = false;
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, pageNum = 1,
pageRendering = false, pageRendering = false,
pageNumPending = null, pageNumPending = null,
@ -59,15 +133,15 @@ var pdfDoc = null,
canvas = document.getElementById('inline-viewer'), canvas = document.getElementById('inline-viewer'),
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
/** /**
* Get page info from document, resize canvas accordingly, and render page. * Get page info from document, resize canvas accordingly, and render page.
* @param num Page number. * @param num Page number.
*/ */
function renderPage(num) { function renderPage(num) {
pageRendering = true; pageRendering = true;
// Using promise to fetch the page // Using promise to fetch the page
pdfDoc.getPage(num).then(function(page) { pdfDoc.getPage(num).then(function (page) {
var viewport = page.getViewport({scale: scale}); var viewport = page.getViewport({ scale: scale });
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
@ -78,8 +152,15 @@ function renderPage(num) {
}; };
var renderTask = page.render(renderContext); var renderTask = page.render(renderContext);
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 // Wait for rendering to finish
renderTask.promise.then(function() { renderTask.promise.then(function () {
pageRendering = false; pageRendering = false;
if (pageNumPending !== null) { if (pageNumPending !== null) {
// New page rendering is pending // New page rendering is pending
@ -90,104 +171,219 @@ function renderPage(num) {
}); });
// Update page counters // Update page counters
document.getElementById('page_num').textContent = num; document.getElementById('page-num').textContent = num;
// get the page tags
let tag = data.pageTags[num];
if(tag) {
selectTag(tag);
} else {
if (selectedTag) {
setTag(selectedTag.dataset.tag);
} }
}
}
/** /**
* If another page rendering in progress, waits until the rendering is * If another page rendering in progress, waits until the rendering is
* finised. Otherwise, executes rendering immediately. * finised. Otherwise, executes rendering immediately.
*/ */
function queueRenderPage(num) { function queueRenderPage(num) {
if (pageRendering) { if (pageRendering) {
pageNumPending = num; pageNumPending = num;
} else { } else {
renderPage(num); renderPage(num);
} }
} }
/** /**
* Displays previous page. * Displays previous page.
*/ */
function onPrevPage() { function onPrevPage() {
if (pageNum <= 1) { if (pageNum <= 1) {
return; return;
} }
pageNum--; pageNum--;
queueRenderPage(pageNum); queueRenderPage(pageNum);
} }
document.getElementById('prev').addEventListener('click', onPrevPage); //document.getElementById('prev').addEventListener('click', onPrevPage);
/** /**
* Displays next page. * Displays next page.
*/ */
function onNextPage() { function onNextPage() {
if (pageNum >= pdfDoc.numPages) { if (pageNum >= pdfDoc.numPages) {
return; return;
} }
pageNum++; pageNum++;
queueRenderPage(pageNum); queueRenderPage(pageNum);
} }
document.getElementById('next').addEventListener('click', onNextPage); //document.getElementById('next').addEventListener('click', onNextPage);
/** /**
* Asynchronously downloads PDF. * Asynchronously downloads PDF.
*/ */
pdfjsLib.getDocument(url).promise.then(function(pdfDoc_) { pdfjsLib.getDocument(url).promise.then(function (pdfDoc_) {
pdfDoc = pdfDoc_; pdfDoc = pdfDoc_;
document.getElementById('page_count').textContent = pdfDoc.numPages;
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 // Initial/first page rendering
renderPage(pageNum); renderPage(pageNum);
});
function setTag(tag) { tagArea.innerHTML = '';
data.pageTags[pageNum] = tag; for (let pageTag of data.pageTags) {
addTag(pageTag[0], pageTag[1], pageTag[2]);
}
dirty = false;
});
function addInstrument() {
let name = document.getElementById('add-instrument-name');
let variant = document.getElementById('add-instrument-variant');
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);
}
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);
}
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; dirty = true;
selectTag(tag);
}
function selectTag(tag) {
if( selectedTag ) {
selectedTag.classList.remove('selected');
} }
selectedTag = document.getElementById('tag-' + tag);
if( selectedTag) {
selectedTag.classList.add('selected');
}
}
function saveTags() { function setTagStart(el) {
console.log(data.pageTags); if (pageNum > el.dataset.end) {
alert("Select the first page and click extend");
return;
}
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() {
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("", { fetch("", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}"}, headers: { "Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}" },
body: JSON.stringify(data.pageTags)} body: JSON.stringify(pageTags)
}
).then((response) => { ).then((response) => {
if(response.ok) { if (response.ok) {
window.location = "{% url 'work_detail' document.work.pk %}" window.location = "{% url 'work_detail' document.work.pk %}"
} else { } else {
alert("Failed: " + response.statusText) alert("Failed: " + response.statusText)
} }
}); });
dirty = false; dirty = false;
} }
function checkSaved(e) { 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) { if (dirty) {
e.preventDefault(); e.preventDefault();
} }
} }
window.addEventListener('beforeunload', checkSaved); window.addEventListener('beforeunload', checkSaved);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,34 +1,60 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% block admin %} {% 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 %} {% endblock %}
{% block page %} {% block page %}
<p> <form action="" method="post" target="_blank">
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;">
{% csrf_token %} {% csrf_token %}
<div style="margin: 0px 20px; text-align: right;"> <div>
<label for="instrument-select">Pick your instrument</label> <div class="field is-grouped is-grouped-centered">
<select id="instrument-select" name="instrument" onchange="updateParts()"> <div class="field has-addons control">
{% for tag, instrument in instruments %}
<option value="{{ tag }}">{{ instrument }}</option> <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 %} {% endfor %}
</select><br/><br/> </datalist>
<label for="part-preference">Part preference</label> </span>
<input id="part-preference" name="part" type="number" value="0" min="0" max="4" onchange="updateParts()" size="1"/><br/><br/> <span class="control">
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button> <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> </div>
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
<table class="table is-striped is-fullwidth">
<thead> <thead>
<tr> <tr>
<th/> <th/>
<th>Piece</th> <th>Piece</th>
<th>Running time</th>
<th>Part</th> <th>Part</th>
<th/>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -36,32 +62,78 @@
<tr> <tr>
<td>{{ forloop.counter }}.</td> <td>{{ forloop.counter }}.</td>
<td> <td>
{% if request.is_admin %}
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a> <a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
{% else %}
{{ item.work.name }}
{% endif %}
</td> </td>
<td>{% firstof item.work.running_time "------" %}</td>
<td class="select-cell"> <td class="select-cell">
<input type="hidden" name="works" value="{{ item.work.pk }}"/> <input type="hidden" name="works" value="{{ item.work.pk }}"/>
<span class="select is-small">
<select name="instruments"> <select name="instruments">
<option value='-'>None</option> <option value='-'>None</option>
{% for part in item.work.parts %} {% for part in item.work.digital_parts %}
<option value='{{ part.tag }}'>{{ part.instrument }}</option> <option value='{{ part.tag }}'>{{ part.instrument }}</option>
{% endfor %} {% endfor %}
</select> </select>
</span>
</td>
<td>
<a href=""><i class="fas fa-download"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </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> </table>
</form> </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 %} {% endblock %}
{% block scripts %} {% block scripts %}
{{ instruments|json_script:'instruments' }}
<script type="text/javascript"> <script type="text/javascript">
const INSTRUMENTS = JSON.parse(document.getElementById('instruments').innerText);
function updateParts() { 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 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"); let instruments = document.getElementsByName("instruments");
for(let i=0; i<instruments.length; i++) { for(let i=0; i<instruments.length; i++) {
@ -82,9 +154,10 @@ function updateParts() {
} }
document.getElementById("instrument-select").value="{{instrument}}"; document.getElementById("instrument-name").value = localStorage.getItem('instrument-name', 'All');
document.getElementById("part-preference").value="{{part}}"; document.getElementById("part-preference").value = localStorage.getItem('part-preference', 'Any');
updateParts(); updateParts();
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,8 +1,14 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% block admin %} {% 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 %}" class="button is-link">
<a href="{% url 'item_list_append' project.pk %}"><i class="fas fa-plus-circle"></i><span class="smhide action">Add</a></a> <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 %} {% endblock %}
{% block page %} {% 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 %} {% 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 %} {% endif %}
{% if request.is_admin %} {% if request.is_admin %}
<a href="{% url 'work_list' %}">Library</a> <li><a href="{% url 'work_list' %}">Library</a></li>
{% endif %} {% endif %}
</ul>

View File

@ -1,37 +1,160 @@
{% extends 'interface/project_base.html' %} {% extends 'interface/project_base.html' %}
{% load path_filters %} {% 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 %} {% 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 %} {% endblock %}
{% block page %} {% block page %}
<h2>{{ work.name }} {% if work.running_time %}({{ work.duration }}){% endif %} <small>[{{ work.identifier }}]</small></h2> <h3 class="title">
<h4>{{ work.composer }}{% if work.version %} - {{ work.version }}{% endif %}</h4> {{ work.name }}
<p>{{ work.notes }}</p> {% for tag in work.tags %}
{% if work.collection %} <span class="tag is-success">{{ tag }}</span>
<p>Location: {{ work.collection }} [{{ work.collection_index }}]</p> {% endfor %}
{% 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>
{% if work.parent %} <p class="block">
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a></p> Location: <a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]<br/>
{% endif %} 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.related_works.count %} {% if work.parent %}
<h3>Related</h3> <p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
<ul> </p>
{% endif %}
{% if work.related_works.count %}
<h3>Related</h3>
<ul>
{% for related in work.related_works.all %} {% for related in work.related_works.all %}
<li><a href="{% url 'work_detail' related.pk %}">{{ related.name }} - {{ related.composer }}</a></li> <li><a href="{% url 'work_detail' related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
</section>
<section class="block">
<h4>Loans <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 %} {% if request.is_admin %}
<a href="{% url 'work_add_to_project' work.pk %}"><i class="fas fa-plus-circle"></i></a> <section class="block">
{% endif %} <div class="box">
</h4> <div class="level">
<table style="margin: 10px auto"> <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> <thead>
<tr> <tr>
<th>Ensemble</th> <th>Ensemble</th>
@ -43,51 +166,46 @@
<tbody> <tbody>
{% for item in work.current_loans %} {% for item in work.current_loans %}
<tr> <tr>
<td><a href="{% url 'ensemble_detail' item.project.ensemble_id %}">{{ item.project.ensemble.name }}</a></td> <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><a href="{% url 'project_detail' item.project.pk %}">{{ item.project.name }}</a></td>
<td>{{ item.checkout.date|date:"d/m/Y" }}</td> <td>{{ item.checkout.date|date:"d/m/Y" }}</td>
<td>{{ item.due.date|date:"d/m/Y" }}</td> <td>{{ item.due.date|date:"d/m/Y" }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td>No current loans</td></tr> <tr>
<td>No current loans</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
</section>
{% endif %}
{% 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>Printed Parts</h4>
<p>
{% for inst, c in work.physical_parts %}
<span class="badge">{{ inst }} ({{ c }})</span>
{% empty %}
No physical parts available
{% endfor %}
</p>
<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 %} {% endblock %}

View File

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

View File

@ -1,31 +1,37 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% block page %} {% 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"> <form action="" method="post" target="_blank">
{% csrf_token %} {% csrf_token %}
<table style="margin: 0px auto">
<thead> <div class="admin-tools is-pulled-right">
<tr> <button type="submit" class="button is-link">
<th>Part</th> <span class="icon"><i class="fas fa-print"></i></span>
<th>Copies</th> <span>Print Set</span>
</tr> </button>
</thead> <a class="button is-link is-light" href="{% url 'work_detail' pk=object.pk %}">
<tbody> <span>Cancel</span>
{% for part in work.parts %} </a>
<tr> </div>
<td>{{ part.instrument }}</td>
<td>
<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="parts" type="hidden" value="{{ part.tag }}">
<input name="copies" type="number" value="{% if part.tag == 'Score' %}0{% else %}1{% endif %}" size="1"> <input name="copies" type="number" value="{% if part.tag == 'Score' %}0{% else %}1{% endif %}" size="1">
</td> </div>
</tr>
{% endfor %} {% endfor %}
<tr> </div>
<td colspan="2" style="text-align: center;"><button type="submit"><i class="fas fa-print"></i> Print</button></td>
</tr>
</tbody>
</table>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -20,3 +20,31 @@ class IntegrationTestCase(TestCase):
def test_integration(self): def test_integration(self):
pass 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

@ -9,16 +9,21 @@ urlpatterns = [
path('projects/<int:project>/items/manage', views.ProjectItemManageView.as_view(), name="item_list_manage"), 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('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', 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>', 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>/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/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>/download', views.DocumentDownloadView.as_view(), name="document_download"),
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"), 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"), path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
] ]
from django.views.static import serve 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.detail import DetailView, SingleObjectMixin, View
from django.views.generic.list import ListView, MultipleObjectMixin from django.views.generic.list import ListView, MultipleObjectMixin
from django.views.generic.edit import CreateView, FormView, UpdateView 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 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.utils.timezone import now
from django.urls import reverse
import json import json
import os.path
from interface.views import EnsembleMixin, ProjectMixin from interface.views import EnsembleMixin, ProjectMixin
from interface.models import Project 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 . import forms, models
from .pdf_utils import extract_pages, extract_and_concat from .pdf_utils import extract_pages, extract_and_concat
class ProjectItemListView(ProjectMixin, ListView): class ProjectItemListView(ProjectMixin, ListView):
template_name = "library/item_list.html" template_name = "library/item_list.html"
model = models.Item model = models.ProjectItem
def post(self, request, **kwargs): def post(self, request, **kwargs):
@ -44,7 +47,7 @@ class ProjectItemListView(ProjectMixin, ListView):
if tag == '-': if tag == '-':
continue 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)) sections.append((part.doc.upload.path, part.doc.work.name, part.start, part.end, 1))
result = extract_and_concat(sections) result = extract_and_concat(sections)
@ -64,11 +67,16 @@ class ProjectItemListView(ProjectMixin, ListView):
data['instruments'] = INSTRUMENTS data['instruments'] = INSTRUMENTS
data['instrument'] = self.request.session.get('instrument', 'Score') data['instrument'] = self.request.session.get('instrument', 'Score')
data['part'] = self.request.session.get('part', '0') 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 return data
class ProjectItemManageView(ProjectMixin, ListView): class ProjectItemManageView(ProjectMixin, ListView):
template_name = "library/item_list_manage.html" template_name = "library/item_list_manage.html"
model = models.Item model = models.ProjectItem
def post(self, request, **kwargs): def post(self, request, **kwargs):
self.request = request self.request = request
@ -99,38 +107,61 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
def get_object(self): def get_object(self):
return self.get_project() return self.get_project()
class CollectionListView(EnsembleMixin, ListView):
def get_queryset(self):
return Collection.objects.filter(administrators=self.request.user)
class WorkListView(EnsembleMixin, ListView): class WorkListView(EnsembleMixin, ListView):
paginate_by = 20 paginate_by = 20
def get_queryset(self): def get_works(self):
#works = Work.objects.filter(collection__ensembles=self.request.ensemble_id).order_by('name').select_related('collection') return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').distinct().select_related('collection')
works = Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').select_related('collection')
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None)) def get_context_data(self, *args, **kwargs):
works = works.annotate(loan_count=loan_count) data = super(WorkListView, self).get_context_data(*args, **kwargs)
data['title'] = f'Music available to {self.ensemble.name}'
return data
def get_queryset(self):
works = self.get_works().order_by('name')
q = self.request.GET.get('filter') q = self.request.GET.get('filter')
if q: 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 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): class WorkAddView(EnsembleMixin, FormView):
template_name = "interface/default_form.html" template_name = "interface/default_form.html"
form_class = forms.WorkCreateForm form_class = forms.WorkCreateForm
def get_form(self): title = "Add a new work"
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
def form_valid(self, form): def form_valid(self, form):
work = form.save(commit=False) work = form.save(commit=False)
work.ensemble_id = self.request.ensemble_id work.ensemble_id = self.request.ensemble_id
work.collection_id = self.kwargs['pk']
work.save() work.save()
# handle the files # handle the files
@ -144,12 +175,26 @@ class WorkAddView(EnsembleMixin, FormView):
else: else:
return redirect('work_detail', pk=work.pk) return redirect('work_detail', pk=work.pk)
class WorkDetailView(EnsembleMixin, DetailView): class WorkMixin(object):
def get_queryset(self): 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) 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): class WorkAddToProject(EnsembleMixin, FormView):
admin_required = True
form_class = forms.ProjectSelectForm form_class = forms.ProjectSelectForm
template_name = "interface/default_form.html" template_name = "interface/default_form.html"
title = "Select project to add work to" title = "Select project to add work to"
@ -188,7 +233,7 @@ class WorkPartSetView(EnsembleMixin, DetailView):
sections = [] sections = []
for i, tag in enumerate(parts): 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]))) sections.append((part.doc.upload.path, part.instrument, part.start, part.end, int(copies[i])))
result = extract_and_concat(sections) result = extract_and_concat(sections)
@ -200,25 +245,59 @@ class WorkPartSetView(EnsembleMixin, DetailView):
return response return response
def get_queryset(self): 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): class WorkAddDocumentView(EnsembleMixin, CreateView):
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
class DocumentAddView(EnsembleMixin, CreateView):
template_name = "interface/default_form.html" template_name = "interface/default_form.html"
model = Document model = Document
fields = ['upload'] fields = ['upload']
def form_valid(self, form): def title(self):
self.object = form.save(commit=False) work = Work.objects.get(pk=self.kwargs['pk'])
self.object.work_id = self.kwargs['pk'] return f"Add a document to {work.name}"
self.object.save()
return redirect('document_annotate', self.object.pk)
class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View): 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):
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)
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): def get(self, request, **args):
self.request = request self.request = request
@ -229,10 +308,7 @@ class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View):
#return response #return response
return redirect(self.object.upload.url) return redirect(self.object.upload.url)
def get_queryset(self): class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView):
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id)
class DocumentAnnotateView(EnsembleMixin, DetailView):
template_name = 'library/document_annotate.html' template_name = 'library/document_annotate.html'
def post(self, request, **args): def post(self, request, **args):
@ -242,36 +318,24 @@ class DocumentAnnotateView(EnsembleMixin, DetailView):
data = json.loads(request.body) data = json.loads(request.body)
tags = {} self.object.sections.all().delete()
for page, tag in data.items(): for tag, start, end in data:
tags.setdefault(tag, []).append(int(page)) #pages.sort()
try: #end = pages[-1] if len(pages) > 1 else None
del(tags['None']) o = self.object.sections.create(tag=tag, start=start, end=end)
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)
return HttpResponse(status=204) return HttpResponse(status=204)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super(DocumentAnnotateView, self).get_context_data(**kwargs) data = super(DocumentAnnotateView, self).get_context_data(**kwargs)
pages = {} pages = []
for part in data['document'].parts.all(): for part in data['document'].sections.all():
for i in range(part.start, (part.end or part.start)+1): pages.append((part.tag, part.start, part.end))
pages[i] = part.tag
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.orchestration} data['json_data'] = {'pageTags': pages, 'instruments': dict(INSTRUMENTS)}
return data 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): class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
@ -289,4 +353,9 @@ class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
return response return response
def get_queryset(self): 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 SECRET_KEY = None
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = False
ALLOWED_HOSTS = ['localhost'] ALLOWED_HOSTS = ['localhost']
# Application definition # Application definition
POLYPHONIC_MODULES = [
'library',
'submissions'
]
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@ -38,10 +43,14 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_markdown2', 'django_markdown2',
'crispy_forms',
'crispy_bulma',
'byostorage', 'byostorage',
'interface', 'interface',
'library', ] + POLYPHONIC_MODULES
]
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
CRISPY_TEMPLATE_PACK = "bulma"
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',

View File

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