4 Commits
Author SHA1 Message Date
tris 1301d19c08 Switched to material icons 2026-06-16 09:18:21 +10:00
tris ca3effcad1 Fixed my music page 2026-05-29 22:47:33 +10:00
tris 5468f6d3e7 Added handling of gdrive resource_keys - fixes #16 2026-05-27 22:46:08 +10:00
tris e46d8145a7 Install from git 2026-05-27 09:35:15 +10:00
23 changed files with 274 additions and 155 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
FROM alpine:latest FROM alpine:latest
ENV TARGET=/opt/polyphonic ENV TARGET=/opt/polyphonic
ENV RELEASE=polyphonic-0.8.4-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
RUN apk add --no-cache python3 py3-pip git ghostscript sqlite RUN apk add --no-cache python3 py3-pip git ghostscript sqlite
@@ -10,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
+3 -2
View File
@@ -11,7 +11,8 @@
<script src="{% static 'interface/js/interface.js' %}"></script> <script src="{% static 'interface/js/interface.js' %}"></script>
<script src="//unpkg.com/alpinejs" defer></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="//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> <!-- 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>
@@ -23,7 +24,7 @@
<nav class="navbar" role="navigation"> <nav class="navbar" role="navigation">
<div class="navbar-brand has-text-primary"> <div class="navbar-brand has-text-primary">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<span class="icon fancy is-size-2 is-size-4-mobile mx-4"><i class="fas fa-random"></i></span> <span class="icon fancy mx-4"><span class="material-symbols-outlined is-size-1 is-size-3-mobile">groups</span></span>
<span class="fancy is-size-2 is-size-4-mobile">Polyphonic</span> <span class="fancy is-size-2 is-size-4-mobile">Polyphonic</span>
</a> </a>
<span class="navbar-item is-hidden-mobile fancy is-size-5">Musical Ensemble Manager</span> <span class="navbar-item is-hidden-mobile fancy is-size-5">Musical Ensemble Manager</span>
@@ -6,7 +6,7 @@
{% crispy_field field 'class' 'file-input'%} {% crispy_field field 'class' 'file-input'%}
<span class="file-cta"> <span class="file-cta">
<span class="file-icon"> <span class="file-icon">
<i class="fas fa-upload"></i> <span class="material-symbols-outlined">file_upload</span>
</span> </span>
<span class="file-label"> <span class="file-label">
Choose a file… Choose a file…
@@ -1,19 +1,20 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load md2 %} {% load md2 %}
{% load polyphonic %}
{% block admin %} {% block admin %}
<a href="{% url 'project_create' object.slug %}" class="button is-link"> <a href="{% url 'project_create' object.slug %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span> {{ "add_notes"|icon }}
<span>Add project</span> <span>Add project</span>
</a> </a>
{% if inactive %} {% if inactive %}
<a href="?" class="button is-link"> <a href="?" class="button is-link">
<span class="icon"><i class="fas fa-archive"></i></span> {{ "preview_off"|icon }}
<span>Hide old</span> <span>Hide old</span>
</a> </a>
{% else %} {% else %}
<a href="?inactive" class="button is-link"> <a href="?inactive" class="button is-link">
<span class="icon"><i class="fas fa-archive"></i></span> {{ "preview"|icon }}
<span>Show all</span> <span>Show all</span>
</a> </a>
{% endif %} {% endif %}
@@ -5,7 +5,7 @@
{% comment %} {% comment %}
<div class="admin-tools is-pulled-right"> <div class="admin-tools is-pulled-right">
<a class="button is-link" href="{% url 'register' %}"> <a class="button is-link" href="{% url 'register' %}">
<span class="icon"><i class="fas fa-plus-circle"></i></span> {% icon "add_file" %}
<span>Register another</span> <span>Register another</span>
</a> </a>
</div> </div>
@@ -25,10 +25,13 @@
<img src="https://www.gravatar.com/avatar/{{ ensemble.email }}?d=mp" alt="Placeholder image"> <img src="https://www.gravatar.com/avatar/{{ ensemble.email }}?d=mp" alt="Placeholder image">
</figure> </figure>
</div> </div>
<div class="media-content" style="min-height: 60px"> <div class="media-content" style="min-height: 100px">
<a href="{% url 'ensemble_detail' ensemble.slug %}"> <a href="{% url 'ensemble_detail' ensemble.slug %}">
<p class="title is-4">{{ ensemble.name }}</p> <p class="title is-4">{{ ensemble.name }}</p>
</a> </a>
<div class="mt-3">
{{ ensemble.details|markdown }}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -4,11 +4,11 @@
{% block admin %} {% block admin %}
<a href="{% url 'wiki_create' project=project.pk %}" class="button is-link"> <a href="{% url 'wiki_create' project=project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-file"></i></span> {{ "add_notes"|icon }}
<span>Add Page</span> <span>Add Page</span>
</a> </a>
<a href="{% url 'project_edit' project=project.pk %}" class="button is-link"> <a href="{% url 'project_edit' project=project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span> {{ "edit"|icon }}
<span>Edit</span> <span>Edit</span>
</a> </a>
{% endblock %} {% endblock %}
@@ -1,9 +1,10 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load md2 %} {% load md2 %}
{% load polyphonic %}
{% block admin %} {% block admin %}
<a class="button is-link" href="{% url 'resource_create' project=project.pk %}"> <a class="button is-link" href="{% url 'resource_create' project=project.pk %}">
<span class="icon"><i class="fas fa-plus-circle"></i></span> {% icon "add_notes" %}
<span>Add new</span> <span>Add new</span>
</a> </a>
{% endblock %} {% endblock %}
@@ -27,11 +28,11 @@
<div class="card-header-icon"> <div class="card-header-icon">
{% if request.is_admin %} {% if request.is_admin %}
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}" class="icon" title="Upload"> <a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}" title="Upload">
<i class="fas fa-upload"></i> {% icon "upload_file" %}
</a> </a>
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}" class="icon" title="Edit"> <a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}" title="Edit">
<i class="fas fa-edit"></i> {% icon "edit" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
@@ -1,8 +1,9 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
{% block admin %} {% block admin %}
<a href="{% url 'wiki_edit' project=project.pk pk=wikipage.pk %}" class="button is-link"> <a href="{% url 'wiki_edit' project=project.pk pk=wikipage.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span> {{ "edit"|icon }}
<span>Edit</span> <span>Edit</span>
</a> </a>
{% endblock %} {% endblock %}
@@ -1,9 +1,27 @@
from django import template from django import template
from django.utils import timesince from django.utils import timesince
from django.utils.html import format_html
register = template.Library() register = template.Library()
@register.filter("icon", is_safe=True)
def material_icon(value):
return f'<span class="icon"><span class="material-symbols-outlined">{value}</span></span>'
@register.simple_tag
def icon(name, element="span", classes=[]):
classes = ["icon"] + classes
return format_html(
'<{} class="{}"><span class="material-symbols-outlined">{}</span></{}>',
element,
" ".join(classes),
name,
element,
)
def roughtimesince(value): def roughtimesince(value):
return timesince.timesince(value, depth=1) return timesince.timesince(value, depth=1)
+15 -8
View File
@@ -7,7 +7,10 @@ logger = logging.getLogger(__name__)
def sync_work(work: Work): def sync_work(work: Work):
try:
folder_id = work.meta_info.get(name="folderid").value folder_id = work.meta_info.get(name="folderid").value
except WorkMeta.DoesNotExist as err:
raise IndexError("Work not currently linked to a gdrive folder") from err
logger.info("Syncing '%s' from %r", work.name, folder_id) logger.info("Syncing '%s' from %r", work.name, folder_id)
@@ -15,7 +18,7 @@ def sync_work(work: Work):
existing = set( existing = set(
[ [
storage.parse_id(x.partition(":")[2]) storage.parse_resource(x.partition(":")[2]).id
for x in work.docs.values_list("upload", flat=True) for x in work.docs.values_list("upload", flat=True)
] ]
) )
@@ -42,16 +45,19 @@ def sync_work(work: Work):
logger.warning("Local entry not in folder: %s", uri) logger.warning("Local entry not in folder: %s", uri)
def sync_partial_collection(collection: Collection, sync_existing: bool = True):
works = Work.objects.filter(collection=collection, meta_info__name="folderid")
for work in works:
sync_work(work)
def sync_collection(collection: Collection, sync_existing: bool = False): def sync_collection(collection: Collection, sync_existing: bool = False):
logger.info("Syncing '%s'", collection) logger.info("Syncing '%s'", collection)
if not collection.storage.storage.endswith("GDriveLinkStorage"): if not collection.storage.storage.endswith("GDriveLinkStorage"):
raise RuntimeError("Not a gdrive storage") return sync_partial_collection(collection, sync_existing)
try:
folder_id = collection.settings["folder_id"]
except KeyError:
raise KeyError("Missing 'folder_id' in settings")
existing = dict( existing = dict(
WorkMeta.objects.filter( WorkMeta.objects.filter(
@@ -59,8 +65,9 @@ def sync_collection(collection: Collection, sync_existing: bool = False):
).values_list("value", "work_id") ).values_list("value", "work_id")
) )
folder = collection.prefix
storage = collection.storage.instance() storage = collection.storage.instance()
folders, _ = storage.listdir(folder_id) folders, _ = storage.listdir(folder)
for folder in folders: for folder in folders:
if folder[0] == "_": if folder[0] == "_":
+56 -34
View File
@@ -8,19 +8,26 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SHARED_FOLDER = re.compile(r"https://drive.google.com/drive[u0-9\/]+folders/([\w\-]+)") SHARED_FOLDER = re.compile(
SHARED_FILE = re.compile(r"https://drive.google.com/file/d/([\w\-]+)") r"https://drive.google.com/drive[u0-9\/]+folders/([\w\-]+)(\?resourcekey=([\w\-]+))?"
)
SHARED_FILE = re.compile(
r"https://drive.google.com/file/d/([\w\-]+)(\?resourcekey=([\w\-]+))?"
)
FILES_API = "https://www.googleapis.com/drive/v3/files" FILES_API = "https://www.googleapis.com/drive/v3/files"
class DriveObject(namedtuple("DriveObject", ("id", "name"))): class DriveObject(namedtuple("DriveObject", ("id", "key", "name"))):
@classmethod @classmethod
def from_string(cls, s): def from_string(cls, s: str):
return cls(*s.split("/", 1)) resource, _, name = s.partition("/")
id, _, key = resource.partition("#")
return cls(id, key, name)
def __str__(self): def __str__(self):
return f"{self.id}/{self.name}" resource = f"{self.id}#{self.key}" if self.key else self.id
return f"{resource}/{self.name}"
class GDriveLinkStorage(Storage): class GDriveLinkStorage(Storage):
@@ -30,21 +37,28 @@ class GDriveLinkStorage(Storage):
self.api_key = api_key self.api_key = api_key
super().__init__() super().__init__()
def parse_id(self, name): def parse_resource(self, name) -> DriveObject:
parts = name.split("/") return DriveObject.from_string(name)
return parts[0]
def extract_id(self, url, *patterns): def extract_resource(self, url, *patterns) -> DriveObject:
logger.debug("EXTRACT_ID: %r", url) logger.debug("EXTRACT_RESOURCE: %r", url)
for pattern in patterns: for pattern in patterns:
match = pattern.match(url) match = pattern.match(url)
if match: if match:
return match.groups()[0] groups = match.groups()
logger.debug(groups)
if len(groups) == 3:
return DriveObject(groups[0], groups[2], "")
return DriveObject(groups[0], None, "")
raise FileNotFoundError(f"Not a valid url: {url}") raise FileNotFoundError(f"Not a valid url: {url}")
def get_json(self, url): def get_json(self, url, resource: DriveObject):
logger.debug("GET_JSON: %s", url) headers = {}
response = requests.get(url) if resource.key:
headers["X-Goog-Drive-Resource-Keys"] = f"{resource.id}/{resource.key}"
logger.debug("GET_JSON: %s %r", url, headers)
response = requests.get(url, headers=headers)
data = response.json() data = response.json()
logger.debug("Data: %r", data) logger.debug("Data: %r", data)
return data return data
@@ -55,31 +69,36 @@ class GDriveLinkStorage(Storage):
if path == "": if path == "":
return [], [] return [], []
folder_id = self.parse_id(path) folder = self.parse_resource(path)
url = f"{FILES_API}?q='{folder_id}'+in+parents&key={self.api_key}" url = f"{FILES_API}?q='{folder.id}'+in+parents&key={self.api_key}"
data = self.get_json(url) data = self.get_json(url, folder)
files = [] files = []
folders = [] folders = []
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(f"{x['id']}/{x['name']}")
folders.append(DriveObject(x["id"], x["name"])) folders.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
else: else:
# files.append(f"{x['id']}/{x['name']}") # files.append(f"{x['id']}/{x['name']}")
files.append(DriveObject(x["id"], x["name"])) files.append(DriveObject(x["id"], x.get("resourceKey"), x["name"]))
return folders, files return folders, files
def get_meta(self, name): def get_meta(self, name):
file_id = self.parse_id(name) file_resource = self.parse_resource(name)
url = f"{FILES_API}/{file_id}?key={self.api_key}" url = f"{FILES_API}/{file_resource.id}?key={self.api_key}"
return self.get_json(url) return self.get_json(url, file_resource)
def open(self, name, mode="rb"): def open(self, name, mode="rb"):
file_id = self.parse_id(name) resource = self.parse_resource(name)
url = f"{FILES_API}/{file_id}?alt=media&key={self.api_key}" url = f"{FILES_API}/{resource.id}?alt=media&key={self.api_key}"
logger.info("URL: %s", url)
response = requests.get(url, stream=True) headers = {}
if resource.key:
headers["X-Goog-Drive-Resource-Keys"] = f"{resource.id}/{resource.key}"
logger.info("URL: %s [%r]", url, headers)
response = requests.get(url, headers=headers, stream=True)
return GzipFile(name, "rb", 9, response.raw) return GzipFile(name, "rb", 9, response.raw)
def size(self, name): def size(self, name):
@@ -90,25 +109,28 @@ class GDriveLinkStorage(Storage):
def url(self, name): def url(self, name):
logger.debug("URL: %r", name) logger.debug("URL: %r", name)
file_id = self.parse_id(name) resource = self.parse_resource(name)
return f"https://drive.usercontent.google.com/download?export=download&id={file_id}&confirm=yes" uri = f"https://drive.usercontent.google.com/download?export=download&id={resource.id}&confirm=yes"
if resource.key:
uri += f"&resourcekey=${resource.key}"
return uri
def get_folder_id(self, url): def get_folder_id(self, url):
try: try:
return self.extract_id(url, SHARED_FOLDER) return self.extract_resource(url, SHARED_FOLDER)
except FileNotFoundError: except FileNotFoundError:
return None return None
def get_file_id(self, url): def get_file_id(self, url):
try: try:
return self.extract_id(url, SHARED_FILE) return self.extract_resource(url, SHARED_FILE)
except FileNotFoundError: except FileNotFoundError:
return None return None
def import_link(self, url) -> str: def import_link(self, url) -> str:
file_id = self.extract_id(url, SHARED_FILE) file_resource = self.extract_resource(url, SHARED_FILE)
meta = self.get_meta(file_id) meta = self.get_meta(file_resource)
return f"{file_id}/{meta['name']}" return f"{file_resource}/{meta['name']}"
""" """
+4
View File
@@ -19,6 +19,10 @@ cb Double Bass
mall Mallet Percussion mall Mallet Percussion
vln Violin vln Violin
vla Viola vla Viola
kit Drumkit
asax Alto Sax
tsax Tenor Sax
bsax Bari Sax
acc Accordion acc Accordion
afl Alto flute afl Alto flute
@@ -1,4 +1,5 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
{% block page %} {% block page %}
<h3 class="title">Library collections for {% firstof request.user.first_name request.user.username %}</h3> <h3 class="title">Library collections for {% firstof request.user.first_name request.user.username %}</h3>
@@ -10,7 +11,9 @@
<input class="input" name="q" type="text" placeholder="Find a work" value="{{ request.GET.filter }}"/> <input class="input" name="q" type="text" placeholder="Find a work" value="{{ request.GET.filter }}"/>
</div> </div>
<div class="control"> <div class="control">
<a class="button" href="?"><i class="fas fa-times"></i></a> <a class="button" href="?">
{% icon "close" %}
</a>
</div> </div>
</div> </div>
</form> </form>
@@ -47,4 +50,5 @@
<div> <div>
<small>{{ ensemble.ensemble_code }}</small> <small>{{ ensemble.ensemble_code }}</small>
</div> </div>
{% endblock %} {% endblock %}
@@ -1,11 +1,13 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
{% block admin %} {% block admin %}
<a href="#" onclick="saveTags()" class="button is-link"> <a href="#" onclick="saveTags()" class="button is-link">
<span class="icon"><i class="fas fa-save"></i></span> {% icon "save" %}
<span>Save</span> <span>Save</span>
</a> </a>
<a href="{% url 'work_detail' collection=collection.pk pk=object.work_id %}" class="button is-link is-light"> <a href="{% url 'work_detail' collection=collection.pk pk=object.work_id %}" class="button is-link is-light">
{% icon "backspace" %}
<span>Cancel</span> <span>Cancel</span>
</a> </a>
{% endblock %} {% endblock %}
@@ -76,7 +78,11 @@
</div> </div>
<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="is-clickable" onclick="assignInstrument('{{tag}}', this)")>{{ inst }}</li> <li>
<span class="is-clickable" onclick="assignInstrument('{{tag}}', this)")>{{ inst }}</span>
&nbsp;
<span class="is-clickable" onclick="addNumberedInstrument('{{tag}}', this)">...</span>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
<a onclick="document.getElementById('add-modal').classList.add('is-active')">Add Tag</a> <a onclick="document.getElementById('add-modal').classList.add('is-active')">Add Tag</a>
@@ -276,6 +282,16 @@
document.getElementById('add-instrument-variant').value = ""; document.getElementById('add-instrument-variant').value = "";
} }
function addNumberedInstrument(tag, e) {
let modal = document.getElementById('add-modal');
document.getElementById("add-instrument-name").value = data.instruments[tag];
document.getElementById("add-instrument-variant").value = 1;
document.getElementById("add-instrument-variant").focus();
modal.classList.add('is-active');
}
function addInstrument() { function addInstrument() {
let name = document.getElementById('add-instrument-name'); let name = document.getElementById('add-instrument-name');
let variant = document.getElementById('add-instrument-variant'); let variant = document.getElementById('add-instrument-variant');
@@ -326,7 +342,7 @@
let setStart = document.createElement('span'); let setStart = document.createElement('span');
setStart.className = "icon is-action"; setStart.className = "icon is-action";
setStart.innerHTML = '<i class="fas fa-sort-amount-down" title="Set start page"></i>'; setStart.innerHTML = '<span class="material-symbols-outlined" title="Set start page">vertical_align_top</span>';
setStart.addEventListener('click', () => setTagStart(el)); setStart.addEventListener('click', () => setTagStart(el));
el.appendChild(setStart); el.appendChild(setStart);
@@ -336,22 +352,20 @@
let name = document.createElement('b'); let name = document.createElement('b');
name.innerHTML = get_instrument(tag); name.innerHTML = get_instrument(tag);
label.appendChild(name); label.appendChild(name);
el.appendChild(label);
let del = document.createElement('span'); let del = document.createElement('span');
del.className = "icon is-action"; del.className = "icon is-action";
del.innerHTML = '<i class="fas fa-trash-alt" title="Remove this tag"></i>'; del.innerHTML = '<span class="material-symbols-outlined" title="Remove this tag">delete</span>';
del.addEventListener('click', () => { del.addEventListener('click', () => {
el.remove(); el.remove();
dirty=true; dirty=true;
}); });
label.appendChild(del) el.appendChild(del)
el.appendChild(label);
let setEnd = document.createElement('span'); let setEnd = document.createElement('span');
setEnd.className = "icon is-action"; setEnd.className = "icon is-action";
setEnd.innerHTML = '<i class="fas fa-sort-amount-up" title="Set end page"></i>'; setEnd.innerHTML = '<span class="material-symbols-outlined" title="Set end page">vertical_align_bottom</span>';
setEnd.addEventListener('click', () => setTagEnd(el)); setEnd.addEventListener('click', () => setTagEnd(el));
el.appendChild(setEnd); el.appendChild(setEnd);
@@ -1,7 +1,10 @@
{% load path_filters %} {% load path_filters %}
{% load polyphonic %}
<tr> <tr>
<td><a href="{{ doc.upload.url }}" target="_blank"> <td style="white-space: nowrap">
{{ doc.upload.name|basename }}</a></td> <a href="{{ doc.upload.url }}" target="_blank">
{{ doc.upload.name|basename }}</a>
</td>
<td> <td>
{% for section in doc.sections.all %} {% for section in doc.sections.all %}
<a class="tag is-{{ section.bulma_class }}" target="_blank" href="{% url 'part_download' collection.pk section.pk section.filename %}">{{ section.name }}</a> <a class="tag is-{{ section.bulma_class }}" target="_blank" href="{% url 'part_download' collection.pk section.pk section.filename %}">{{ section.name }}</a>
@@ -10,10 +13,13 @@
<td class="has-text-right" style="white-space: nowrap;"> <td class="has-text-right" style="white-space: nowrap;">
{% if request.is_admin %} {% if request.is_admin %}
{% if doc.doctype == 1 %} {% if doc.doctype == 1 %}
<a href="{% url 'document_annotate' collection.pk doc.pk %}"><i class="fas fa-tags" <a href="{% url 'document_annotate' collection.pk doc.pk %}" title="Annotate">
title="Manage Tags"></i></a> {% icon "toc" %}
</a>
{% endif %} {% endif %}
<a href="{% url 'document_delete' collection.pk doc.pk %}"><i class="fas fa-trash-alt" title="Delete Document"></i></a> <a href="{% url 'document_delete' collection.pk doc.pk %}" title="Delete">
{% icon "delete" %}
</a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@@ -1,12 +1,18 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
{% block admin %} {% block admin %}
<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" %}
<span>Back to work</span> <span>Back to work</span>
</a> </a>
{% endblock %} {% endblock %}
{% block page %} {% block page %}
<h2 class="title">
{% icon "add_to_drive" %}
<span>Google Drive - Shared Links
</h2>
<h3 class="subtitle"><a href="{% url 'work_detail' collection.pk object.pk %}">{{ object.name }}</a></h3> <h3 class="subtitle"><a href="{% url 'work_detail' collection.pk object.pk %}">{{ object.name }}</a></h3>
<div class="m-3"> <div class="m-3">
<p>This page lets you link a work to a google drive folder. You can either paste a public link to a folder to enable syncing or a file to add individually</p> <p>This page lets you link a work to a google drive folder. You can either paste a public link to a folder to enable syncing or a file to add individually</p>
@@ -14,25 +20,23 @@
<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>. Pasting a new folder link will overwrite this. This work is currently linked to <b>{{ meta.folderid }}</b>.<br/>
Pasting a new folder link will overwrite this.
{% 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 %}
</p> </p>
</div> </div>
<div> <div class="mt-5">
<form method="post"> <form method="post">
<div class="field"> <div class="field has-addons mx-6">
<div class="control"> <div class="control is-expanded">
<input name="link" class="input is-expanded" type="text" placeholder="Shared link"> <input class="input" name="link" type="text" placeholder="Paste shared link">
</div> </div>
{% for error in form.errors.link %}
<p class="help is-danger">{{ error }}</p>
{% endfor %}
</div>
<div class="field">
<div class="control"> <div class="control">
<button class="button is-info" type="submit">Add</button> <button class="button" type="submit">
Add
</button>
</div> </div>
</div> </div>
{% csrf_token %} {% csrf_token %}
@@ -1,4 +1,5 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
{% block page %} {% block page %}
<form action="" method="post" target="_blank"> <form action="" method="post" target="_blank">
@@ -31,12 +32,6 @@
</span> </span>
</span> </span>
</div> </div>
<span class="control">
<button type="submit" class="button is-primary">
<span class="icon"><i class="fas fa-copy"></i></span>
<span>Get My Parts!</span>
</button>
</span>
</div> </div>
</div> </div>
@@ -45,6 +40,7 @@
<tr> <tr>
<th/> <th/>
<th>Piece</th> <th>Piece</th>
<th>Composer</th>
<th class="is-hidden-mobile">Running time</th> <th class="is-hidden-mobile">Running time</th>
<th>Part</th> <th>Part</th>
<th/> <th/>
@@ -61,11 +57,12 @@
{{ item.work.name }} {{ item.work.name }}
{% endif %} {% endif %}
</td> </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%">
<select name="instruments" id="part-{{ item.work.pk }}" style="width: 100%"> <select name="instrument-selection" id="part-{{ item.work.pk }}" style="width: 100%">
<option value='-'>None</option> <option value='-'>None</option>
{% for tag, name in item.work.digital_parts %} {% for tag, name in item.work.digital_parts %}
<option value='{{ tag }}'>{{ name }}</option> <option value='{{ tag }}'>{{ name }}</option>
@@ -74,8 +71,9 @@
</span> </span>
</td> </td>
<td> <td>
<span class="is-action" onclick="downloadPart({{ item.work.collection_id }}, {{ item.work.pk }})"> <span class="button is-link is-small" onclick="downloadPart({{ item.work.collection_id }}, {{ item.work.pk }})">
<i class="fas fa-download" title="Download Part"></i> {% icon "download" %}
<span>Get</span>
</span> </span>
</td> </td>
</tr> </tr>
@@ -85,13 +83,17 @@
<tr> <tr>
<td/> <td/>
<td/> <td/>
<td>{% firstof running_time "------" %}</td>
<td/> <td/>
<td> <td>{% firstof running_time "------" %}</td>
<!-- <td colspan="2">
<button class="button is-link is-small" type="submit"><span class="icon"><i class="fas fa-copy"></i></span><span>Single combined PDF</span></button> <button class="button is-link is-small" type="submit" name="action" value="pdf">
<a class="button is-link is-small"><span class="icon"><i class="fas fa-archive"></i></span><span>Individual files (zipped)</span></a> {% icon "two_pager" %}
--> <span>&nbsp;Single combined PDF</span>
</button>
<button class="button is-link is-small" type="submit" name="action" value="zip">
{% icon "folder_zip" %}
<span>&nbsp;Individual files (zipped)</span>
</button>
</td> </td>
</tr> </tr>
</tfoot> </tfoot>
@@ -112,16 +114,23 @@
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);
for (let i of INSTRUMENTS) { for (let i of INSTRUMENTS) {
if (i[1] === inst) inst = i[0]; if (i[1].toLowerCase() === inst) {
inst = i[0];
break;
} }
}
console.log("Instrument code:", inst);
let part = document.getElementById("part-preference").value; let part = document.getElementById("part-preference").value;
window.localStorage.setItem('part-preference', part); window.localStorage.setItem('part-preference', part);
console.log("Part preference:", part);
selectParts(inst, part); selectParts(inst, part);
@@ -132,10 +141,11 @@ function selectParts(inst, part) {
let prefix = inst + "-" + part; let prefix = inst + "-" + part;
let instruments = document.getElementsByName("instruments"); let selections = document.getElementsByName("instrument-selection");
for(let i=0; i<instruments.length; i++) { console.log("Updating selections:", prefix, selections);
for(let i=0; i<selections.length; i++) {
var result = "-" var result = "-"
let options = instruments[i].children; let options = selections[i].children;
for(let j=0; j<options.length; j++) { for(let j=0; j<options.length; j++) {
let value = options[j].value; let value = options[j].value;
if (value.startsWith(prefix)) { if (value.startsWith(prefix)) {
@@ -146,7 +156,8 @@ function selectParts(inst, part) {
result = value; result = value;
} }
} }
instruments[i].value = result; console.log("Selected:", result);
selections[i].value = result;
} }
} }
@@ -1,12 +1,13 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
{% block admin %} {% block admin %}
<a href="{% url 'item_list_append' project.pk %}" class="button is-link"> <a href="{% url 'item_list_append' project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span> {% icon "add_circle" %}
<span>Add</span> <span>Add</span>
</a> </a>
<a href="#" onclick="save()" class="button is-link"> <a href="#" onclick="save()" class="button is-link">
<span class="icon"><i class="fas fa-save"></i></span> {% icon "save" %}
<span>Save</span> <span>Save</span>
</a> </a>
{% endblock %} {% endblock %}
@@ -26,9 +27,15 @@
<td>{{ item.work.name }}</td> <td>{{ item.work.name }}</td>
<td>{{ item.work.duration }}</td> <td>{{ item.work.duration }}</td>
<td style="text-align: center;"> <td style="text-align: center;">
<i class="fas fa-arrow-up clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)"></i> <span class="clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)">
<i class="fas fa-arrow-down clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)"></i> {% icon "arrow_upward" %}
<i class="fas fa-trash clickable" title="Remove" onClick="moveItem({{ item.pk }}, 0)"></i> </span>
<span class="clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)">
{% icon "arrow_downward" %}
</span>
<span class="clickable" title="Remove" onClick="moveItem({{ item.pk }}, 0)">
{% icon "delete" %}
</span>
</td> </td>
</tr> </tr>
@@ -1,5 +1,6 @@
{% extends 'interface/project_base.html' %} {% extends 'interface/project_base.html' %}
{% load path_filters %} {% load path_filters %}
{% load polyphonic %}
{% block media %} {% block media %}
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script> <script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
@@ -8,11 +9,11 @@
{% block admin %} {% block admin %}
<a href="{% url 'work_edit' collection.pk work.pk %}" class="button is-link"> <a href="{% url 'work_edit' collection.pk work.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span> {% icon "edit" %}
<span>Edit</span> <span>Edit</span>
</a> </a>
<a href="{% url 'work_add_to_project' collection.pk work.pk %}" class="button is-link"> <a href="{% url 'work_add_to_project' collection.pk work.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span> {% icon "add_ad" %}
<span>Add to project</span> <span>Add to project</span>
</a> </a>
{% endblock %} {% endblock %}
@@ -68,8 +69,8 @@
<div class="column is-half"> <div class="column is-half">
<div class="box"> <div class="box">
<h4 class="subtitle is-size-4"> <h4 class="subtitle is-size-4">
<span class="icon"><i class="fas fa-book"></i></span> {% icon "menu_book" %}
Printed Parts <span>Printed Parts</span>
</h4> </h4>
<div class="tags"> <div class="tags">
{% for inst, c in work.physical_parts %} {% for inst, c in work.physical_parts %}
@@ -81,8 +82,8 @@
</div> </div>
<div class="box"> <div class="box">
<h4 class="subtitle is-size-4"> <h4 class="subtitle is-size-4">
<span class="icon"><i class="fas fa-print"></i></span> {% icon "file_copy" %}
Digital Parts <span>Digital Parts</span>
</h4> </h4>
<div class="tags"> <div class="tags">
{% with sections=work.digital_parts %} {% with sections=work.digital_parts %}
@@ -105,8 +106,8 @@
<div class="box"> <div class="box">
<div class="level"> <div class="level">
<h4 class="subtitle is-size-4"> <h4 class="subtitle is-size-4">
<span class="icon"><i class="fas fa-file"></i></span> {% icon "file_present" %}
Files <span>Files<span>
</h4> </h4>
</div> </div>
<div class="columns"> <div class="columns">
@@ -136,9 +137,9 @@
{% endif %} {% endif %}
{% if "gdrive" in methods %} {% if "gdrive" in methods %}
<div class="has-text-centered mt-3"> <div class="has-text-centered mt-3">
<a class="button button-primary is-size-7" href="{% url 'work_gdrive' collection.pk object.pk %}"> <a class="button button-is-primary is-size-7" href="{% url 'work_gdrive' collection.pk object.pk %}">
<span class="icon"><i class="fa-brands fa-google-drive"></i></span> {% icon "add_to_drive" %}
Link Google Drive Files <span>Google Drive</span>
</a><br/> </a><br/>
</div> </div>
{% endif %} {% endif %}
@@ -152,12 +153,14 @@
<div class="box"> <div class="box">
<div class="level"> <div class="level">
<h4 class="is-size-4"> <h4 class="is-size-4">
<span class="icon"><i class="fas fa-book-reader"></i></span> {{ "folder_open"| icon }}
Loans Projects
</h4> </h4>
<span class="level-right"> <span class="level-right">
<a class="icon-text" href="{% url 'work_add_to_project' collection.pk work.pk %}"><span class="icon"><i <a class="icon-text" href="{% url 'work_add_to_project' collection.pk work.pk %}">
class="fas fa-plus-circle"></i></span> Checkout</a> {% icon "shopping_cart_checkout" %}
<span>Checkout</span>
</a>
</span> </span>
</div> </div>
<table class="table is-fullwidth"> <table class="table is-fullwidth">
@@ -181,7 +184,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td>No current loans</td> <td>No current assignments</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -1,10 +1,11 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load url_tools %} {% load url_tools %}
{% load polyphonic %}
{% block admin %} {% block admin %}
{% if collection %} {% if collection %}
<a href="{% url 'work_add' collection.pk %}" class="button is-link"> <a href="{% url 'work_add' collection.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span> {% icon "add_notes" %}
<span>Add a work</span> <span>Add a work</span>
</a> </a>
{% endif %} {% endif %}
@@ -18,7 +19,9 @@
<input class="input" name="q" type="text" placeholder="Filter" value="{{ request.GET.q }}"/> <input class="input" name="q" type="text" placeholder="Filter" value="{{ request.GET.q }}"/>
</div> </div>
<div class="control"> <div class="control">
<a class="button" href="?"><i class="fas fa-times"></i></a> <a class="button" href="?">
{% icon "close" %}
</a>
</div> </div>
</div> </div>
</form> </form>
@@ -65,6 +68,7 @@
{% else %} {% else %}
disabled disabled
{% endif %}> {% endif %}>
{% icon "arrow_back" %}
Previous</a> Previous</a>
<a class="pagination-next" <a class="pagination-next"
{% if page_obj.has_next %} {% if page_obj.has_next %}
@@ -73,6 +77,7 @@
disabled disabled
{% endif %}> {% endif %}>
Next Next
{% icon "arrow_forward" %}
</a> </a>
<ul class="pagination-list"> <ul class="pagination-list">
{% for page in page_range %} {% for page in page_range %}
@@ -1,4 +1,5 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
@@ -35,12 +36,13 @@
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-link"> <button class="button is-link">
<span class="icon"><i class="fas fa-print"></i></span> {% icon "print" %}
<span>Print Set</span> <span>Print Set</span>
</button> </button>
</div> </div>
<div class="control"> <div class="control">
<a class="button is-link is-light" href="{% url 'work_detail' collection.pk object.pk %}"> <a class="button is-link is-light" href="{% url 'work_detail' collection.pk object.pk %}">
{% icon "backspace" %}
<span>Cancel</span> <span>Cancel</span>
</a> </a>
</div> </div>
+6 -2
View File
@@ -38,7 +38,8 @@ class ProjectItemListView(ProjectMixin, ListView):
project_works = self.project.works.all() project_works = self.project.works.all()
instruments = request.POST.getlist("instruments") print(request.POST)
instruments = request.POST.getlist("instrument-selection")
works = request.POST.getlist("works") works = request.POST.getlist("works")
request.session["part"] = request.POST.get("part", "") request.session["part"] = request.POST.get("part", "")
@@ -65,14 +66,17 @@ class ProjectItemListView(ProjectMixin, ListView):
(part.doc.upload.path, part.doc.work.name, part.start, part.end, 1) (part.doc.upload.path, part.doc.work.name, part.start, part.end, 1)
) )
action = request.POST.get("action")
if action == "pdf":
result = extract_and_concat(sections) result = extract_and_concat(sections)
download_name = f"{self.project.name}.pdf" download_name = f"{self.project.name}.pdf"
response = FileResponse(result, content_type="application/pdf") response = FileResponse(result, content_type="application/pdf")
response["Content-Disposition"] = f'inline; filename="{download_name}"' response["Content-Disposition"] = f'inline; filename="{download_name}"'
return response return response
return HttpResponse(f"Unknown action: {action}", status=400)
def get_queryset(self): def get_queryset(self):
return ( return (
super(ProjectItemListView, self) super(ProjectItemListView, self)