Author SHA1 Message Date
tris 0bf7841345 Adding icons to sync buttons 2026-06-19 22:35:42 +10:00
tris 965573c03b Merged from master 2026-06-19 22:29:44 +10:00
tris b5d8d2d775 Basic web sync working 2026-06-19 22:27:03 +10:00
tris 0e69cdeca4 Tweeked deployment (again!) 2026-06-19 14:03:50 +10:00
tris e797876c16 Merge pull request 'Material Icons' (#17) from material into master
Reviewed-on: #17
2026-06-19 13:58:45 +10:00
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
tris 3444cdbc59 Merge pull request 'gdrive' (#15) from gdrive into master
Reviewed-on: #15
2026-05-24 16:45:13 +10:00
tris 6e77474d15 Couple of fixes 2026-05-24 16:42:51 +10:00
tris 72bad08a61 Merge branch 'master' into gdrive 2026-05-24 14:24:34 +10:00
tris b217fbea5e Sorted out linting 2026-05-24 14:23:59 +10:00
tris cec64ecd2f Working with manual sync 2026-05-24 13:51:23 +10:00
tris d30005d5b6 Indexing bug fix 2026-05-24 11:48:42 +10:00
31 changed files with 466 additions and 178 deletions
+3
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
+1
View File
@@ -2,6 +2,7 @@ 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
+18 -4
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:
+18
View File
@@ -1,4 +1,5 @@
from .base import * # noqa from .base import * # noqa
from os import environ
DEBUG = True DEBUG = True
@@ -8,3 +9,20 @@ SECRET_KEY = "DO NOT USE IN PRODUCTION"
INSTALLED_APPS.append("debug_toolbar") # noqa INSTALLED_APPS.append("debug_toolbar") # noqa
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa
INTERNAL_IPS = ["127.0.0.1"] INTERNAL_IPS = ["127.0.0.1"]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": environ.get("DEBUG_LEVEL", "WARNING"),
},
},
"loggers": {
"polyphonic": {
"handlers": ["console"],
"level": environ.get("DEBUG_LEVEL", "WARNING"),
}
},
}
@@ -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);
}
+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,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 %}
@@ -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)
+55 -14
View File
@@ -1,4 +1,5 @@
from polyphonic.library.models import Collection, Work, WorkMeta, Document from polyphonic.library.models import Collection, Work, WorkMeta, Document
from byostorage.models import UserStorage
import logging import logging
@@ -6,21 +7,35 @@ logger = logging.getLogger(__name__)
def sync_work(work: Work): def sync_work(work: Work):
logger.info("Syncing '%s'", work.name) 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
storage = work.collection.storage.instance() logger.info("Syncing '%s' from %r", work.name, folder_id)
prefix = work.collection.storage.name
_, files = storage.listdir(folder_id) results = {
"folder_id": folder_id,
"added": [],
"skipped": [],
"missing": [],
}
storage = UserStorage.objects.get(name="gdrive").instance()
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)
] ]
) )
logger.debug("%d existing documents", len(existing)) logger.debug("%d existing documents", len(existing))
_, files = storage.listdir(folder_id)
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)
@@ -29,24 +44,36 @@ 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"{prefix}:{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):
works = Work.objects.filter(collection=collection, meta_info__name="folderid")
result = []
for work in works:
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):
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)
if not collection.prefix:
raise KeyError("Prefix must store folder id")
existing = dict( existing = dict(
WorkMeta.objects.filter( WorkMeta.objects.filter(
@@ -54,10 +81,20 @@ 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(collection.prefix) folders, _ = storage.listdir(folder)
results = {
"folders": len(folders),
"added": [],
"missing": [],
}
for folder in folders: for folder in folders:
if folder[0] == "_":
continue
if folder.id in existing: if folder.id in existing:
if sync_existing: if sync_existing:
logger.info("%40s: Syncing (%s)", folder.name, folder.id[:12]) logger.info("%40s: Syncing (%s)", folder.name, folder.id[:12])
@@ -69,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 -42
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/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,57 +37,74 @@ 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
def listdir(self, path) -> tuple[list[str], list[str]]: def listdir(self, path) -> tuple[list[str], list[str]]:
# used to test for valid connection parameters - should do something to validate API key here # used to test for valid connection parameters - should do something to validate API key here
logger.debug("listdir: %s", path)
if path == "": if path == "":
return [], [] return [], []
logger.debug("LISTDIR: %s", path)
folder_id = self.parse_id(path)
url = f"{FILES_API}?q='{folder_id}'+in+parents&key={self.api_key}"
data = self.get_json(url)
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["name"]))
else:
# files.append(f"{x['id']}/{x['name']}")
files.append(DriveObject(x["id"], 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_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):
@@ -91,25 +115,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']}"
""" """
+5 -1
View File
@@ -6,6 +6,8 @@ from polyphonic.library.models import Work, Document
from polyphonic.library import forms from polyphonic.library import forms
from byostorage.models import UserStorage from byostorage.models import UserStorage
from . import sync_work
class WorkGDriveView(CollectionMixin, SingleObjectMixin, FormView): class WorkGDriveView(CollectionMixin, SingleObjectMixin, FormView):
model = Work model = Work
@@ -37,11 +39,13 @@ class WorkGDriveView(CollectionMixin, SingleObjectMixin, FormView):
self.object.meta_info.update_or_create( self.object.meta_info.update_or_create(
name="folderid", defaults={"value": folderid} name="folderid", defaults={"value": folderid}
) )
sync_work(self.object)
return redirect("work_detail", self.collection.pk, self.kwargs["pk"]) return redirect("work_detail", self.collection.pk, self.kwargs["pk"])
link = storage.import_link(link) link = storage.import_link(link)
if link is None: if link is None:
form.add_error("link", str(e)) form.add_error("link", "Not a valid link")
return self.form_invalid(form) return self.form_invalid(form)
work = self.collection.works.get(pk=self.kwargs["pk"]) work = self.collection.works.get(pk=self.kwargs["pk"])
+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,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 %}
@@ -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,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>
@@ -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 %}
@@ -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>
+10
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",
),
] ]
+13 -11
View File
@@ -23,7 +23,7 @@ from polyphonic.library.models import Collection, Work, Document, Section
from polyphonic.library.music_tags import MUSIC_TAGS, MusicTag from polyphonic.library.music_tags import MUSIC_TAGS, 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 indexer, model_search from polyphonic.library.indexer import index_works, model_search
import logging import logging
@@ -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 (
@@ -304,8 +308,7 @@ class WorkAddView(CollectionMixin, FormView):
for f in uploads: for f in uploads:
docs.append(work.docs.create(upload=f).pk) docs.append(work.docs.create(upload=f).pk)
ix = indexer.get_index() index_works([work])
indexer.index_works(ix, [work])
if len(docs) == 1: if len(docs) == 1:
return redirect("document_annotate", docs[0]) return redirect("document_annotate", docs[0])
@@ -339,8 +342,7 @@ class WorkUpdateView(CollectionMixin, UpdateView):
def form_valid(self, form): def form_valid(self, form):
response = super().form_valid(form) response = super().form_valid(form)
ix = indexer.get_index() index_works([self.object])
indexer.index_works(ix, [self.object])
return response return response
+25
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)
+10
View File
@@ -26,12 +26,22 @@ dependencies = [
django-debug-toolbar = "5.2" django-debug-toolbar = "5.2"
ruff = "^0.15.12" ruff = "^0.15.12"
coverage = "^7.14.0" coverage = "^7.14.0"
django-types = "^0.24.0"
[tool.poetry.scripts] [tool.poetry.scripts]
poly-tool = "polyphonic.manage:main" poly-tool = "polyphonic.manage:main"
[tool.ruff] [tool.ruff]
extend-exclude = ["**/migrations/"] extend-exclude = ["**/migrations/"]
lint.extend-select = [
#"DJ", # flake8-django: Django-specific bugs
#"E", # pycodestyle errors
"F", # Pyflakes
#"W", # pycodestyle warnings
#"I", # isort
#"UP", # pyupgrade
#"B", # flake8-bugbear
]
[build-system] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["poetry-core>=2.0.0,<3.0.0"]