Compare commits

...

16 Commits

27 changed files with 571 additions and 324 deletions

View File

@ -1,8 +1,10 @@
FROM alpine:latest FROM alpine:latest
ARG VERSION=0.8.4
ENV TARGET=/opt/polyphonic ENV TARGET=/opt/polyphonic
ENV RELEASE=polyphonic-0.8.4-py3-none-any.whl #ENV RELEASE=polyphonic-${VERSION}-py3-none-any.whl
#ENV RELEASE=git+https://gitea.tfconsulting.com.au/projects/polyphonic.git ENV RELEASE=git+https://gitea.tfconsulting.com.au/projects/polyphonic.git@${VERSION}
RUN apk add --no-cache python3 py3-pip git ghostscript sqlite RUN apk add --no-cache python3 py3-pip git ghostscript sqlite
@ -11,7 +13,7 @@ WORKDIR /root
RUN python3 -m venv ${TARGET} RUN python3 -m venv ${TARGET}
ENV PATH="${TARGET}/bin:$PATH" ENV PATH="${TARGET}/bin:$PATH"
COPY dist/${RELEASE} . #COPY dist/${RELEASE} .
RUN pip3 install ${RELEASE} --no-cache-dir RUN pip3 install ${RELEASE} --no-cache-dir
RUN pip3 install gunicorn whitenoise RUN pip3 install gunicorn whitenoise

View File

@ -1,5 +1,9 @@
PYTHON=env/bin/python PYTHON=env/bin/python
LIBRARY=polyphonic/interface/static
DROPZONE=5.7.0 DROPZONE=5.7.0
BULMA=1.0.4
ALPINE=3.15.12
VERSION=0.8.4 VERSION=0.8.4
@ -39,7 +43,18 @@ upgrade:
poetry run manage collectstatic poetry run manage collectstatic
${MAKE} libraries ${MAKE} libraries
libraries: static/dropzone static/fonts/Quicksand_Book.otf #libraries: static/dropzone static/fonts/Quicksand_Book.otf
libraries: ${LIBRARY}/css/bulma.min.css ${LIBRARY}/js/alpine.js ${LIBRARY}/css/material.css
${LIBRARY}/css/bulma.min.css:
curl -o $@ "https://cdn.jsdelivr.net/npm/bulma@${BULMA}/css/bulma.min.css"
${LIBRARY}/js/alpine.js:
curl -o $@ "https://cdn.jsdelivr.net/npm/alpinejs@${ALPINE}/dist/cdn.min.js"
${LIBRARY}/css/material.css:
curl -o $@ "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
static/dropzone: static/dropzone:
wget -O dropzone-${DROPZONE}.zip https://github.com/enyo/dropzone/archive/v${DROPZONE}.zip wget -O dropzone-${DROPZONE}.zip https://github.com/enyo/dropzone/archive/v${DROPZONE}.zip

View File

@ -1,7 +1,7 @@
services: services:
polyphonic: polyphonic:
image: "polyphonic:0.8.4" image: "polyphonic:0.8.4"
build: "." build: .
ports: ports:
- "8001:8000" - "8001:8000"
volumes: volumes:
@ -12,3 +12,4 @@ services:
DJANGO_SETTINGS_MODULE: local_settings DJANGO_SETTINGS_MODULE: local_settings
PYTHONPATH: /opt/polyphonic PYTHONPATH: /opt/polyphonic
WORK_DIR: /var/polyphonic WORK_DIR: /var/polyphonic
VERSION: 0.8.4

View File

@ -31,12 +31,12 @@ class ProjectForm(forms.ModelForm, BaseForm):
class Meta: class Meta:
model = models.Project model = models.Project
fields = ["name", "description", "modules", "event_date"] fields = ["name", "description", "modules", "event_date"]
# widgets = { widgets = {"event_date": forms.DateTimeInput()}
# 'event_date': forms.DateTimeInput(attrs={'type': 'date'})
# }
modules = forms.MultipleChoiceField( modules = forms.MultipleChoiceField(
choices=[(x, x.title()) for x in models.settings.POLYPHONIC_MODULES], choices=[
(x, x.replace(".", " ").title()) for x in models.settings.POLYPHONIC_MODULES
],
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False, required=False,
) )

View File

