Compare commits
26
Commits
93c4926dfd
...
0.8.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a22264d3f | ||
|
|
fefa0a28b6 | ||
|
|
0e69cdeca4 | ||
|
|
e797876c16 | ||
|
|
1301d19c08 | ||
|
|
ca3effcad1 | ||
|
|
5468f6d3e7 | ||
|
|
e46d8145a7 | ||
|
|
3444cdbc59 | ||
|
|
6e77474d15 | ||
|
|
72bad08a61 | ||
|
|
b217fbea5e | ||
|
|
cec64ecd2f | ||
|
|
d30005d5b6 | ||
|
|
ef981e06e8 | ||
|
|
4cef5800bc | ||
|
|
78441dc142 | ||
|
|
c96a2ad80b | ||
|
|
7e0c77f260 | ||
|
|
ff17114514 | ||
|
|
533d6f09e8 | ||
|
|
eeb35ce4f6 | ||
|
|
947626c2af | ||
|
|
c1f0e48f80 | ||
|
|
ab7d32d46e | ||
|
|
5116247ae8 |
@@ -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
|
||||||
|
|||||||
+6
-5
@@ -1,7 +1,8 @@
|
|||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
ENV TARGET=/opt/polyphonic
|
ENV TARGET=/opt/polyphonic
|
||||||
ENV RELEASE=polyphonic-0.8.3-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
|
||||||
|
|
||||||
@@ -12,13 +13,13 @@ 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
|
||||||
|
|
||||||
WORKDIR ${TARGET}
|
WORKDIR ${TARGET}
|
||||||
|
|
||||||
COPY docker_settings.py local_settings.py
|
RUN SECRET_KEY=_ poly-tool collectstatic --noinput
|
||||||
RUN SECRET_KEY=_ ${TARGET}/bin/manage collectstatic --noinput
|
|
||||||
|
|
||||||
VOLUME ["/var/polyphonic"]
|
VOLUME ["/var/polyphonic"]
|
||||||
|
EXPOSE 8000/tcp
|
||||||
|
|
||||||
ENTRYPOINT ["manage"]
|
CMD ["gunicorn", "-b", "0.0.0.0", "polyphonic.config.wsgi"]
|
||||||
CMD ["runserver", "0.0.0.0:8000", "--insecure"]
|
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
PYTHON=env/bin/python
|
PYTHON=env/bin/python
|
||||||
DROPZONE=5.7.0
|
DROPZONE=5.7.0
|
||||||
|
|
||||||
test: check
|
VERSION=0.8.4
|
||||||
poetry run coverage run --include "app/*" --omit "*/migrations/*" app/manage.py test app
|
|
||||||
|
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 html
|
poetry run coverage html
|
||||||
poetry run coverage report
|
poetry run coverage report
|
||||||
|
|
||||||
check:
|
.lint: polyphonic
|
||||||
poetry run ruff check app
|
poetry run ruff check polyphonic
|
||||||
poetry run ruff format --check app
|
poetry run ruff format --check polyphonic
|
||||||
|
touch $@
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
from django import template
|
|
||||||
from django.utils import timesince
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
def roughtimesince(value):
|
|
||||||
return timesince.timesince(value, depth=1)
|
|
||||||
|
|
||||||
|
|
||||||
register.filter("roughtimesince", roughtimesince)
|
|
||||||
|
|
||||||
|
|
||||||
def roughtimeuntil(value):
|
|
||||||
return timesince.timeuntil(value, depth=1)
|
|
||||||
|
|
||||||
|
|
||||||
register.filter("roughtimeuntil", roughtimeuntil)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
from django.core.files.storage import Storage
|
|
||||||
from collections import namedtuple
|
|
||||||
import requests
|
|
||||||
import re
|
|
||||||
from gzip import GzipFile
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
SHARED_FOLDER = re.compile(r"https://drive.google.com/drive/folders/(\w+)")
|
|
||||||
SHARED_FILE = re.compile(r"https://drive.google.com/file/d/([\w\-]+)")
|
|
||||||
|
|
||||||
FILES_API = "https://www.googleapis.com/drive/v3/files"
|
|
||||||
|
|
||||||
|
|
||||||
class DriveObject(namedtuple("DriveObject", ("id", "name"))):
|
|
||||||
@classmethod
|
|
||||||
def from_string(cls, s):
|
|
||||||
return cls(*s.split("/", 1))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.id}/{self.name}"
|
|
||||||
|
|
||||||
|
|
||||||
class GDriveLinkStorage(Storage):
|
|
||||||
is_writable = False
|
|
||||||
|
|
||||||
def __init__(self, api_key):
|
|
||||||
self.api_key = api_key
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def parse_id(self, name):
|
|
||||||
parts = name.split("/")
|
|
||||||
return parts[0]
|
|
||||||
|
|
||||||
def extract_id(self, url, *patterns):
|
|
||||||
logger.debug("EXTRACT_ID: %r", url)
|
|
||||||
for pattern in patterns:
|
|
||||||
match = pattern.match(url)
|
|
||||||
if match:
|
|
||||||
return match.groups()[0]
|
|
||||||
raise FileNotFoundError(f"Not a valid url: {url}")
|
|
||||||
|
|
||||||
def get_json(self, url):
|
|
||||||
logger.debug("GET_JSON: %s", url)
|
|
||||||
response = requests.get(url)
|
|
||||||
data = response.json()
|
|
||||||
logger.debug("Data: %r", data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def listdir(self, path) -> tuple[list[str], list[str]]:
|
|
||||||
|
|
||||||
# used to test for valid connection parameters - should do something to validate API key here
|
|
||||||
if path == "":
|
|
||||||
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 = []
|
|
||||||
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
|
|
||||||
|
|
||||||
def get_meta(self, name):
|
|
||||||
file_id = self.parse_id(name)
|
|
||||||
url = f"{FILES_API}/{file_id}?key={self.api_key}"
|
|
||||||
return self.get_json(url)
|
|
||||||
|
|
||||||
def open(self, name, mode="rb"):
|
|
||||||
file_id = self.parse_id(name)
|
|
||||||
url = f"{FILES_API}/{file_id}?alt=media&key={self.api_key}"
|
|
||||||
logger.info("URL: %s", url)
|
|
||||||
response = requests.get(url, stream=True)
|
|
||||||
return GzipFile(name, "rb", 9, response.raw)
|
|
||||||
|
|
||||||
def size(self, name):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def delete(self, name):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def url(self, name):
|
|
||||||
logger.debug("URL: %r", name)
|
|
||||||
file_id = self.parse_id(name)
|
|
||||||
return f"https://drive.usercontent.google.com/download?export=download&id={file_id}&confirm=yes"
|
|
||||||
|
|
||||||
def get_folder_id(self, url):
|
|
||||||
try:
|
|
||||||
return self.extract_id(url, SHARED_FOLDER)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_file_id(self, url):
|
|
||||||
try:
|
|
||||||
return self.extract_id(url, SHARED_FILE)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
def import_link(self, url) -> str:
|
|
||||||
file_id = self.extract_id(url, SHARED_FILE)
|
|
||||||
meta = self.get_meta(file_id)
|
|
||||||
return f"{file_id}/{meta['name']}"
|
|
||||||
|
|
||||||
def folder_import(self, url) -> list[str]:
|
|
||||||
folder_id = self.extract_id(url, SHARED_FOLDER)
|
|
||||||
_, files = self.listdir(folder_id)
|
|
||||||
return files
|
|
||||||
"""
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
{% extends "interface/project_base.html" %}
|
|
||||||
|
|
||||||
{% block admin %}
|
|
||||||
<a href="#" onclick="saveTags()" class="button is-link">
|
|
||||||
<span class="icon"><i class="fas fa-save"></i></span>
|
|
||||||
<span>Save</span>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'work_detail' collection=collection.pk pk=object.work_id %}" class="button is-link is-light">
|
|
||||||
<span>Cancel</span>
|
|
||||||
</a>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block media %}
|
|
||||||
<style>
|
|
||||||
.tag-grid {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.grid-column {
|
|
||||||
margin-left: 1em;
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
.grid-page {
|
|
||||||
height: 25px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
border: 1px solid #999;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
width: 50px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.grid-page.is-active {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.grid-tag {
|
|
||||||
border: 2px solid var(--primary);
|
|
||||||
background-color: rgba(240, 240, 240, 0.5);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
min-width: 200px;
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
#tag-area {
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
.instrument {
|
|
||||||
display: list-item !important;
|
|
||||||
}
|
|
||||||
.instrument:hover {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block page %}
|
|
||||||
<h3 class="subtitle"><a href="{% url 'work_detail' collection.pk document.work_id %}">{{ document.work.name }}</a></h3>
|
|
||||||
<div id="annotation-area" class="columns is-centered">
|
|
||||||
<div class="column is-narrow">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-narrow">
|
|
||||||
<button class="button is-small is-primary" id="prev"><</button>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<span id="page-num">-</span> / <span id="page-count">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="column is-narrow">
|
|
||||||
<button class="button is-small is-primary" id="next">></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul id="unassigned-area">
|
|
||||||
{% for tag, inst in document.work.music_tags %}
|
|
||||||
<li class="is-clickable" onclick="assignInstrument('{{tag}}', this)")>{{ inst }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<a onclick="document.getElementById('add-modal').classList.add('is-active')">Add Tag</a>
|
|
||||||
</div>
|
|
||||||
<div class="column is-narrow">
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<div class="box" style="display: inline-block;">
|
|
||||||
<canvas id="inline-viewer" style="width: 500px;"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-narrow">
|
|
||||||
|
|
||||||
<div class="tag-grid">
|
|
||||||
<div class="grid-column" id="page-list">
|
|
||||||
</div>
|
|
||||||
<div class="grid-column" id="tag-area">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="button is-primary" style="width: 100%" onclick="expandEntries()">Expand</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal" id="add-modal">
|
|
||||||
<div class="modal-background" onclick="closeAddModal()"></div>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">Add Instrument</p>
|
|
||||||
<button class="delete" aria-label="close" onclick="closeAddModal()"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<span class="control">
|
|
||||||
<input type="text" class="input" list="instrument-list" id="add-instrument-name"/>
|
|
||||||
<datalist id="instrument-list">
|
|
||||||
{% for inst in json_data.instruments.values %}
|
|
||||||
<option value="{{inst}}"/>
|
|
||||||
{% endfor %}
|
|
||||||
</datalist>
|
|
||||||
</span>
|
|
||||||
<span class="control">
|
|
||||||
<input type="number" class="input" max="4" min="1" size="3" id="add-instrument-variant"/>
|
|
||||||
</span>
|
|
||||||
<span class="control">
|
|
||||||
<button class="button is-primary" onclick="addInstrument();">Add</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>{{ document.upload.name }}</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.min.js"
|
|
||||||
integrity="sha512-g4FwCPWM/fZB1Eie86ZwKjOP+yBIxSBM/b2gQAiSVqCgkyvZ0XxYPDEcN2qqaKKEvK6a05+IPL1raO96RrhYDQ=="
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
{{ json_data|json_script:"data" }}
|
|
||||||
<script type="text/javascript">
|
|
||||||
let url = "{% url 'document_download' collection.pk document.pk %}";
|
|
||||||
|
|
||||||
// Loaded via <script> tag, create shortcut to access PDF.js exports.
|
|
||||||
let pdfjsLib = window['pdfjs-dist/build/pdf'];
|
|
||||||
|
|
||||||
// The workerSrc property shall be specified.
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js';
|
|
||||||
|
|
||||||
// get current page tags
|
|
||||||
let data = JSON.parse(document.getElementById('data').textContent);
|
|
||||||
let tagArea = document.getElementById('tag-area');
|
|
||||||
var dirty = false;
|
|
||||||
|
|
||||||
//document.getElementById('tag-list').onclick = (e) => setTag(e.target.dataset.tag);
|
|
||||||
|
|
||||||
var pdfDoc = null,
|
|
||||||
pageNum = 1,
|
|
||||||
pageRendering = false,
|
|
||||||
pageNumPending = null,
|
|
||||||
scale = 1,
|
|
||||||
canvas = document.getElementById('inline-viewer'),
|
|
||||||
ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get page info from document, resize canvas accordingly, and render page.
|
|
||||||
* @param num Page number.
|
|
||||||
*/
|
|
||||||
function renderPage(num) {
|
|
||||||
pageRendering = true;
|
|
||||||
// Using promise to fetch the page
|
|
||||||
pdfDoc.getPage(num).then(function (page) {
|
|
||||||
var viewport = page.getViewport({ scale: scale });
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
|
|
||||||
// Render PDF page into canvas context
|
|
||||||
var renderContext = {
|
|
||||||
canvasContext: ctx,
|
|
||||||
viewport: viewport
|
|
||||||
};
|
|
||||||
var renderTask = page.render(renderContext);
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.getElementById('grid-page-' + pageNum).classList.remove('is-active');
|
|
||||||
} catch(e) {}
|
|
||||||
|
|
||||||
pageNum = num;
|
|
||||||
document.getElementById('grid-page-' + pageNum).classList.add('is-active');
|
|
||||||
|
|
||||||
// Wait for rendering to finish
|
|
||||||
renderTask.promise.then(function () {
|
|
||||||
pageRendering = false;
|
|
||||||
if (pageNumPending !== null) {
|
|
||||||
// New page rendering is pending
|
|
||||||
renderPage(pageNumPending);
|
|
||||||
pageNumPending = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update page counters
|
|
||||||
document.getElementById('page-num').textContent = num;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If another page rendering in progress, waits until the rendering is
|
|
||||||
* finised. Otherwise, executes rendering immediately.
|
|
||||||
*/
|
|
||||||
function queueRenderPage(num) {
|
|
||||||
if (pageRendering) {
|
|
||||||
pageNumPending = num;
|
|
||||||
} else {
|
|
||||||
renderPage(num);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays previous page.
|
|
||||||
*/
|
|
||||||
function onPrevPage() {
|
|
||||||
if (pageNum <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
queueRenderPage(pageNum-1);
|
|
||||||
}
|
|
||||||
document.getElementById('prev').addEventListener('click', onPrevPage);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays next page.
|
|
||||||
*/
|
|
||||||
function onNextPage() {
|
|
||||||
if (pageNum >= pdfDoc.numPages) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
queueRenderPage(pageNum+1);
|
|
||||||
}
|
|
||||||
document.getElementById('next').addEventListener('click', onNextPage);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously downloads PDF.
|
|
||||||
*/
|
|
||||||
pdfjsLib.getDocument(url).promise.then(function (pdfDoc_) {
|
|
||||||
pdfDoc = pdfDoc_;
|
|
||||||
|
|
||||||
const pageList = document.getElementById('page-list');
|
|
||||||
pageList.innerHTML = '';
|
|
||||||
for (var i=0; i<pdfDoc.numPages; i++) {
|
|
||||||
let page = i+1;
|
|
||||||
let el = document.createElement('div');
|
|
||||||
el.className = 'grid-page';
|
|
||||||
el.id = 'grid-page-' + page;
|
|
||||||
el.innerHTML = page;
|
|
||||||
el.addEventListener('click', (evt) => {
|
|
||||||
queueRenderPage(page);
|
|
||||||
});
|
|
||||||
pageList.appendChild(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('page-count').textContent = pdfDoc.numPages;
|
|
||||||
|
|
||||||
// Initial/first page rendering
|
|
||||||
renderPage(pageNum);
|
|
||||||
|
|
||||||
tagArea.innerHTML = '';
|
|
||||||
for (let pageTag of data.pageTags) {
|
|
||||||
addTag(pageTag[0], pageTag[1], pageTag[2]);
|
|
||||||
}
|
|
||||||
dirty = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
function closeAddModal() {
|
|
||||||
document.getElementById('add-modal').classList.remove('is-active');
|
|
||||||
document.getElementById('add-instrument-name').value = "";
|
|
||||||
document.getElementById('add-instrument-variant').value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function addInstrument() {
|
|
||||||
let name = document.getElementById('add-instrument-name');
|
|
||||||
let variant = document.getElementById('add-instrument-variant');
|
|
||||||
|
|
||||||
let inst_name = name.value;
|
|
||||||
var tag = null;
|
|
||||||
for (let key in data.instruments) {
|
|
||||||
if (data.instruments[key] == inst_name) {
|
|
||||||
tag = key;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tag) {
|
|
||||||
alert("Unknown tag: " + name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variant.value) {
|
|
||||||
tag += "-" + variant.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
variant.value = '';
|
|
||||||
name.value = '';
|
|
||||||
|
|
||||||
let li = document.createElement('li');
|
|
||||||
li.classList.add("is-clickable");
|
|
||||||
li.addEventListener('click', () => assignInstrument(tag, li));
|
|
||||||
li.innerHTML = get_instrument(tag);
|
|
||||||
document.getElementById('unassigned-area').appendChild(li);
|
|
||||||
|
|
||||||
addTag(tag, pageNum, pageNum);
|
|
||||||
closeAddModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function assignInstrument(tag, el) {
|
|
||||||
addTag(tag, pageNum, pageNum);
|
|
||||||
//el.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTag(tag, start, end) {
|
|
||||||
console.log("addTag", tag, start, end);
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.className = 'grid-tag';
|
|
||||||
el.dataset.tag = tag;
|
|
||||||
el.dataset.start = start;
|
|
||||||
el.dataset.end = end;
|
|
||||||
|
|
||||||
let setStart = document.createElement('span');
|
|
||||||
setStart.className = "icon is-action";
|
|
||||||
setStart.innerHTML = '<i class="fas fa-sort-amount-down" title="Set start page"></i>';
|
|
||||||
setStart.addEventListener('click', () => setTagStart(el));
|
|
||||||
el.appendChild(setStart);
|
|
||||||
|
|
||||||
|
|
||||||
let label = document.createElement('span');
|
|
||||||
|
|
||||||
let name = document.createElement('b');
|
|
||||||
name.innerHTML = get_instrument(tag);
|
|
||||||
label.appendChild(name);
|
|
||||||
|
|
||||||
let del = document.createElement('span');
|
|
||||||
del.className = "icon is-action";
|
|
||||||
del.innerHTML = '<i class="fas fa-trash-alt" title="Remove this tag"></i>';
|
|
||||||
del.addEventListener('click', () => {
|
|
||||||
el.remove();
|
|
||||||
dirty=true;
|
|
||||||
});
|
|
||||||
label.appendChild(del)
|
|
||||||
|
|
||||||
el.appendChild(label);
|
|
||||||
|
|
||||||
|
|
||||||
let setEnd = document.createElement('span');
|
|
||||||
setEnd.className = "icon is-action";
|
|
||||||
setEnd.innerHTML = '<i class="fas fa-sort-amount-up" title="Set end page"></i>';
|
|
||||||
setEnd.addEventListener('click', () => setTagEnd(el));
|
|
||||||
el.appendChild(setEnd);
|
|
||||||
|
|
||||||
updateTag(el);
|
|
||||||
tagArea.appendChild(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTag(tag) {
|
|
||||||
let start = tag.dataset.start;
|
|
||||||
let end = tag.dataset.end;
|
|
||||||
let span = end-start+1;
|
|
||||||
let height = span * 25 + (span-1) * 5;
|
|
||||||
let top = (start-1) * 30;
|
|
||||||
|
|
||||||
tag.style.height = height + 'px';
|
|
||||||
tag.style.marginTop = top + 'px';
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTagStart(el) {
|
|
||||||
if (pageNum > el.dataset.end) {
|
|
||||||
alert("Select the first page and click extend");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
el.dataset.start = pageNum;
|
|
||||||
updateTag(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTagEnd(el) {
|
|
||||||
if (pageNum < el.dataset.start) {
|
|
||||||
alert("Select the last page and click extend");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
el.dataset.end = pageNum;
|
|
||||||
updateTag(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
function expandEntries() {
|
|
||||||
const entries = Array.from(tagArea.children);
|
|
||||||
entries.sort((a, b) => a.dataset.start-b.dataset.start);
|
|
||||||
const c = entries.length;
|
|
||||||
for (let i=0; i<c-1; i++) {
|
|
||||||
entries[i].dataset.end = entries[i+1].dataset.start - 1;
|
|
||||||
updateTag(entries[i]);
|
|
||||||
|
|
||||||
}
|
|
||||||
entries[c-1].dataset.end = pdfDoc.numPages;
|
|
||||||
updateTag(entries[c-1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveTags() {
|
|
||||||
const pageTags = [];
|
|
||||||
for (let pageTag of tagArea.children ) {
|
|
||||||
let start = pageTag.dataset.start;
|
|
||||||
let end = pageTag.dataset.end;
|
|
||||||
if (start == 1 && end == pdfDoc.numPages) {
|
|
||||||
start = null;
|
|
||||||
end = null;
|
|
||||||
}
|
|
||||||
pageTags.push([pageTag.dataset.tag, start, end])
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(pageTags);
|
|
||||||
fetch("", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}" },
|
|
||||||
body: JSON.stringify(pageTags)
|
|
||||||
}
|
|
||||||
).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
window.location = "{% url 'work_detail' collection.pk document.work_id %}"
|
|
||||||
} else {
|
|
||||||
alert("Failed: " + response.statusText)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_instrument(s) {
|
|
||||||
let parts = s.split('-');
|
|
||||||
let instrument = data.instruments[parts[0]];
|
|
||||||
if (parts.length == 2) {
|
|
||||||
return instrument + " " + parts[1];
|
|
||||||
}
|
|
||||||
return instrument;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkSaved(e) {
|
|
||||||
if (dirty) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('beforeunload', checkSaved);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
|
|
||||||
try:
|
|
||||||
from local_settings import * # noqa
|
|
||||||
except ImportError:
|
|
||||||
from .default_settings import * # noqa
|
|
||||||
|
|
||||||
INSTALLED_APPS += POLYPHONIC_MODULES # type: ignore #noqa
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
polyphonic:
|
||||||
|
image: "polyphonic:0.8.4"
|
||||||
|
build: "."
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
volumes:
|
||||||
|
- "./data:/var/polyphonic"
|
||||||
|
- "./local_settings.py:/opt/polyphonic/local_settings.py"
|
||||||
|
env_file: "compose.env"
|
||||||
|
environment:
|
||||||
|
DJANGO_SETTINGS_MODULE: local_settings
|
||||||
|
PYTHONPATH: /opt/polyphonic
|
||||||
|
WORK_DIR: /var/polyphonic
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from polyphonic.default_settings import *
|
|
||||||
import os
|
|
||||||
|
|
||||||
DEBUG = bool(os.environ.get("DEBUG", False))
|
|
||||||
|
|
||||||
CACHE_DIR = "/var/polyphonic/cache"
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
|
||||||
"NAME": "/var/polyphonic/db.sqlite3",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'polyphonic.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polyphonic.settings")
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
@@ -11,26 +11,29 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from os import environ
|
import os
|
||||||
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
|
||||||
|
|
||||||
SECRET_KEY = environ.get("SECRET_KEY")
|
# A place to put things
|
||||||
|
WORK_DIR = os.environ.get("WORK_DIR") or os.path.join(BASE_DIR, "data")
|
||||||
|
|
||||||
|
# Will fail to start if not defined
|
||||||
|
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||||
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["localhost"]
|
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
|
||||||
INTERNAL_IPS = ["127.0.0.1"]
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
POLYPHONIC_MODULES = ["library"]
|
POLYPHONIC_MODULES = ["polyphonic.library"]
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
@@ -44,9 +47,11 @@ INSTALLED_APPS = [
|
|||||||
"crispy_forms",
|
"crispy_forms",
|
||||||
"crispy_bulma",
|
"crispy_bulma",
|
||||||
"byostorage",
|
"byostorage",
|
||||||
"interface",
|
"polyphonic.interface",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
INSTALLED_APPS += POLYPHONIC_MODULES
|
||||||
|
|
||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
||||||
CRISPY_TEMPLATE_PACK = "bulma"
|
CRISPY_TEMPLATE_PACK = "bulma"
|
||||||
|
|
||||||
@@ -60,7 +65,7 @@ MIDDLEWARE = [
|
|||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "polyphonic.urls"
|
ROOT_URLCONF = "polyphonic.config.urls"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
@@ -78,7 +83,7 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "polyphonic.wsgi.application"
|
WSGI_APPLICATION = "polyphonic.config.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
@@ -87,7 +92,7 @@ WSGI_APPLICATION = "polyphonic.wsgi.application"
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
"NAME": BASE_DIR / "db.sqlite3",
|
"NAME": os.path.join(WORK_DIR, "db.sqlite3"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,17 +125,18 @@ LANGUAGE_CODE = "en-us"
|
|||||||
TIME_ZONE = "Australia/Melbourne"
|
TIME_ZONE = "Australia/Melbourne"
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = "static"
|
STATIC_ROOT = "static"
|
||||||
|
|
||||||
# Need to set this
|
# Library settings
|
||||||
AWS_BUCKET = ""
|
|
||||||
|
CACHED_STORAGE_REMOTE = "byostorage.user.BYOStorage"
|
||||||
|
CACHED_STORAGE_DIR = os.path.join(WORK_DIR, "cache")
|
||||||
|
WHOOSH_INDEX = os.path.join(WORK_DIR, "index")
|
||||||
|
STORAGE_CLASSES = ["polyphonic.library.gdrive.storage.GDriveLinkStorage"]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from .base import * # noqa
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
SECRET_KEY = "DO NOT USE IN PRODUCTION"
|
||||||
|
|
||||||
|
# Enable debug toolbar
|
||||||
|
INSTALLED_APPS.append("debug_toolbar") # noqa
|
||||||
|
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .base import * # noqa
|
||||||
|
|
||||||
|
# Enable WhiteNoise for static files
|
||||||
|
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") # noqa
|
||||||
@@ -19,9 +19,9 @@ from django.urls import path, include
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("", include("interface.urls")),
|
path("", include("polyphonic.interface.urls")),
|
||||||
# path('', include('submissions.urls')),
|
# path('', include('submissions.urls')),
|
||||||
path("", include("library.urls")),
|
path("", include("polyphonic.library.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'polyphonic.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polyphonic.config.settings.base")
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceConfig(AppConfig):
|
class InterfaceConfig(AppConfig):
|
||||||
name = "interface"
|
name = "polyphonic.interface"
|
||||||
+3
-3
@@ -4,7 +4,7 @@ import byostorage.user
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import interface.models
|
import polyphonic.interface.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(help_text='Display name', max_length=100)),
|
('name', models.CharField(help_text='Display name', max_length=100)),
|
||||||
('slug', models.SlugField(editable=False, help_text='Short name for the ensemble - used for folders', max_length=100, unique=True)),
|
('slug', models.SlugField(editable=False, help_text='Short name for the ensemble - used for folders', max_length=100, unique=True)),
|
||||||
('code', models.CharField(default=interface.models.generate_code, help_text='Ensemble registration code', max_length=9)),
|
('code', models.CharField(default=polyphonic.interface.models.generate_code, help_text='Ensemble registration code', max_length=9)),
|
||||||
('passphrase', models.CharField(help_text='Used to register ensembles', max_length=100)),
|
('passphrase', models.CharField(help_text='Used to register ensembles', max_length=100)),
|
||||||
('details', models.TextField(blank=True, help_text='Description of the ensemble (markdown)')),
|
('details', models.TextField(blank=True, help_text='Description of the ensemble (markdown)')),
|
||||||
('admins', models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL)),
|
('admins', models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL)),
|
||||||
@@ -60,7 +60,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('description', models.TextField(blank=True)),
|
('description', models.TextField(blank=True)),
|
||||||
('file', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=interface.models.resource_key)),
|
('file', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=polyphonic.interface.models.resource_key)),
|
||||||
('media_type', models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10)),
|
('media_type', models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10)),
|
||||||
('visible', models.BooleanField(default=True)),
|
('visible', models.BooleanField(default=True)),
|
||||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
|
||||||
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
+2
-1
@@ -32,10 +32,11 @@
|
|||||||
.menu-label {
|
.menu-label {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
.button.is-primary, .button.is-primary:hover {
|
.button.is-primary, .button.is-primary:hover {
|
||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
A.admin-link:after {
|
A.admin-link:after {
|
||||||
content: "*";
|
content: "*";
|
||||||
|
Before Width: | Height: | Size: 258 B After Width: | Height: | Size: 258 B |
@@ -1,17 +1,19 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<!-- Required meta tags -->
|
<!-- Required meta tags -->
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" />
|
<link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" />
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||||
|
<!--link rel="stylesheet" href="{% static 'interface/css/bulmaswatch.min.css' %}"></link-->
|
||||||
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
|
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
|
||||||
<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 +25,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>
|
||||||
+1
-1
@@ -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…
|
||||||
+4
-3
@@ -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
-2
@@ -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>
|
||||||
+3
-3
@@ -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 %}
|
||||||
+6
-5
@@ -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>
|
||||||
+3
-2
@@ -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 %}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from django import template
|
||||||
|
from django.utils import timesince
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
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):
|
||||||
|
return timesince.timesince(value, depth=1)
|
||||||
|
|
||||||
|
|
||||||
|
register.filter("roughtimesince", roughtimesince)
|
||||||
|
|
||||||
|
|
||||||
|
def roughtimeuntil(value):
|
||||||
|
return timesince.timeuntil(value, depth=1)
|
||||||
|
|
||||||
|
|
||||||
|
register.filter("roughtimeuntil", roughtimeuntil)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from interface import models
|
from polyphonic.interface import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
|
from polyphonic.interface import models
|
||||||
from interface import models
|
|
||||||
|
|
||||||
from . import AccessTestCase
|
from . import AccessTestCase
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ from django.contrib.auth import logout
|
|||||||
from markdown2 import markdown
|
from markdown2 import markdown
|
||||||
|
|
||||||
from . import models, forms
|
from . import models, forms
|
||||||
from interface.utils import check_signed_url
|
from .utils import check_signed_url
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class LibraryConfig(AppConfig):
|
class LibraryConfig(AppConfig):
|
||||||
name = "library"
|
name = "polyphonic.library"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import Work
|
from .models import Work
|
||||||
from interface.models import Project
|
from polyphonic.interface.models import Project
|
||||||
from interface.forms import BaseForm
|
from polyphonic.interface.forms import BaseForm
|
||||||
|
|
||||||
|
|
||||||
class WorkCreateForm(forms.ModelForm, BaseForm):
|
class WorkCreateForm(forms.ModelForm, BaseForm):
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from 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,26 @@ 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)
|
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)
|
||||||
|
|
||||||
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)
|
||||||
@@ -32,21 +38,26 @@ def sync_work(work: Work):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info("%40s: Adding", file.name)
|
logger.info("%40s: Adding", file.name)
|
||||||
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:
|
||||||
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)
|
||||||
|
|
||||||
if not collection.prefix:
|
|
||||||
raise KeyError("Prefix must store folder id")
|
|
||||||
|
|
||||||
existing = dict(
|
existing = dict(
|
||||||
WorkMeta.objects.filter(
|
WorkMeta.objects.filter(
|
||||||
@@ -54,10 +65,14 @@ 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)
|
||||||
|
|
||||||
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])
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
from django.core.files.storage import Storage
|
||||||
|
from collections import namedtuple
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
from gzip import GzipFile
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SHARED_FOLDER = re.compile(
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
class DriveObject(namedtuple("DriveObject", ("id", "key", "name"))):
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, s: str):
|
||||||
|
resource, _, name = s.partition("/")
|
||||||
|
id, _, key = resource.partition("#")
|
||||||
|
return cls(id, key, name)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
resource = f"{self.id}#{self.key}" if self.key else self.id
|
||||||
|
return f"{resource}/{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class GDriveLinkStorage(Storage):
|
||||||
|
is_writable = False
|
||||||
|
|
||||||
|
def __init__(self, api_key):
|
||||||
|
self.api_key = api_key
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def parse_resource(self, name) -> DriveObject:
|
||||||
|
return DriveObject.from_string(name)
|
||||||
|
|
||||||
|
def extract_resource(self, url, *patterns) -> DriveObject:
|
||||||
|
logger.debug("EXTRACT_RESOURCE: %r", url)
|
||||||
|
for pattern in patterns:
|
||||||
|
match = pattern.match(url)
|
||||||
|
if match:
|
||||||
|
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}")
|
||||||
|
|
||||||
|
def get_json(self, url, resource: DriveObject):
|
||||||
|
headers = {}
|
||||||
|
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()
|
||||||
|
logger.debug("Data: %r", data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def listdir(self, path) -> tuple[list[str], list[str]]:
|
||||||
|
# used to test for valid connection parameters - should do something to validate API key here
|
||||||
|
logger.debug("listdir: %s", path)
|
||||||
|
if path == "":
|
||||||
|
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 = []
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_meta(self, name):
|
||||||
|
file_resource = self.parse_resource(name)
|
||||||
|
url = f"{FILES_API}/{file_resource.id}?key={self.api_key}"
|
||||||
|
return self.get_json(url, file_resource)
|
||||||
|
|
||||||
|
def open(self, name, mode="rb"):
|
||||||
|
resource = self.parse_resource(name)
|
||||||
|
url = f"{FILES_API}/{resource.id}?alt=media&key={self.api_key}"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def size(self, name):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def url(self, name):
|
||||||
|
logger.debug("URL: %r", name)
|
||||||
|
resource = self.parse_resource(name)
|
||||||
|
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):
|
||||||
|
try:
|
||||||
|
return self.extract_resource(url, SHARED_FOLDER)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_file_id(self, url):
|
||||||
|
try:
|
||||||
|
return self.extract_resource(url, SHARED_FILE)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def import_link(self, url) -> str:
|
||||||
|
file_resource = self.extract_resource(url, SHARED_FILE)
|
||||||
|
meta = self.get_meta(file_resource)
|
||||||
|
return f"{file_resource}/{meta['name']}"
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def folder_import(self, url) -> list[str]:
|
||||||
|
folder_id = self.extract_id(url, SHARED_FOLDER)
|
||||||
|
_, files = self.listdir(folder_id)
|
||||||
|
return files
|
||||||
|
"""
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
from django.shortcuts import resolve_url, redirect
|
from django.shortcuts import resolve_url, redirect
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from library.views import CollectionMixin
|
from polyphonic.library.views import CollectionMixin
|
||||||
from library.models import Work, Document
|
from polyphonic.library.models import Work, Document
|
||||||
from library import forms
|
from polyphonic.library import forms
|
||||||
|
from byostorage.models import UserStorage
|
||||||
|
|
||||||
|
from . import sync_work
|
||||||
|
|
||||||
|
|
||||||
class WorkGDriveView(CollectionMixin, SingleObjectMixin, FormView):
|
class WorkGDriveView(CollectionMixin, SingleObjectMixin, FormView):
|
||||||
@@ -27,31 +30,30 @@ class WorkGDriveView(CollectionMixin, SingleObjectMixin, FormView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
link = form.cleaned_data["link"]
|
link = form.cleaned_data["link"]
|
||||||
|
|
||||||
storage = self.collection.storage.instance()
|
# storage = self.collection.storage.instance()
|
||||||
|
storage = UserStorage.objects.get(name="gdrive").instance()
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
||||||
try:
|
folderid = storage.get_folder_id(link)
|
||||||
folderid = storage.get_folder_id(link)
|
if folderid:
|
||||||
self.object.meta_info.update_or_create(
|
self.object.meta_info.update_or_create(
|
||||||
name="folderid", defaults={"value": folderid}
|
name="folderid", defaults={"value": folderid}
|
||||||
)
|
)
|
||||||
return redirect("work_detail", self.collection.pk, self.kwargs["pk"])
|
sync_work(self.object)
|
||||||
except FileNotFoundError:
|
|
||||||
pass # not a folder id
|
|
||||||
|
|
||||||
try:
|
return redirect("work_detail", self.collection.pk, self.kwargs["pk"])
|
||||||
link = self.collection.storage.instance().import_link(link)
|
|
||||||
except AttributeError:
|
link = storage.import_link(link)
|
||||||
pass
|
if link is None:
|
||||||
except FileNotFoundError as e:
|
form.add_error("link", "Not a valid link")
|
||||||
form.add_error("link", str(e))
|
|
||||||
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"])
|
||||||
|
|
||||||
doc = Document(
|
doc = Document(
|
||||||
work=work,
|
work=work,
|
||||||
upload=f"{self.collection.storage.name}:{link}",
|
upload=f"gdrive:{link}",
|
||||||
|
# upload=f"{self.collection.storage.name}:{link}",
|
||||||
doctype=Document.DOCTYPE_PDF,
|
doctype=Document.DOCTYPE_PDF,
|
||||||
)
|
)
|
||||||
doc.save()
|
doc.save()
|
||||||
@@ -3,9 +3,9 @@ from django.conf import settings
|
|||||||
from django.utils.module_loading import import_module
|
from django.utils.module_loading import import_module
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
from library.models import Work, Collection
|
from polyphonic.library.models import Work, Collection
|
||||||
|
|
||||||
instance = getattr(settings, "INDEXER", "library.indexer.whoosh")
|
instance = getattr(settings, "INDEXER", "polyphonic.library.indexer.whoosh")
|
||||||
|
|
||||||
|
|
||||||
class Indexer(Protocol):
|
class Indexer(Protocol):
|
||||||
@@ -3,7 +3,7 @@ from whoosh.analysis import StemmingAnalyzer, CharsetFilter
|
|||||||
from whoosh.support.charset import accent_map
|
from whoosh.support.charset import accent_map
|
||||||
from whoosh.fields import Schema, TEXT, KEYWORD, NUMERIC
|
from whoosh.fields import Schema, TEXT, KEYWORD, NUMERIC
|
||||||
from whoosh.qparser import QueryParser
|
from whoosh.qparser import QueryParser
|
||||||
from whoosh.query import Term, NullQuery, Prefix, FuzzyTerm
|
from whoosh.query import Term, NullQuery, FuzzyTerm
|
||||||
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -24,7 +24,7 @@ schema = Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
index_path = os.path.join(os.path.dirname(settings.BASE_DIR), "index")
|
index_path = settings.WHOOSH_INDEX
|
||||||
|
|
||||||
|
|
||||||
def create_index() -> Index:
|
def create_index() -> Index:
|
||||||
@@ -69,7 +69,6 @@ def search(
|
|||||||
limit = NullQuery
|
limit = NullQuery
|
||||||
for c in collections:
|
for c in collections:
|
||||||
limit |= Term("collection", c)
|
limit |= Term("collection", c)
|
||||||
print(limit)
|
|
||||||
|
|
||||||
hits = []
|
hits = []
|
||||||
ix = get_index()
|
ix = get_index()
|
||||||
@@ -78,7 +77,7 @@ def search(
|
|||||||
|
|
||||||
# if no results, do a prefix search
|
# if no results, do a prefix search
|
||||||
if results.results.is_empty():
|
if results.results.is_empty():
|
||||||
qp.termclass = Prefix
|
qp.termclass = FuzzyTerm
|
||||||
q = qp.parse(query)
|
q = qp.parse(query)
|
||||||
results = searcher.search_page(q & limit, page, pagesize)
|
results = searcher.search_page(q & limit, page, pagesize)
|
||||||
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from library import models
|
from polyphonic.library import models
|
||||||
from library.indexer import model_search, index_works, indexer
|
from polyphonic.library.indexer import model_search, index_works, indexer
|
||||||
|
|
||||||
FORMATTER = "{w.name:50s} {w.edition:15s} {w.collection.name:15s}"
|
FORMATTER = "{w.name:50s} {w.edition:15s} {w.collection.name:15s}"
|
||||||
|
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from library.models import Work, Collection
|
from polyphonic.library.models import Work, Collection
|
||||||
from library.gdrive import sync_work, sync_collection
|
from polyphonic.library.gdrive import sync_work, sync_collection
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
+2
-2
@@ -4,7 +4,7 @@ import byostorage.user
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import library.models
|
import polyphonic.library.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('doctype', models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source')], default=1)),
|
('doctype', models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source')], default=1)),
|
||||||
('upload', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=library.models.doc_upload_filename)),
|
('upload', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=polyphonic.library.models.doc_upload_filename)),
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
('version', models.CharField(blank=True, max_length=30)),
|
('version', models.CharField(blank=True, max_length=30)),
|
||||||
],
|
],
|
||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
import byostorage.cached
|
import byostorage.cached
|
||||||
import byostorage.user
|
import byostorage.user
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import library.models
|
import polyphonic.library.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='document',
|
model_name='document',
|
||||||
name='upload',
|
name='upload',
|
||||||
field=models.FileField(storage=byostorage.cached.CachedStorage(byostorage.user.BYOStorage()), upload_to=library.models.doc_upload_filename),
|
field=models.FileField(storage=byostorage.cached.CachedStorage(byostorage.user.BYOStorage()), upload_to=polyphonic.library.models.doc_upload_filename),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='work',
|
model_name='work',
|
||||||
@@ -11,8 +11,8 @@ import os.path
|
|||||||
from byostorage.user import BYOStorage
|
from byostorage.user import BYOStorage
|
||||||
from byostorage.cached import CachedStorage
|
from byostorage.cached import CachedStorage
|
||||||
|
|
||||||
from library.music_tags import MusicTag, auto_tag
|
from polyphonic.library.music_tags import MusicTag, auto_tag
|
||||||
from interface.utils import sign_data
|
from polyphonic.interface.utils import sign_data
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -366,8 +366,8 @@ class Work(models.Model):
|
|||||||
|
|
||||||
def music_tags(self):
|
def music_tags(self):
|
||||||
tags = dict(self.orchestration.as_list())
|
tags = dict(self.orchestration.as_list())
|
||||||
for section in Section.objects.filter(doc__work_id=self.pk):
|
# for section in Section.objects.filter(doc__work_id=self.pk):
|
||||||
tags.setdefault(section.tag, section.name)
|
# tags.setdefault(section.tag, section.name)
|
||||||
|
|
||||||
return tags.items()
|
return tags.items()
|
||||||
|
|
||||||
@@ -4,9 +4,10 @@ import re
|
|||||||
GENERAL = """
|
GENERAL = """
|
||||||
mvmt Movement
|
mvmt Movement
|
||||||
ex Excerpt
|
ex Excerpt
|
||||||
sec Section
|
sect Section
|
||||||
pce Piece
|
pce Piece
|
||||||
no Number
|
no No.
|
||||||
|
page Page
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_MusicTags
|
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_MusicTags
|
||||||
@@ -19,6 +20,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
|
||||||
+5
-1
@@ -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 %}
|
||||||
@@ -0,0 +1,732 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
{% load polyphonic %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="#" onclick="saveTags()" class="button is-link">
|
||||||
|
{% icon "save" %}
|
||||||
|
<span>Save</span>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'work_detail' collection=collection.pk pk=object.work_id %}" class="button is-link is-light">
|
||||||
|
{% icon "backspace" %}
|
||||||
|
<span>Cancel</span>
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block media %}
|
||||||
|
<style>
|
||||||
|
#unassigned-area {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
#unassigned-area .tag {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#area-left h3 {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.tag-grid {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.grid-column {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.grid-page {
|
||||||
|
height: 25px;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #DDD;
|
||||||
|
text-align: center;
|
||||||
|
width: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.grid-page:last-child {
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
.grid-page.is-active {
|
||||||
|
/*background-color: var(--bulma-link-light);*/
|
||||||
|
color: var(--bulma-link);
|
||||||
|
background-color: #EEE;
|
||||||
|
border-radius: 0px 6px 6px 0px;
|
||||||
|
}
|
||||||
|
.grid-tag {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
border-radius: 0px 4px 4px 0px;
|
||||||
|
border-left: none;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
min-width: 100px;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: top;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: 4px 0 6px -4px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.grid-tag:hover {
|
||||||
|
z-index: 100 !important;
|
||||||
|
}
|
||||||
|
.tag-handle {
|
||||||
|
position: relative;
|
||||||
|
width: 20px;
|
||||||
|
background-color: var(--bulma-warning-light);
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.tag-name {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.tag-handle-resize {
|
||||||
|
position: absolute;
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
border-radius: 3px 0px 0px 0px;
|
||||||
|
background-color: var(--bulma-warning);
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
{% for tag, colour in tag_colours %}
|
||||||
|
.tag-type-{{ tag }} .tag-handle {
|
||||||
|
background-color: var(--bulma-{{ colour }}-light);
|
||||||
|
}
|
||||||
|
.tag-type-{{ tag }} .tag-handle-resize {
|
||||||
|
background-color: var(--bulma-{{ colour }});
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
#tag-area {
|
||||||
|
min-width: 220px;
|
||||||
|
position: relative;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
.instrument {
|
||||||
|
display: list-item !important;
|
||||||
|
}
|
||||||
|
.instrument:hover {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h3 class="subtitle"><a href="{% url 'work_detail' collection.pk document.work_id %}">{{ document.work.name }}</a></h3>
|
||||||
|
<div id="annotation-area" class="columns is-centered">
|
||||||
|
<div class="column is-narrow" id="area-left">
|
||||||
|
<h3>Page control</h3>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<button class="button is-small is-primary" id="prev"><</button>
|
||||||
|
</div>
|
||||||
|
<div class="column has-text-centered">
|
||||||
|
<span id="page-num">-</span> / <span id="page-count">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<button class="button is-small is-primary" id="next">></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Quick Tags</h3>
|
||||||
|
<ul id="unassigned-area">
|
||||||
|
{% for tag, inst in document.work.music_tags %}
|
||||||
|
<li class="tag is-warning" onclick="assignTag('{{tag}}', this)">{{ inst }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<h3>Actions</h3>
|
||||||
|
<div>
|
||||||
|
<a class="button is-small is-primary" onclick="showAddModal()">New Tag</a>
|
||||||
|
<button class="button is-small is-primary" onclick="expandEntries()">Expand Tags</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<div class="box" style="display: inline-block; padding: 0px;">
|
||||||
|
<canvas id="inline-viewer" style="width: 500px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
|
||||||
|
<div class="tag-grid">
|
||||||
|
<div class="grid-column" id="page-list">
|
||||||
|
</div>
|
||||||
|
<div class="grid-column" id="tag-area">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="add-modal">
|
||||||
|
<div class="modal-background" onclick="closeAddModal()"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<p class="modal-card-title">Add Tag</p>
|
||||||
|
<button class="delete" aria-label="close" onclick="closeAddModal()"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<div class="field has-addons is-justify-content-center">
|
||||||
|
<span class="control">
|
||||||
|
<span class="select">
|
||||||
|
<select onChange="changeTagType()" id="tag-type-selection">
|
||||||
|
<option value="inst">Instrument</option>
|
||||||
|
<option value="mvmt">Movement</option>
|
||||||
|
<option value="sect">Section</option>
|
||||||
|
<option value="no">Number</option>
|
||||||
|
<option value="page">Page</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="control add-tag-option enable-page">
|
||||||
|
<span class="select">
|
||||||
|
<select id="add-tag-page">
|
||||||
|
<option>Left</option>
|
||||||
|
<option>Right</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="control add-tag-option enable-sect">
|
||||||
|
<input type="text" class="input" id="add-tag-name"/>
|
||||||
|
</span>
|
||||||
|
<span class="control add-tag-option enable-inst">
|
||||||
|
<input type="text" class="input" list="instrument-list" id="add-tag-inst"/>
|
||||||
|
<datalist id="instrument-list">
|
||||||
|
{% for inst in json_data.instruments.values %}
|
||||||
|
<option value="{{inst}}"/>
|
||||||
|
{% endfor %}
|
||||||
|
</datalist>
|
||||||
|
</span>
|
||||||
|
<span class="control add-tag-option enable-inst enable-mvmt enable-no">
|
||||||
|
<input type="number" class="input" min="1" size="3" id="add-tag-ordinal"/>
|
||||||
|
</span>
|
||||||
|
<span class="control">
|
||||||
|
<button class="button is-primary" onclick="createNewTag();">Add</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{{ document.upload.name }}</p>
|
||||||
|
|
||||||
|
<template id="blank-tag">
|
||||||
|
<div class="grid-tag">
|
||||||
|
<span class="tag-name"></span>
|
||||||
|
<div class="tag-handle">
|
||||||
|
<div class="tag-handle-resize"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.min.js"
|
||||||
|
integrity="sha512-g4FwCPWM/fZB1Eie86ZwKjOP+yBIxSBM/b2gQAiSVqCgkyvZ0XxYPDEcN2qqaKKEvK6a05+IPL1raO96RrhYDQ=="
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
{{ json_data|json_script:"data" }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
let url = "{% url 'document_download' collection.pk document.pk %}";
|
||||||
|
|
||||||
|
// Loaded via <script> tag, create shortcut to access PDF.js exports.
|
||||||
|
let pdfjsLib = window['pdfjs-dist/build/pdf'];
|
||||||
|
|
||||||
|
// The workerSrc property shall be specified.
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js';
|
||||||
|
|
||||||
|
// get current page tags
|
||||||
|
let data = JSON.parse(document.getElementById('data').textContent);
|
||||||
|
let tagArea = document.getElementById('tag-area');
|
||||||
|
var dirty = false;
|
||||||
|
|
||||||
|
//document.getElementById('tag-list').onclick = (e) => setTag(e.target.dataset.tag);
|
||||||
|
|
||||||
|
var pdfDoc = null,
|
||||||
|
pageNum = 1,
|
||||||
|
pageRendering = false,
|
||||||
|
pageNumPending = null,
|
||||||
|
scale = 1,
|
||||||
|
canvas = document.getElementById('inline-viewer'),
|
||||||
|
ctx = canvas.getContext('2d'),
|
||||||
|
draggedTag = null,
|
||||||
|
dragMode = null,
|
||||||
|
dragOrigStart = null,
|
||||||
|
dragOrigEnd = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page info from document, resize canvas accordingly, and render page.
|
||||||
|
* @param num Page number.
|
||||||
|
*/
|
||||||
|
function renderPage(num) {
|
||||||
|
pageRendering = true;
|
||||||
|
// Using promise to fetch the page
|
||||||
|
pdfDoc.getPage(num).then(function (page) {
|
||||||
|
var viewport = page.getViewport({ scale: scale });
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
|
// Render PDF page into canvas context
|
||||||
|
var renderContext = {
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
var renderTask = page.render(renderContext);
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.getElementById('grid-page-' + pageNum).classList.remove('is-active');
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
pageNum = num;
|
||||||
|
document.getElementById('grid-page-' + pageNum).classList.add('is-active');
|
||||||
|
|
||||||
|
// Wait for rendering to finish
|
||||||
|
renderTask.promise.then(function () {
|
||||||
|
pageRendering = false;
|
||||||
|
if (pageNumPending !== null) {
|
||||||
|
// New page rendering is pending
|
||||||
|
renderPage(pageNumPending);
|
||||||
|
pageNumPending = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update page counters
|
||||||
|
document.getElementById('page-num').textContent = num;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If another page rendering in progress, waits until the rendering is
|
||||||
|
* finised. Otherwise, executes rendering immediately.
|
||||||
|
*/
|
||||||
|
function queueRenderPage(num) {
|
||||||
|
if (pageRendering) {
|
||||||
|
pageNumPending = num;
|
||||||
|
} else {
|
||||||
|
renderPage(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays previous page.
|
||||||
|
*/
|
||||||
|
function onPrevPage() {
|
||||||
|
if (pageNum <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queueRenderPage(pageNum-1);
|
||||||
|
}
|
||||||
|
document.getElementById('prev').addEventListener('click', onPrevPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays next page.
|
||||||
|
*/
|
||||||
|
function onNextPage() {
|
||||||
|
if (pageNum >= pdfDoc.numPages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queueRenderPage(pageNum+1);
|
||||||
|
}
|
||||||
|
document.getElementById('next').addEventListener('click', onNextPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously downloads PDF.
|
||||||
|
*/
|
||||||
|
pdfjsLib.getDocument(url).promise.then(function (pdfDoc_) {
|
||||||
|
pdfDoc = pdfDoc_;
|
||||||
|
|
||||||
|
const pageList = document.getElementById('page-list');
|
||||||
|
const tagGrid = document.querySelector('.tag-grid');
|
||||||
|
pageList.innerHTML = '';
|
||||||
|
for (var i=0; i<pdfDoc.numPages; i++) {
|
||||||
|
let page = i+1;
|
||||||
|
let el = document.createElement('div');
|
||||||
|
el.className = 'grid-page';
|
||||||
|
el.id = 'grid-page-' + page;
|
||||||
|
el.innerHTML = page;
|
||||||
|
el.addEventListener('click', (evt) => {
|
||||||
|
queueRenderPage(page);
|
||||||
|
});
|
||||||
|
pageList.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagGrid.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
if (draggedTag) {
|
||||||
|
const rect = pageList.getBoundingClientRect();
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
const page = Math.min(Math.max(Math.floor(y / 25) + 1, 1), pdfDoc.numPages);
|
||||||
|
if (dragMode === 'resize') {
|
||||||
|
if (page >= parseInt(draggedTag.dataset.start) && page !== parseInt(draggedTag.dataset.end)) {
|
||||||
|
draggedTag.dataset.end = page;
|
||||||
|
updateTag(draggedTag);
|
||||||
|
resolveOverlaps();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const length = dragOrigEnd - dragOrigStart;
|
||||||
|
if (page !== parseInt(draggedTag.dataset.start)) {
|
||||||
|
draggedTag.dataset.start = page;
|
||||||
|
draggedTag.dataset.end = Math.min(page + length, pdfDoc.numPages);
|
||||||
|
updateTag(draggedTag);
|
||||||
|
resolveOverlaps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tagGrid.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (draggedTag) {
|
||||||
|
draggedTag = null;
|
||||||
|
dragMode = null;
|
||||||
|
}
|
||||||
|
resolveOverlaps();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('page-count').textContent = pdfDoc.numPages;
|
||||||
|
|
||||||
|
// Initial/first page rendering
|
||||||
|
renderPage(pageNum);
|
||||||
|
setTimeout(() => {
|
||||||
|
const previewBox = document.querySelector('.box');
|
||||||
|
tagGrid.style.maxHeight = previewBox.offsetHeight + 'px';
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
tagArea.innerHTML = '';
|
||||||
|
for (let pageTag of data.pageTags) {
|
||||||
|
addTag(pageTag[0], pageTag[1], pageTag[2]);
|
||||||
|
}
|
||||||
|
dirty = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function showAddModal() {
|
||||||
|
document.getElementById("add-modal").classList.add("is-active");
|
||||||
|
changeTagType();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAddModal() {
|
||||||
|
document.getElementById('add-modal').classList.remove('is-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 changeTagType() {
|
||||||
|
const tag_select = document.querySelector("#tag-type-selection");
|
||||||
|
const selected = tag_select.value;
|
||||||
|
document.querySelectorAll(".add-tag-option").forEach((x) => {x.classList.add("is-hidden")});
|
||||||
|
document.querySelectorAll(".add-tag-option input").forEach((x) => {x.value=""});
|
||||||
|
document.querySelectorAll(".enable-" + selected).forEach((x) => {x.classList.remove("is-hidden")});
|
||||||
|
tag_select.focus();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateNewTag() {
|
||||||
|
const selected = document.querySelector("#tag-type-selection").value;
|
||||||
|
const n = document.querySelector("#add-tag-ordinal").value;
|
||||||
|
switch (selected) {
|
||||||
|
case "mvmt":
|
||||||
|
case "no":
|
||||||
|
return selected + "-" + n;
|
||||||
|
case "sect":
|
||||||
|
return "sect-" + document.querySelector("#add-tag-name").value;
|
||||||
|
case "inst":
|
||||||
|
const inst_name = document.querySelector("#add-tag-inst").value;
|
||||||
|
var tag = null;
|
||||||
|
for (let key in data.instruments) {
|
||||||
|
if (data.instruments[key] == inst_name) {
|
||||||
|
tag = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
tag = inst_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n) {
|
||||||
|
tag += "-" + n;
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
case "page":
|
||||||
|
return "page-" + document.querySelector("#add-tag-page").value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createNewTag() {
|
||||||
|
|
||||||
|
const tag = generateNewTag()
|
||||||
|
|
||||||
|
addTag(tag, pageNum, pageNum);
|
||||||
|
closeAddModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignTag(tag, el) {
|
||||||
|
addTag(tag, pageNum, pageNum);
|
||||||
|
//el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTag(tag, start, end) {
|
||||||
|
|
||||||
|
|
||||||
|
//console.log("addTag", tag, start, end);
|
||||||
|
const fragment = document.importNode(document.querySelector("#blank-tag").content, true);
|
||||||
|
const el = fragment.querySelector("div");
|
||||||
|
const tagName = el.querySelector(".tag-name");
|
||||||
|
tagName.innerHTML=get_tag_name(tag);
|
||||||
|
|
||||||
|
//el.className = 'grid-tag';
|
||||||
|
el.dataset.start = start;
|
||||||
|
el.dataset.end = end;
|
||||||
|
el.dataset.tag = tag;
|
||||||
|
|
||||||
|
const parts = tag.split("-")
|
||||||
|
el.classList.add("tag-type-" + parts[0]);
|
||||||
|
|
||||||
|
|
||||||
|
const handle = el.querySelector('.tag-handle');
|
||||||
|
handle.draggable = true;
|
||||||
|
handle.addEventListener('dragstart', (e) => {
|
||||||
|
draggedTag = el;
|
||||||
|
dragMode = 'move';
|
||||||
|
dragOrigStart = parseInt(el.dataset.start);
|
||||||
|
dragOrigEnd = parseInt(el.dataset.end);
|
||||||
|
el.style.zIndex = 1000;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
const blankImg = new Image();
|
||||||
|
blankImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
|
e.dataTransfer.setDragImage(blankImg, 0, 0);
|
||||||
|
});
|
||||||
|
handle.addEventListener('dragend', () => {
|
||||||
|
if (draggedTag === el && dragMode === 'move') {
|
||||||
|
el.dataset.start = dragOrigStart;
|
||||||
|
el.dataset.end = dragOrigEnd;
|
||||||
|
updateTag(el);
|
||||||
|
el.remove();
|
||||||
|
dirty = true;
|
||||||
|
} else if (draggedTag === el) {
|
||||||
|
el.dataset.start = dragOrigStart;
|
||||||
|
el.dataset.end = dragOrigEnd;
|
||||||
|
updateTag(el);
|
||||||
|
el.style.zIndex = '';
|
||||||
|
}
|
||||||
|
resolveOverlaps();
|
||||||
|
draggedTag = null;
|
||||||
|
dragMode = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resize = el.querySelector('.tag-handle-resize');
|
||||||
|
resize.draggable = true;
|
||||||
|
resize.addEventListener('dragstart', (e) => {
|
||||||
|
draggedTag = el;
|
||||||
|
dragMode = 'resize';
|
||||||
|
dragOrigEnd = parseInt(el.dataset.end);
|
||||||
|
el.style.zIndex = 1000;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
const blankImg2 = new Image();
|
||||||
|
blankImg2.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
|
e.dataTransfer.setDragImage(blankImg2, 0, 0);
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
resize.addEventListener('dragend', () => {
|
||||||
|
if (draggedTag === el) {
|
||||||
|
el.dataset.end = dragOrigEnd;
|
||||||
|
updateTag(el);
|
||||||
|
el.style.zIndex = '';
|
||||||
|
}
|
||||||
|
resolveOverlaps();
|
||||||
|
draggedTag = null;
|
||||||
|
dragMode = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTag(el);
|
||||||
|
tagArea.appendChild(el);
|
||||||
|
resolveOverlaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTag(tag) {
|
||||||
|
let start = tag.dataset.start;
|
||||||
|
let end = tag.dataset.end;
|
||||||
|
let span = end-start+1;
|
||||||
|
let height = span * 25 + 1;
|
||||||
|
let top = (start-1) * 25;
|
||||||
|
|
||||||
|
tag.style.height = height + 'px';
|
||||||
|
tag.style.marginTop = top + 'px';
|
||||||
|
dirty = true;
|
||||||
|
|
||||||
|
const name = tag.querySelector('.tag-name');
|
||||||
|
const handle = tag.querySelector('.tag-handle');
|
||||||
|
if (name) {
|
||||||
|
if (span >= 3) {
|
||||||
|
name.style.position = 'absolute';
|
||||||
|
name.style.top = '4px';
|
||||||
|
name.style.right = '20px';
|
||||||
|
name.style.writingMode = 'vertical-rl';
|
||||||
|
tag.style.justifyContent = 'flex-end';
|
||||||
|
if (handle) handle.style.marginLeft = 'auto';
|
||||||
|
} else {
|
||||||
|
name.style.position = '';
|
||||||
|
name.style.top = '';
|
||||||
|
name.style.right = '';
|
||||||
|
name.style.writingMode = '';
|
||||||
|
tag.style.justifyContent = '';
|
||||||
|
if (handle) handle.style.marginLeft = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOverlaps() {
|
||||||
|
const tags = Array.from(tagArea.children);
|
||||||
|
tags.sort((a, b) => parseInt(a.dataset.start) - parseInt(b.dataset.start));
|
||||||
|
|
||||||
|
const groups = [];
|
||||||
|
let current = [tags[0]];
|
||||||
|
for (let i = 1; i < tags.length; i++) {
|
||||||
|
const prevEnd = parseInt(tags[i-1].dataset.end);
|
||||||
|
const currStart = parseInt(tags[i].dataset.start);
|
||||||
|
if (currStart <= prevEnd) {
|
||||||
|
current.push(tags[i]);
|
||||||
|
} else {
|
||||||
|
groups.push(current);
|
||||||
|
current = [tags[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.length) groups.push(current);
|
||||||
|
|
||||||
|
for (let group of groups) {
|
||||||
|
group.sort((a, b) => {
|
||||||
|
const spanA = parseInt(a.dataset.end) - parseInt(a.dataset.start);
|
||||||
|
const spanB = parseInt(b.dataset.end) - parseInt(b.dataset.start);
|
||||||
|
return spanB - spanA;
|
||||||
|
});
|
||||||
|
const n = group.length;
|
||||||
|
group.forEach((tag, i) => {
|
||||||
|
tag.style.minWidth = (120 + (n - 1 - i) * 70) + 'px';
|
||||||
|
tag.style.zIndex = i;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTagStart(el) {
|
||||||
|
if (pageNum > el.dataset.end) {
|
||||||
|
alert("Select the first page and click extend");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.dataset.start = pageNum;
|
||||||
|
updateTag(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTagEnd(el) {
|
||||||
|
if (pageNum < el.dataset.start) {
|
||||||
|
alert("Select the last page and click extend");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.dataset.end = pageNum;
|
||||||
|
updateTag(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandEntries() {
|
||||||
|
const answer = confirm("This will expand all tags to fill up available pages");
|
||||||
|
if (!answer) return;
|
||||||
|
|
||||||
|
|
||||||
|
const entries = Array.from(tagArea.children);
|
||||||
|
entries.sort((a, b) => a.dataset.start-b.dataset.start);
|
||||||
|
const c = entries.length;
|
||||||
|
for (let i=0; i<c-1; i++) {
|
||||||
|
entries[i].dataset.end = entries[i+1].dataset.start - 1;
|
||||||
|
updateTag(entries[i]);
|
||||||
|
|
||||||
|
}
|
||||||
|
entries[c-1].dataset.end = pdfDoc.numPages;
|
||||||
|
updateTag(entries[c-1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = document.querySelector("body");
|
||||||
|
function keymap(e) {
|
||||||
|
if (e.target != body) return;
|
||||||
|
switch(e.key) {
|
||||||
|
case "ArrowRight":
|
||||||
|
case "l":
|
||||||
|
case "ArrowDown":
|
||||||
|
case "j":
|
||||||
|
onNextPage();
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "h":
|
||||||
|
case "ArrowUp":
|
||||||
|
case "k":
|
||||||
|
onPrevPage();
|
||||||
|
break;
|
||||||
|
case "n":
|
||||||
|
showAddModal();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
}
|
||||||
|
body.addEventListener("keydown", keymap)
|
||||||
|
|
||||||
|
function saveTags() {
|
||||||
|
const pageTags = [];
|
||||||
|
for (let pageTag of tagArea.children ) {
|
||||||
|
let start = pageTag.dataset.start;
|
||||||
|
let end = pageTag.dataset.end;
|
||||||
|
if (start == 1 && end == pdfDoc.numPages) {
|
||||||
|
start = null;
|
||||||
|
end = null;
|
||||||
|
}
|
||||||
|
pageTags.push([pageTag.dataset.tag, start, end])
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(pageTags);
|
||||||
|
fetch("", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}" },
|
||||||
|
body: JSON.stringify(pageTags)
|
||||||
|
}
|
||||||
|
).then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
window.location = "{% url 'work_detail' collection.pk document.work_id %}"
|
||||||
|
} else {
|
||||||
|
alert("Failed: " + response.statusText)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_tag_name(s) {
|
||||||
|
let parts = s.split('-');
|
||||||
|
let instrument = data.instruments[parts[0]] || parts[0];
|
||||||
|
if (parts.length == 2) {
|
||||||
|
return instrument + " " + parts[1];
|
||||||
|
}
|
||||||
|
return instrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSaved(e) {
|
||||||
|
if (dirty) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('beforeunload', checkSaved);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
+11
-5
@@ -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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user