Compare commits

...

7 Commits

28 changed files with 337 additions and 125 deletions

3
.gitignore vendored
View File

@ -5,7 +5,10 @@ __pycache__
credentials.json credentials.json
credentials credentials
local_settings.py local_settings.py
local.mk
.coverage .coverage
.lint
.deploy
Session.vim Session.vim
poetry.lock poetry.lock
/env /env

View File

@ -1,8 +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 #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
@ -11,7 +11,7 @@ WORKDIR /root
RUN python3 -m venv ${TARGET} RUN python3 -m venv ${TARGET}
ENV PATH="${TARGET}/bin:$PATH" ENV PATH="${TARGET}/bin:$PATH"
#COPY dist/${RELEASE} . COPY dist/${RELEASE} .
RUN pip3 install ${RELEASE} --no-cache-dir RUN pip3 install ${RELEASE} --no-cache-dir
RUN pip3 install gunicorn whitenoise RUN pip3 install gunicorn whitenoise

View File

@ -1,18 +1,32 @@
PYTHON=env/bin/python PYTHON=env/bin/python
DROPZONE=5.7.0 DROPZONE=5.7.0
test: VERSION=0.8.4
export DJANGO_SETTINGS_MODULE=polyphonic.config.settings.dev
-include local.mk
test: .coverage
check: .lint
pre-commit: check test
.coverage: polyphonic
poetry run coverage run --include "polyphonic/*" --omit "*/migrations/*" polyphonic/manage.py test polyphonic poetry run coverage run --include "polyphonic/*" --omit "*/migrations/*" polyphonic/manage.py test polyphonic
poetry run coverage html poetry run coverage html
poetry run coverage report poetry run coverage report
check: .lint: polyphonic
poetry run ruff check polyphonic poetry run ruff check polyphonic
poetry run ruff format --check polyphonic poetry run ruff format --check polyphonic
touch $@
pre-commit: check test
build: build: dist/polyphonic-${VERSION}-py3-none-any.whl
dist/polyphonic-${VERSION}-py3-none-any.whl: polyphonic
poetry build poetry build
dev-setup: dev-setup:

View File

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

View File

@ -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>

View File

@ -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…
@ -25,4 +25,4 @@
fileName.textContent = fileInput.files[0].name; fileName.textContent = fileInput.files[0].name;
} }
} }
</script> </script>

View 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 %}
@ -54,4 +55,4 @@ Contacts:
<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>
{% endblock %} {% endblock %}

View File

@ -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>
@ -45,4 +48,4 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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 %}

View File

@ -33,4 +33,4 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -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>

View File

@ -1,9 +1,10 @@
{% 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 %}
@ -12,4 +13,4 @@
<div class="box content wiki-page"> <div class="box content wiki-page">
{{ wiki_html|safe }} {{ wiki_html|safe }}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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)

View File

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

View File

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

View File

@ -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

View File

@ -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 %}

View File

@ -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>
@ -275,6 +281,16 @@
document.getElementById('add-instrument-name').value = ""; document.getElementById('add-instrument-name').value = "";
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');
@ -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);

View File

@ -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>

View File

@ -1,12 +1,28 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% 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" %}
<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,28 +30,28 @@
<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 folder <span class="tag is-success">{{ meta.folderid }}</span><br/>
<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 %}
</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 %} <div class="control">
<p class="help is-danger">{{ error }}</p> <button class="button" type="submit">
{% endfor %} Add
</div> </button>
<div class="field"> </div>
<div class="control">
<button class="button is-info" type="submit">Add</button>
</div>
</div> </div>
{% csrf_token %} {% csrf_token %}
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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,6 +57,7 @@
{{ 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 }}"/>
@ -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>

View File

@ -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>
@ -106,4 +113,4 @@ function checkSaved(e) {
window.addEventListener('beforeunload', checkSaved); window.addEventListener('beforeunload', checkSaved);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -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,12 +9,12 @@
{% 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>

View File

@ -1,13 +1,23 @@
{% 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 %}
<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">
<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 %}
{% endblock %} {% endblock %}
{% block page %} {% block page %}
@ -18,7 +28,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 +77,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 +86,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 %}

View File

@ -1,4 +1,5 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% load polyphonic %}
@ -35,13 +36,14 @@
<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 %}">
<span>Cancel</span> {% icon "backspace" %}
<span>Cancel</span>
</a> </a>
</div> </div>
</div> </div>
@ -50,4 +52,4 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -126,4 +126,14 @@ urlpatterns = [
api.CollectionImportView.as_view(), api.CollectionImportView.as_view(),
name="collection_import", name="collection_import",
), ),
path(
"api/collections/<int:collection>/sync",
api.CollectionSyncView.as_view(),
name="collection_sync",
),
path(
"api/works/<int:work>/sync",
api.WorkSyncView.as_view(),
name="work_sync",
),
] ]

View File

@ -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,13 +66,16 @@ 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)
) )
result = extract_and_concat(sections) action = request.POST.get("action")
if action == "pdf":
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["Content-Disposition"] = f'inline; filename="{download_name}"'
return response
response = FileResponse(result, content_type="application/pdf") return HttpResponse(f"Unknown action: {action}", status=400)
response["Content-Disposition"] = f'inline; filename="{download_name}"'
return response
def get_queryset(self): def get_queryset(self):
return ( return (

View File

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