Compare commits
3 Commits
master
...
background
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bf7841345 | |||
| 965573c03b | |||
| b5d8d2d775 |
@ -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
|
||||||
|
|
||||||
|
|||||||
17
Makefile
17
Makefile
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
@ -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: "*";
|
||||||
|
Before Width: | Height: | Size: 258 B After Width: | Height: | Size: 258 B |
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
<header class="card header">
|
||||||
|
<p class="card-header-title">Admin Details</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ ensemble_link }}">Ensemble Sharing Link</a></li>
|
<li><a href="{{ ensemble_link }}">Ensemble Sharing Link</a></li>
|
||||||
<li><a href="{% url 'project_create' ensemble.pk %}">Add a new project</a></li>
|
<li><a href="{% url 'project_create' ensemble.pk %}">Add a new project</a></li>
|
||||||
</ul>
|
</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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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,37 +40,41 @@
|
|||||||
{% 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if 'library' in modules %}
|
||||||
|
<div class="column is-one-third">
|
||||||
|
{% include 'library/project_detail.html' %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'submission' in modules %}
|
||||||
|
<div class="column">
|
||||||
|
{% include 'submissions/project_detail.html' %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if request.is_admin %}
|
{% if request.is_admin %}
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h3 class="subtitle">Admin Actions</h3>
|
<h3 class="subtitle">Admin Actions</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ project_link }}">Project Link</a></li>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-one-third">
|
|
||||||
{% if 'polyphonic.library' in modules %}
|
|
||||||
{% include 'library/project_detail.html' %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if 'polyphonic.submission' in modules %}
|
|
||||||
{% include 'submissions/project_detail.html' %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'forget_resource' 'project' project.pk %}">Forget this project</a>
|
<a href="{% url 'forget_resource' 'project' project.pk %}">Forget this project</a>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 = []
|
||||||
|
|
||||||
|
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"]:
|
for x in data["files"]:
|
||||||
if x["mimeType"] == "application/vnd.google-apps.folder":
|
if x["mimeType"] == "application/vnd.google-apps.folder":
|
||||||
# folders.append(f"{x['id']}/{x['name']}")
|
folders.append(
|
||||||
folders.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
|
DriveObject(x["id"], x.get("resourceKey"), x["name"])
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# files.append(f"{x['id']}/{x['name']}")
|
|
||||||
files.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
|
files.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
|
||||||
|
|
||||||
|
token = data.get("nextPageToken")
|
||||||
|
if token is None:
|
||||||
return folders, files
|
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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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__":
|
||||||
|
|||||||
@ -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"><</button>
|
<button class="button is-small is-primary" id="prev"><</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column has-text-centered">
|
<div class="column">
|
||||||
<span id="page-num">-</span> / <span id="page-count">-</span>
|
<span id="page-num">-</span> / <span id="page-count">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<button class="button is-small is-primary" id="next">></button>
|
<button class="button is-small is-primary" id="next">></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3>Quick Tags</h3>
|
|
||||||
<ul id="unassigned-area">
|
<ul id="unassigned-area">
|
||||||
{% for tag, inst in document.work.music_tags %}
|
{% for tag, inst in document.work.music_tags %}
|
||||||
<li class="tag is-warning" onclick="assignTag('{{tag}}', this)">{{ inst }}</li>
|
<li>
|
||||||
|
<span class="is-clickable" onclick="assignInstrument('{{tag}}', this)")>{{ inst }}</span>
|
||||||
|
|
||||||
|
<span class="is-clickable" onclick="addNumberedInstrument('{{tag}}', this)">...</span>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Actions</h3>
|
<a onclick="document.getElementById('add-modal').classList.add('is-active')">Add Tag</a>
|
||||||
<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>
|
||||||
<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() {
|
|
||||||
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) {
|
for (let key in data.instruments) {
|
||||||
if (data.instruments[key] == name) {
|
if (data.instruments[key] == inst_name) {
|
||||||
name = key;
|
tag = key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
name = name.replace(" ", "_");
|
|
||||||
|
|
||||||
const prefix = (selected == "inst") ? "" : selected + ":";
|
if (!tag) {
|
||||||
|
alert("Unknown tag: " + name);
|
||||||
const tag = (n) ? prefix + name + "-" + n : prefix + name;
|
return;
|
||||||
|
|
||||||
closeTagModal();
|
|
||||||
tagModalCallback(tag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
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) => {
|
let name = document.createElement('b');
|
||||||
draggedTag = el;
|
name.innerHTML = get_instrument(tag);
|
||||||
dragMode = 'move';
|
label.appendChild(name);
|
||||||
dragOrigStart = parseInt(el.dataset.start);
|
el.appendChild(label);
|
||||||
dragOrigEnd = parseInt(el.dataset.end);
|
|
||||||
el.style.zIndex = 1000;
|
let del = document.createElement('span');
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
del.className = "icon is-action";
|
||||||
const blankImg = new Image();
|
del.innerHTML = '<span class="material-symbols-outlined" title="Remove this tag">delete</span>';
|
||||||
blankImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
del.addEventListener('click', () => {
|
||||||
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();
|
el.remove();
|
||||||
dirty=true;
|
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)
|
||||||
|
|
||||||
const resize = el.querySelector('.tag-handle-resize');
|
let setEnd = document.createElement('span');
|
||||||
resize.draggable = true;
|
setEnd.className = "icon is-action";
|
||||||
resize.addEventListener('dragstart', (e) => {
|
setEnd.innerHTML = '<span class="material-symbols-outlined" title="Set end page">vertical_align_bottom</span>';
|
||||||
draggedTag = el;
|
setEnd.addEventListener('click', () => setTagEnd(el));
|
||||||
dragMode = 'resize';
|
el.appendChild(setEnd);
|
||||||
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 + 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: "",
|
|
||||||
number: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
let parts = tag.split(":");
|
|
||||||
if (parts.length == 2) {
|
if (parts.length == 2) {
|
||||||
result.prefix = parts[0];
|
return instrument + " " + parts[1];
|
||||||
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,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 %}
|
||||||
|
|
||||||
|
|||||||
@ -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)) {
|
||||||
if(!selection.value) selection.value = "-";
|
result = value;
|
||||||
|
}
|
||||||
console.log("Selected:", selection.value);
|
}
|
||||||
|
console.log("Selected:", result);
|
||||||
|
selections[i].value = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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');
|
||||||
|
|||||||
@ -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,10 +13,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<div class="columns">
|
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
|
||||||
|
|
||||||
<div class="column card">
|
|
||||||
<table style="max-width: 600px; margin: 10pt auto;" class="table is-striped is-narrow">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Item</th>
|
<th>Item</th>
|
||||||
@ -28,13 +25,16 @@
|
|||||||
{% 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 class="clickable" title="Remove" onClick="moveItem({{ item.pk }}, 0)">
|
||||||
|
{% icon "delete" %}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -44,17 +44,6 @@
|
|||||||
{% 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,81 +51,35 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
if(i >= items.length) return;
|
||||||
|
|
||||||
|
// check the direction is sensible
|
||||||
|
if (i + dir < 0 || i + dir >= items.length) return;
|
||||||
|
|
||||||
|
if (dir === 0) {
|
||||||
items[i].dataset.order = -1;
|
items[i].dataset.order = -1;
|
||||||
items[i].style = "display: none";
|
items[i].style = "display: none";
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
} else {
|
||||||
workList.insertBefore(dragSrc, target.nextElementSibling);
|
items[i].dataset.order = parseInt(items[i].dataset.order) + dir;
|
||||||
|
items[i+dir].dataset.order = parseInt(items[i+dir].dataset.order) - dir;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
workList.addEventListener('drop', function(e) {
|
items.sort((a, b) => parseInt(a.dataset.order) - parseInt(b.dataset.order))
|
||||||
e.preventDefault();
|
|
||||||
dragSrcNext = null;
|
workList.innerHTML = ""
|
||||||
reorderItems();
|
for(let j=0; j<items.length; j++) {
|
||||||
|
workList.appendChild(items[j]);
|
||||||
|
}
|
||||||
dirty = true;
|
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() {
|
||||||
|
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
<a href="?start={{ letter }}">{{ letter }}</a>
|
||||||
{% 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 %}
|
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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):
|
||||||
|
|||||||
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user