Compare commits

..

3 Commits

Author SHA1 Message Date
0bf7841345 Adding icons to sync buttons 2026-06-19 22:35:42 +10:00
965573c03b Merged from master 2026-06-19 22:29:44 +10:00
b5d8d2d775 Basic web sync working 2026-06-19 22:27:03 +10:00
31 changed files with 511 additions and 919 deletions

View File

@ -1,10 +1,8 @@
FROM alpine:latest FROM alpine:latest
ARG VERSION=0.8.4
ENV TARGET=/opt/polyphonic ENV TARGET=/opt/polyphonic
#ENV RELEASE=polyphonic-${VERSION}-py3-none-any.whl ENV RELEASE=polyphonic-0.8.4-py3-none-any.whl
ENV RELEASE=git+https://gitea.tfconsulting.com.au/projects/polyphonic.git@${VERSION} #ENV RELEASE=git+https://gitea.tfconsulting.com.au/projects/polyphonic.git
RUN apk add --no-cache python3 py3-pip git ghostscript sqlite RUN apk add --no-cache python3 py3-pip git ghostscript sqlite
@ -13,7 +11,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,9 +1,5 @@
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
@ -43,18 +39,7 @@ 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,4 +12,3 @@ 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 = {"event_date": forms.DateTimeInput()} # widgets = {
# 'event_date': forms.DateTimeInput(attrs={'type': 'date'})
# }
modules = forms.MultipleChoiceField( modules = forms.MultipleChoiceField(
choices=[ choices=[(x, x.title()) for x in models.settings.POLYPHONIC_MODULES],
(x, x.replace(".", " ").title()) for x in models.settings.POLYPHONIC_MODULES
],
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False, required=False,
) )

View File

