Got library sort of working

This commit is contained in:
Tris 2021-09-04 10:29:22 +10:00
parent 7aedc6bd07
commit 598ee5ad7e
37 changed files with 994 additions and 209 deletions

View File

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

View File

@ -44,6 +44,7 @@ class Ensemble(models.Model):
passphrase = models.CharField(max_length=100)
admins = models.ManyToManyField('auth.User', related_name='ensembles')
slug = models.SlugField(max_length=100, editable=False)
details = models.TextField(blank=True)
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL)
def active_projects(self):
@ -53,10 +54,10 @@ class Ensemble(models.Model):
code = str(self.code)
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
def save(self):
def save(self, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super(Ensemble, self).save()
super(Ensemble, self).save(**kwargs)
def __str__(self):
return self.name
@ -86,7 +87,7 @@ class Project(models.Model):
@property
def has_happened(self):
return self.event_date < timezone.now()
return self.event_date < timezone.now().date()
def save(self):
if not self.slug:

View File

@ -3,6 +3,7 @@
--border-color: #292929;
--gray-blue: #667788;
--light-blue: #c5eff7;
--light-grey: #EEEEEE;
}
@font-face {
@ -36,7 +37,7 @@ BODY {
}
.main {
max-width: 1000px;
max-width: 1280px;
margin: 10px auto;
border: 1px solid var(--border-color);
border-radius: 5px;
@ -55,16 +56,30 @@ BODY {
margin: 0px auto;
}
.wide {
width: 1200px;
}
.collapse {
display: flex;
flex-direction: row;
justify-content: space-around;
}
@media all and (max-width: 1200px) {
.wide {
width: 900px;
}
}
@media all and (max-width: 900px) {
.mdhide {
display: none;
}
.wide {
width: auto;
}
}
@media all and (max-width: 700px) {
@ -74,6 +89,9 @@ BODY {
.collapse {
flex-direction: column;
}
.wide {
width: auto;
}
}
@ -138,6 +156,16 @@ INPUT[type=checkbox] {
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;
@ -232,10 +260,28 @@ H1 {
text-align: center;
}
TD {
padding: 5px;
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;
@ -298,12 +344,12 @@ TABLE SELECT {
border: 1px solid #999;
}
TD.select-cell {
padding: 0;
padding: 0px !important;
}
.select-cell SELECT {
border: none;
background-color: white;
background-color: transparent;
height: 30px;
font-family: inherit;
font-size: inherit;

View File

@ -2,7 +2,7 @@
{% block page %}
<div>
<h3>{{ title }}</h3>
<h3>{% firstof title view.title %}</h3>
<form class="vertical" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}

View File

@ -1,22 +1,12 @@
{% extends "interface/project_base.html" %}
{% extends "base.html" %}
{% block page %}
<div style="flex-grow: 1">
<h1>Projects for {{ ensemble.name }}</h1>
<div class="list-group narrow">
{% for project in ensemble.active_projects %}
<a class="" href="{% url 'project_detail' project=project.id %}">
<h3>{{ project.name }}</h3>
<p><small>
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
{% if project.works.count %}{{ project.works.count }} works<br/>{% endif %}
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
</small></p>
</a>
{% block content %}
<h2>{{ ensemble.name }}</h2>
<p>{{ ensemble.details }}</p>
<h4>Administrators</h4>
<ul>
{% for admin in ensemble.admins.all %}
<li><a href="mailto:{{ admin.email }}">{% firstof admin.get_full_name admin.get_username %}</a></li>
{% endfor %}
</div>
<div style="float: right; margin-top: 10px; color: #999;">
<small>{{ ensemble.ensemble_code }}</small>
</div>
</div>
{% endblock %}
</ul>
{% endblock %}

View File

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

View File

@ -7,9 +7,9 @@
{% if project.event_date %}
<h3 class="text-center">
{% if project.has_happened %}
{{ project.event_date|timesince }}
{{ project.event_date|timesince }} ago.
{% else %}
{{ project.event_date|}}
In {{ project.event_date|timeuntil }}.
{% endif %}
</h3>
{% endif %}

View File

@ -0,0 +1,10 @@
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def url_update(context, **kwargs):
params = context.request.GET.copy()
for k in kwargs:
params[k] = kwargs[k]
return "?" + params.urlencode()

View File

@ -0,0 +1,6 @@
from django.test import TestCase
class IntegrationTestCase(TestCase):
def test_runs(self):
self.assertTrue(True)

View File

@ -10,7 +10,9 @@ urlpatterns = [
path('register', views.register, name="register"),
path('manage', views.ManageView.as_view(), name="manage"),
path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
path('', views.EnsembleProjectListView.as_view(), name='ensemble_detail'),
path('ensembles/<int:pk>', views.EnsembleDetailView.as_view(), name='ensemble_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"),

View File

@ -60,6 +60,10 @@ class EnsembleMixin(object):
return super().dispatch(request, *args, **kwargs)
@property
def ensemble(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
class ProjectMixin(EnsembleMixin):
def get_project(self):
@ -195,7 +199,8 @@ def logout(request):
return redirect('/')
class EnsembleDetailView(EnsembleMixin, DetailView):
class EnsembleProjectListView(EnsembleMixin, DetailView):
template_name = 'interface/ensemble_project_list.html'
def dispatch(self, request, *args, **kwargs):
# capture provided urls
@ -207,6 +212,9 @@ class EnsembleDetailView(EnsembleMixin, DetailView):
def get_object(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
class EnsembleDetailView(DetailView):
model = models.Ensemble
class ProjectDetailView(ProjectMixin, DetailView):
def get_object(self):

13
library/README.md Normal file
View File

@ -0,0 +1,13 @@
# Model overview
Collections contain Works
Only the collection's administrators can add/remove/edit items
Collections can be enabled for Ensembles which makes everything availble in the Library
## How do loans work?
1. Project admins need to be able to search for available music - list with access type
2. Item can be attached to Project

View File

@ -2,17 +2,29 @@ from django.contrib import admin
from . import models
class OrchestrationAdmin(admin.ModelAdmin):
list_display = ['name', 'ensemble']
list_filter = ['ensemble']
#class OrchestrationAdmin(admin.ModelAdmin):
# list_display = ['name', 'ensemble']
# list_filter = ['ensemble']
#admin.site.register(models.Orchestration, OrchestrationAdmin)
class CollectionAdmin(admin.ModelAdmin):
list_display = ['name', 'location']
admin.site.register(models.Collection, CollectionAdmin)
class ItemInline(admin.TabularInline):
model = models.Item
class DocInline(admin.TabularInline):
model = models.Document
class WorkAdmin(admin.ModelAdmin):
list_display = ['name', 'composer', 'orchestration']
list_filter = ['ensemble']
inlines = [ItemInline]
list_display = ['name', 'edition', 'composer', 'running_time']
list_filter = ['collection']
inlines = [DocInline, ItemInline]
admin.site.register(models.Work, WorkAdmin)
class PartInline(admin.TabularInline):
model = models.Part
@ -20,14 +32,19 @@ class PartInline(admin.TabularInline):
class DocumentAdmin(admin.ModelAdmin):
list_display = ['work', '__str__']
list_filter = ['work__ensemble']
list_filter = ['work__collection']
inlines = [PartInline]
admin.site.register(models.Document, DocumentAdmin)
class ItemAdmin(admin.ModelAdmin):
list_display = ['project', 'work', 'order']
list_filter = ['project']
admin.site.register(models.Orchestration, OrchestrationAdmin)
admin.site.register(models.Work, WorkAdmin)
admin.site.register(models.Document, DocumentAdmin)
admin.site.register(models.Item, ItemAdmin)
admin.site.register(models.Item, ItemAdmin)
class EnsembleAccessAdmin(admin.ModelAdmin):
list_display = ['ensemble', 'collection', 'access_type']
list_filter = ['ensemble']
admin.site.register(models.EnsembleAccess, EnsembleAccessAdmin)

View File

@ -1,12 +1,15 @@
from django import forms
from .models import Work
from interface.models import Project
from django.db.models import Q
class WorkCreateForm(forms.ModelForm):
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}))
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False)
class Meta:
model = Work
fields = ['uploads', 'name', 'orchestration', 'running_time', 'notes']
fields = ['uploads', 'name', 'composer', 'edition', 'collection', 'code', 'running_time', 'notes']
class PlaylistAddForm(forms.Form):
work = forms.ModelChoiceField(queryset=Work.objects.all())
@ -21,4 +24,7 @@ class PlaylistAddForm(forms.Form):
self.instance = instance
def save(self):
self.instance.works.add(self.cleaned_data['work'])
self.instance.works.add(self.cleaned_data['work'])
class ProjectSelectForm(forms.Form):
project = forms.ModelChoiceField(queryset=Project.objects.all())

View File

View File

View File

@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand, CommandError
import argparse
import csv
from library import models
class Command(BaseCommand):
help = 'Imports works from a csv file'
def add_arguments(self, parser):
parser.add_argument('collection', type=int, help="Collection ID")
parser.add_argument('source', type=argparse.FileType('r'), help="Source CSV")
def handle(self, *args, **options):
collection = models.Collection.objects.get(pk=options['collection'])
reader = csv.DictReader(options['source'])
for row in reader:
collection.works.create(name=row['Piece'], composer=row['Composer'], notes=row['Notes'])

View File

@ -1,9 +1,10 @@
# Generated by Django 3.1.1 on 2021-03-11 07:07
# Generated by Django 3.1.1 on 2021-04-28 03:53
import byostorage.cached
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import library.models
import byostorage.cached
class Migration(migrations.Migration):
@ -11,14 +12,29 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('interface', '0022_auto_20210303_2043'),
('interface', '0027_auto_20210322_1154'),
('byostorage', '0003_auto_20210323_1047'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Collection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('location', models.CharField(help_text='Physical location', max_length=100)),
('notes', models.TextField(blank=True)),
('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')),
],
),
migrations.CreateModel(
name='Document',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('doctype', models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source')], default=1)),
('upload', models.FileField(storage=byostorage.cached.CachedStorage(), upload_to=library.models.doc_upload_filename)),
],
),
@ -48,15 +64,19 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(editable=False, max_length=100)),
('name', models.CharField(max_length=255)),
('running_time', models.IntegerField(blank=True, null=True)),
('version', models.CharField(blank=True, help_text='Version or edition details', max_length=100)),
('composer', models.CharField(blank=True, help_text='Use Composer / Arranger format', max_length=255)),
('code', models.CharField(blank=True, help_text='Collection specific code or number', max_length=100)),
('licence', models.PositiveSmallIntegerField(choices=[(2, 'Public Domain'), (4, 'Copyright Expired'), (6, 'Copyrighted'), (10, 'Internal use only')], default=6, help_text='Copyright status')),
('max_loans', models.BooleanField(default=1, help_text='How many projects can this work be attached to')),
('running_time', models.IntegerField(blank=True, help_text='Running time in seconds', null=True)),
('notes', models.TextField(blank=True)),
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='interface.ensemble')),
('tag_list', models.CharField(blank=True, help_text='Multiple tags for the work', max_length=255)),
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.collection')),
('orchestration', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.orchestration')),
('projects', models.ManyToManyField(related_name='works', through='library.Item', to='interface.Project')),
('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')),
],
options={
'unique_together': {('ensemble', 'slug')},
},
),
migrations.CreateModel(
name='Part',
@ -75,7 +95,16 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='item',
name='work',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.work'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_items', to='library.work'),
),
migrations.CreateModel(
name='EnsembleAccess',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access_type', models.PositiveSmallIntegerField(choices=[(1, 'Unlimited'), (2, 'Approval required')], default=2)),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.collection')),
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='interface.ensemble')),
],
),
migrations.AddField(
model_name='document',

View File

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

View File

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

View File

@ -1,27 +0,0 @@
# Generated by Django 3.1.1 on 2021-03-11 06:26
from django.db import migrations
ORCHESTRATIONS = {
'SATB': ('S', 'A', 'T', 'B'),
'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'),
'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb',
'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Ob1', 'Ob2', 'Hn1', 'Hn2',
'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba',
'Timp', 'Drum', 'Perc'),
}
def add_orchestrations(apps, schema_editor):
q = apps.get_model('library', 'Orchestration').objects
for name, instruments in ORCHESTRATIONS.items():
q.create(name=name, instruments=", ".join(instruments))
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RunPython(add_orchestrations, lambda apps, schema_editor: None),
]