@ -192,8 +192,9 @@ class Project(models.Model):
class Module(models.Model): class Module(models.Model):
"""Enable modules on a oriject""" """Enable modules on a oriject"""
name = models.SlugField( name = models.CharField(
max_length=20, choices=[(x, x.title()) for x in settings.POLYPHONIC_MODULES] max_length=100,
choices=[(x, x.replace(".", " ").title()) for x in settings.POLYPHONIC_MODULES],
) )
project = models.ForeignKey( project = models.ForeignKey(
Project, related_name="modules", on_delete=models.CASCADE Project, related_name="modules", on_delete=models.CASCADE

View File

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 426 KiB

View File

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 258 B

View File

@ -5,15 +5,11 @@
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" /> <link rel="icon" type="image/png" href="{% static 'images/icon.png' %}" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css"> <link rel="stylesheet" href="{% static 'css/bulma.min.css' %}" />
<!--link rel="stylesheet" href="{% static 'interface/css/bulmaswatch.min.css' %}"></link--> <link rel="stylesheet" href="{% static 'css/polyphonic.css' %}" />
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link> <link rel="stylesheet" href="{% static 'css/material.css' %}" />
<script src="{% static 'interface/js/interface.js' %}"></script> <script src="{% static 'js/interface.js' %}"></script>
<script src="//unpkg.com/alpinejs" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" defer></script>
<!-- script src="//kit.fontawesome.com/c837098e5b.js" crossorigin="anonymous" defer></script -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<title>{% block title %}Polyphonic{% endblock %}</title> <title>{% block title %}Polyphonic{% endblock %}</title>
{% block media %}{% endblock %} {% block media %}{% endblock %}
<style>{% block style %}{% endblock %}</style> <style>{% block style %}{% endblock %}</style>

View File

@ -36,22 +36,16 @@ Contacts:
{% include 'interface/project_items.html' %} {% include 'interface/project_items.html' %}
{% if request.is_admin %} {% if request.is_admin %}
<div class=""> <div class="box">
<div class="card"> <h3 class="subtitle">Admin Details</h3>
<header class="card header"> <ul>
<p class="card-header-title">Admin Details</p> <li><a href="{{ ensemble_link }}">Ensemble Sharing Link</a></li>
</header> <li><a href="{% url 'project_create' ensemble.pk %}">Add a new project</a></li>
<div class="card-content"> </ul>
<ul>
<li><a href="{{ ensemble_link }}">Ensemble Sharing Link</a></li>
<li><a href="{% url 'project_create' ensemble.pk %}">Add a new project</a></li>
</ul>
</div>
</div>
</div> </div>
{% endif %} {% endif %}
<div> <div class="has-text-right is-size-6">
<a href="{% url 'forget_resource' 'ensemble' ensemble.slug %}">Forget this ensemble</a> <a href="{% url 'forget_resource' 'ensemble' ensemble.slug %}">Forget this ensemble</a>
</div> </div>

View File

@ -43,8 +43,11 @@
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="hero"> <div class="hero mx-auto my-4">
You don't currently have access to any ensembles - ask your administrator for a link. <div class="hero-body has-text-centered">
<p class="title">You don't currently have access to any ensembles</p>
<p class="subtitle">Ask your administrator for a link.</p>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -21,7 +21,7 @@
<p class="menu-label">This Project</p> <p class="menu-label">This Project</p>
<ul class="menu-list"> <ul class="menu-list">
<li><a role="tab" href="{% url 'project_detail' project=project.id %}">Project Info</a></li> <li><a role="tab" href="{% url 'project_detail' project=project.id %}">Project Info</a></li>
{% if 'library' in modules %} {% if 'polyphonic.library' in modules %}
<li><a class="nav-link" href="{% url 'item_list' project=project.pk %}">My Music</a></li> <li><a class="nav-link" href="{% url 'item_list' project=project.pk %}">My Music</a></li>
{% endif %} {% endif %}
{% for page in project.wiki_pages.all %} {% for page in project.wiki_pages.all %}

View File

@ -29,7 +29,7 @@
</h3> </h3>
{% endif %} {% endif %}
<div class="block"> <div class="block content">
{% if project.description %} {% if project.description %}
<p class="content">{{ project.description|markdown }}</p> <p class="content">{{ project.description|markdown }}</p>
{% else %} {% else %}
@ -40,40 +40,36 @@
{% if project.owner %} {% if project.owner %}
<div class="block"> <div class="block">
{% if project.owner.email %} {% if project.owner.email %}
The project owner is <a href="mailto:{{ project.owner.email }}">{{ project.owner }}</a> The project owner is <strong><a href="mailto:{{ project.owner.email }}">{{ project.owner }}</a></strong>.
{% else %} {% else %}
The project owner is {{ project.owner }}. The project owner is <strong>{{ project.owner }}</strong>.
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if request.is_admin %}
<div class="box">
<h3 class="subtitle">Admin Actions</h3>
<ul>
<li><a href="{{ project_link }}">Project Link</a></li>
</ul>
</div>
{% endif %}
</div> </div>
{% if 'library' in modules %}
<div class="column is-one-third"> <div class="column is-one-third">
{% if 'polyphonic.library' in modules %}
{% include 'library/project_detail.html' %} {% include 'library/project_detail.html' %}
</div>
{% endif %} {% endif %}
{% if 'submission' in modules %} {% if 'polyphonic.submission' in modules %}
<div class="column">
{% include 'submissions/project_detail.html' %} {% include 'submissions/project_detail.html' %}
</div>
{% endif %} {% endif %}
</div>
</div> </div>
{% if request.is_admin %}
<div class="box">
<h3 class="subtitle">Admin Actions</h3>
<ul>
<li><a href="{{ project_link }}">Project Link</a></li>
{% if 'library' in modules %}
<li><a href="{% url 'item_list_manage' project=project.pk %}">Manage items</a></li>
{% endif %}
</ul>
</div>
{% endif %}
<!-- <!--
<div> <div>

View File

@ -12,7 +12,8 @@
<p class="card-header-icon" style="color: black;">{{ project.rough_date }}</p> <p class="card-header-icon" style="color: black;">{{ project.rough_date }}</p>
</header> </header>
</a> </a>
<div class="card-content" style="height: 100px; overflow: hidden"> <div class="card-content" style="height: 150px; overflow: hidden">
<div><b>{{ project.event_date|date:"l jS F Y, g:i A" }}</b></div>
<div class="content"> <div class="content">
{{ project.description | markdown }} {{ project.description | markdown }}
</div> </div>
@ -26,8 +27,8 @@
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="hero"> <div class="hero mx-auto my-4">
<div class="hero-body"> <div class="hero-body has-text-centered">
<p class="title">No projects currently planned</p> <p class="title">No projects currently planned</p>
<p class="subtitle">Go put your feet up!</p> <p class="subtitle">Go put your feet up!</p>
</div> </div>

View File

@ -5,7 +5,7 @@ register = template.Library()
def basename(value): def basename(value):
return os.path.basename(value) return os.path.basename(str(value))
register.filter("basename", basename) register.filter("basename", basename)

View File

@ -292,7 +292,11 @@ class Work(models.Model):
@property @property
def digital_parts(self): def digital_parts(self):
sections = [(s.tag, s.name) for s in Section.objects.filter(doc__work=self.pk)] sections = [
(s.tag, s.name)
for s in Section.objects.filter(doc__work=self.pk)
if ":" not in s.tag
]
sections.sort(key=self.orchestration.sorter()) sections.sort(key=self.orchestration.sorter())
# return [ s[1] for s in sections ] # return [ s[1] for s in sections ]
sections = list(dict(sections).items()) # primitive unique() sections = list(dict(sections).items()) # primitive unique()
@ -434,7 +438,7 @@ class Document(models.Model):
filename = os.path.basename(str(self.upload)) filename = os.path.basename(str(self.upload))
inst = auto_tag(filename) inst = auto_tag(filename)
if inst: if inst:
self.sections.get_or_create(tag=inst.abbreviate()) self.sections.get_or_create(tag=inst.tag)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
self.upload.delete(save=False) self.upload.delete(save=False)

View File

@ -1,14 +1,15 @@
from collections import namedtuple from dataclasses import dataclass
import re import re
GENERAL = """
mvmt Movement TAG_PREFIXES = {
ex Excerpt "mvmt": "Movement",
sect Section "ex": "Excerpt",
pce Piece "sect": "Section",
no No. "name": "Name",
page Page "no": "Number",
""" "page": "Page",
}
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_MusicTags # taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_MusicTags
# Include any aliases at the top # Include any aliases at the top
@ -160,71 +161,110 @@ xyl Xylophone
zith Zither zith Zither
""" """
MUSIC_TAGS = [] MUSIC_TAG = re.compile(r"((?P<prefix>\w+):)?(?P<name>.*?)(\-(?P<number>[0-9]+))?")
GENERAL_TAGS = set()
for i, abbreviations in enumerate((GENERAL, INSTRUMENTS)):
for line in abbreviations.split("\n"):
parts = line.strip().split(maxsplit=1)
if len(parts) < 2:
continue
name, _, _ = parts[1].partition("(")
MUSIC_TAGS.append((parts[0], name))
if i == 0:
GENERAL_TAGS.add(parts[0])
MUSIC_TAGS = []
TAG_ALIASES = {}
# GENERAL_TAGS = set()
for line in INSTRUMENTS.split("\n"):
parts = line.strip().split(maxsplit=1)
if len(parts) < 2:
continue
name, _, _ = parts[1].partition("(")
MUSIC_TAGS.append((parts[0], name.strip()))
# if i == 0:
# GENERAL_TAGS.add(parts[0])
TAG_ALIASES.setdefault(name, []).append(parts[0])
MUSIC_NAME_BY_TAG = dict(MUSIC_TAGS) MUSIC_NAME_BY_TAG = dict(MUSIC_TAGS)
MUSIC_TAG_BY_NAME = dict(((x[1].lower(), x[0]) for x in MUSIC_TAGS)) MUSIC_TAG_BY_NAME = dict(((x[1].lower(), x[0]) for x in MUSIC_TAGS))
class MusicTag(namedtuple("MusicTag", ("name", "variant"), defaults=[None])): def slug(s):
"""
>>> slug("This is a test")
'This_is_a_test'
"""
return str(s).replace(" ", "_")
def deslug(s):
return s.replace("_", " ")
@dataclass
class MusicTag:
name: str
number: int | None = None
prefix: str = ""
@classmethod @classmethod
def from_tag(cls, tag): def from_tag(cls, tag):
""" """
>>> MusicTag.from_tag('vn-1') >>> MusicTag.from_tag('vn-1')
MusicTag(name='Violin', variant='1') MusicTag(name='Violin', number=1, prefix='')
>>> MusicTag.from_tag('db') >>> MusicTag.from_tag('db')
MusicTag(name='Double Bass', variant=None) MusicTag(name='Double Bass', number=None, prefix='')
>>> MusicTag.from_tag('Jaws Harp') >>> MusicTag.from_tag('Jaws Harp')
MusicTag(name='Jaws Harp', variant=None) MusicTag(name='Jaws Harp', number=None, prefix='')
>>> MusicTag.from_tag('mvmt-2') >>> MusicTag.from_tag('mvmt:Largo-2')
MusicTag(name='Movement', variant='2') MusicTag(name='Largo', number=2, prefix='mvmt')
>>> MusicTag.from_tag('pce-A2') >>> MusicTag.from_tag('name:A2')
MusicTag(name='Piece', variant='A2') MusicTag(name='A2', number=None, prefix='name')
>>> MusicTag.from_tag('name:Ode_to_Joy')
MusicTag(name='Ode to Joy', number=None, prefix='name')
>>> MusicTag.from_tag('no:-2')
MusicTag(name='', number=2, prefix='no')
""" """
abbr, _, variant = tag.partition("-")
name = MUSIC_NAME_BY_TAG.get(abbr.lower(), abbr)
if variant: match = MUSIC_TAG.fullmatch(tag)
return cls(name, variant)
return cls(name, None)
@property if match is None:
def tag(self): raise ValueError("Not a valid tag")
lc = self.name.lower()
return MUSIC_TAG_BY_NAME.get(lc, lc) result = match.groupdict()
if result["prefix"] is None:
result["name"] = MUSIC_NAME_BY_TAG.get(
result["name"].lower(), deslug(result["name"])
)
result["prefix"] = ""
else:
result["name"] = deslug(result["name"])
result["number"] = int(result["number"]) if result["number"] else None
return cls(**result)
@property @property
def is_general(self): def is_general(self):
""" """
>>> MusicTag('Piece', 'A3').is_general >>> MusicTag('A3', prefix="name").is_general
True True
>>> MusicTag('Violin', 2).is_general >>> MusicTag('Violin', 2).is_general
False False
""" """
return self.tag in GENERAL_TAGS return self.prefix in TAG_PREFIXES
def abbreviate(self): @property
def tag(self):
""" """
>>> MusicTag('Violin', 1).abbreviate() >>> MusicTag('Violin', 1).tag
'vn-1' 'vn-1'
>>> MusicTag('Double Bass').abbreviate() >>> MusicTag('Double Bass').tag
'db' 'db'
>>> MusicTag('left', prefix="page:").tag
'page:left'
>>> MusicTag("Ode to Joy", prefix="name:").tag
'name:Ode_to_Joy'
>>> MusicTag('Unknown Instrument').tag
'Unknown_Instrument'
""" """
tag = MUSIC_TAG_BY_NAME.get(self.name.lower()) parts = [self.prefix]
if self.variant: parts.append(MUSIC_TAG_BY_NAME.get(self.name.lower(), slug(self.name)))
tag = f"{tag}-{self.variant}" if self.number:
return tag parts.extend(["-", str(self.number)])
return "".join(parts)
def __str__(self): def __str__(self):
""" """
@ -232,15 +272,17 @@ class MusicTag(namedtuple("MusicTag", ("name", "variant"), defaults=[None])):
'Violin 1' 'Violin 1'
>>> str(MusicTag('Double Bass')) >>> str(MusicTag('Double Bass'))
'Double Bass' 'Double Bass'
>>> str(MusicTag('Unknown Instrument'))
'Unknown Instrument'
""" """
if self.variant: if self.number:
return f"{self.name} {self.variant}" return f"{self.name} {self.number}"
return self.name return self.name
PATTERNS = [ PATTERNS = [
re.compile(r"(?P<inst>[A-Za-z]+)[_\- ]*(?P<ord>\d+)"), re.compile(r"(?P<inst>[A-Za-z]+)[_\- ]*(?P<number>\d+)"),
re.compile(r"(?P<ord>\d+)(st|nd|rd|th)[_\- ]*(?P<inst>[A-Za-z]+)"), re.compile(r"(?P<number>\d+)(st|nd|rd|th)[_\- ]*(?P<inst>[A-Za-z]+)"),
re.compile(r"(?P<inst>[A-Za-z]+)()"), re.compile(r"(?P<inst>[A-Za-z]+)()"),
] ]
@ -248,34 +290,33 @@ PATTERNS = [
def auto_tag(filename): def auto_tag(filename):
""" """
>>> auto_tag('Ode to Joy - Violin 1.pdf') >>> auto_tag('Ode to Joy - Violin 1.pdf').tag
MusicTag(name='Violin', variant=1) 'vn-1'
>>> auto_tag('Ode to Joy_Cello.pdf') >>> auto_tag('Ode to Joy_Cello.pdf').tag
MusicTag(name='Cello', variant=None) 'vc'
>>> auto_tag('Ode to Joy violin - 1.pdf') >>> auto_tag('Ode to Joy violin - 1.pdf').tag
MusicTag(name='Violin', variant=1) 'vn-1'
>>> auto_tag('Ode to Joy - vla.pdf') >>> auto_tag('Ode to Joy - vla.pdf').tag
MusicTag(name='Viola', variant=None) 'va'
>>> auto_tag('Ode to Joy - fl-2 (piccolo).pdf') >>> auto_tag('Ode to Joy - fl-2 (piccolo).pdf').tag
MusicTag(name='Flute', variant=2) 'fl-2'
>>> auto_tag('1st Violin - Ode to Joy.pdf') >>> auto_tag('1st Violin - Ode to Joy.pdf').tag
MusicTag(name='Violin', variant=1) 'vn-1'
>>> auto_tag('Ode to Joy - 2nd Violin.pdf') >>> auto_tag('Ode to Joy - 2nd Violin.pdf').tag
MusicTag(name='Violin', variant=2) 'vn-2'
""" """
for pattern in PATTERNS: for pattern in PATTERNS:
for m in pattern.finditer(filename): for m in pattern.finditer(filename):
inst = m["inst"].lower() inst = m["inst"].lower()
try: try:
ordinal = int(m["ord"]) number = int(m["number"])
except IndexError: except IndexError:
ordinal = None number = None
if inst in MUSIC_TAG_BY_NAME: if inst in MUSIC_TAG_BY_NAME:
return MusicTag(inst.title(), ordinal) return MusicTag(inst.title(), number)
if inst in MUSIC_NAME_BY_TAG: if inst in MUSIC_NAME_BY_TAG:
return MusicTag(MUSIC_NAME_BY_TAG[inst], ordinal) return MusicTag(MUSIC_NAME_BY_TAG[inst], number)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -80,6 +80,7 @@
.tag-name { .tag-name {
flex: 1; flex: 1;
text-align: right; text-align: right;
cursor: pointer;
} }
.tag-handle-resize { .tag-handle-resize {
position: absolute; position: absolute;
@ -140,7 +141,7 @@
</ul> </ul>
<h3>Actions</h3> <h3>Actions</h3>
<div> <div>
<a class="button is-small is-primary" onclick="showAddModal()">New Tag</a> <a class="button is-small is-primary" onclick="showTagModal(createNewTag)">New Tag</a>
<button class="button is-small is-primary" onclick="expandEntries()">Expand Tags</button> <button class="button is-small is-primary" onclick="expandEntries()">Expand Tags</button>
</div> </div>
</div> </div>
@ -163,18 +164,19 @@
</div> </div>
</div> </div>
<div class="modal" id="add-modal"> <div class="modal" id="tag-modal">
<div class="modal-background" onclick="closeAddModal()"></div> <div class="modal-background" onclick="closeTagModal()"></div>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">Add Tag</p> <p class="modal-card-title">Tag Editor</p>
<button class="delete" aria-label="close" onclick="closeAddModal()"></button> <button class="delete" aria-label="close" onclick="closeTagModal()"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<form>
<div class="field has-addons is-justify-content-center"> <div class="field has-addons is-justify-content-center">
<span class="control"> <span class="control">
<span class="select"> <span class="select">
<select onChange="changeTagType()" id="tag-type-selection"> <select onChange="onChangeTagType()" id="tag-prefix">
<option value="inst">Instrument</option> <option value="inst">Instrument</option>
<option value="mvmt">Movement</option> <option value="mvmt">Movement</option>
<option value="sect">Section</option> <option value="sect">Section</option>
@ -183,32 +185,34 @@
</select> </select>
</span> </span>
</span> </span>
<span class="control add-tag-option enable-page"> <span class="control tag-option enable-page">
<span class="select"> <span class="select tag-name">
<select id="add-tag-page"> <select id="tag-name-page">
<option>Left</option> <option value="left">Left</option>
<option>Right</option> <option value="right">Right</option>
<option value="blank">Add Blank</option>
</select> </select>
</span> </span>
</span> </span>
<span class="control add-tag-option enable-sect"> <span class="control tag-option enable-sect enable-mvmt">
<input type="text" class="input" id="add-tag-name"/> <input type="text" class="input tag-name" id="tag-name" />
</span> </span>
<span class="control add-tag-option enable-inst"> <span class="control tag-option enable-inst">
<input type="text" class="input" list="instrument-list" id="add-tag-inst"/> <input type="text" class="input tag-name" list="instrument-list" id="tag-name-inst"/>
<datalist id="instrument-list"> <datalist id="instrument-list">
{% for inst in json_data.instruments.values %} {% for inst in json_data.instrumentNames %}
<option value="{{inst}}"/> <option value="{{inst}}"/>
{% endfor %} {% endfor %}
</datalist> </datalist>
</span> </span>
<span class="control add-tag-option enable-inst enable-mvmt enable-no"> <span class="control tag-option enable-inst enable-mvmt enable-no">
<input type="number" class="input" min="1" size="3" id="add-tag-ordinal"/> <input type="number" class="input tag-number" min="1" size="3" id="tag-number"/>
</span> </span>
<span class="control"> <span class="control">
<button class="button is-primary" onclick="createNewTag();">Add</button> <button type="submit" class="button is-primary" onclick="event.preventDefault(); confirmTagModal(); ">Save</button>
</span> </span>
</div> </div>
</form>
</section> </section>
</div> </div>
</div> </div>
@ -244,6 +248,7 @@
let data = JSON.parse(document.getElementById('data').textContent); let data = JSON.parse(document.getElementById('data').textContent);
let tagArea = document.getElementById('tag-area'); let tagArea = document.getElementById('tag-area');
var dirty = false; var dirty = false;
var tagModalCallback = null;
//document.getElementById('tag-list').onclick = (e) => setTag(e.target.dataset.tag); //document.getElementById('tag-list').onclick = (e) => setTag(e.target.dataset.tag);
@ -405,13 +410,25 @@
dirty = false; dirty = false;
}); });
function showAddModal() { function showTagModal(callback, tag) {
document.getElementById("add-modal").classList.add("is-active"); document.getElementById("tag-modal").classList.add("is-active");
changeTagType();
tagModalCallback = callback;
if(tag) {
const parts = parseTag(tag);
console.log(parts);
document.querySelector("#tag-prefix").value = parts.prefix;
onChangeTagType();
document.querySelectorAll(".tag-name").forEach(el => el.value = parts.name);
document.querySelector(".tag-number").value = parts.number;
} else {
onChangeTagType();
}
} }
function closeAddModal() { function closeTagModal() {
document.getElementById('add-modal').classList.remove('is-active'); document.getElementById('tag-modal').classList.remove('is-active');
} }
function addNumberedInstrument(tag, e) { function addNumberedInstrument(tag, e) {
@ -424,55 +441,48 @@
modal.classList.add('is-active'); modal.classList.add('is-active');
} }
function changeTagType() { function onChangeTagType() {
const tag_select = document.querySelector("#tag-type-selection"); const tag_prefix = document.querySelector("#tag-prefix");
const selected = tag_select.value; const selected = tag_prefix.value;
document.querySelectorAll(".add-tag-option").forEach((x) => {x.classList.add("is-hidden")}); document.querySelectorAll(".tag-option").forEach((x) => {x.classList.add("is-hidden")});
document.querySelectorAll(".add-tag-option input").forEach((x) => {x.value=""}); document.querySelectorAll(".tag-option input").forEach((x) => {x.value=""});
document.querySelectorAll(".enable-" + selected).forEach((x) => {x.classList.remove("is-hidden")}); document.querySelectorAll(".enable-" + selected).forEach((x) => {x.classList.remove("is-hidden")});
tag_select.focus(); tag_prefix.focus();
} }
function generateNewTag() { function confirmTagModal() {
const selected = document.querySelector("#tag-type-selection").value; const selected = document.querySelector("#tag-prefix").value;
const n = document.querySelector("#add-tag-ordinal").value; let name = document.querySelector("#tag-name").value;
const n = document.querySelector("#tag-number").value;
switch (selected) { switch (selected) {
case "mvmt":
case "no": case "no":
return selected + "-" + n; if (!n) throw new Error("Missing number");
case "sect": case "page":
return "sect-" + document.querySelector("#add-tag-name").value; name = document.querySelector("#tag-name-page").value;
case "inst": case "inst":
const inst_name = document.querySelector("#add-tag-inst").value; name = document.querySelector("#tag-name-inst").value;
var tag = null;
for (let key in data.instruments) { for (let key in data.instruments) {
if (data.instruments[key] == inst_name) { if (data.instruments[key] == name) {
tag = key; name = key;
break; break;
} }
} }
if (!tag) {
tag = inst_name;
}
if (n) {
tag += "-" + n;
}
return tag;
case "page":
return "page-" + document.querySelector("#add-tag-page").value;
} }
name = name.replace(" ", "_");
const prefix = (selected == "inst") ? "" : selected + ":";
const tag = (n) ? prefix + name + "-" + n : prefix + name;
closeTagModal();
tagModalCallback(tag);
} }
function createNewTag() { function createNewTag(tag) {
const tag = generateNewTag()
addTag(tag, pageNum, pageNum); addTag(tag, pageNum, pageNum);
closeAddModal(); dirty = true;
} }
function assignTag(tag, el) { function assignTag(tag, el) {
@ -480,6 +490,16 @@
//el.remove(); //el.remove();
} }
function editTag(e) {
console.log("EDIT", e.target);
showTagModal((tag) => {
console.log("Updating to " + tag);
e.target.innerHTML = get_tag_name(tag);
e.target.parentNode.dataset["tag"] = tag;
dirty = true;
}, e.target.parentNode.dataset["tag"]);
}
function addTag(tag, start, end) { function addTag(tag, start, end) {
@ -489,13 +509,15 @@
const tagName = el.querySelector(".tag-name"); const tagName = el.querySelector(".tag-name");
tagName.innerHTML=get_tag_name(tag); tagName.innerHTML=get_tag_name(tag);
tagName.addEventListener('dblclick', editTag);
//el.className = 'grid-tag'; //el.className = 'grid-tag';
el.dataset.start = start; el.dataset.start = start;
el.dataset.end = end; el.dataset.end = end;
el.dataset.tag = tag; el.dataset.tag = tag;
const parts = tag.split("-") const parts = parseTag(tag)
el.classList.add("tag-type-" + parts[0]); el.classList.add("tag-type-" + parts.prefix);
const handle = el.querySelector('.tag-handle'); const handle = el.querySelector('.tag-handle');
@ -674,7 +696,7 @@
onPrevPage(); onPrevPage();
break; break;
case "n": case "n":
showAddModal(); showTagModal(createNewTag);
break; break;
default: default:
return; return;
@ -712,13 +734,43 @@
dirty = false; dirty = false;
} }
function get_tag_name(s) { function parseTag(tag) {
let parts = s.split('-'); const result = {
let instrument = data.instruments[parts[0]] || parts[0]; prefix: "inst",
if (parts.length == 2) { name: "",
return instrument + " " + parts[1]; number: "",
};
let parts = tag.split(":");
if(parts.length == 2) {
result.prefix = parts[0];
tag = parts[1];
} }
return instrument;
parts = tag.split('-');
if(parts.length == 2) {
result.number = parts[1];
}
result.name = parts[0];
if(result.prefix == "inst") {
result.name = data.instruments[result.name] || result.name;
}
result.name = result.name.replace("_", " ");
return result;
}
function get_tag_name(s) {
tag = parseTag(s);
console.log(tag);
let prefix = (tag.prefix == "inst") ? "" : data.prefixes[tag.prefix] + " ";
return (tag.number) ? prefix + tag.name + " " + tag.number : prefix + tag.name;
} }
function checkSaved(e) { function checkSaved(e) {

View File

@ -2,8 +2,6 @@
{% load polyphonic %} {% load polyphonic %}
{% block page %} {% block page %}
<form action="" method="post" target="_blank">
{% csrf_token %}
<div> <div>
<div class="field is-grouped is-grouped-centered is-grouped-multiline"> <div class="field is-grouped is-grouped-centered is-grouped-multiline">
<div class="field has-addons control has-addons-centered is-expanded"> <div class="field has-addons control has-addons-centered is-expanded">
@ -16,7 +14,7 @@
<input type="text" class="input" list="instrument-list" id="instrument-name" onchange="updateParts()"/> <input type="text" class="input" list="instrument-list" id="instrument-name" onchange="updateParts()"/>
<datalist id="instrument-list"> <datalist id="instrument-list">
{% for inst in instruments %} {% for inst in instruments %}
<option value="{{inst.1}}"/> <option value="{{inst}}"/>
{% endfor %} {% endfor %}
</datalist> </datalist>
</span> </span>
@ -35,6 +33,9 @@
</div> </div>
</div> </div>
<form action="" method="post" target="_blank">
{% csrf_token %}
<table class="table is-striped mx-auto"> <table class="table is-striped mx-auto">
<thead> <thead>
<tr> <tr>
@ -58,7 +59,7 @@
{% endif %} {% endif %}
</td> </td>
<td class="is-hidden-mobile">{{ item.work.composer }}</td> <td class="is-hidden-mobile">{{ item.work.composer }}</td>
<td class="is-hidden-mobile">{% firstof item.work.running_time "------" %}</td> <td class="is-hidden-mobile">{% firstof item.work.running_time "-:--" %}</td>
<td class="select-cell"> <td class="select-cell">
<input type="hidden" name="works" value="{{ item.work.pk }}"/> <input type="hidden" name="works" value="{{ item.work.pk }}"/>
<span class="select is-small" style="width: 100%"> <span class="select is-small" style="width: 100%">
@ -84,7 +85,7 @@
<td/> <td/>
<td/> <td/>
<td/> <td/>
<td>{% firstof running_time "------" %}</td> <td>{% firstof running_time "-:--" %}</td>
<td colspan="2"> <td colspan="2">
<button class="button is-link is-small" type="submit" name="action" value="pdf"> <button class="button is-link is-small" type="submit" name="action" value="pdf">
{% icon "two_pager" %} {% icon "two_pager" %}
@ -114,50 +115,49 @@
const INSTRUMENTS = JSON.parse(document.getElementById('instruments').innerText); const INSTRUMENTS = JSON.parse(document.getElementById('instruments').innerText);
function updateParts() { function updateParts() {
var inst = document.getElementById("instrument-name").value.toLowerCase(); var inst = document.getElementById("instrument-name").value;
window.localStorage.setItem('instrument-name', inst); window.localStorage.setItem('instrument-name', inst);
console.log("Changing to", inst); console.log("Changing to", inst);
for (let i of INSTRUMENTS) { for (let i in INSTRUMENTS) {
if (i[1].toLowerCase() === inst) { if (i.toLowerCase() === inst.toLowerCase()) {
inst = i[0]; inst = i;
break; break;
} }
} }
console.log("Instrument code:", inst);
let part = document.getElementById("part-preference").value; let part = document.getElementById("part-preference").value;
window.localStorage.setItem('part-preference', part); window.localStorage.setItem('part-preference', part);
console.log("Part preference:", part); console.log("Part preference:", inst, part);
selectParts(inst, part); selectParts(INSTRUMENTS[inst] || [inst.toLowerCase()], part);
} }
function selectParts(inst, part) { function selectParts(codes, part) {
let n = parseInt(part);
let preferences = [];
for (let i=n; i>0; i--) {
for (let code of codes) preferences.push(code + "-" + i);
}
preferences.push(...codes);
console.log("Preferences:", preferences);
let prefix = inst + "-" + part;
let selections = document.getElementsByName("instrument-selection"); let selections = document.getElementsByName("instrument-selection");
console.log("Updating selections:", prefix, selections);
for(let i=0; i<selections.length; i++) { for(let i=0; i<selections.length; i++) {
var result = "-" const selection = selections[i];
let options = selections[i].children; for(let pref of preferences) {
for(let j=0; j<options.length; j++) { selection.value = pref;
let value = options[j].value; if(selection.value == pref) break;
if (value.startsWith(prefix)) {
result = value;
break;
}
if (result==="-" && value.startsWith(inst)) {
result = value;
}
} }
console.log("Selected:", result);
selections[i].value = result; if(!selection.value) selection.value = "-";
console.log("Selected:", selection.value);
} }
} }
@ -170,7 +170,7 @@ function downloadPart(collection, work) {
} }
console.log(part); console.log(part);
let url = "/collections/" + collection + "/works/" + work + "/download?tag=" + part; let url = "/collections/" + collection + "/works/" + work + "/download?tag=" + part;
window.location = url; window.open(url, "_blank");
} }
document.getElementById("instrument-name").value = localStorage.getItem('instrument-name', 'All'); document.getElementById("instrument-name").value = localStorage.getItem('instrument-name', 'All');