@ -192,9 +192,8 @@ class Project(models.Model):
class Module(models.Model): class Module(models.Model):
"""Enable modules on a oriject""" """Enable modules on a oriject"""
name = models.CharField( name = models.SlugField(
max_length=100, max_length=20, choices=[(x, x.title()) for x in settings.POLYPHONIC_MODULES]
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

@ -32,11 +32,10 @@
.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: "*";

View File

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 258 B

View File

@ -27,3 +27,30 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
async function background_api(el) {
console.log(el.dataset["api"]);
let text = el.innerHTML;
el.disabled = true;
el.innerHTML = "Running..."
const response = await fetch(el.dataset.api);
console.log(response);
el.innerHTML = text;
if(!response.ok) {
el.disabled = false;
throw new Error(`Response: ${response.status}`);
}
if (el.dataset.success) {
text = el.dataset.success;
}
el.innerHTML = text;
const data = await response.json();
console.log(data);
}

View File

@ -1,15 +1,18 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="light"> <html lang="en">
<head> <head>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="{% static 'images/icon.png' %}" /> <link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" />
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
<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,16 +36,22 @@ Contacts:
{% include 'interface/project_items.html' %} {% include 'interface/project_items.html' %}
{% if request.is_admin %} {% if request.is_admin %}
<div class="box"> <div class="">
<h3 class="subtitle">Admin Details</h3> <div class="card">
<ul> <header class="card header">
<li><a href="{{ ensemble_link }}">Ensemble Sharing Link</a></li> <p class="card-header-title">Admin Details</p>
<li><a href="{% url 'project_create' ensemble.pk %}">Add a new project</a></li> </header>
</ul> <div class="card-content">
<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 class="has-text-right is-size-6"> <div>
<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,11 +43,8 @@
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="hero mx-auto my-4"> <div class="hero">
<div class="hero-body has-text-centered"> You don't currently have access to any ensembles - ask your administrator for a link.
<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 'polyphonic.library' in modules %} {% if '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 content"> <div class="block">
{% if project.description %} {% if project.description %}
<p class="content">{{ project.description|markdown }}</p> <p class="content">{{ project.description|markdown }}</p>
{% else %} {% else %}
@ -40,36 +40,40 @@
{% if project.owner %} {% if project.owner %}
<div class="block"> <div class="block">
{% if project.owner.email %} {% if project.owner.email %}
The project owner is <strong><a href="mailto:{{ project.owner.email }}">{{ project.owner }}</a></strong>. The project owner is <a href="mailto:{{ project.owner.email }}">{{ project.owner }}</a>
{% else %} {% else %}
The project owner is <strong>{{ project.owner }}</strong>. The project owner is {{ project.owner }}.
{% 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 'polyphonic.submission' in modules %} {% if 'submission' in modules %}
<div class="column">
{% include 'submissions/project_detail.html' %} {% include 'submissions/project_detail.html' %}
{% endif %}
</div> </div>
{% 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>
{% 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,8 +12,7 @@
<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: 150px; overflow: hidden"> <div class="card-content" style="height: 100px; 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>
@ -27,8 +26,8 @@
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="hero mx-auto my-4"> <div class="hero">
<div class="hero-body has-text-centered"> <div class="hero-body">
<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(str(value)) return os.path.basename(value)
register.filter("basename", basename) register.filter("basename", basename)

View File

@ -14,6 +14,13 @@ def sync_work(work: Work):
logger.info("Syncing '%s' from %r", work.name, folder_id) logger.info("Syncing '%s' from %r", work.name, folder_id)
results = {
"folder_id": folder_id,
"added": [],
"skipped": [],
"missing": [],
}
storage = UserStorage.objects.get(name="gdrive").instance() storage = UserStorage.objects.get(name="gdrive").instance()
existing = set( existing = set(
@ -27,6 +34,8 @@ def sync_work(work: Work):
_, files = storage.listdir(folder_id) _, files = storage.listdir(folder_id)
logger.debug("Remote files: %r", files) logger.debug("Remote files: %r", files)
results["files"] = len(files)
for file in files: for file in files:
if file.id in existing: if file.id in existing:
logger.debug("%30s: Skipping existing (%s)", file.name, file.id) logger.debug("%30s: Skipping existing (%s)", file.name, file.id)
@ -35,22 +44,29 @@ def sync_work(work: Work):
if not file.name.lower().endswith(".pdf"): if not file.name.lower().endswith(".pdf"):
logger.debug("%40s: Not a PDF", file.name) logger.debug("%40s: Not a PDF", file.name)
results["skipped"].append({"file": file.name, "file_id": file.id})
continue continue
logger.info("%40s: Adding", file.name) results["added"].append({"file": file.name, "file_id": file.id})
doc = work.docs.create(upload=f"gdrive:{file}", doctype=Document.DOCTYPE_PDF) doc = work.docs.create(upload=f"gdrive:{file}", doctype=Document.DOCTYPE_PDF)
doc.auto_tag() doc.auto_tag()
for uri in existing: for uri in existing:
results["missing"].append({"uri": uri})
logger.warning("Local entry not in folder: %s", uri) logger.warning("Local entry not in folder: %s", uri)
return results
def sync_partial_collection(collection: Collection, sync_existing: bool = True): def sync_partial_collection(collection: Collection, sync_existing: bool = True):
works = Work.objects.filter(collection=collection, meta_info__name="folderid") works = Work.objects.filter(collection=collection, meta_info__name="folderid")
result = []
for work in works: for work in works:
sync_work(work) result.append(sync_work(work))
return result
def sync_collection(collection: Collection, sync_existing: bool = False): def sync_collection(collection: Collection, sync_existing: bool = False):
@ -69,6 +85,12 @@ def sync_collection(collection: Collection, sync_existing: bool = False):
storage = collection.storage.instance() storage = collection.storage.instance()
folders, _ = storage.listdir(folder) folders, _ = storage.listdir(folder)
results = {
"folders": len(folders),
"added": [],
"missing": [],
}
for folder in folders: for folder in folders:
if folder[0] == "_": if folder[0] == "_":
continue continue
@ -84,7 +106,11 @@ def sync_collection(collection: Collection, sync_existing: bool = False):
work = Work(name=folder.name, collection=collection) work = Work(name=folder.name, collection=collection)
work.save() work.save()
work.meta_info.create(name="folderid", value=folder.id) work.meta_info.create(name="folderid", value=folder.id)
results["added"].append({"work": work.pk, "folder_id": folder.id})
sync_work(work) sync_work(work)
for folderid, work in existing: for folderid, work in existing.items():
results["missing"].append({"work": work, "folder_id": folderid})
logger.warning("Folder for work %d no longer in drive (%s)", work, folderid) logger.warning("Folder for work %d no longer in drive (%s)", work, folderid)
return results

View File

@ -69,20 +69,26 @@ class GDriveLinkStorage(Storage):
if path == "": if path == "":
return [], [] return [], []
folder = self.parse_resource(path)
url = f"{FILES_API}?q='{folder.id}'+in+parents&key={self.api_key}"
data = self.get_json(url, folder)
files = [] files = []
folders = [] folders = []
for x in data["files"]:
if x["mimeType"] == "application/vnd.google-apps.folder":
# folders.append(f"{x['id']}/{x['name']}")
folders.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
else:
# files.append(f"{x['id']}/{x['name']}")
files.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
return folders, files folder = self.parse_resource(path)
url = f"{FILES_API}?q='{folder.id}'+in+parents&key={self.api_key}"
data = self.get_json(url, folder)
while True:
for x in data["files"]:
if x["mimeType"] == "application/vnd.google-apps.folder":
folders.append(
DriveObject(x["id"], x.get("resourceKey"), x["name"])
)
else:
files.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
token = data.get("nextPageToken")
if token is None:
return folders, files
data = self.get_json(f"{url}&pageToken={token}", folder)
def get_meta(self, name): def get_meta(self, name):
file_resource = self.parse_resource(name) file_resource = self.parse_resource(name)

View File

@ -292,11 +292,7 @@ class Work(models.Model):
@property @property
def digital_parts(self): def digital_parts(self):
sections = [ sections = [(s.tag, s.name) for s in Section.objects.filter(doc__work=self.pk)]
(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()
@ -370,8 +366,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()
@ -438,7 +434,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.tag) self.sections.get_or_create(tag=inst.abbreviate())
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
self.upload.delete(save=False) self.upload.delete(save=False)

View File

@ -1,15 +1,13 @@
from dataclasses import dataclass from collections import namedtuple
import re import re
GENERAL = """
TAG_PREFIXES = { mvmt Movement
"mvmt": "Movement", ex Excerpt
"ex": "Excerpt", sec Section
"sect": "Section", pce Piece
"name": "Name", no Number
"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
@ -161,110 +159,71 @@ xyl Xylophone
zith Zither zith Zither
""" """
MUSIC_TAG = re.compile(r"((?P<prefix>\w+):)?(?P<name>.*?)(\-(?P<number>[0-9]+))?")
MUSIC_TAGS = [] MUSIC_TAGS = []
TAG_ALIASES = {} GENERAL_TAGS = set()
# GENERAL_TAGS = set() for i, abbreviations in enumerate((GENERAL, INSTRUMENTS)):
for line in INSTRUMENTS.split("\n"): for line in abbreviations.split("\n"):
parts = line.strip().split(maxsplit=1) parts = line.strip().split(maxsplit=1)
if len(parts) < 2: if len(parts) < 2:
continue continue
name, _, _ = parts[1].partition("(") name, _, _ = parts[1].partition("(")
MUSIC_TAGS.append((parts[0], name.strip())) MUSIC_TAGS.append((parts[0], name))
# if i == 0: if i == 0:
# GENERAL_TAGS.add(parts[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))
def slug(s): class MusicTag(namedtuple("MusicTag", ("name", "variant"), defaults=[None])):
"""
>>> 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', number=1, prefix='') MusicTag(name='Violin', variant='1')
>>> MusicTag.from_tag('db') >>> MusicTag.from_tag('db')
MusicTag(name='Double Bass', number=None, prefix='') MusicTag(name='Double Bass', variant=None)
>>> MusicTag.from_tag('Jaws Harp') >>> MusicTag.from_tag('Jaws Harp')
MusicTag(name='Jaws Harp', number=None, prefix='') MusicTag(name='Jaws Harp', variant=None)
>>> MusicTag.from_tag('mvmt:Largo-2') >>> MusicTag.from_tag('mvmt-2')
MusicTag(name='Largo', number=2, prefix='mvmt') MusicTag(name='Movement', variant='2')
>>> MusicTag.from_tag('name:A2') >>> MusicTag.from_tag('pce-A2')
MusicTag(name='A2', number=None, prefix='name') MusicTag(name='Piece', variant='A2')
>>> 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)
match = MUSIC_TAG.fullmatch(tag) if variant:
return cls(name, variant)
return cls(name, None)
if match is None: @property
raise ValueError("Not a valid tag") def tag(self):
lc = self.name.lower()
result = match.groupdict() return MUSIC_TAG_BY_NAME.get(lc, lc)
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('A3', prefix="name").is_general >>> MusicTag('Piece', 'A3').is_general
True True
>>> MusicTag('Violin', 2).is_general >>> MusicTag('Violin', 2).is_general
False False
""" """
return self.prefix in TAG_PREFIXES return self.tag in GENERAL_TAGS
@property def abbreviate(self):
def tag(self):
""" """
>>> MusicTag('Violin', 1).tag >>> MusicTag('Violin', 1).abbreviate()
'vn-1' 'vn-1'
>>> MusicTag('Double Bass').tag >>> MusicTag('Double Bass').abbreviate()
'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'
""" """
parts = [self.prefix] tag = MUSIC_TAG_BY_NAME.get(self.name.lower())
parts.append(MUSIC_TAG_BY_NAME.get(self.name.lower(), slug(self.name))) if self.variant:
if self.number: tag = f"{tag}-{self.variant}"
parts.extend(["-", str(self.number)]) return tag
return "".join(parts)
def __str__(self): def __str__(self):
""" """
@ -272,17 +231,15 @@ class MusicTag:
'Violin 1' 'Violin 1'
>>> str(MusicTag('Double Bass')) >>> str(MusicTag('Double Bass'))
'Double Bass' 'Double Bass'
>>> str(MusicTag('Unknown Instrument'))
'Unknown Instrument'
""" """
if self.number: if self.variant:
return f"{self.name} {self.number}" return f"{self.name} {self.variant}"
return self.name return self.name
PATTERNS = [ PATTERNS = [
re.compile(r"(?P<inst>[A-Za-z]+)[_\- ]*(?P<number>\d+)"), re.compile(r"(?P<inst>[A-Za-z]+)[_\- ]*(?P<ord>\d+)"),
re.compile(r"(?P<number>\d+)(st|nd|rd|th)[_\- ]*(?P<inst>[A-Za-z]+)"), re.compile(r"(?P<ord>\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]+)()"),
] ]
@ -290,33 +247,34 @@ PATTERNS = [
def auto_tag(filename): def auto_tag(filename):
""" """
>>> auto_tag('Ode to Joy - Violin 1.pdf').tag >>> auto_tag('Ode to Joy - Violin 1.pdf')
'vn-1' MusicTag(name='Violin', variant=1)
>>> auto_tag('Ode to Joy_Cello.pdf').tag >>> auto_tag('Ode to Joy_Cello.pdf')
'vc' MusicTag(name='Cello', variant=None)
>>> auto_tag('Ode to Joy violin - 1.pdf').tag >>> auto_tag('Ode to Joy violin - 1.pdf')
'vn-1' MusicTag(name='Violin', variant=1)
>>> auto_tag('Ode to Joy - vla.pdf').tag >>> auto_tag('Ode to Joy - vla.pdf')
'va' MusicTag(name='Viola', variant=None)
>>> auto_tag('Ode to Joy - fl-2 (piccolo).pdf').tag >>> auto_tag('Ode to Joy - fl-2 (piccolo).pdf')
'fl-2' MusicTag(name='Flute', variant=2)
>>> auto_tag('1st Violin - Ode to Joy.pdf').tag >>> auto_tag('1st Violin - Ode to Joy.pdf')
'vn-1' MusicTag(name='Violin', variant=1)
>>> auto_tag('Ode to Joy - 2nd Violin.pdf').tag >>> auto_tag('Ode to Joy - 2nd Violin.pdf')
'vn-2' MusicTag(name='Violin', variant=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:
number = int(m["number"]) ordinal = int(m["ord"])
except IndexError: except IndexError:
number = None ordinal = None
if inst in MUSIC_TAG_BY_NAME: if inst in MUSIC_TAG_BY_NAME:
return MusicTag(inst.title(), number) return MusicTag(inst.title(), ordinal)
if inst in MUSIC_NAME_BY_TAG: if inst in MUSIC_NAME_BY_TAG:
return MusicTag(MUSIC_NAME_BY_TAG[inst], number) return MusicTag(MUSIC_NAME_BY_TAG[inst], ordinal)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -14,98 +14,42 @@
{% 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: 0; margin-left: 1em;
margin-right: 0; margin-right: 1em;
} }
.grid-page { .grid-page {
height: 25px; height: 25px;
border: none; margin-bottom: 5px;
border-top: 1px solid #DDD; border: 1px solid #999;
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(--bulma-link-light);*/ background-color: var(--primary);
color: var(--bulma-link); color: white;
background-color: #EEE;
border-radius: 0px 6px 6px 0px;
} }
.grid-tag { .grid-tag {
background: #fff; border: 2px solid var(--primary);
border: 1px solid #DDD; background-color: rgba(240, 240, 240, 0.5);
border-radius: 0px 4px 4px 0px; border-radius: 5px;
border-left: none; padding-left: 1em;
padding-left: 0.5em; padding-right: 1em;
min-width: 100px; margin-bottom: 5px;
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: top; align-items: start;
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;
@ -120,34 +64,32 @@
{% 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" id="area-left"> <div class="column is-narrow">
<h3>Page control</h3> <div class="columns">
<div class="columns"> <div class="column is-narrow">
<div class="column is-narrow"> <button class="button is-small is-primary" id="prev">&lt;</button>
<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>
<h3>Quick Tags</h3> <div class="column">
<ul id="unassigned-area"> <span id="page-num">-</span> / <span id="page-count">-</span>
{% 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; padding: 0px;"> <div class="box" style="display: inline-block;">
<canvas id="inline-viewer" style="width: 500px;"></canvas> <canvas id="inline-viewer" style="width: 500px;"></canvas>
</div> </div>
</div> </div>
@ -160,74 +102,41 @@
<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="tag-modal"> <div class="modal" id="add-modal">
<div class="modal-background" onclick="closeTagModal()"></div> <div class="modal-background" onclick="closeAddModal()"></div>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">Tag Editor</p> <p class="modal-card-title">Add Instrument</p>
<button class="delete" aria-label="close" onclick="closeTagModal()"></button> <button class="delete" aria-label="close" onclick="closeAddModal()"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<form> <div class="field has-addons">
<div class="field has-addons is-justify-content-center">
<span class="control"> <span class="control">
<span class="select"> <input type="text" class="input" list="instrument-list" id="add-instrument-name"/>
<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.instrumentNames %} {% for inst in json_data.instruments.values %}
<option value="{{inst}}"/> <option value="{{inst}}"/>
{% endfor %} {% endfor %}
</datalist> </datalist>
</span> </span>
<span class="control tag-option enable-inst enable-mvmt enable-no"> <span class="control">
<input type="number" class="input tag-number" min="1" size="3" id="tag-number"/> <input type="number" class="input" max="4" min="1" size="3" id="add-instrument-variant"/>
</span> </span>
<span class="control"> <span class="control">
<button type="submit" class="button is-primary" onclick="event.preventDefault(); confirmTagModal(); ">Save</button> <button class="button is-primary" onclick="addInstrument();">Add</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 %}
@ -248,7 +157,6 @@
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);
@ -258,11 +166,7 @@
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.
@ -347,7 +251,6 @@
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;
@ -361,47 +264,10 @@
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) {
@ -410,25 +276,10 @@
dirty = false; dirty = false;
}); });
function showTagModal(callback, tag) { function closeAddModal() {
document.getElementById("tag-modal").classList.add("is-active"); document.getElementById('add-modal').classList.remove('is-active');
document.getElementById('add-instrument-name').value = "";
tagModalCallback = callback; document.getElementById('add-instrument-variant').value = "";
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) {
@ -441,207 +292,97 @@
modal.classList.add('is-active'); modal.classList.add('is-active');
} }
function onChangeTagType() { function addInstrument() {
const tag_prefix = document.querySelector("#tag-prefix"); let name = document.getElementById('add-instrument-name');
const selected = tag_prefix.value; let variant = document.getElementById('add-instrument-variant');
document.querySelectorAll(".tag-option").forEach((x) => {x.classList.add("is-hidden")});
document.querySelectorAll(".tag-option input").forEach((x) => {x.value=""});
document.querySelectorAll(".enable-" + selected).forEach((x) => {x.classList.remove("is-hidden")});
tag_prefix.focus();
} let inst_name = name.value;
var tag = null;
function confirmTagModal() { for (let key in data.instruments) {
const selected = document.querySelector("#tag-prefix").value; if (data.instruments[key] == inst_name) {
let name = document.querySelector("#tag-name").value; tag = key;
const n = document.querySelector("#tag-number").value; break;
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 + ":"; if (!tag) {
alert("Unknown tag: " + name);
return;
}
const tag = (n) ? prefix + name + "-" + n : prefix + name; if (variant.value) {
tag += "-" + variant.value;
}
closeTagModal(); variant.value = '';
tagModalCallback(tag); 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);
function createNewTag(tag) {
addTag(tag, pageNum, pageNum); addTag(tag, pageNum, pageNum);
dirty = true; closeAddModal();
} }
function assignTag(tag, el) { function assignInstrument(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');
//console.log("addTag", tag, start, end); el.className = 'grid-tag';
const fragment = document.importNode(document.querySelector("#blank-tag").content, true); el.dataset.tag = tag;
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;
const parts = parseTag(tag) let setStart = document.createElement('span');
el.classList.add("tag-type-" + parts.prefix); setStart.className = "icon is-action";
setStart.innerHTML = '<span class="material-symbols-outlined" title="Set start page">vertical_align_top</span>';
setStart.addEventListener('click', () => setTagStart(el));
el.appendChild(setStart);
const handle = el.querySelector('.tag-handle'); let label = document.createElement('span');
handle.draggable = true;
handle.addEventListener('dragstart', (e) => {
draggedTag = el;
dragMode = 'move';
dragOrigStart = parseInt(el.dataset.start);
dragOrigEnd = parseInt(el.dataset.end);
el.style.zIndex = 1000;
e.dataTransfer.effectAllowed = 'move';
const blankImg = new Image();
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;
});
const resize = el.querySelector('.tag-handle-resize'); let name = document.createElement('b');
resize.draggable = true; name.innerHTML = get_instrument(tag);
resize.addEventListener('dragstart', (e) => { label.appendChild(name);
draggedTag = el; el.appendChild(label);
dragMode = 'resize';
dragOrigEnd = parseInt(el.dataset.end); let del = document.createElement('span');
el.style.zIndex = 1000; del.className = "icon is-action";
e.dataTransfer.effectAllowed = 'move'; del.innerHTML = '<span class="material-symbols-outlined" title="Remove this tag">delete</span>';
const blankImg2 = new Image(); del.addEventListener('click', () => {
blankImg2.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; el.remove();
e.dataTransfer.setDragImage(blankImg2, 0, 0); dirty=true;
e.stopPropagation();
});
resize.addEventListener('dragend', () => {
if (draggedTag === el) {
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);
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 + 1; let height = span * 25 + (span-1) * 5;
let top = (start-1) * 25; let top = (start-1) * 30;
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) {
@ -663,10 +404,6 @@
} }
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;
@ -679,33 +416,6 @@
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 ) {
@ -734,43 +444,13 @@
dirty = false; dirty = false;
} }
function parseTag(tag) { function get_instrument(s) {
const result = { let parts = s.split('-');
prefix: "inst", let instrument = data.instruments[parts[0]];
name: "", if (parts.length == 2) {
number: "", return instrument + " " + parts[1];
};
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,10 +2,20 @@
{% load polyphonic %} {% load polyphonic %}
{% block admin %} {% block admin %}
{% if meta.folderid %}
<button class="button is-link" data-api="{% url 'work_sync' work=object.pk %}" data-success="Synced" onclick="background_api(this)">
{% icon "sync" %}
<span>Sync with Drive</span>
</button>
{% endif %}
<a href="{% url 'work_detail' collection=collection.pk pk=object.pk %}" class="button is-link is-light"> <a href="{% url 'work_detail' collection=collection.pk pk=object.pk %}" class="button is-link is-light">
{% icon "backspace" %} {% icon "backspace" %}
<span>Back to work</span> <span>Back to work</span>
</a> </a>
{% endblock %} {% endblock %}
{% block page %} {% block page %}
@ -20,8 +30,8 @@
<div class="m-3"> <div class="m-3">
<p> <p>
{% if meta.folderid %} {% if meta.folderid %}
This work is currently linked to <b>{{ meta.folderid }}</b>.<br/> This work is currently linked to folder <span class="tag is-success">{{ meta.folderid }}</span><br/>
Pasting a new folder link will overwrite this. <em>Pasting a new folder link will overwrite this.</em>
{% else %} {% else %}
There is currently no shared drive folder linked to this work - paste one here to enable syncing. There is currently no shared drive folder linked to this work - paste one here to enable syncing.
{% endif %} {% endif %}
@ -42,4 +52,6 @@ There is currently no shared drive folder linked to this work - paste one here t
{% csrf_token %} {% csrf_token %}
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,6 +2,8 @@
{% 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">
@ -14,7 +16,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}}"/> <option value="{{inst.1}}"/>
{% endfor %} {% endfor %}
</datalist> </datalist>
</span> </span>
@ -33,9 +35,6 @@
</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>
@ -59,7 +58,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%">
@ -85,7 +84,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" %}
@ -115,49 +114,50 @@
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; var inst = document.getElementById("instrument-name").value.toLowerCase();
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 in INSTRUMENTS) { for (let i of INSTRUMENTS) {
if (i.toLowerCase() === inst.toLowerCase()) { if (i[1].toLowerCase() === inst) {
inst = i; inst = i[0];
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:", inst, part); console.log("Part preference:", part);
selectParts(INSTRUMENTS[inst] || [inst.toLowerCase()], part); selectParts(inst, part);
} }
function selectParts(codes, part) { function selectParts(inst, 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++) {
const selection = selections[i]; var result = "-"
for(let pref of preferences) { let options = selections[i].children;
selection.value = pref; for(let j=0; j<options.length; j++) {
if(selection.value == pref) break; let value = options[j].value;
if (value.startsWith(prefix)) {
result = value;
break;
}
if (result==="-" && value.startsWith(inst)) {
result = value;
}
} }
console.log("Selected:", result);
if(!selection.value) selection.value = "-"; selections[i].value = result;
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.open(url, "_blank"); window.location = url;
} }
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 'project_work_list' project.pk %}" class="button is-link"> <a href="{% url 'item_list_append' project.pk %}" class="button is-link">
{% icon "add_circle" %} {% icon "add_circle" %}
<span>Add</span> <span>Add</span>
</a> </a>
@ -13,48 +13,37 @@
{% endblock %} {% endblock %}
{% block page %} {% block page %}
<div class="columns"> <table style="max-width: 600px; margin: 10pt auto;" class="zebra">
<thead>
<div class="column card">
<table style="max-width: 600px; margin: 10pt auto;" class="table is-striped is-narrow">
<thead>
<tr> <tr>
<th>Item</th> <th>Item</th>
<th>Time</th> <th>Time</th>
<th/> <th/>
</tr> </tr>
</thead> </thead>
<tbody id="work-list"> <tbody id="work-list">
{% for item in object_list %} {% for item in object_list %}
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}"> <tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
<td>{{ item.work.name }}</td> <td>{{ item.work.name }}</td>
<td style="min-width: 100px;">{% firstof item.work.duration '-:--' %}</td> <td>{{ item.work.duration }}</td>
<td style="text-align: center;"> <td style="text-align: center;">
<span class="clickable is-clickable" title="Remove" onClick="removeItem({{ item.pk }})"> <span class="clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)">
{% icon "delete" %} {% icon "arrow_upward" %}
</span> </span>
<span class="clickable drag-handle cursor-drag" title="Reorder"> <span class="clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)">
{% icon "reorder" %} {% icon "arrow_downward" %}
</span> </span>
</td> <span class="clickable" title="Remove" onClick="moveItem({{ item.pk }}, 0)">
{% icon "delete" %}
</span>
</td>
</tr> </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 %}
@ -62,82 +51,36 @@
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 reorderItems() { function moveItem(pk, dir) {
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 (let i = 0; i < items.length; i++) { for(i=0; i<items.length; i++) {
if (items[i].dataset.pk === pk) { if(items[i].dataset.pk === pk) break;
items[i].dataset.order = -1; }
items[i].style = "display: none"; if(i >= items.length) return;
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);
@ -155,7 +98,7 @@ function save() {
}).then((response) => { }).then((response) => {
if (response.ok) { if (response.ok) {
dirty=false; dirty=false;
window.location = "{% url 'project_detail' project=project.pk %}"; window.location = "{% url 'item_list' project=project.pk %}";
} else { } else {
alert("Failed: " + response.statusText) alert("Failed: " + response.statusText)
} }

View File

@ -1,9 +1,6 @@
<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 %}
@ -23,7 +20,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">Available Parts</p> <p class="modal-card-title">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

@ -3,18 +3,21 @@
{% load polyphonic %} {% load polyphonic %}
{% block admin %} {% block admin %}
{% if collection %} {% if collection %}
<button class="button is-link" data-api="{% url 'collection_sync' collection=collection.pk %}" data-success="Synced" onclick="background_api(this)">
{% icon "sync" %}
<span>Sync Collection</span>
</button>
<a href="{% url 'work_add' collection.pk %}" class="button is-link"> <a href="{% url 'work_add' collection.pk %}" class="button is-link">
{% icon "add_notes" %} {% icon "add_notes" %}
<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 %}
@ -36,7 +39,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 }}&sort={{ sort }}">{{ letter }}</a>&nbsp; <a href="?start={{ letter }}">{{ letter }}</a>&nbsp;
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@ -44,11 +47,10 @@
<table class="table is-striped is-fullwidth"> <table class="table is-striped is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>Work <span class="icon-text"><a href="?start={{ start }}&sort=name">{{ "list_arrow"|icon }}</a></span></th> <th>Work</th>
<th>Composer <span class="icon-text"><a href="?start={{ start }}&sort=composer">{{ "list_arrow"|icon }}</a></span></th> <th>Composer</th>
<th class="is-hidden-mobile">Edition</th> <th class="is-hidden-mobile">Edition</th>
{% if not collection and not project %}<th class="is-hidden-touch">Collection</th>{% endif %} {% if not collection %}<th class="is-hidden-touch">Collection</th>{% endif %}
{% if project %}<th/>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -59,14 +61,7 @@
</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 and not project %}<td class="is-hidden-touch">{{ work.collection.name }}</td>{% endif %} {% if not collection %}<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>
@ -114,24 +109,3 @@ 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,10 +1,27 @@
<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="_blank">{{ name }}</a> <a class="tag is-info" href="{% url 'work_download' work.collection_id work.pk %}?tag={{ tag }}" target="polyphonic_parts">{{ name }}</a>
{% endfor %} {% endfor %}
</div> </div>
<p class="has-text-right"> <!--
<a href="{% url 'work_detail' work.collection_id work.pk %}">Full details for {{ work.name }}</a> <h3 class="subtitle">Files</h3>
<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 polyphonic.library import music_tags from library import music_tags
def load_tests(loader, tests, ignore): def load_tests(loader, tests, ignore):

View File

@ -27,11 +27,6 @@ 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(),
@ -131,4 +126,14 @@ urlpatterns = [
api.CollectionImportView.as_view(), api.CollectionImportView.as_view(),
name="collection_import", name="collection_import",
), ),
path(
"api/collections/<int:collection>/sync",
api.CollectionSyncView.as_view(),
name="collection_sync",
),
path(
"api/works/<int:work>/sync",
api.WorkSyncView.as_view(),
name="work_sync",
),
] ]

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, HttpResponse from django.http import HttpRequest
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,7 +11,6 @@ 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
@ -21,13 +20,7 @@ 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 ( from polyphonic.library.music_tags import MUSIC_TAGS, MusicTag
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
@ -42,6 +35,7 @@ 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)
@ -92,7 +86,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"] = TAG_ALIASES data["instruments"] = MUSIC_TAGS
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"))[
@ -206,8 +200,13 @@ 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, ctx = self.get_filtered_queryset() qs = self.get_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
@ -224,27 +223,9 @@ class WorkListView(CollectionMixin, TemplateView):
def get_collections(self): def get_collections(self):
raise NotImplementedError raise NotImplementedError
def get_filtered_queryset(self): def get_queryset(self):
works = self.get_works() works = self.get_works()
ctx = {} return works.order_by("name", "composer", "edition", "pk").distinct()
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:
@ -301,39 +282,6 @@ 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
@ -649,19 +597,8 @@ 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"] = { data["json_data"] = {"pageTags": pages, "instruments": dict(MUSIC_TAGS)}
"pageTags": pages,
"instruments": MUSIC_NAME_BY_TAG,
"instrumentNames": list({x[1] for x in MUSIC_TAGS}),
"prefixes": TAG_PREFIXES,
}
return data return data

View File

@ -3,9 +3,12 @@ from polyphonic.interface.views import AuthorizedResourceMixin
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework import generics from rest_framework import generics
from rest_framework.views import APIView
from rest_framework.response import Response
from polyphonic.library.models import Collection, Work, Document, Section, WorkMeta from polyphonic.library.models import Collection, Work, Document, Section, WorkMeta
from polyphonic.library.gdrive import sync_collection, sync_work
import requests import requests
import urllib import urllib
@ -208,3 +211,25 @@ class CollectionImportView(AuthorizedResourceMixin, generics.CreateAPIView):
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(collection_id=self.kwargs["pk"]) serializer.save(collection_id=self.kwargs["pk"])
class WorkSyncView(AuthorizedResourceMixin, APIView):
admin_required = True
def get(self, request, work, format=None):
obj = Work.objects.get(pk=work)
result = sync_work(obj)
return Response(result)
class CollectionSyncView(AuthorizedResourceMixin, APIView):
admin_required = True
def get(self, request, collection, format=None):
obj = Collection.objects.get(pk=collection)
result = sync_collection(obj)
return Response(result)