Removing old files
This commit is contained in:
parent
598ee5ad7e
commit
8f18b9ab9d
@ -1 +1,2 @@
|
|||||||
pylint==2.6.0
|
pylint==2.6.0
|
||||||
|
django-debug-toolbar
|
||||||
@ -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)
|
||||||
@ -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']
|
|
||||||
@ -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')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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)
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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}"
|
||||||
|
"""
|
||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -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>
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div style="float: right; margin-top: 10px; color: #999;">
|
|
||||||
<small>{{ ensemble.ensemble_code }}</small>
|
{% 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 %}
|
||||||
@ -2,40 +2,109 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-narrow is-hidden-touch" id="projectMenu">
|
||||||
|
<div style="margin: auto 2em;">
|
||||||
|
<aside class="menu">
|
||||||
|
|
||||||
|
<p class="menu-label">My Things</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li><a href="{% url 'ensemble_list' %}">Ensembles</a></li>
|
||||||
|
<li><a href="{% url 'ensemble_detail' %}">Projects</a></li>
|
||||||
|
<li><a href="{% url 'work_list' %}">Library</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if project %}
|
||||||
|
<p class="menu-label">This Project</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li><a role="tab" href="{% url 'project_detail' project=project.id %}">Project Info</a></li>
|
||||||
|
{% for page in project.wiki_pages.all %}
|
||||||
|
<li><a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
|
||||||
|
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<li><a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
|
||||||
|
|
||||||
|
{% if 'library' in modules %}
|
||||||
|
<li><a href="{% url 'item_list' project=project.id %}">My Music</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'submission' in modules %}
|
||||||
|
<!--a role="tab" href="">Record a submission</a-->
|
||||||
{% if request.is_admin %}
|
{% if request.is_admin %}
|
||||||
<div class="admin-tools">
|
<li><a role="tab" class="admin-link" href="{% url 'submission_list' project=project.id %}">Submissions</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li><a role="tab" href="{% url 'submission_create' project=project.id %}">Send a File</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="menu-label">Admin</p>
|
||||||
|
{% if request.is_admin %}
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li><a class="admin-link" href="{% url 'collection_list' %}">Collections</a></li>
|
||||||
|
{% if request.user.is_superuser %}
|
||||||
|
<li><a class="admin-link" href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="menu-list">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<li><a href="{% url 'logout' %}">Logout</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{% url 'login' %}">Login</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
{% if project %}
|
||||||
|
<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 %}
|
{% block admin %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if project %}<h3 class="title">{{ project.name }}</h3>{% endif %}
|
||||||
<h1>{{ project.name }}</h1>
|
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
No content
|
No content
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<div class="project-links">
|
</section>
|
||||||
<div class="pills" role="tablist">
|
|
||||||
|
|
||||||
{% if project %}
|
|
||||||
<a role="tab" href="{% url 'project_detail' project=project.id %}">Project info</a>
|
|
||||||
{% for page in project.wiki_pages.all %}
|
|
||||||
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
|
|
||||||
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if project and project.enable_submissions%}
|
|
||||||
<a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a>
|
|
||||||
<!--a role="tab" href="">Record a submission</a-->
|
|
||||||
{% if request.is_admin %}
|
|
||||||
<a role="tab" href="{% url 'submission_list' project=project.id %}">Submissions</a>
|
|
||||||
{% endif %}
|
|
||||||
<a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% include "library/project_menu.html" %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if ensemble %}
|
||||||
|
<div class="project-footer">
|
||||||
|
{{ ensemble.name }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,31 +1,61 @@
|
|||||||
{% 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 %}
|
||||||
|
</div>
|
||||||
{% if project.enable_submissions %}
|
|
||||||
{% include 'submissions/project_detail.html' %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if project.enable_library %}
|
|
||||||
{% include 'library/project_detail.html' %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
ALL = {{ targets|join:" " }}
|
|
||||||
|
|
||||||
-include "local.mk"
|
|
||||||
|
|
||||||
all: ${ALL}
|
|
||||||
|
|
||||||
{% for s in submissions %}
|
|
||||||
{{ s.name }}:
|
|
||||||
curl -o $@ -L {{ s.url }}
|
|
||||||
{% endfor %}
|
|
||||||
@ -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 %}
|
||||||
|
|||||||
@ -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>
|
|
||||||
<div class="list-group narrow">
|
|
||||||
{% for resource in object_list %}
|
{% for resource in object_list %}
|
||||||
{% with download=resource.presigned_url %}
|
{% with download=resource.file.url %}
|
||||||
<div>
|
<div class="column is-half">
|
||||||
|
<div class="card {% if not object.visible %}disabled{% endif %}">
|
||||||
|
<div class="card-header">
|
||||||
|
|
||||||
|
<div class="card-header-title">
|
||||||
|
{% if download %}
|
||||||
|
<a href="{{ download }}">{{ resource.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ resource.name }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-header-icon">
|
||||||
{% if request.is_admin %}
|
{% 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% empty %}
|
||||||
|
<div class="column">
|
||||||
|
<p>There are resources for this project</p>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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"),
|
|
||||||
]
|
]
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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']
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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):
|
||||||
|
|||||||
@ -1,38 +1,113 @@
|
|||||||
{% 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>
|
<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>
|
||||||
|
<div class="box" style="display: inline-block;">
|
||||||
|
<canvas id="inline-viewer" style="width: 500px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
|
||||||
|
<div class="tag-grid">
|
||||||
|
<div class="grid-column" id="page-list">
|
||||||
|
</div>
|
||||||
|
<div class="grid-column" id="tag-area">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p>{{ document.upload.name }}</p>
|
<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 }}";
|
||||||
@ -45,11 +120,10 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs
|
|||||||
|
|
||||||
// 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,
|
||||||
@ -78,6 +152,13 @@ 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;
|
||||||
@ -90,17 +171,8 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,7 +197,7 @@ function onPrevPage() {
|
|||||||
pageNum--;
|
pageNum--;
|
||||||
queueRenderPage(pageNum);
|
queueRenderPage(pageNum);
|
||||||
}
|
}
|
||||||
document.getElementById('prev').addEventListener('click', onPrevPage);
|
//document.getElementById('prev').addEventListener('click', onPrevPage);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays next page.
|
* Displays next page.
|
||||||
@ -137,41 +209,156 @@ function onNextPage() {
|
|||||||
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);
|
||||||
|
|
||||||
|
tagArea.innerHTML = '';
|
||||||
|
for (let pageTag of data.pageTags) {
|
||||||
|
addTag(pageTag[0], pageTag[1], pageTag[2]);
|
||||||
|
}
|
||||||
|
dirty = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function setTag(tag) {
|
function addInstrument() {
|
||||||
data.pageTags[pageNum] = tag;
|
let name = document.getElementById('add-instrument-name');
|
||||||
dirty = true;
|
let variant = document.getElementById('add-instrument-variant');
|
||||||
selectTag(tag);
|
|
||||||
|
let inst_name = name.value;
|
||||||
|
var tag = null;
|
||||||
|
for (let key in data.instruments) {
|
||||||
|
if (data.instruments[key] == inst_name) {
|
||||||
|
tag = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTag(tag) {
|
if (!tag) {
|
||||||
if( selectedTag ) {
|
alert("Unknown tag: " + name);
|
||||||
selectedTag.classList.remove('selected');
|
return;
|
||||||
}
|
}
|
||||||
selectedTag = document.getElementById('tag-' + tag);
|
|
||||||
if( selectedTag) {
|
if (variant.value) {
|
||||||
selectedTag.classList.add('selected');
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTagStart(el) {
|
||||||
|
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() {
|
function saveTags() {
|
||||||
console.log(data.pageTags);
|
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 %}"
|
||||||
@ -182,6 +369,15 @@ function saveTags() {
|
|||||||
dirty = false;
|
dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function checkSaved(e) {
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -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>
|
</div>
|
||||||
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
|
<span class="control">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
|
<span class="icon"><i class="fas fa-copy"></i></span>
|
||||||
|
<span>Get My Parts!</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table 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 %}
|
||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -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>
|
||||||
@ -1,20 +1,45 @@
|
|||||||
{% 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>
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
Location: <a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]<br/>
|
||||||
|
Running time: {{ work.duration }}<br/>
|
||||||
|
Licence: {{ work.get_licence_display }}<br/>
|
||||||
|
{% for meta in work.meta %}
|
||||||
|
{{ meta.get_name_display }}: {{ meta.value }}<br/>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
|
||||||
{% if work.parent %}
|
{% if work.parent %}
|
||||||
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a></p>
|
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if work.related_works.count %}
|
{% if work.related_works.count %}
|
||||||
@ -25,13 +50,111 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<section class="block">
|
||||||
|
|
||||||
<h4>Loans
|
<div class="columns">
|
||||||
{% if request.is_admin %}
|
<div class="column is-half">
|
||||||
<a href="{% url 'work_add_to_project' work.pk %}"><i class="fas fa-plus-circle"></i></a>
|
<div class="box">
|
||||||
{% endif %}
|
<h4 class="subtitle is-size-4">
|
||||||
|
<span class="icon"><i class="fas fa-book"></i></span>
|
||||||
|
Printed Parts
|
||||||
</h4>
|
</h4>
|
||||||
<table style="margin: 10px auto">
|
<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 %}
|
||||||
|
<section class="block">
|
||||||
|
<div class="box">
|
||||||
|
<div class="level">
|
||||||
|
<h4 class="is-size-4">
|
||||||
|
<span class="icon"><i class="fas fa-book-reader"></i></span>
|
||||||
|
Loans
|
||||||
|
</h4>
|
||||||
|
<span class="level-right">
|
||||||
|
<a class="icon-text" href="{% url 'work_add_to_project' work.pk %}"><span class="icon"><i
|
||||||
|
class="fas fa-plus-circle"></i></span> Checkout</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
<thead>
|
<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>
|
||||||
<h4>Printed Parts</h4>
|
</section>
|
||||||
<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>
|
|
||||||
|
|
||||||
{% 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 %}
|
{% endif %}
|
||||||
</li>
|
|
||||||
{% endfor %}
|
{% block scripts %}
|
||||||
</ul>
|
<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 %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -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">
|
||||||
<input name="filter" type="text" placeholder="Filter" value="{{ request.GET.filter }}"/>
|
<div class="field has-addons">
|
||||||
<a href="?">Clear</a>
|
<div class="control is-expanded">
|
||||||
</form>
|
<input class="input" name="filter" type="text" placeholder="Filter" value="{{ request.GET.filter }}"/>
|
||||||
</div>
|
</div>
|
||||||
<table class="zebra wide">
|
<div class="control">
|
||||||
|
<a class="button" href="?"><i class="fas fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table class="table is-striped is-fullwidth">
|
||||||
<thead>
|
<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">«</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">‹</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">›</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">»</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 %}
|
||||||
@ -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 %}
|
||||||
@ -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'), [])
|
||||||
@ -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'}))
|
||||||
189
library/views.py
189
library/views.py
@ -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')
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user