Author SHA1 Message Date
tris 6869fef0e8 Fixed path strings: 2026-08-27 10:12:47 +10:00
tris 24cb447579 Improved project item management 2026-08-27 09:42:09 +10:00
tris 56827fa6b2 Merge 2026-08-05 08:24:50 +10:00
tris 6bb15ae8ad Fixed up library management 2026-08-05 08:21:30 +10:00
tris ab123ba7b1 Item list management 2026-08-05 08:10:25 +10:00
tris 471e465133 Music tag fixes 2026-08-05 08:10:16 +10:00
tris 965fc49501 Test fix 2026-08-05 08:04:45 +10:00
tris 980c1491aa Layout fixes 2026-08-05 07:58:57 +10:00
tris 521b3fd6c5 Fixed module paths 2026-08-05 07:53:05 +10:00
tris 29bdebf3ed Fixed up library management 2026-08-05 07:43:41 +10:00
tris 375d08488d Inline icon 2026-07-14 09:01:22 +10:00
tris f5349beaae Merge branch 'master' of gitea.tfconsulting.com.au:projects/polyphonic 2026-07-14 08:31:40 +10:00
tris 450eda5c26 Added sort columns 2026-07-14 08:31:32 +10:00
tris abb66f7b40 Updated dockerfiles 2026-07-13 08:50:57 +10:00
tris 4a22264d3f Rewrote tags code in annotate (with help from OpenCode) 2026-07-08 10:54:19 +10:00
tris fefa0a28b6 Switched to bulma 1.0 2026-07-08 10:52:45 +10:00
27 changed files with 906 additions and 383 deletions
+5 -3
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
+16 -1
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
+2 -1
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
+4 -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,
) )
+3 -2
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
@@ -32,10 +32,11 @@
.menu-label { .menu-label {
color: var(--primary); color: var(--primary);
} }
/*
.button.is-primary, .button.is-primary:hover { .button.is-primary, .button.is-primary:hover {
background-color: var(--primary); background-color: var(--primary);
} }
*/
A.admin-link:after { A.admin-link:after {
content: "*"; content: "*";
@@ -60,4 +61,4 @@ TEXTAREA.input {
padding: 10px 10px; padding: 10px 10px;
margin-top: 30px; margin-top: 30px;
border-top-left-radius: 6px; border-top-left-radius: 6px;
} }

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 426 KiB

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 258 B

+6 -9
View File
@@ -1,18 +1,15 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="light">
<head> <head>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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@0.9.3/css/bulma.min.css"> <link rel="stylesheet" href="{% static 'css/bulma.min.css' %}" />
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link> <link rel="stylesheet" href="{% static 'css/polyphonic.css' %}" />
<script src="{% static 'interface/js/interface.js' %}"></script> <link rel="stylesheet" href="{% static 'css/material.css' %}" />
<script src="//unpkg.com/alpinejs" defer></script> <script src="{% static 'js/interface.js' %}"></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>
@@ -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>
@@ -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>
@@ -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 %}
@@ -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>
@@ -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>
@@ -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)
+7 -3
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()
@@ -366,8 +370,8 @@ class Work(models.Model):
def music_tags(self): def music_tags(self):
tags = dict(self.orchestration.as_list()) tags = dict(self.orchestration.as_list())
for section in Section.objects.filter(doc__work_id=self.pk): # for section in Section.objects.filter(doc__work_id=self.pk):
tags.setdefault(section.tag, section.name) # tags.setdefault(section.tag, section.name)
return tags.items() return tags.items()
+110 -68
View File
@@ -1,13 +1,15 @@
from collections import namedtuple from dataclasses import dataclass
import re import re
GENERAL = """
mvmt Movement TAG_PREFIXES = {
ex Excerpt "mvmt": "Movement",
sec Section "ex": "Excerpt",
pce Piece "sect": "Section",
no Number "name": "Name",
""" "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
@@ -159,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):
""" """
@@ -231,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]+)()"),
] ]
@@ -247,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__":
@@ -14,42 +14,98 @@
{% block media %} {% block media %}
<style> <style>
#unassigned-area {
max-width: 200px;
}
#unassigned-area .tag {
cursor: pointer;
}
#area-left h3 {
margin-top: 10px;
text-align: center;
}
.tag-grid { .tag-grid {
display: flex; display: flex;
margin-bottom: 5px;
overflow-y: auto;
} }
.grid-column { .grid-column {
margin-left: 1em; margin-left: 0;
margin-right: 1em; margin-right: 0;
} }
.grid-page { .grid-page {
height: 25px; height: 25px;
margin-bottom: 5px; border: none;
border: 1px solid #999; border-top: 1px solid #DDD;
border-radius: 5px;
text-align: center; text-align: center;
width: 50px; width: 50px;
cursor: pointer; cursor: pointer;
} }
.grid-page:last-child {
border-bottom: 1px solid #DDD;
height: 26px;
}
.grid-page.is-active { .grid-page.is-active {
background-color: var(--primary); /*background-color: var(--bulma-link-light);*/
color: white; color: var(--bulma-link);
background-color: #EEE;
border-radius: 0px 6px 6px 0px;
} }
.grid-tag { .grid-tag {
border: 2px solid var(--primary); background: #fff;
background-color: rgba(240, 240, 240, 0.5); border: 1px solid #DDD;
border-radius: 5px; border-radius: 0px 4px 4px 0px;
padding-left: 1em; border-left: none;
padding-right: 1em; padding-left: 0.5em;
margin-bottom: 5px; min-width: 100px;
min-width: 200px;
position: absolute; position: absolute;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: start; align-items: top;
z-index: 1;
margin: 0;
box-shadow: 4px 0 6px -4px rgba(0,0,0,0.12);
} }
.grid-tag:hover {
z-index: 100 !important;
}
.tag-handle {
position: relative;
width: 20px;
background-color: var(--bulma-warning-light);
margin-left: 10px;
cursor: move;
}
.tag-name {
flex: 1;
text-align: right;
cursor: pointer;
}
.tag-handle-resize {
position: absolute;
height: 10px;
width: 10px;
bottom: 0px;
right: 0px;
border-radius: 3px 0px 0px 0px;
background-color: var(--bulma-warning);
cursor: ns-resize;
}
{% for tag, colour in tag_colours %}
.tag-type-{{ tag }} .tag-handle {
background-color: var(--bulma-{{ colour }}-light);
}
.tag-type-{{ tag }} .tag-handle-resize {
background-color: var(--bulma-{{ colour }});
}
{% endfor %}
#tag-area { #tag-area {
min-width: 220px; min-width: 220px;
position: relative;
font-size: 9pt;
} }
.instrument { .instrument {
display: list-item !important; display: list-item !important;
@@ -64,32 +120,34 @@
{% block page %} {% block page %}
<h3 class="subtitle"><a href="{% url 'work_detail' collection.pk document.work_id %}">{{ document.work.name }}</a></h3> <h3 class="subtitle"><a href="{% url 'work_detail' collection.pk document.work_id %}">{{ document.work.name }}</a></h3>
<div id="annotation-area" class="columns is-centered"> <div id="annotation-area" class="columns is-centered">
<div class="column is-narrow"> <div class="column is-narrow" id="area-left">
<div class="columns"> <h3>Page control</h3>
<div class="column is-narrow"> <div class="columns">
<button class="button is-small is-primary" id="prev">&lt;</button> <div class="column is-narrow">
<button class="button is-small is-primary" id="prev">&lt;</button>
</div>
<div class="column has-text-centered">
<span id="page-num">-</span> / <span id="page-count">-</span>
</div>
<div class="column is-narrow">
<button class="button is-small is-primary" id="next">&gt;</button>
</div>
</div> </div>
<div class="column"> <h3>Quick Tags</h3>
<span id="page-num">-</span> / <span id="page-count">-</span> <ul id="unassigned-area">
{% for tag, inst in document.work.music_tags %}
<li class="tag is-warning" onclick="assignTag('{{tag}}', this)">{{ inst }}</li>
{% endfor %}
</ul>
<h3>Actions</h3>
<div>
<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>
</div> </div>
<div class="column is-narrow">
<button class="button is-small is-primary" id="next">&gt;</button>
</div>
</div>
<ul id="unassigned-area">
{% for tag, inst in document.work.music_tags %}
<li>
<span class="is-clickable" onclick="assignInstrument('{{tag}}', this)")>{{ inst }}</span>
&nbsp;
<span class="is-clickable" onclick="addNumberedInstrument('{{tag}}', this)">...</span>
</li>
{% endfor %}
</ul>
<a onclick="document.getElementById('add-modal').classList.add('is-active')">Add Tag</a>
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<div class="has-text-centered"> <div class="has-text-centered">
<div class="box" style="display: inline-block;"> <div class="box" style="display: inline-block; padding: 0px;">
<canvas id="inline-viewer" style="width: 500px;"></canvas> <canvas id="inline-viewer" style="width: 500px;"></canvas>
</div> </div>
</div> </div>
@@ -102,41 +160,74 @@
<div class="grid-column" id="tag-area"> <div class="grid-column" id="tag-area">
</div> </div>
</div> </div>
<button class="button is-primary" style="width: 100%" onclick="expandEntries()">Expand</button>
</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 Instrument</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">
<div class="field has-addons"> <form>
<div class="field has-addons is-justify-content-center">
<span class="control"> <span class="control">
<input type="text" class="input" list="instrument-list" id="add-instrument-name"/> <span class="select">
<select onChange="onChangeTagType()" id="tag-prefix">
<option value="inst">Instrument</option>
<option value="mvmt">Movement</option>
<option value="sect">Section</option>
<option value="no">Number</option>
<option value="page">Page</option>
</select>
</span>
</span>
<span class="control tag-option enable-page">
<span class="select tag-name">
<select id="tag-name-page">
<option value="left">Left</option>
<option value="right">Right</option>
<option value="blank">Add Blank</option>
</select>
</span>
</span>
<span class="control tag-option enable-sect enable-mvmt">
<input type="text" class="input tag-name" id="tag-name" />
</span>
<span class="control tag-option enable-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"> <span class="control tag-option enable-inst enable-mvmt enable-no">
<input type="number" class="input" max="4" min="1" size="3" id="add-instrument-variant"/> <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="addInstrument();">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>
<p>{{ document.upload.name }}</p> <p>{{ document.upload.name }}</p>
<template id="blank-tag">
<div class="grid-tag">
<span class="tag-name"></span>
<div class="tag-handle">
<div class="tag-handle-resize"></div>
</div>
</div>
</template>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@@ -157,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);
@@ -166,7 +258,11 @@
pageNumPending = null, pageNumPending = null,
scale = 1, scale = 1,
canvas = document.getElementById('inline-viewer'), canvas = document.getElementById('inline-viewer'),
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d'),
draggedTag = null,
dragMode = null,
dragOrigStart = null,
dragOrigEnd = null;
/** /**
* Get page info from document, resize canvas accordingly, and render page. * Get page info from document, resize canvas accordingly, and render page.
@@ -251,6 +347,7 @@
pdfDoc = pdfDoc_; pdfDoc = pdfDoc_;
const pageList = document.getElementById('page-list'); const pageList = document.getElementById('page-list');
const tagGrid = document.querySelector('.tag-grid');
pageList.innerHTML = ''; pageList.innerHTML = '';
for (var i=0; i<pdfDoc.numPages; i++) { for (var i=0; i<pdfDoc.numPages; i++) {
let page = i+1; let page = i+1;
@@ -264,10 +361,47 @@
pageList.appendChild(el); pageList.appendChild(el);
} }
tagGrid.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (draggedTag) {
const rect = pageList.getBoundingClientRect();
const y = e.clientY - rect.top;
const page = Math.min(Math.max(Math.floor(y / 25) + 1, 1), pdfDoc.numPages);
if (dragMode === 'resize') {
if (page >= parseInt(draggedTag.dataset.start) && page !== parseInt(draggedTag.dataset.end)) {
draggedTag.dataset.end = page;
updateTag(draggedTag);
resolveOverlaps();
}
} else {
const length = dragOrigEnd - dragOrigStart;
if (page !== parseInt(draggedTag.dataset.start)) {
draggedTag.dataset.start = page;
draggedTag.dataset.end = Math.min(page + length, pdfDoc.numPages);
updateTag(draggedTag);
resolveOverlaps();
}
}
}
});
tagGrid.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedTag) {
draggedTag = null;
dragMode = null;
}
resolveOverlaps();
});
document.getElementById('page-count').textContent = pdfDoc.numPages; document.getElementById('page-count').textContent = pdfDoc.numPages;
// Initial/first page rendering // Initial/first page rendering
renderPage(pageNum); renderPage(pageNum);
setTimeout(() => {
const previewBox = document.querySelector('.box');
tagGrid.style.maxHeight = previewBox.offsetHeight + 'px';
}, 100);
tagArea.innerHTML = ''; tagArea.innerHTML = '';
for (let pageTag of data.pageTags) { for (let pageTag of data.pageTags) {
@@ -276,12 +410,27 @@
dirty = false; dirty = false;
}); });
function closeAddModal() { function showTagModal(callback, tag) {
document.getElementById('add-modal').classList.remove('is-active'); document.getElementById("tag-modal").classList.add("is-active");
document.getElementById('add-instrument-name').value = "";
document.getElementById('add-instrument-variant').value = ""; 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 closeTagModal() {
document.getElementById('tag-modal').classList.remove('is-active');
}
function addNumberedInstrument(tag, e) { function addNumberedInstrument(tag, e) {
let modal = document.getElementById('add-modal'); let modal = document.getElementById('add-modal');
@@ -292,97 +441,207 @@
modal.classList.add('is-active'); modal.classList.add('is-active');
} }
function addInstrument() { function onChangeTagType() {
let name = document.getElementById('add-instrument-name'); const tag_prefix = document.querySelector("#tag-prefix");
let variant = document.getElementById('add-instrument-variant'); const selected = tag_prefix.value;
document.querySelectorAll(".tag-option").forEach((x) => {x.classList.add("is-hidden")});
let inst_name = name.value; document.querySelectorAll(".tag-option input").forEach((x) => {x.value=""});
var tag = null; document.querySelectorAll(".enable-" + selected).forEach((x) => {x.classList.remove("is-hidden")});
for (let key in data.instruments) { tag_prefix.focus();
if (data.instruments[key] == inst_name) {
tag = key;
break;
}
}
if (!tag) {
alert("Unknown tag: " + name);
return;
}
if (variant.value) {
tag += "-" + variant.value;
}
variant.value = '';
name.value = '';
let li = document.createElement('li');
li.classList.add("is-clickable");
li.addEventListener('click', () => assignInstrument(tag, li));
li.innerHTML = get_instrument(tag);
document.getElementById('unassigned-area').appendChild(li);
addTag(tag, pageNum, pageNum);
closeAddModal();
} }
function assignInstrument(tag, el) { function confirmTagModal() {
const selected = document.querySelector("#tag-prefix").value;
let name = document.querySelector("#tag-name").value;
const n = document.querySelector("#tag-number").value;
switch (selected) {
case "no":
if (!n) throw new Error("Missing number");
case "page":
name = document.querySelector("#tag-name-page").value;
case "inst":
name = document.querySelector("#tag-name-inst").value;
for (let key in data.instruments) {
if (data.instruments[key] == name) {
name = key;
break;
}
}
}
name = name.replace(" ", "_");
const prefix = (selected == "inst") ? "" : selected + ":";
const tag = (n) ? prefix + name + "-" + n : prefix + name;
closeTagModal();
tagModalCallback(tag);
}
function createNewTag(tag) {
addTag(tag, pageNum, pageNum);
dirty = true;
}
function assignTag(tag, el) {
addTag(tag, pageNum, pageNum); addTag(tag, pageNum, pageNum);
//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) {
console.log("addTag", tag, start, end);
const el = document.createElement('div');
el.className = 'grid-tag'; //console.log("addTag", tag, start, end);
el.dataset.tag = tag; const fragment = document.importNode(document.querySelector("#blank-tag").content, true);
const el = fragment.querySelector("div");
const tagName = el.querySelector(".tag-name");
tagName.innerHTML=get_tag_name(tag);
tagName.addEventListener('dblclick', editTag);
//el.className = 'grid-tag';
el.dataset.start = start; el.dataset.start = start;
el.dataset.end = end; el.dataset.end = end;
el.dataset.tag = tag;
let setStart = document.createElement('span'); const parts = parseTag(tag)
setStart.className = "icon is-action"; el.classList.add("tag-type-" + parts.prefix);
setStart.innerHTML = '<span class="material-symbols-outlined" title="Set start page">vertical_align_top</span>';
setStart.addEventListener('click', () => setTagStart(el));
el.appendChild(setStart);
let label = document.createElement('span');
let name = document.createElement('b'); const handle = el.querySelector('.tag-handle');
name.innerHTML = get_instrument(tag); handle.draggable = true;
label.appendChild(name); handle.addEventListener('dragstart', (e) => {
el.appendChild(label); draggedTag = el;
dragMode = 'move';
let del = document.createElement('span'); dragOrigStart = parseInt(el.dataset.start);
del.className = "icon is-action"; dragOrigEnd = parseInt(el.dataset.end);
del.innerHTML = '<span class="material-symbols-outlined" title="Remove this tag">delete</span>'; el.style.zIndex = 1000;
del.addEventListener('click', () => { e.dataTransfer.effectAllowed = 'move';
el.remove(); const blankImg = new Image();
dirty=true; blankImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
e.dataTransfer.setDragImage(blankImg, 0, 0);
});
handle.addEventListener('dragend', () => {
if (draggedTag === el && dragMode === 'move') {
el.dataset.start = dragOrigStart;
el.dataset.end = dragOrigEnd;
updateTag(el);
el.remove();
dirty = true;
} else if (draggedTag === el) {
el.dataset.start = dragOrigStart;
el.dataset.end = dragOrigEnd;
updateTag(el);
el.style.zIndex = '';
}
resolveOverlaps();
draggedTag = null;
dragMode = null;
}); });
el.appendChild(del)
let setEnd = document.createElement('span');
setEnd.className = "icon is-action";
setEnd.innerHTML = '<span class="material-symbols-outlined" title="Set end page">vertical_align_bottom</span>';
setEnd.addEventListener('click', () => setTagEnd(el));
el.appendChild(setEnd);
const resize = el.querySelector('.tag-handle-resize');
resize.draggable = true;
resize.addEventListener('dragstart', (e) => {
draggedTag = el;
dragMode = 'resize';
dragOrigEnd = parseInt(el.dataset.end);
el.style.zIndex = 1000;
e.dataTransfer.effectAllowed = 'move';
const blankImg2 = new Image();
blankImg2.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
e.dataTransfer.setDragImage(blankImg2, 0, 0);
e.stopPropagation();
});
resize.addEventListener('dragend', () => {
if (draggedTag === el) {
el.dataset.end = dragOrigEnd;
updateTag(el);
el.style.zIndex = '';
}
resolveOverlaps();
draggedTag = null;
dragMode = null;
});
updateTag(el); updateTag(el);
tagArea.appendChild(el); tagArea.appendChild(el);
resolveOverlaps();
} }
function updateTag(tag) { function updateTag(tag) {
let start = tag.dataset.start; let start = tag.dataset.start;
let end = tag.dataset.end; let end = tag.dataset.end;
let span = end-start+1; let span = end-start+1;
let height = span * 25 + (span-1) * 5; let height = span * 25 + 1;
let top = (start-1) * 30; let top = (start-1) * 25;
tag.style.height = height + 'px'; tag.style.height = height + 'px';
tag.style.marginTop = top + 'px'; tag.style.marginTop = top + 'px';
dirty = true; dirty = true;
const name = tag.querySelector('.tag-name');
const handle = tag.querySelector('.tag-handle');
if (name) {
if (span >= 3) {
name.style.position = 'absolute';
name.style.top = '4px';
name.style.right = '20px';
name.style.writingMode = 'vertical-rl';
tag.style.justifyContent = 'flex-end';
if (handle) handle.style.marginLeft = 'auto';
} else {
name.style.position = '';
name.style.top = '';
name.style.right = '';
name.style.writingMode = '';
tag.style.justifyContent = '';
if (handle) handle.style.marginLeft = '';
}
}
}
function resolveOverlaps() {
const tags = Array.from(tagArea.children);
tags.sort((a, b) => parseInt(a.dataset.start) - parseInt(b.dataset.start));
const groups = [];
let current = [tags[0]];
for (let i = 1; i < tags.length; i++) {
const prevEnd = parseInt(tags[i-1].dataset.end);
const currStart = parseInt(tags[i].dataset.start);
if (currStart <= prevEnd) {
current.push(tags[i]);
} else {
groups.push(current);
current = [tags[i]];
}
}
if (current.length) groups.push(current);
for (let group of groups) {
group.sort((a, b) => {
const spanA = parseInt(a.dataset.end) - parseInt(a.dataset.start);
const spanB = parseInt(b.dataset.end) - parseInt(b.dataset.start);
return spanB - spanA;
});
const n = group.length;
group.forEach((tag, i) => {
tag.style.minWidth = (120 + (n - 1 - i) * 70) + 'px';
tag.style.zIndex = i;
});
}
} }
function setTagStart(el) { function setTagStart(el) {
@@ -404,6 +663,10 @@
} }
function expandEntries() { function expandEntries() {
const answer = confirm("This will expand all tags to fill up available pages");
if (!answer) return;
const entries = Array.from(tagArea.children); const entries = Array.from(tagArea.children);
entries.sort((a, b) => a.dataset.start-b.dataset.start); entries.sort((a, b) => a.dataset.start-b.dataset.start);
const c = entries.length; const c = entries.length;
@@ -416,6 +679,33 @@
updateTag(entries[c-1]); updateTag(entries[c-1]);
} }
const body = document.querySelector("body");
function keymap(e) {
if (e.target != body) return;
switch(e.key) {
case "ArrowRight":
case "l":
case "ArrowDown":
case "j":
onNextPage();
break;
case "ArrowLeft":
case "h":
case "ArrowUp":
case "k":
onPrevPage();
break;
case "n":
showTagModal(createNewTag);
break;
default:
return;
}
e.preventDefault();
}
body.addEventListener("keydown", keymap)
function saveTags() { function saveTags() {
const pageTags = []; const pageTags = [];
for (let pageTag of tagArea.children ) { for (let pageTag of tagArea.children ) {
@@ -444,13 +734,43 @@
dirty = false; dirty = false;
} }
function get_instrument(s) { function parseTag(tag) {
let parts = s.split('-'); const result = {
let instrument = data.instruments[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) {
@@ -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');
@@ -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)
} }
@@ -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">
@@ -47,4 +50,4 @@ function closeDownloadModal() {
document.getElementById("download-modal").classList.remove('is-active'); document.getElementById("download-modal").classList.remove('is-active');
} }
</script> </script>
@@ -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 %}
@@ -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"> </p>
<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>
+1 -1
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):
+5
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(),
+77 -14
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
@@ -35,7 +42,6 @@ class ProjectItemListView(ProjectMixin, ListView):
model = models.ProjectItem model = models.ProjectItem
def post(self, request: HttpRequest, **kwargs): def post(self, request: HttpRequest, **kwargs):
project_works = self.project.works.all() project_works = self.project.works.all()
print(request.POST) print(request.POST)
@@ -86,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"))[
@@ -200,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
@@ -223,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:
@@ -282,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
@@ -597,8 +649,19 @@ class DocumentAnnotateView(DocumentMixin, DetailView):
collection=data["collection"].pk, collection=data["collection"].pk,
pk=data["document"].pk, pk=data["document"].pk,
) )
data["tag_colours"] = [
("page", "danger"),
("no", "link"),
("mvmt", "link"),
("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