View File

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

View File

@ -1,4 +1,4 @@
# Generated by Django 3.1.1 on 2021-03-22 23:47
# Generated by Django 3.1.1 on 2021-05-06 02:41
from django.db import migrations, models
@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0003_orchestrations'),
('library', '0004_auto_20210505_0927'),
]
operations = [
migrations.AddField(
model_name='work',
name='composer',
field=models.CharField(blank=True, max_length=255),
name='parts',
field=models.JSONField(blank=True, null=True),
),
]

View File

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

View File

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

View File

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

View File

@ -1,7 +1,11 @@
from os import SCHED_OTHER
from django.conf import settings
from django.db import models
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.functional import cached_property
from django.core.files.storage import get_storage_class
from django.db.models import Q
import re
@ -14,42 +18,181 @@ except (ImportError, AttributeError):
library_storage = get_storage_class()()
logger.info("Library storage: %s", library_storage.__class__.__name__)
INSTRUMENTS = [
('Score', 'Score'),
('S', 'Soprano'),
('A', 'Alto'),
('T', 'Tenor'),
('B', 'Bass'),
('V', 'Vocals'),
('Vln', 'Violin'),
('Vla', 'Viola'),
('Vc', 'Violoncello'),
('Cb', 'Contrabass'),
('Fl', 'Flute'),
('Picc', 'Piccolo'),
('Cl', 'Clarinet'),
('Ob', 'Oboe'),
('Hn', 'Horn'),
('Tpt', 'Trumpet'),
('Tbn', 'Trombone'),
('Tuba', 'Tuba'),
('Timp', 'Timpani'),
('Drum', 'Drumset'),
('Perc', 'Percussion'),
('Pno', 'Piano'),
('Hp', 'Harp'),
]
# 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 = {
'SATB': ('S', 'A', 'T', 'B'),
'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'),
'String Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb'),
'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb',
'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Hn1', 'Hn2',
'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba',
'Timp', 'Drum', 'Perc'),
'RWE': ('Fl1', 'Fl2', 'Cl', 'Tbn', 'Vln1', 'Vln2', 'Vla', 'Vc'),
'Custom': ()
'Custom': (),
}
'''
DOCTYPES = [
(1, 'PDF'),
(2, 'Audio'),
(3, 'Video'),
(4, 'Source'),
]
LICENCE_TYPES = [
(2, 'Public Domain'),
(4, 'Copyright Expired'),
(6, 'Copyrighted'),
(10, 'Internal use only'),
]
ACCESS_TYPES = [
(1, 'Unlimited'),
(2, 'Approval required'),
]
def tag_to_instrument(tag):
m = re.match(r'([A-Za-z]+)(\d*)', tag)
@ -58,13 +201,18 @@ def tag_to_instrument(tag):
l = m.groups()
return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip()
'''
class Orchestration(models.Model):
"""
Stores a list of instrument codes as a single entry (space delimited).
Can be global or ensemble specific
"""
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="orchestrations", null=True, blank=True)
name = models.CharField(max_length=100)
instruments = models.TextField()
def as_list(self):
tags = [ t.strip() for t in self.instruments.split(',') ]
tags = [ t.strip() for t in self.instruments.split(' ') ]
return [ (t, tag_to_instrument(t)) for t in tags if t ]
def save(self):
@ -73,44 +221,21 @@ class Orchestration(models.Model):
def __str__(self):
return self.name
class Work(models.Model):
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="works")
slug = models.SlugField(max_length=100, editable=False)
name = models.CharField(max_length=255)
composer = models.CharField(max_length=255, blank=True)
orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works')
running_time = models.IntegerField(null=True, blank=True)
notes = models.TextField(blank=True)
projects = models.ManyToManyField('interface.Project', through='Item', related_name="works")
class Meta:
unique_together = ['ensemble', 'slug']
@property
def parts(self):
return Part.objects.filter(doc__work=self.pk)
@property
def instruments(self):
return self.orchestration.as_list()
def save(self):
if not self.slug:
self.slug = slugify(self.name)
super(Work, self).save()
def __str__(self):
return self.name
'''
class Item(models.Model):
"""
Item represents a Work in a Project e.g. item in set list or programme
Item represents a specic version of a Work in a Project e.g. item in set list or programme
It also allows works to be shared from one ensemble to another on a per-project basis.
"""
project = models.ForeignKey('interface.Project', on_delete=models.CASCADE)
work = models.ForeignKey('Work', on_delete=models.CASCADE)
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name='project_items')
checkout = models.DateTimeField()
due = models.DateTimeField(null=True, blank=True)
returned = models.DateTimeField(null=True, blank=True)
approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
order = models.SmallIntegerField(default=0)
version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag")
class Meta:
ordering = ['order', 'work']
@ -118,20 +243,151 @@ class Item(models.Model):
def __str__(self):
return f"<{self.project.slug}:{self.work.slug}>"
class Collection(models.Model):
"""
Storage location for works (physical or virtual)
"""
name = models.CharField(max_length=255, help_text="Name of the collection")
administrators = models.ManyToManyField('auth.User', related_name="collections", help_text="Administrators for this collection")
location = models.CharField(max_length=100, help_text="Physical location (institution, town...)")
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True, help_text="Storage for documents")
notes = models.TextField(blank=True, help_text="Publicly visible notes about collection and loans policy")
#ensembles = models.ManyToManyField('interface.Ensemble', related_name="collections", through='EnsembleAccess')
def __str__(self):
return self.name
class EnsembleAccess(models.Model):
"""
Can have different access levels to a collection
"""
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="allowed_collections")
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="allowed_ensembles")
access_type = models.PositiveSmallIntegerField(choices=ACCESS_TYPES, default=2)
class Meta:
verbose_name_plural = "Ensemble access"
class Work(models.Model):
"""
A musical work 'owned' by a collection from a licencing perspective.
"""
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works", help_text="Owner")
slug = models.SlugField(max_length=100, editable=False)
name = models.CharField(max_length=255)
edition = models.CharField(max_length=100, blank=True, help_text="Edition details")
parent = models.ForeignKey('Work', null=True, blank=True, on_delete=models.SET_NULL, related_name="related_works",
help_text="Arrangement of another work or part of an anthology")
composer = models.CharField(max_length=255, blank=True, help_text="Use Composer / Arranger format")
#orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works', blank=True)
orchestration = models.CharField(max_length=255, blank=True, help_text="IMDB format instrumentation")
parts = models.JSONField(null=True, blank=True)
# Collection details
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works")
code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number")
licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status")
max_loans = models.IntegerField(default=1, help_text="How many projects can this work be attached to")
# Extra info
running_time = models.IntegerField(null=True, blank=True, help_text="Running time in seconds")
notes = models.TextField(blank=True)
tag_list = models.CharField(max_length=255, blank=True, help_text="Multiple tags for the work")
projects = models.ManyToManyField('interface.Project', through='Item', related_name="works", help_text="Current usage")
@property
def duration(self):
if self.running_time is None:
return "-:--"
return "{0:d}:{1:02d}".format(int(self.running_time / 60), self.running_time % 60)
@property
def tags(self):
return self.tag_list.split(';') if self.tag_list else []
@tags.setter
def set_tags(self, tags):
self.tag_list = ";".join(tags)
@property
def digital_parts(self):
return Part.objects.filter(doc__work=self.pk)
@property
def physical_parts(self):
if not self.parts:
return []
return [ (tag_to_instrument(k), v) for (k, v) in self.parts.items() ]
#@property
#def instruments(self):
# return self.orchestration.as_list()
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super(Work, self).save(*args, **kwargs)
@property
def active_projects(self):
return self.projects.filter(active=True)
@property
def current_loans(self):
return self.project_items.filter(checkout__lte=now(), returned=None).select_related('project')
@cached_property
def loans(self):
try:
return self.loan_count
except AttributeError:
return self.project_items.filter(checkout__lte=now(), returned=None).count()
@property
def is_available(self):
if self.max_loans < 0:
return True
return self.max_loans > self.loans
@property
def available(self):
if self.max_loans < 0:
return 'Unlimited'
a = self.max_loans - self.loans
return '{0} of {1}'.format(max(a, 0), self.max_loans)
@property
def identifier(self):
return f"{self.collection.pk:03d}-{self.pk:03d}"
def __str__(self):
return f"{self.name} ({self.composer})"
def doc_upload_filename(doc, filename):
storage = doc.work.ensemble.storage
storage = doc.work.collection.storage
if not storage:
raise RuntimeError("Storage not set")
return f'{storage}:{doc.work.ensemble.slug}/works/{doc.work.slug}/{filename}'
raise RuntimeError("Collection has no storage attached")
return f'{storage}:works/{doc.work.slug}-{doc.work.pk}/{filename}'
class Document(models.Model):
"""
Document represents a single file stored in the storage backend.
"""
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1)
upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage)
created = models.DateTimeField(auto_now_add=True)
version = models.CharField(max_length=30, blank=True)
def __str__(self):
return self.upload.name
class Part(models.Model):
"""
Part is a tagged portion of a Document
"""
doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="parts")
tag = models.SlugField(max_length=20)
start = models.SmallIntegerField(null=True, blank=True)

View File

@ -1,7 +1,7 @@
{% extends "interface/project_base.html" %}
{% block admin %}
<a href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin">Change items</span></a>
<a href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin"> Change items</span></a>
{% endblock %}
{% block page %}
@ -23,15 +23,22 @@
<input id="part-preference" name="part" type="number" value="0" min="0" max="4" onchange="updateParts()" size="1"/><br/><br/>
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button>
</div>
<table style="max-width: 600px; margin: 10pt auto;" class="">
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
<thead>
<tr>
<th/>
<th>Piece</th>
<th>Part</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr>
<td>{{ forloop.counter }})</td>
<td>{{ forloop.counter }}.</td>
<td>
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
</td>
<td class="">
<td class="select-cell">
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
<select name="instruments">
<option value='-'>None</option>

View File

@ -6,18 +6,19 @@
{% endblock %}
{% block page %}
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
<thead>
<tr>
<th>Item</th>
<th>Time</th>
<th/>
</tr>
</thead>
<tbody id="work-list">
{% for item in object_list %}
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
<td>{{ item.work.name }}</td>
<td>{% firstof item.work.running_time '?' %}</td>
<td>{{ item.work.duration }}</td>
<td style="text-align: center;">
<i class="fas fa-arrow-up clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)"></i>
<i class="fas fa-arrow-down clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)"></i>

View File

@ -2,5 +2,5 @@
<a href="{% url 'item_list' project=project.pk %}">My Music</a>
{% endif %}
{% if request.is_admin %}
<a href="{% url 'work_list' %}">Works</a>
<a href="{% url 'work_list' %}">Library</a>
{% endif %}

View File

@ -6,23 +6,84 @@
{% endblock %}
{% block page %}
<h2><a href="{% url 'work_list' %}">Works</a> / {{ work.name }}</h2>
<h2>{{ work.name }} {% if work.running_time %}({{ work.duration }}){% endif %} <small>[{{ work.identifier }}]</small></h2>
<h4>{{ work.composer }}{% if work.version %} - {{ work.version }}{% endif %}</h4>
<p>{{ work.notes }}</p>
<h3>Parts</h3>
<ul>
{% for part in work.parts %}
<li><a href="{% url 'part_download' pk=part.pk filename=part.filename %}" target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a></li>
{% endfor %}
</ul>
<a href="{% url 'work_partset' pk=work.pk %}"><i class="fas fa-print"></i> Print part set</a>
{% if work.collection %}
<p>Location: {{ work.collection }} [{{ work.collection_index }}]</p>
{% endif %}
<h3>Original Documents</h3>
{% if work.parent %}
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a></p>
{% endif %}
{% if work.related_works.count %}
<h3>Related</h3>
<ul>
{% for related in work.related_works.all %}
<li><a href="{% url 'work_detail' related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
{% endfor %}
</ul>
{% endif %}
<h4>Loans
{% if request.is_admin %}
<a href="{% url 'work_add_to_project' work.pk %}"><i class="fas fa-plus-circle"></i></a>
{% endif %}
</h4>
<table style="margin: 10px auto">
<thead>
<tr>
<th>Ensemble</th>
<th>Project</th>
<th>Checked Out</th>
<th>Due Back</th>
</tr>
</thead>
<tbody>
{% for item in work.current_loans %}
<tr>
<td><a href="{% url 'ensemble_detail' item.project.ensemble_id %}">{{ item.project.ensemble.name }}</a></td>
<td><a href="{% url 'project_detail' item.project.pk %}">{{ item.project.name }}</a></td>
<td>{{ item.checkout.date|date:"d/m/Y" }}</td>
<td>{{ item.due.date|date:"d/m/Y" }}</td>
</tr>
{% empty %}
<tr><td>No current loans</td></tr>
{% endfor %}
</tbody>
</table>
<h4>Printed Parts</h4>
<p>
{% for inst, c in work.physical_parts %}
<span class="badge">{{ inst }} ({{ c }})</span>
{% empty %}
No physical parts available
{% endfor %}
</p>
<h4>Digital Parts
<a href="{% url 'work_partset' pk=work.pk %}" title="Print part set"><i class="fas fa-print"></i></a>
</h4>
<p>
{% for part in work.digital_parts %}
<a class="badge" href="{% url 'part_download' pk=part.pk filename=part.filename %}"
target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a>
{% empty %}
No digital parts available
{% endfor %}
</p>
<h4>Documents</h4>
<ul>
{% for doc in work.docs.all %}
<li>
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name|basename }}</a>
&nbsp;
[{{ doc.parts.count }} parts]
{% 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 %}

View File

@ -1,25 +1,61 @@
{% extends "interface/project_base.html" %}
{% load url_tools %}
{% block admin %}
<a href="{% url 'work_add' %}"><i class="fas fa-plus-circle"></i> Add new</a>
{% endblock %}
{% block page %}
<h2>Works</h2>
<table style="max-width: 800px; margin: 10pt auto;">
<h2>Library for {{ view.ensemble }}</h2>
<div style="margin-bottom: 1em;">
<form method="GET">
<input name="filter" type="text" placeholder="Filter" value="{{ request.GET.filter }}"/>
<a href="?">Clear</a>
</form>
</div>
<table class="zebra wide">
<thead>
<tr>
<th>Work</th>
<th>Composer</th>
<th class="smhide">Edition</th>
<th class="smhide">Orchestration</th>
<th class="smhide">Collection</th>
<th>Copies</th>
</tr>
</thead>
<tbody>
{% for work in object_list %}
{% with work.docs.count as doc_count %}
{% with work.parts.count as part_count %}
<tr>
<td><a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></td>
<td>{{ doc_count }} file{{ doc_count|pluralize }} with {{ part_count }} part{{ part_count|pluralize }}</td>
<td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td>
<td class="smhide" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
<td class="smhid" title="{{ work.orchestration }}">{{ work.orchestration|truncatewords:2}}</td>
<td class="smhide">{{ work.collection.name }}</td>
<td style="color: {{ work.is_available|yesno:'green,red' }};">{{ work.available }}</td>
</tr>
{% endwith %}
{% endwith %}
{% empty %}
<tr><td colspan="4">No works found</td></tr>
{% endfor %}
</tbody>
</table>
<div class="pagination" style="text-align: right;">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="{% url_update page=1 %}" title="First">&laquo;</a>
<a href="{% url_update page=page_obj.previous_page_number %}" title="Previous">&lsaquo;</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{% url_update page=page_obj.next_page_number %}" title="Next">&rsaquo;</a>
<a href="{% url_update page=page_obj.paginator.num_pages %}" title="Last">&raquo;</a>
{% endif %}
</span>
</div>
{% endblock %}

View File

@ -1,3 +1,22 @@
from django.test import TestCase
# Create your tests here.
from django.contrib.auth.models import User
from interface.models import Ensemble, Project
from . import models
class IntegrationTestCase(TestCase):
def setUp(self):
self.homer = User.objects.create(username='homer')
self.ned = User.objects.create(username="ned")
self.lisa = User.objects.create(username="lisa")
self.dewey = User.objects.create(username="dewey")
self.be_sharps = self.homer.ensembles.create(name='Be Sharps', code="barbershop")
self.sesd = self.dewey.ensembles.create(name="Springfield Elementary School Band", code="sax")
self.sel = self.lisa.collections.create(name="Springfield Elementary Library")
self.flanders = self.ned.collections.create(name="Neds Shed")
def test_integration(self):
pass

View File

@ -13,8 +13,12 @@ urlpatterns = [
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>/partset', views.WorkPartSetView.as_view(), name="work_partset"),
path('library/works/<int:pk>/upload', views.DocumentAddView.as_view(), name="document_add"),
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/documents/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
]
]
from django.views.static import serve
urlpatterns.append(path('localstorage/<path:path>', serve, {'document_root': 'local_storage'}))

View File

@ -4,6 +4,8 @@ from django.views.generic.list import ListView, MultipleObjectMixin
from django.views.generic.edit import CreateView, FormView, UpdateView
from django.http import FileResponse, HttpResponse
from django.db import IntegrityError
from django.db.models import Q, Count
from django.utils.timezone import now
import json
@ -98,9 +100,20 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
return self.get_project()
class WorkListView(EnsembleMixin, ListView):
paginate_by = 20
def get_queryset(self):
return Work.objects.filter(ensemble=self.request.ensemble_id).order_by('name')
#works = Work.objects.filter(collection__ensembles=self.request.ensemble_id).order_by('name').select_related('collection')
works = Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').select_related('collection')
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None))
works = works.annotate(loan_count=loan_count)
q = self.request.GET.get('filter')
if q:
works = works.filter(Q(name__contains=q) | Q(composer__contains=q) | Q(tag_list__contains=q))
return works
class WorkAddView(EnsembleMixin, FormView):
template_name = "interface/default_form.html"
@ -108,34 +121,59 @@ class WorkAddView(EnsembleMixin, FormView):
def get_form(self):
f = super(WorkAddView, self).get_form()
qs = f.fields['orchestration'].queryset
f.fields['orchestration'].queryset = qs.filter(ensemble_id__isnull=True) | qs.filter(ensemble_id=self.request.ensemble_id)
#qs = f.fields['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):
obj = form.save(commit=False)
obj.ensemble_id = self.request.ensemble_id
try:
obj.save()
except IntegrityError:
form.add_error('name', 'Name must be unique')
return self.form_invalid(form)
work = form.save(commit=False)
work.ensemble_id = self.request.ensemble_id
work.save()
# handle the files
uploads = self.request.FILES.getlist('uploads')
docs = []
for f in uploads:
docs.append(obj.docs.create(upload=f).pk)
docs.append(work.docs.create(upload=f).pk)
if len(docs) == 1:
return redirect('document_annotate', docs[0])
else:
return redirect('work_detail', pk=obj.pk)
return redirect('work_detail', pk=work.pk)
class WorkDetailView(EnsembleMixin, DetailView):
def get_queryset(self):
return Work.objects.filter(ensemble=self.request.ensemble_id)
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
class WorkAddToProject(EnsembleMixin, FormView):
form_class = forms.ProjectSelectForm
template_name = "interface/default_form.html"
title = "Select project to add work to"
def get_object(self):
return Work.objects.get(pk=self.kwargs['pk'])
def get_form(self):
f = super(WorkAddToProject, self).get_form()
qs = f.fields['project'].queryset
work = self.get_object()
qs = qs.filter(ensemble_id=self.request.ensemble_id).exclude(pk__in=work.projects.all())
f.fields['project'].queryset = qs
return f
def form_valid(self, form):
work = self.get_object()
project = form.cleaned_data['project']
work.project_items.create(project=project, approved_by=self.request.user, checkout=now())
return redirect('item_list', project=project.pk)
class WorkPartSetView(EnsembleMixin, DetailView):
template_name = "library/work_partset.html"
@ -162,7 +200,7 @@ class WorkPartSetView(EnsembleMixin, DetailView):
return response
def get_queryset(self):
return Work.objects.filter(ensemble_id=self.request.ensemble_id)
return Version.objects.filter(work__ensemble_id=self.request.ensemble_id)
class DocumentDetailView(EnsembleMixin, DetailView):
@ -192,7 +230,7 @@ class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View):
return redirect(self.object.upload.url)
def get_queryset(self):
return Document.objects.filter(work__ensemble=self.request.ensemble_id)
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id)
class DocumentAnnotateView(EnsembleMixin, DetailView):
template_name = 'library/document_annotate.html'
@ -228,11 +266,11 @@ class DocumentAnnotateView(EnsembleMixin, DetailView):
for i in range(part.start, (part.end or part.start)+1):
pages[i] = part.tag
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.instruments}
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.orchestration}
return data
def get_queryset(self):
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('work')
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
@ -251,4 +289,4 @@ class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
return response
def get_queryset(self):
return Part.objects.filter(doc__work__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work')
return Part.objects.filter(doc__work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work')

View File

@ -84,6 +84,7 @@ DATABASES = {
}
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
@ -126,4 +127,4 @@ STATIC_URL = '/static/'
# Need to set this
AWS_BUCKET = ''
MEDIA_ROOT = 'media'
MEDIA_ROOT = 'media'

View File

@ -1,14 +1,14 @@
asgiref==3.2.10
boto3==1.15.11
botocore==1.18.11
Django==3.1.1
asgiref==3.4.1
boto3==1.18.34
botocore==1.21.34
Django==3.2.7
django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@c67d636d2457faa57644cd812ca1b5a916e23766
django-markdown2==0.3.1
jmespath==0.10.0
markdown2==2.3.9
python-dateutil==2.8.1
pytz==2020.1
s3transfer==0.3.3
six==1.15.0
sqlparse==0.3.1
urllib3==1.25.10
git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@master#egg=django_byostorage
markdown2==2.4.1
python-dateutil==2.8.2
pytz==2021.1
s3transfer==0.5.0
six==1.16.0
sqlparse==0.4.1
urllib3==1.26.6