View File

@ -2,7 +2,7 @@
{% load polyphonic %} {% load polyphonic %}
{% block admin %} {% block admin %}
<a href="{% url 'item_list_append' project.pk %}" class="button is-link"> <a href="{% url 'project_work_list' project.pk %}" class="button is-link">
{% icon "add_circle" %} {% icon "add_circle" %}
<span>Add</span> <span>Add</span>
</a> </a>
@ -13,37 +13,48 @@
{% endblock %} {% endblock %}
{% block page %} {% block page %}
<table style="max-width: 600px; margin: 10pt auto;" class="zebra"> <div class="columns">
<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>{{ item.work.duration }}</td>
<td style="text-align: center;">
<span class="clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)">
{% icon "arrow_upward" %}
</span>
<span class="clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)">
{% icon "arrow_downward" %}
</span>
<span class="clickable" title="Remove" onClick="moveItem({{ item.pk }}, 0)">
{% icon "delete" %}
</span>
</td>
</tr> <div class="column card">
<table style="max-width: 600px; margin: 10pt auto;" class="table is-striped is-narrow">
<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 style="min-width: 100px;">{% firstof item.work.duration '-:--' %}</td>
<td style="text-align: center;">
<span class="clickable is-clickable" title="Remove" onClick="removeItem({{ item.pk }})">
{% icon "delete" %}
</span>
<span class="clickable drag-handle cursor-drag" title="Reorder">
{% icon "reorder" %}
</span>
</td>
</tr>
{% empty %} {% empty %}
<tr><td colspan="2">No items</td></tr> <tr><td colspan="2">No items</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div>
</div>
{% endblock %}
{% block style %}
{{ block.super }}
<style>
tr.dragging { opacity: 0.4; }
.drag-handle { cursor: move; }
</style>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@ -51,36 +62,82 @@
let workList = document.getElementById('work-list'); let workList = document.getElementById('work-list');
var dirty = false; var dirty = false;
var dragSrc = null;
var dragSrcNext = null;
function moveItem(pk, dir) { function reorderItems() {
let items = Array.prototype.slice.call(workList.children, 0);
for (let j = 0; j < items.length; j++) {
items[j].dataset.order = j;
}
}
function removeItem(pk) {
let items = Array.prototype.slice.call(workList.children, 0); let items = Array.prototype.slice.call(workList.children, 0);
var i=0;
pk = "" + pk; pk = "" + pk;
for(i=0; i<items.length; i++) { for (let i = 0; i < items.length; i++) {
if(items[i].dataset.pk === pk) break; if (items[i].dataset.pk === pk) {
} items[i].dataset.order = -1;
if(i >= items.length) return; items[i].style = "display: none";
break;
// check the direction is sensible }
if (i + dir < 0 || i + dir >= items.length) return;
if (dir === 0) {
items[i].dataset.order = -1;
items[i].style = "display: none";
} else {
items[i].dataset.order = parseInt(items[i].dataset.order) + dir;
items[i+dir].dataset.order = parseInt(items[i+dir].dataset.order) - dir;
}
items.sort((a, b) => parseInt(a.dataset.order) - parseInt(b.dataset.order))
workList.innerHTML = ""
for(let j=0; j<items.length; j++) {
workList.appendChild(items[j]);
} }
dirty = true; dirty = true;
} }
workList.addEventListener('mousedown', function(e) {
let handle = e.target.closest('.drag-handle');
if (handle) handle.closest('tr').draggable = true;
});
workList.addEventListener('mouseup', function(e) {
let handle = e.target.closest('.drag-handle');
if (handle) handle.closest('tr').draggable = false;
});
workList.addEventListener('dragstart', function(e) {
dragSrc = e.target.closest('tr');
if (!dragSrc) return;
dragSrcNext = dragSrc.nextElementSibling;
dragSrc.classList.add('dragging');
e.dataTransfer.setDragImage(e.target, 0, 0);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', dragSrc.dataset.pk);
});
workList.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
let target = e.target.closest('tr');
if (!target || target === dragSrc) return;
let rect = target.getBoundingClientRect();
let midY = rect.top + rect.height / 2;
if (e.clientY < midY) {
workList.insertBefore(dragSrc, target);
} else {
workList.insertBefore(dragSrc, target.nextElementSibling);
}
});
workList.addEventListener('drop', function(e) {
e.preventDefault();
dragSrcNext = null;
reorderItems();
dirty = true;
});
workList.addEventListener('dragend', function(e) {
if (!dragSrc) return;
dragSrc.classList.remove('dragging');
if (dragSrcNext) {
workList.insertBefore(dragSrc, dragSrcNext);
}
dragSrc = null;
dragSrcNext = null;
});
function save() { function save() {
let items = Array.prototype.slice.call(workList.children, 0); let items = Array.prototype.slice.call(workList.children, 0);
@ -98,7 +155,7 @@ function save() {
}).then((response) => { }).then((response) => {
if (response.ok) { if (response.ok) {
dirty=false; dirty=false;
window.location = "{% url 'item_list' project=project.pk %}"; window.location = "{% url 'project_detail' project=project.pk %}";
} else { } else {
alert("Failed: " + response.statusText) alert("Failed: " + response.statusText)
} }

View File

@ -1,6 +1,9 @@
<div class="panel"> <div class="panel">
<p class="panel-heading"> <p class="panel-heading">
Items Items
{% if request.is_admin %}
<a href="{% url 'item_list_manage' project=project.pk %}" class="button is-small is-pulled-right">Edit Items</a>
{% endif %}
</p> </p>
{% for item in project.items.all %} {% for item in project.items.all %}
@ -20,7 +23,7 @@
<div class="modal-background" onclick="closeDownloadModal()"></div> <div class="modal-background" onclick="closeDownloadModal()"></div>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">Parts</p> <p class="modal-card-title">Available Parts</p>
<button class="delete" aria-label="close" onclick="closeDownloadModal()"></button> <button class="delete" aria-label="close" onclick="closeDownloadModal()"></button>
</header> </header>
<section class="modal-card-body" id="download-target"> <section class="modal-card-body" id="download-target">

View File

@ -9,6 +9,12 @@
<span>Add a work</span> <span>Add a work</span>
</a> </a>
{% endif %} {% endif %}
{% if project %}
<a href="{% url 'item_list_manage' project.pk %}" class="button">
{% icon "add_notes" %}
<span>Back</span>
</a>
{% endif %}
{% endblock %} {% endblock %}
{% block page %} {% block page %}
@ -30,7 +36,7 @@
<div class="has-text-centered"> <div class="has-text-centered">
<a href="?">_</a> <a href="?">_</a>
{% for letter in letters %} {% for letter in letters %}
<a href="?start={{ letter }}">{{ letter }}</a>&nbsp; <a href="?start={{ letter }}&sort={{ sort }}">{{ letter }}</a>&nbsp;
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@ -38,10 +44,11 @@
<table class="table is-striped is-fullwidth"> <table class="table is-striped is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>Work</th> <th>Work <span class="icon-text"><a href="?start={{ start }}&sort=name">{{ "list_arrow"|icon }}</a></span></th>
<th>Composer</th> <th>Composer <span class="icon-text"><a href="?start={{ start }}&sort=composer">{{ "list_arrow"|icon }}</a></span></th>
<th class="is-hidden-mobile">Edition</th> <th class="is-hidden-mobile">Edition</th>
{% if not collection %}<th class="is-hidden-touch">Collection</th>{% endif %} {% if not collection and not project %}<th class="is-hidden-touch">Collection</th>{% endif %}
{% if project %}<th/>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -52,7 +59,14 @@
</td> </td>
<td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td> <td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td>
<td class="is-hidden-mobile" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td> <td class="is-hidden-mobile" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
{% if not collection %}<td class="is-hidden-touch">{{ work.collection.name }}</td>{% endif %} {% if not collection and not project %}<td class="is-hidden-touch">{{ work.collection.name }}</td>{% endif %}
{% if project %}
<td>
<button data-work="{{ work.pk }}" onclick="addToProject({{ work.pk }})" class="button is-primary is-small">
{% if work.pk in current %} Added {% else %} Add {% endif %}
</button>
</td>
{% endif %}
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="4">No works found</td></tr> <tr><td colspan="4">No works found</td></tr>
@ -100,3 +114,24 @@ Query="{{ meta.query }}"
</footer> </footer>
{% endblock %} {% endblock %}
{% block scripts %}
<script>
async function addToProject(work) {
console.log("ADD", work);
const response = await fetch("", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: new URLSearchParams({ work, csrfmiddlewaretoken: "{{ csrf_token }}" }),
});
if (response.status === 201) {
document.querySelector(`[data-work='${work}']`).innerHTML = "Added";
} else {
alert("Failed to add work\nCheck console for details");
console.error(response);
}
console.log(response);
}
</script>
{% endblock %}

View File

@ -1,27 +1,10 @@
<h3 class="subtitle">{{ work.name }}</h3> <h3 class="subtitle">{{ work.name }}</h3>
<div class="block tags"> <div class="block tags">
{% for tag, name in work.digital_parts %} {% for tag, name in work.digital_parts %}
<a class="tag is-info" href="{% url 'work_download' work.collection_id work.pk %}?tag={{ tag }}" target="polyphonic_parts">{{ name }}</a> <a class="tag is-info" href="{% url 'work_download' work.collection_id work.pk %}?tag={{ tag }}" target="_blank">{{ name }}</a>
{% endfor %} {% endfor %}
</div> </div>
<!-- <p class="has-text-right">
<h3 class="subtitle">Files</h3> <a href="{% url 'work_detail' work.collection_id work.pk %}">Full details for {{ work.name }}</a>
<table class="table is-narrow is-fullwidth">
<tbody>
{% for doc in work.pdfs %}
<tr>
<td><a href="{{ doc.upload.url }}">{{ doc.filename }}</a></td>
</tr>
{% empty %}
<tr>
<td>There are no files available for this work</td>
</tr>
{% endfor %}
</tbody>
</table>
-->
<p>
<a href="{% url 'work_detail' work.collection_id work.pk %}">More details...</a>
</p> </p>

View File

@ -1,6 +1,6 @@
from doctest import DocTestSuite from doctest import DocTestSuite
from library import music_tags from polyphonic.library import music_tags
def load_tests(loader, tests, ignore): def load_tests(loader, tests, ignore):

View File

@ -27,6 +27,11 @@ urlpatterns = [
), ),
path("library", views.LibraryWorkListView.as_view(), name="work_list"), path("library", views.LibraryWorkListView.as_view(), name="work_list"),
path("collections", views.CollectionListView.as_view(), name="collection_list"), path("collections", views.CollectionListView.as_view(), name="collection_list"),
path(
"projects/<int:project>/collections",
views.ProjectWorkListView.as_view(),
name="project_work_list",
),
path( path(
"collections/<int:collection>", "collections/<int:collection>",
views.CollectionWorkListView.as_view(), views.CollectionWorkListView.as_view(),

View File

@ -1,5 +1,5 @@
from django.shortcuts import get_object_or_404, redirect, resolve_url from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.detail import DetailView, SingleObjectMixin, View from django.views.generic.detail import DetailView, SingleObjectMixin, View
from django.views.generic.list import ListView from django.views.generic.list import ListView
@ -11,6 +11,7 @@ from django.utils.timezone import now
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
import json import json
@ -20,7 +21,13 @@ import string
from polyphonic.interface.views import ProjectMixin, AuthorizedResourceMixin from polyphonic.interface.views import ProjectMixin, AuthorizedResourceMixin
from polyphonic.interface.utils import signed_url from polyphonic.interface.utils import signed_url
from polyphonic.library.models import Collection, Work, Document, Section from polyphonic.library.models import Collection, Work, Document, Section
from polyphonic.library.music_tags import MUSIC_TAGS, MusicTag from polyphonic.library.music_tags import (
MUSIC_TAGS,
MUSIC_NAME_BY_TAG,
TAG_PREFIXES,
TAG_ALIASES,
MusicTag,
)
from polyphonic.library import forms, models from polyphonic.library import forms, models
from polyphonic.library.pdf_utils import extract_pages, extract_and_concat from polyphonic.library.pdf_utils import extract_pages, extract_and_concat
from polyphonic.library.indexer import index_works, model_search from polyphonic.library.indexer import index_works, model_search
@ -85,7 +92,7 @@ class ProjectItemListView(ProjectMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super(ProjectItemListView, self).get_context_data(**kwargs) data = super(ProjectItemListView, self).get_context_data(**kwargs)
data["instruments"] = MUSIC_TAGS data["instruments"] = TAG_ALIASES
data["instrument"] = self.request.session.get("instrument", "Score") data["instrument"] = self.request.session.get("instrument", "Score")
data["part"] = self.request.session.get("part", "0") data["part"] = self.request.session.get("part", "0")
data["running_time"] = self.get_queryset().aggregate(Sum("work__running_time"))[ data["running_time"] = self.get_queryset().aggregate(Sum("work__running_time"))[
@ -199,13 +206,8 @@ class WorkListView(CollectionMixin, TemplateView):
data["meta"] = qs.meta data["meta"] = qs.meta
# data["page_range"] = data["page_obj"]["paginator"] # data["page_range"] = data["page_obj"]["paginator"]
else: else:
qs = self.get_queryset() qs, ctx = self.get_filtered_queryset()
data.update(ctx)
start = self.request.GET.get("start")
if start:
start = start.upper()
qs = qs.filter(name__gte=start, name__lt=start + "~")
data["start"] = start
data["letters"] = string.ascii_uppercase data["letters"] = string.ascii_uppercase
@ -222,9 +224,27 @@ class WorkListView(CollectionMixin, TemplateView):
def get_collections(self): def get_collections(self):
raise NotImplementedError raise NotImplementedError
def get_queryset(self): def get_filtered_queryset(self):
works = self.get_works() works = self.get_works()
return works.order_by("name", "composer", "edition", "pk").distinct() ctx = {}
sort = self.request.GET.get("sort", "name").lower()
order = ["name", "composer", "edition"]
if sort not in order:
raise KeyError(f"Cannot sort by {sort}")
order.remove(sort)
order = [sort] + order + ["pk"]
ctx["sort"] = sort
start = self.request.GET.get("start")
if start:
start = start.upper()
filters = {f"{sort}__gte": start, f"{sort}__lt": start + "~"}
works = works.filter(**filters)
ctx["start"] = start
return works.order_by(*order).distinct(), ctx
def get_results(self, query, page): def get_results(self, query, page):
try: try:
@ -281,6 +301,39 @@ class CollectionWorkListView(WorkListView):
return data return data
class ProjectWorkListView(WorkListView, ProjectMixin):
http_method_names = ["get", "post"]
def is_authorized(self):
return ProjectMixin.is_authorized(self)
def post(self, request, project):
work = request.POST.get("work")
print("WORK", work)
self.project.items.create(
work_id=work, checkout=now(), approved_by=request.user, order=100
)
return HttpResponse(status=201)
def get_works(self):
collections = self.get_collections() or []
print(collections)
return Work.objects.filter(collection_id__in=collections)
def get_collections(self):
return self.project.ensemble.allowed_collections.values_list(
"collection_id", flat=True
)
def get_context_data(self, *args, **kwargs):
data = super(ProjectWorkListView, self).get_context_data(*args, **kwargs)
data["title"] = f"Library for {self.project.ensemble}"
data["current"] = set(self.project.items.values_list("work_id", flat=True))
return data
class WorkAddView(CollectionMixin, FormView): class WorkAddView(CollectionMixin, FormView):
template_name = "interface/default_form.html" template_name = "interface/default_form.html"
form_class = forms.WorkCreateForm form_class = forms.WorkCreateForm
@ -603,7 +656,12 @@ class DocumentAnnotateView(DocumentMixin, DetailView):
("sect", "success"), ("sect", "success"),
] ]
data["json_data"] = {"pageTags": pages, "instruments": dict(MUSIC_TAGS)} data["json_data"] = {
"pageTags": pages,
"instruments": MUSIC_NAME_BY_TAG,
"instrumentNames": list({x[1] for x in MUSIC_TAGS}),
"prefixes": TAG_PREFIXES,
}
return data return data