Got library sort of working
This commit is contained in:
parent
7aedc6bd07
commit
598ee5ad7e
18
interface/migrations/0028_ensemble_details.py
Normal file
18
interface/migrations/0028_ensemble_details.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -44,6 +44,7 @@ class Ensemble(models.Model):
|
|||||||
passphrase = models.CharField(max_length=100)
|
passphrase = models.CharField(max_length=100)
|
||||||
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
||||||
slug = models.SlugField(max_length=100, editable=False)
|
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)
|
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL)
|
||||||
|
|
||||||
def active_projects(self):
|
def active_projects(self):
|
||||||
@ -53,10 +54,10 @@ class Ensemble(models.Model):
|
|||||||
code = str(self.code)
|
code = str(self.code)
|
||||||
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
|
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
|
||||||
|
|
||||||
def save(self):
|
def save(self, **kwargs):
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
super(Ensemble, self).save()
|
super(Ensemble, self).save(**kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -86,7 +87,7 @@ class Project(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_happened(self):
|
def has_happened(self):
|
||||||
return self.event_date < timezone.now()
|
return self.event_date < timezone.now().date()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
--border-color: #292929;
|
--border-color: #292929;
|
||||||
--gray-blue: #667788;
|
--gray-blue: #667788;
|
||||||
--light-blue: #c5eff7;
|
--light-blue: #c5eff7;
|
||||||
|
--light-grey: #EEEEEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -36,7 +37,7 @@ BODY {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
max-width: 1000px;
|
max-width: 1280px;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -55,16 +56,30 @@ BODY {
|
|||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
.collapse {
|
.collapse {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 1200px) {
|
||||||
|
.wide {
|
||||||
|
width: 900px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 900px) {
|
@media all and (max-width: 900px) {
|
||||||
.mdhide {
|
.mdhide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 700px) {
|
@media all and (max-width: 700px) {
|
||||||
@ -74,6 +89,9 @@ BODY {
|
|||||||
.collapse {
|
.collapse {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.wide {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -138,6 +156,16 @@ INPUT[type=checkbox] {
|
|||||||
margin-top: 20px;
|
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 {
|
.btn {
|
||||||
background-color: var(--gray-blue);
|
background-color: var(--gray-blue);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -232,10 +260,28 @@ H1 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
TD {
|
TABLE {
|
||||||
padding: 5px;
|
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 {
|
TABLE.horizontal TH {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@ -298,12 +344,12 @@ TABLE SELECT {
|
|||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
}
|
}
|
||||||
TD.select-cell {
|
TD.select-cell {
|
||||||
padding: 0;
|
padding: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-cell SELECT {
|
.select-cell SELECT {
|
||||||
border: none;
|
border: none;
|
||||||
background-color: white;
|
background-color: transparent;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<div>
|
<div>
|
||||||
<h3>{{ title }}</h3>
|
<h3>{% firstof title view.title %}</h3>
|
||||||
<form class="vertical" method="POST" enctype="multipart/form-data">
|
<form class="vertical" method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
|
|||||||
@ -1,22 +1,12 @@
|
|||||||
{% extends "interface/project_base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block page %}
|
{% block content %}
|
||||||
<div style="flex-grow: 1">
|
<h2>{{ ensemble.name }}</h2>
|
||||||
<h1>Projects for {{ ensemble.name }}</h1>
|
<p>{{ ensemble.details }}</p>
|
||||||
<div class="list-group narrow">
|
<h4>Administrators</h4>
|
||||||
{% for project in ensemble.active_projects %}
|
<ul>
|
||||||
<a class="" href="{% url 'project_detail' project=project.id %}">
|
{% for admin in ensemble.admins.all %}
|
||||||
<h3>{{ project.name }}</h3>
|
<li><a href="mailto:{{ admin.email }}">{% firstof admin.get_full_name admin.get_username %}</a></li>
|
||||||
<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 %}
|
{% endfor %}
|
||||||
</div>
|
</ul>
|
||||||
<div style="float: right; margin-top: 10px; color: #999;">
|
{% endblock %}
|
||||||
<small>{{ ensemble.ensemble_code }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
22
interface/templates/interface/ensemble_project_list.html
Normal file
22
interface/templates/interface/ensemble_project_list.html
Normal 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 %}
|
||||||
@ -7,9 +7,9 @@
|
|||||||
{% if project.event_date %}
|
{% if project.event_date %}
|
||||||
<h3 class="text-center">
|
<h3 class="text-center">
|
||||||
{% if project.has_happened %}
|
{% if project.has_happened %}
|
||||||
{{ project.event_date|timesince }}
|
{{ project.event_date|timesince }} ago.
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ project.event_date|}}
|
In {{ project.event_date|timeuntil }}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
10
interface/templatetags/url_tools.py
Normal file
10
interface/templatetags/url_tools.py
Normal 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()
|
||||||
6
interface/tests/test_integration.py
Normal file
6
interface/tests/test_integration.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
class IntegrationTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_runs(self):
|
||||||
|
self.assertTrue(True)
|
||||||
@ -10,7 +10,9 @@ urlpatterns = [
|
|||||||
path('register', views.register, name="register"),
|
path('register', views.register, name="register"),
|
||||||
path('manage', views.ManageView.as_view(), name="manage"),
|
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>', views.ProjectDetailView.as_view(), name="project_detail"),
|
||||||
path('projects/<int:project>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
|
path('projects/<int:project>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,10 @@ class EnsembleMixin(object):
|
|||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ensemble(self):
|
||||||
|
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
|
||||||
|
|
||||||
class ProjectMixin(EnsembleMixin):
|
class ProjectMixin(EnsembleMixin):
|
||||||
|
|
||||||
def get_project(self):
|
def get_project(self):
|
||||||
@ -195,7 +199,8 @@ def logout(request):
|
|||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
class EnsembleDetailView(EnsembleMixin, DetailView):
|
class EnsembleProjectListView(EnsembleMixin, DetailView):
|
||||||
|
template_name = 'interface/ensemble_project_list.html'
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# capture provided urls
|
# capture provided urls
|
||||||
@ -207,6 +212,9 @@ class EnsembleDetailView(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)
|
||||||
|
|
||||||
|
class EnsembleDetailView(DetailView):
|
||||||
|
model = models.Ensemble
|
||||||
|
|
||||||
class ProjectDetailView(ProjectMixin, DetailView):
|
class ProjectDetailView(ProjectMixin, DetailView):
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
|
|||||||
13
library/README.md
Normal file
13
library/README.md
Normal 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
|
||||||
|
|
||||||
@ -2,17 +2,29 @@ from django.contrib import admin
|
|||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
class OrchestrationAdmin(admin.ModelAdmin):
|
#class OrchestrationAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'ensemble']
|
# list_display = ['name', 'ensemble']
|
||||||
list_filter = ['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):
|
class ItemInline(admin.TabularInline):
|
||||||
model = models.Item
|
model = models.Item
|
||||||
|
|
||||||
|
class DocInline(admin.TabularInline):
|
||||||
|
model = models.Document
|
||||||
|
|
||||||
class WorkAdmin(admin.ModelAdmin):
|
class WorkAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'composer', 'orchestration']
|
list_display = ['name', 'edition', 'composer', 'running_time']
|
||||||
list_filter = ['ensemble']
|
list_filter = ['collection']
|
||||||
inlines = [ItemInline]
|
inlines = [DocInline, ItemInline]
|
||||||
|
|
||||||
|
admin.site.register(models.Work, WorkAdmin)
|
||||||
|
|
||||||
class PartInline(admin.TabularInline):
|
class PartInline(admin.TabularInline):
|
||||||
model = models.Part
|
model = models.Part
|
||||||
@ -20,14 +32,19 @@ class PartInline(admin.TabularInline):
|
|||||||
|
|
||||||
class DocumentAdmin(admin.ModelAdmin):
|
class DocumentAdmin(admin.ModelAdmin):
|
||||||
list_display = ['work', '__str__']
|
list_display = ['work', '__str__']
|
||||||
list_filter = ['work__ensemble']
|
list_filter = ['work__collection']
|
||||||
inlines = [PartInline]
|
inlines = [PartInline]
|
||||||
|
|
||||||
|
admin.site.register(models.Document, DocumentAdmin)
|
||||||
|
|
||||||
class ItemAdmin(admin.ModelAdmin):
|
class ItemAdmin(admin.ModelAdmin):
|
||||||
list_display = ['project', 'work', 'order']
|
list_display = ['project', 'work', 'order']
|
||||||
list_filter = ['project']
|
list_filter = ['project']
|
||||||
|
|
||||||
admin.site.register(models.Orchestration, OrchestrationAdmin)
|
admin.site.register(models.Item, ItemAdmin)
|
||||||
admin.site.register(models.Work, WorkAdmin)
|
|
||||||
admin.site.register(models.Document, DocumentAdmin)
|
class EnsembleAccessAdmin(admin.ModelAdmin):
|
||||||
admin.site.register(models.Item, ItemAdmin)
|
list_display = ['ensemble', 'collection', 'access_type']
|
||||||
|
list_filter = ['ensemble']
|
||||||
|
|
||||||
|
admin.site.register(models.EnsembleAccess, EnsembleAccessAdmin)
|
||||||
@ -1,12 +1,15 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import Work
|
from .models import Work
|
||||||
|
from interface.models import Project
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
class WorkCreateForm(forms.ModelForm):
|
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:
|
class Meta:
|
||||||
model = Work
|
model = Work
|
||||||
fields = ['uploads', 'name', 'orchestration', 'running_time', 'notes']
|
fields = ['uploads', 'name', 'composer', 'edition', 'collection', '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())
|
||||||
@ -21,4 +24,7 @@ class PlaylistAddForm(forms.Form):
|
|||||||
self.instance = instance
|
self.instance = instance
|
||||||
|
|
||||||
def save(self):
|
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())
|
||||||
0
library/management/__init__.py
Normal file
0
library/management/__init__.py
Normal file
0
library/management/commands/__init__.py
Normal file
0
library/management/commands/__init__.py
Normal file
20
library/management/commands/import_works.py
Normal file
20
library/management/commands/import_works.py
Normal 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'])
|
||||||
@ -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
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import library.models
|
import library.models
|
||||||
import byostorage.cached
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -11,14 +12,29 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('interface', '0022_auto_20210303_2043'),
|
('interface', '0027_auto_20210322_1154'),
|
||||||
|
('byostorage', '0003_auto_20210323_1047'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name='Document',
|
name='Document',
|
||||||
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)),
|
||||||
('upload', models.FileField(storage=byostorage.cached.CachedStorage(), upload_to=library.models.doc_upload_filename)),
|
('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')),
|
('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)),
|
||||||
('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)),
|
('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')),
|
('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(
|
migrations.CreateModel(
|
||||||
name='Part',
|
name='Part',
|
||||||
@ -75,7 +95,16 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='item',
|
model_name='item',
|
||||||
name='work',
|
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(
|
migrations.AddField(
|
||||||
model_name='document',
|
model_name='document',
|
||||||
|
|||||||
34
library/migrations/0002_auto_20210504_1830.py
Normal file
34
library/migrations/0002_auto_20210504_1830.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
||||||
24
library/migrations/0003_auto_20210504_1845.py
Normal file
24
library/migrations/0003_auto_20210504_1845.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
|
||||||
]
|
|
||||||
44
library/migrations/0004_auto_20210505_0927.py
Normal file
44
library/migrations/0004_auto_20210505_0927.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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
|
from django.db import migrations, models
|
||||||
|
|
||||||
@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('library', '0003_orchestrations'),
|
('library', '0004_auto_20210505_0927'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='work',
|
model_name='work',
|
||||||
name='composer',
|
name='parts',
|
||||||
field=models.CharField(blank=True, max_length=255),
|
field=models.JSONField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
48
library/migrations/0006_auto_20210902_1355.py
Normal file
48
library/migrations/0006_auto_20210902_1355.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
33
library/migrations/0007_auto_20210902_1407.py
Normal file
33
library/migrations/0007_auto_20210902_1407.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
library/migrations/0008_work_orchestration.py
Normal file
18
library/migrations/0008_work_orchestration.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,7 +1,11 @@
|
|||||||
|
from os import SCHED_OTHER
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import slugify
|
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.core.files.storage import get_storage_class
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -14,42 +18,181 @@ except (ImportError, AttributeError):
|
|||||||
library_storage = get_storage_class()()
|
library_storage = get_storage_class()()
|
||||||
logger.info("Library storage: %s", library_storage.__class__.__name__)
|
logger.info("Library storage: %s", library_storage.__class__.__name__)
|
||||||
|
|
||||||
INSTRUMENTS = [
|
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_Instruments
|
||||||
('Score', 'Score'),
|
ABBREVIATIONS = """
|
||||||
('S', 'Soprano'),
|
score Score
|
||||||
('A', 'Alto'),
|
acc Accordion
|
||||||
('T', 'Tenor'),
|
afl Alto flute
|
||||||
('B', 'Bass'),
|
alt Alto (voice) (contralto)
|
||||||
('V', 'Vocals'),
|
arp Arpeggione
|
||||||
('Vln', 'Violin'),
|
bag Bagpipe
|
||||||
('Vla', 'Viola'),
|
bar Baritone (voice)
|
||||||
('Vc', 'Violoncello'),
|
bass Bass (voice)
|
||||||
('Cb', 'Contrabass'),
|
bbar Bass baritone (voice)
|
||||||
('Fl', 'Flute'),
|
bc Continuo (Basso continuo)
|
||||||
('Picc', 'Piccolo'),
|
bcl Bass clarinet
|
||||||
('Cl', 'Clarinet'),
|
bell Bell (Chimes)
|
||||||
('Ob', 'Oboe'),
|
bfl Bass flute
|
||||||
('Hn', 'Horn'),
|
bgtr Bass guitar
|
||||||
('Tpt', 'Trumpet'),
|
bjo Banjo
|
||||||
('Tbn', 'Trombone'),
|
bn Bassoon
|
||||||
('Tuba', 'Tuba'),
|
bob Bass oboe (Baritone oboe)
|
||||||
('Timp', 'Timpani'),
|
br Brass instruments
|
||||||
('Drum', 'Drumset'),
|
bryt Baryton
|
||||||
('Perc', 'Percussion'),
|
bstcl Basset clarinet
|
||||||
('Pno', 'Piano'),
|
bsthn Basset horn
|
||||||
('Hp', 'Harp'),
|
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 = {
|
||||||
'SATB': ('S', 'A', 'T', 'B'),
|
'SATB': ('S', 'A', 'T', 'B'),
|
||||||
'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'),
|
'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'),
|
||||||
|
'String Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb'),
|
||||||
'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb',
|
'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb',
|
||||||
'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Hn1', 'Hn2',
|
'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Hn1', 'Hn2',
|
||||||
'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba',
|
'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba',
|
||||||
'Timp', 'Drum', 'Perc'),
|
'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):
|
def tag_to_instrument(tag):
|
||||||
m = re.match(r'([A-Za-z]+)(\d*)', tag)
|
m = re.match(r'([A-Za-z]+)(\d*)', tag)
|
||||||
@ -58,13 +201,18 @@ def tag_to_instrument(tag):
|
|||||||
l = m.groups()
|
l = m.groups()
|
||||||
return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip()
|
return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip()
|
||||||
|
|
||||||
|
'''
|
||||||
class Orchestration(models.Model):
|
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)
|
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="orchestrations", null=True, blank=True)
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
instruments = models.TextField()
|
instruments = models.TextField()
|
||||||
|
|
||||||
def as_list(self):
|
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 ]
|
return [ (t, tag_to_instrument(t)) for t in tags if t ]
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@ -73,44 +221,21 @@ class Orchestration(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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):
|
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.
|
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)
|
||||||
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)
|
order = models.SmallIntegerField(default=0)
|
||||||
|
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']
|
||||||
@ -118,20 +243,151 @@ class Item(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"<{self.project.slug}:{self.work.slug}>"
|
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):
|
def doc_upload_filename(doc, filename):
|
||||||
storage = doc.work.ensemble.storage
|
storage = doc.work.collection.storage
|
||||||
if not storage:
|
if not storage:
|
||||||
raise RuntimeError("Storage not set")
|
raise RuntimeError("Collection has no storage attached")
|
||||||
return f'{storage}:{doc.work.ensemble.slug}/works/{doc.work.slug}/{filename}'
|
return f'{storage}:works/{doc.work.slug}-{doc.work.pk}/{filename}'
|
||||||
|
|
||||||
class Document(models.Model):
|
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")
|
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
|
||||||
|
doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1)
|
||||||
upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage)
|
upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage)
|
||||||
|
created = models.DateTimeField(auto_now_add=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 Part(models.Model):
|
||||||
|
"""
|
||||||
|
Part 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="parts")
|
||||||
tag = models.SlugField(max_length=20)
|
tag = models.SlugField(max_length=20)
|
||||||
start = models.SmallIntegerField(null=True, blank=True)
|
start = models.SmallIntegerField(null=True, blank=True)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{% 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 href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin"> Change items</span></a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% 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/>
|
<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>
|
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button>
|
||||||
</div>
|
</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>
|
<tbody>
|
||||||
{% for item in object_list %}
|
{% for item in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ forloop.counter }})</td>
|
<td>{{ forloop.counter }}.</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
|
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="">
|
<td class="select-cell">
|
||||||
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
|
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
|
||||||
<select name="instruments">
|
<select name="instruments">
|
||||||
<option value='-'>None</option>
|
<option value='-'>None</option>
|
||||||
|
|||||||
@ -6,18 +6,19 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
|
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Item</th>
|
<th>Item</th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
|
<th/>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="work-list">
|
<tbody id="work-list">
|
||||||
{% for item in object_list %}
|
{% for item in object_list %}
|
||||||
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
|
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
|
||||||
<td>{{ item.work.name }}</td>
|
<td>{{ item.work.name }}</td>
|
||||||
<td>{% firstof item.work.running_time '?' %}</td>
|
<td>{{ item.work.duration }}</td>
|
||||||
<td style="text-align: center;">
|
<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-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>
|
<i class="fas fa-arrow-down clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)"></i>
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
<a href="{% url 'item_list' project=project.pk %}">My Music</a>
|
<a href="{% url 'item_list' project=project.pk %}">My Music</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.is_admin %}
|
{% if request.is_admin %}
|
||||||
<a href="{% url 'work_list' %}">Works</a>
|
<a href="{% url 'work_list' %}">Library</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -6,23 +6,84 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% 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>
|
<p>{{ work.notes }}</p>
|
||||||
<h3>Parts</h3>
|
{% if work.collection %}
|
||||||
<ul>
|
<p>Location: {{ work.collection }} [{{ work.collection_index }}]</p>
|
||||||
{% for part in work.parts %}
|
{% endif %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
<ul>
|
||||||
{% for doc in work.docs.all %}
|
{% for doc in work.docs.all %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name|basename }}</a>
|
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name|basename }}</a>
|
||||||
|
|
||||||
[{{ doc.parts.count }} parts]
|
{% with parts=doc.parts.count %}
|
||||||
|
{% if parts %}[{{ parts }} parts]{% endif %}
|
||||||
|
{% endwith %}
|
||||||
{% if request.is_admin %}
|
{% if request.is_admin %}
|
||||||
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"></i></a>
|
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,25 +1,61 @@
|
|||||||
{% extends "interface/project_base.html" %}
|
{% extends "interface/project_base.html" %}
|
||||||
|
{% load url_tools %}
|
||||||
|
|
||||||
{% block admin %}
|
{% block admin %}
|
||||||
<a href="{% url 'work_add' %}"><i class="fas fa-plus-circle"></i> Add new</a>
|
<a href="{% url 'work_add' %}"><i class="fas fa-plus-circle"></i> Add new</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<h2>Works</h2>
|
<h2>Library for {{ view.ensemble }}</h2>
|
||||||
<table style="max-width: 800px; margin: 10pt auto;">
|
<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>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for work in object_list %}
|
{% for work in object_list %}
|
||||||
{% with work.docs.count as doc_count %}
|
|
||||||
{% with work.parts.count as part_count %}
|
|
||||||
<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>{{ 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>
|
</tr>
|
||||||
{% endwith %}
|
{% empty %}
|
||||||
{% endwith %}
|
<tr><td colspan="4">No works found</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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">«</a>
|
||||||
|
<a href="{% url_update page=page_obj.previous_page_number %}" title="Previous">‹</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">›</a>
|
||||||
|
<a href="{% url_update page=page_obj.paginator.num_pages %}" title="Last">»</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,3 +1,22 @@
|
|||||||
from django.test import TestCase
|
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
|
||||||
|
|||||||
@ -13,8 +13,12 @@ urlpatterns = [
|
|||||||
path('library/works/create', views.WorkAddView.as_view(), name="work_add"),
|
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>/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>/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>/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
|
||||||
|
urlpatterns.append(path('localstorage/<path:path>', serve, {'document_root': 'local_storage'}))
|
||||||
@ -4,6 +4,8 @@ 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
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
from django.db.models import Q, Count
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -98,9 +100,20 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
|
|||||||
return self.get_project()
|
return self.get_project()
|
||||||
|
|
||||||
class WorkListView(EnsembleMixin, ListView):
|
class WorkListView(EnsembleMixin, ListView):
|
||||||
|
paginate_by = 20
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
class WorkAddView(EnsembleMixin, FormView):
|
||||||
template_name = "interface/default_form.html"
|
template_name = "interface/default_form.html"
|
||||||
@ -108,34 +121,59 @@ class WorkAddView(EnsembleMixin, FormView):
|
|||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
f = super(WorkAddView, self).get_form()
|
f = super(WorkAddView, self).get_form()
|
||||||
qs = f.fields['orchestration'].queryset
|
#qs = f.fields['orchestration'].queryset
|
||||||
f.fields['orchestration'].queryset = qs.filter(ensemble_id__isnull=True) | qs.filter(ensemble_id=self.request.ensemble_id)
|
#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
|
return f
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
obj = form.save(commit=False)
|
work = form.save(commit=False)
|
||||||
obj.ensemble_id = self.request.ensemble_id
|
work.ensemble_id = self.request.ensemble_id
|
||||||
try:
|
work.save()
|
||||||
obj.save()
|
|
||||||
except IntegrityError:
|
|
||||||
form.add_error('name', 'Name must be unique')
|
|
||||||
return self.form_invalid(form)
|
|
||||||
|
|
||||||
# handle the files
|
# handle the files
|
||||||
uploads = self.request.FILES.getlist('uploads')
|
uploads = self.request.FILES.getlist('uploads')
|
||||||
docs = []
|
docs = []
|
||||||
for f in uploads:
|
for f in uploads:
|
||||||
docs.append(obj.docs.create(upload=f).pk)
|
docs.append(work.docs.create(upload=f).pk)
|
||||||
|
|
||||||
if len(docs) == 1:
|
if len(docs) == 1:
|
||||||
return redirect('document_annotate', docs[0])
|
return redirect('document_annotate', docs[0])
|
||||||
else:
|
else:
|
||||||
return redirect('work_detail', pk=obj.pk)
|
return redirect('work_detail', pk=work.pk)
|
||||||
|
|
||||||
class WorkDetailView(EnsembleMixin, DetailView):
|
class WorkDetailView(EnsembleMixin, DetailView):
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
class WorkPartSetView(EnsembleMixin, DetailView):
|
||||||
template_name = "library/work_partset.html"
|
template_name = "library/work_partset.html"
|
||||||
@ -162,7 +200,7 @@ class WorkPartSetView(EnsembleMixin, DetailView):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
class DocumentDetailView(EnsembleMixin, DetailView):
|
||||||
|
|
||||||
@ -192,7 +230,7 @@ class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
|||||||
return redirect(self.object.upload.url)
|
return redirect(self.object.upload.url)
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
class DocumentAnnotateView(EnsembleMixin, DetailView):
|
||||||
template_name = 'library/document_annotate.html'
|
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):
|
for i in range(part.start, (part.end or part.start)+1):
|
||||||
pages[i] = part.tag
|
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
|
return data
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||||
@ -251,4 +289,4 @@ class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def get_queryset(self):
|
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')
|
||||||
@ -84,6 +84,7 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
||||||
@ -126,4 +127,4 @@ STATIC_URL = '/static/'
|
|||||||
# Need to set this
|
# Need to set this
|
||||||
AWS_BUCKET = ''
|
AWS_BUCKET = ''
|
||||||
|
|
||||||
MEDIA_ROOT = 'media'
|
MEDIA_ROOT = 'media'
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
asgiref==3.2.10
|
asgiref==3.4.1
|
||||||
boto3==1.15.11
|
boto3==1.18.34
|
||||||
botocore==1.18.11
|
botocore==1.21.34
|
||||||
Django==3.1.1
|
Django==3.2.7
|
||||||
|
django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@c67d636d2457faa57644cd812ca1b5a916e23766
|
||||||
django-markdown2==0.3.1
|
django-markdown2==0.3.1
|
||||||
jmespath==0.10.0
|
jmespath==0.10.0
|
||||||
markdown2==2.3.9
|
markdown2==2.4.1
|
||||||
python-dateutil==2.8.1
|
python-dateutil==2.8.2
|
||||||
pytz==2020.1
|
pytz==2021.1
|
||||||
s3transfer==0.3.3
|
s3transfer==0.5.0
|
||||||
six==1.15.0
|
six==1.16.0
|
||||||
sqlparse==0.3.1
|
sqlparse==0.4.1
|
||||||
urllib3==1.25.10
|
urllib3==1.26.6
|
||||||
git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@master#egg=django_byostorage
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user