port to new django, AI automated

This commit is contained in:
2026-03-30 22:35:36 +02:00
parent e2d166e437
commit 372da3caa9
215 changed files with 9283 additions and 2981 deletions

View File

@@ -0,0 +1,95 @@
# Generated by Django 5.1.15 on 2026-03-30 19:15
import django.db.models.deletion
import scoremanager.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Piece',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, unique=True, verbose_name='title')),
('composer', models.CharField(blank=True, max_length=255, verbose_name='composer')),
('repertoire_nr', models.IntegerField(blank=True, default=None, null=True, unique=True)),
],
options={
'ordering': ['title'],
'permissions': (('manage_scores', 'Administrate and manage scores'),),
},
),
migrations.CreateModel(
name='BookLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('book', models.CharField(max_length=100, verbose_name='Buch')),
('page', models.IntegerField(verbose_name='page')),
('piece', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scoremanager.piece')),
],
options={
'ordering': ['book', 'page'],
},
),
migrations.CreateModel(
name='Score',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('score_type', models.CharField(max_length=100, verbose_name='score type')),
('file', models.FileField(upload_to=scoremanager.models.score_filename, verbose_name='file')),
('piece', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scores', to='scoremanager.piece')),
('uploaded_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='uploaded_by')),
],
options={
'ordering': ['score_type'],
'unique_together': {('piece', 'score_type')},
},
),
migrations.CreateModel(
name='Recording',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('artist', models.CharField(max_length=100, verbose_name='Artist')),
('file', models.FileField(upload_to=scoremanager.models.recording_filename, verbose_name='file')),
('piece', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recordings', to='scoremanager.piece')),
('uploaded_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='uploaded_by')),
],
options={
'ordering': ['artist'],
'unique_together': {('piece', 'artist')},
},
),
migrations.CreateModel(
name='ScoreUserMapping',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('piece', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scoremanager.piece')),
('score', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scoremanager.score')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('piece', 'user')},
},
),
migrations.CreateModel(
name='YoutubeRecording',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('link', models.CharField(max_length=300)),
('piece', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='youtubeLinks', to='scoremanager.piece')),
('uploaded_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='uploaded_by')),
],
options={
'unique_together': {('link', 'piece')},
},
),
]

View File

View File

@@ -1,41 +1,44 @@
import os
import re
from django.contrib.auth.models import User
from django.db import models
from django.db.models.fields.related import ForeignKey
from django.utils.translation import ugettext as _
from django.contrib.auth.models import User
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
############################# Helper Functions #######################################################
def space_to_camelcase(value):
"""Convert a space-separated string to camelCase."""
def camelcase():
yield type(value).lower
yield str.lower
while True:
yield type(value).capitalize
yield str.capitalize
c = camelcase()
return "".join(c.next()(x) if x else '_' for x in value.split())
return "".join(next(c)(x) if x else "_" for x in value.split())
def score_filename(score, original_name):
fileExtension = os.path.splitext(original_name)[1]
"""Generate filename for uploaded score files."""
file_extension = os.path.splitext(original_name)[1]
filename = "scores/"
filename += space_to_camelcase(score.piece.title) + "/"
filename += space_to_camelcase(score.score_type)
filename += fileExtension
filename += file_extension
return filename
def recordingFileName(recording, originalName):
fileExtension = os.path.splitext(originalName)[1]
def recording_filename(recording, original_name):
"""Generate filename for uploaded recording files."""
file_extension = os.path.splitext(original_name)[1]
filename = "recordings/"
filename += space_to_camelcase(recording.piece.title) + "/"
filename += space_to_camelcase(recording.artist)
filename += fileExtension
filename += file_extension
return filename
@@ -43,130 +46,195 @@ def recordingFileName(recording, originalName):
class Piece(models.Model):
"""A musical piece in the repertoire."""
title = models.CharField(max_length=255, verbose_name=_("title"), unique=True)
composer = models.CharField(max_length=255, blank=True, verbose_name=_("composer"))
repertoire_nr = models.IntegerField(null=True, blank=True, unique=True, default=None)
def __unicode__(self):
res = self.title
return res
def isInRepertoire(self):
return self.repertoire_nr is not None
def get_score_for_user(self, user):
try:
return ScoreUserMapping.objects.get(user=user, score__in=self.scores.all()).score
except Piece.DoesNotExist:
if len(self.scores.all()) > 0:
return self.scores.all()[0]
else:
return None
@staticmethod
def getRepertoire():
return Piece.objects.filter(repertoire_nr__isnull=False).order_by('repertoire_nr')
repertoire_nr = models.IntegerField(
null=True, blank=True, unique=True, default=None
)
class Meta:
permissions = (
("manage_scores", "Administrate and manage scores"),
ordering = ["title"]
permissions = (("manage_scores", "Administrate and manage scores"),)
def __str__(self):
return self.title
def is_in_repertoire(self):
return self.repertoire_nr is not None
# Backwards compatibility
isInRepertoire = is_in_repertoire
def get_score_for_user(self, user):
"""Get the preferred score for a user, or the first available score."""
try:
return ScoreUserMapping.objects.get(
user=user, score__in=self.scores.all()
).score
except ScoreUserMapping.DoesNotExist:
if self.scores.exists():
return self.scores.first()
return None
@staticmethod
def get_repertoire():
"""Get all pieces that are in the repertoire, ordered by repertoire number."""
return Piece.objects.filter(repertoire_nr__isnull=False).order_by(
"repertoire_nr"
)
# Backwards compatibility
getRepertoire = get_repertoire
class BookLocation(models.Model):
piece = models.ForeignKey('Piece', on_delete=models.PROTECT)
"""Location of a piece in a physical book."""
piece = models.ForeignKey("Piece", on_delete=models.CASCADE)
book = models.CharField(max_length=100, blank=False, verbose_name=_("Buch"))
page = models.IntegerField(verbose_name=_("page"))
def __unicode__(self):
return "%s, %d" % (self.book, self.page)
class Meta:
ordering = ["book", "page"]
def __str__(self):
return f"{self.book}, {self.page}"
class Score(models.Model):
piece = ForeignKey('Piece', related_name="scores", on_delete=models.PROTECT)
score_type = models.CharField(max_length=100, verbose_name="score type") # for example partitur, unterstimmen ...
"""A score file for a piece (e.g., full score, parts, etc.)."""
piece = ForeignKey("Piece", on_delete=models.CASCADE, related_name="scores")
score_type = models.CharField(max_length=100, verbose_name="score type")
file = models.FileField(upload_to=score_filename, verbose_name=_("file"))
uploaded_by = ForeignKey(User, verbose_name=_("uploaded_by"), on_delete=models.PROTECT)
uploaded_by = ForeignKey(
User, on_delete=models.SET_NULL, null=True, verbose_name=_("uploaded_by")
)
class Meta:
unique_together = (("piece", "score_type"),)
ordering = ["score_type"]
def __str__(self):
return f"{self.piece.title} - {self.score_type}"
@property
def image_file_name(self):
"""Get the path for the cached image preview of this score."""
return os.path.splitext("image_cache/" + str(self.file))[0] + ".jpg"
@staticmethod
def pdf2jpg(source_file, target_file, resolution=100, crop=15):
from wand.image import Image
ret = True
"""Convert a PDF to a JPG preview image."""
try:
with Image(filename=source_file, resolution=(resolution, resolution)) as img:
img.crop(crop, crop, width=img.width - 2 * crop, height=int(0.5 * img.height) - 2 * crop)
img.format = 'jpeg'
img.save(filename=target_file)
except Exception as e:
print(e)
ret = False
from wand.image import Image
return ret
with Image(
filename=source_file, resolution=(resolution, resolution)
) as img:
img.crop(
crop,
crop,
width=img.width - 2 * crop,
height=int(0.5 * img.height) - 2 * crop,
)
img.format = "jpeg"
img.save(filename=target_file)
return True
except Exception as e:
print(f"Error converting PDF to JPG: {e}")
return False
def get_image_file(self):
"""Get or create a preview image for this score."""
from django.conf import settings
inputFile = settings.MEDIA_ROOT + "/" + str(self.file)
cacheFile = settings.MEDIA_ROOT + "/" + str(self.image_file_name)
input_file = settings.MEDIA_ROOT + "/" + str(self.file)
cache_file = settings.MEDIA_ROOT + "/" + str(self.image_file_name)
# Create a jpg for this score, if it does not exist yet
if not os.path.exists(cacheFile):
if not os.path.exists(os.path.dirname(cacheFile)):
os.makedirs(os.path.dirname(cacheFile))
Score.pdf2jpg(inputFile, cacheFile)
if not os.path.exists(cache_file):
cache_dir = os.path.dirname(cache_file)
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
Score.pdf2jpg(input_file, cache_file)
return self.image_file_name
def is_active_score(self, user):
return len(ScoreUserMapping.objects.filter(score=self, user=user)) > 0
class Meta:
unique_together = (("piece", "score_type"),)
ordering = ['score_type']
"""Check if this is the active score for a user."""
return ScoreUserMapping.objects.filter(score=self, user=user).exists()
class Recording(models.Model):
piece = ForeignKey('Piece', related_name='recordings', on_delete=models.PROTECT)
"""An audio recording of a piece."""
piece = ForeignKey("Piece", on_delete=models.CASCADE, related_name="recordings")
artist = models.CharField(max_length=100, verbose_name=_("Artist"))
file = models.FileField(upload_to=recordingFileName, verbose_name=_("file"))
uploaded_by = ForeignKey(User, verbose_name=_("uploaded_by"), on_delete=models.PROTECT)
file = models.FileField(upload_to=recording_filename, verbose_name=_("file"))
uploaded_by = ForeignKey(
User, on_delete=models.SET_NULL, null=True, verbose_name=_("uploaded_by")
)
class Meta:
unique_together = (("piece", "artist"),)
ordering = ['artist']
ordering = ["artist"]
def __str__(self):
return f"{self.piece.title} - {self.artist}"
class YoutubeRecording(models.Model):
piece = models.ForeignKey('Piece', related_name="youtubeLinks", on_delete=models.PROTECT)
link = models.CharField(max_length=300, blank=False)
uploaded_by = ForeignKey(User, verbose_name=_("uploaded_by"), on_delete=models.PROTECT)
"""A YouTube link for a piece."""
youtubeRegex = re.compile(u'(?:https://)?(?:http://)?www.youtube.(?:com|de)/watch\?v=(?P<videoID>[-\w]*)')
piece = models.ForeignKey(
"Piece", on_delete=models.CASCADE, related_name="youtubeLinks"
)
link = models.CharField(max_length=300, blank=False)
uploaded_by = ForeignKey(
User, on_delete=models.SET_NULL, null=True, verbose_name=_("uploaded_by")
)
youtube_regex = re.compile(
r"(?:https://)?(?:http://)?www\.youtube\.(?:com|de)/watch\?v=(?P<videoID>[-\w]*)"
)
class Meta:
unique_together = (("link", "piece"),)
def __str__(self):
return f"{self.piece.title} - YouTube"
@property
def embed_html(self):
"""Generate embeddable HTML for this YouTube video."""
replacement = """
<div class="embed-container"><iframe src="//www.youtube.de/embed/\g<videoID>" frameborder="0" allowfullscreen></iframe> </div>
<div class="embed-container">
<iframe src="//www.youtube.com/embed/\\g<videoID>"
frameborder="0" allowfullscreen></iframe>
</div>
"""
return mark_safe(YoutubeRecording.youtubeRegex.sub(replacement, self.link))
class Meta:
unique_together = ("link", "piece")
return mark_safe(self.youtube_regex.sub(replacement, self.link))
class ScoreUserMapping(models.Model):
score = ForeignKey('Score', on_delete=models.PROTECT)
user = models.OneToOneField(User, on_delete=models.PROTECT)
piece = ForeignKey('Piece', on_delete=models.PROTECT)
"""Maps a user's preferred score for each piece."""
score = ForeignKey("Score", on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
piece = ForeignKey("Piece", on_delete=models.CASCADE)
class Meta:
unique_together = (("piece", "user"),)
def __str__(self):
return f"{self.user.username} -> {self.score}"
@staticmethod
def add_user_score_mapping(score, user):
"""Set the preferred score for a user for a given piece."""
piece = score.piece
ScoreUserMapping.objects.filter(user=user, piece=piece).delete()
ScoreUserMapping.objects.create(score=score, user=user, piece=piece)
class Meta:
unique_together = ("piece", "user")

View File

@@ -1,95 +1,157 @@
# ----------------------------- Pdf Views ------------------------------------------
from django.http import HttpResponse
from scoremanager.models import Piece
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Flowable, Table, PageBreak, NextPageTemplate
from reportlab.lib import pagesizes, units, utils
from reportlab.lib.colors import Color
"""
PDF generation views for the score manager application.
"""
import os
class RepertoireDocTemplate( BaseDocTemplate ):
def __init__(self, *args, **kwargs ):
BaseDocTemplate.__init__(self,*args, **kwargs)
from django.http import HttpResponse
from reportlab.lib import pagesizes, units, utils
from reportlab.lib.colors import Color
from reportlab.platypus import (
BaseDocTemplate,
Flowable,
Frame,
NextPageTemplate,
PageBreak,
PageTemplate,
Table,
)
self.pagesize = kwargs['pagesize']
from .models import Piece
leftBorder = 1 * units.cm
rightBorder = 1 * units.cm
topBorder = 3 * units.cm
bottomBorder = 1 * units.cm
frameWidth = (self.pagesize[0] - rightBorder - leftBorder ) /2
frameHeight = (self.pagesize[1] - topBorder - bottomBorder )
class RepertoireDocTemplate(BaseDocTemplate):
"""Document template for repertoire PDFs with header and two-column layout."""
leftColumn = Frame( leftBorder, bottomBorder, frameWidth, frameHeight, showBoundary=0 )
rightColumn = Frame( leftBorder+frameWidth , bottomBorder, frameWidth, frameHeight, showBoundary=0 )
def __init__(self, *args, **kwargs):
BaseDocTemplate.__init__(self, *args, **kwargs)
tocTemplate = PageTemplate( id='TOC', frames=[leftColumn,rightColumn], onPage=RepertoireDocTemplate._drawHeader )
fullPageTemplate = PageTemplate( id='FullPage', frames = [ Frame(0,0, self.pagesize[0], self.pagesize[1] ) ] )
self.pagesize = kwargs["pagesize"]
self.addPageTemplates( [ tocTemplate, fullPageTemplate ] )
left_border = 1 * units.cm
right_border = 1 * units.cm
top_border = 3 * units.cm
bottom_border = 1 * units.cm
frame_width = (self.pagesize[0] - right_border - left_border) / 2
frame_height = self.pagesize[1] - top_border - bottom_border
left_column = Frame(
left_border, bottom_border, frame_width, frame_height, showBoundary=0
)
right_column = Frame(
left_border + frame_width,
bottom_border,
frame_width,
frame_height,
showBoundary=0,
)
toc_template = PageTemplate(
id="TOC",
frames=[left_column, right_column],
onPage=RepertoireDocTemplate._draw_header,
)
full_page_template = PageTemplate(
id="FullPage",
frames=[Frame(0, 0, self.pagesize[0], self.pagesize[1])],
)
self.addPageTemplates([toc_template, full_page_template])
@staticmethod
def _drawHeader(canvas,document):
def _draw_header(canvas, document):
"""Draw the header with logo and gradient background."""
current_path = os.path.dirname(os.path.realpath(__file__))
logo_image_file = current_path + "/static/pdfImg/logo.png"
bg_image_file = current_path + "/static/pdfImg/body_bg.jpg"
currentPath = os.path.dirname(os.path.realpath(__file__))
logoImageFile = currentPath + "/static/pdfImg/logo.png"
bgImageFile = currentPath + "/static/pdfImg/body_bg.jpg"
#Gradient
titleAreaHeight = 2 * units.cm
gradientColors = [ Color(0.1,0.1,0.1), Color(0.2,0.2,0.2)]
# Gradient
title_area_height = 2 * units.cm
gradient_colors = [Color(0.1, 0.1, 0.1), Color(0.2, 0.2, 0.2)]
ps = document.pagesize
gradientStart = ( ps[0]/2, ps[1] )
gradientEnd = ( ps[0]/2, ps[1] - titleAreaHeight)
canvas.linearGradient( gradientStart[0],gradientStart[1], gradientEnd[0], gradientEnd[1],gradientColors, extend=False )
titleAreaMidY = 0.5 * ( gradientStart[1] + gradientEnd[1] )
gradient_start = (ps[0] / 2, ps[1])
gradient_end = (ps[0] / 2, ps[1] - title_area_height)
canvas.linearGradient(
gradient_start[0],
gradient_start[1],
gradient_end[0],
gradient_end[1],
gradient_colors,
extend=False,
)
title_area_mid_y = 0.5 * (gradient_start[1] + gradient_end[1])
# Draw Logo
logoImg = utils.ImageReader( logoImageFile )
logoSize = logoImg.getSize()
logoFraction = 0.6
logoSize = (logoFraction * logoSize[0], logoFraction*logoSize[1])
logoPosition = ( ps[0] - 5*units.cm, titleAreaMidY - 0.5*logoSize[1] )
canvas.drawImage( logoImg, logoPosition[0], logoPosition[1], width=logoSize[0], height=logoSize[1], mask=Color(0,0,0) )
if os.path.exists(logo_image_file):
logo_img = utils.ImageReader(logo_image_file)
logo_size = logo_img.getSize()
logo_fraction = 0.6
logo_size = (logo_fraction * logo_size[0], logo_fraction * logo_size[1])
logo_position = (
ps[0] - 5 * units.cm,
title_area_mid_y - 0.5 * logo_size[1],
)
canvas.drawImage(
logo_img,
logo_position[0],
logo_position[1],
width=logo_size[0],
height=logo_size[1],
mask=Color(0, 0, 0),
)
# Draw Title
text = canvas.beginText()
text.setTextOrigin( 1 * units.cm, titleAreaMidY-25/2 )
text.setFillColorRGB( 0.95,0.95,0.95 )
text.setFont( 'Helvetica', 25 )
text.textLine( "Inhaltsverzeichnis" )
canvas.drawText( text )
text.setTextOrigin(1 * units.cm, title_area_mid_y - 25 / 2)
text.setFillColorRGB(0.95, 0.95, 0.95)
text.setFont("Helvetica", 25)
text.textLine("Inhaltsverzeichnis")
canvas.drawText(text)
# Draw Background
bgImage = utils.ImageReader( bgImageFile )
bgImageSize = bgImage.getSize()
curPos = [0, gradientEnd[1]-bgImageSize[1] ]
while curPos[1] > -bgImageSize[1]:
curPos[0] = 0
while curPos[0] < ps[0]:
canvas.drawImage( bgImage, curPos[0], curPos[1] )
curPos[0] += bgImageSize[0]
curPos[1] -= bgImageSize[1]
if os.path.exists(bg_image_file):
bg_image = utils.ImageReader(bg_image_file)
bg_image_size = bg_image.getSize()
cur_pos = [0, gradient_end[1] - bg_image_size[1]]
while cur_pos[1] > -bg_image_size[1]:
cur_pos[0] = 0
while cur_pos[0] < ps[0]:
canvas.drawImage(bg_image, cur_pos[0], cur_pos[1])
cur_pos[0] += bg_image_size[0]
cur_pos[1] -= bg_image_size[1]
class PdfImage(Flowable):
"""PdfImage wraps the first page from a PDF file as a Flowable
which can be included into a ReportLab Platypus document.
Based on the vectorpdf extension in rst2pdf (http://code.google.com/p/rst2pdf/)"""
"""
PdfImage wraps the first page from a PDF file as a Flowable
which can be included into a ReportLab Platypus document.
def __init__(self, filename_or_object, page=0, width=None, height=None, kind='direct'):
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
Based on the vectorpdf extension in rst2pdf (http://code.google.com/p/rst2pdf/)
"""
def __init__(
self, filename_or_object, page=0, width=None, height=None, kind="direct"
):
Flowable.__init__(self)
try:
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
except ImportError:
raise ImportError(
"pdfrw is required for PDF embedding. Install it with: pip install pdfrw"
)
# If using StringIO buffer, set pointer to beginning
if hasattr(filename_or_object, 'read'):
if hasattr(filename_or_object, "read"):
filename_or_object.seek(0)
page = PdfReader(filename_or_object, decompress=False).pages[page]
self.xobj = pagexobj(page)
pdf_pages = PdfReader(filename_or_object, decompress=False).pages
if page >= len(pdf_pages):
page = 0
self.xobj = pagexobj(pdf_pages[page])
self.imageWidth = width
self.imageHeight = height
x1, y1, x2, y2 = self.xobj.BBox
@@ -99,36 +161,42 @@ class PdfImage(Flowable):
self.imageWidth = self._w
if not self.imageHeight:
self.imageHeight = self._h
self.__ratio = float(self.imageWidth)/self.imageHeight
if kind in ['direct','absolute'] or width==None or height==None:
self.__ratio = float(self.imageWidth) / self.imageHeight
if kind in ["direct", "absolute"] or width is None or height is None:
self.drawWidth = width or self.imageWidth
self.drawHeight = height or self.imageHeight
elif kind in ['bound','proportional']:
factor = min(float(width)/self._w,float(height)/self._h)
self.drawWidth = self._w*factor
self.drawHeight = self._h*factor
elif kind in ["bound", "proportional"]:
factor = min(float(width) / self._w, float(height) / self._h)
self.drawWidth = self._w * factor
self.drawHeight = self._h * factor
def wrap(self, aW, aH):
return self.drawWidth, self.drawHeight
def drawOn(self, canv, x, y, _sW=0):
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from pdfrw.toreportlab import makerl
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
if _sW > 0 and hasattr(self, 'hAlign'):
try:
from pdfrw.toreportlab import makerl
except ImportError:
raise ImportError("pdfrw is required for PDF embedding.")
if _sW > 0 and hasattr(self, "hAlign"):
a = self.hAlign
if a in ('CENTER', 'CENTRE', TA_CENTER):
x += 0.5*_sW
elif a in ('RIGHT', TA_RIGHT):
if a in ("CENTER", "CENTRE", TA_CENTER):
x += 0.5 * _sW
elif a in ("RIGHT", TA_RIGHT):
x += _sW
elif a not in ('LEFT', TA_LEFT):
elif a not in ("LEFT", TA_LEFT):
raise ValueError("Bad hAlign value " + str(a))
xobj = self.xobj
xobj_name = makerl(canv._doc, xobj)
xscale = self.drawWidth/self._w
yscale = self.drawHeight/self._h
xscale = self.drawWidth / self._w
yscale = self.drawHeight / self._h
x -= xobj.BBox[0] * xscale
y -= xobj.BBox[1] * yscale
@@ -141,81 +209,97 @@ class PdfImage(Flowable):
class Bookmark(Flowable):
""" Utility class to display PDF bookmark. """
"""Utility class to display PDF bookmark."""
def __init__(self, title, key):
self.title = title
self.key = key
Flowable.__init__(self)
def wrap(self, availWidth, availHeight):
""" Doesn't take up any space. """
"""Doesn't take up any space."""
return (0, 0)
def draw(self):
# set the bookmark outline to show when the file's opened
self.canv.showOutline()
# step 1: put a bookmark on the
# step 1: put a bookmark on the page
self.canv.bookmarkPage(self.key)
# step 2: put an entry in the bookmark outline
self.canv.addOutlineEntry(self.title, self.key, 0, 0)
def repertoire_pdf( request ):
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="Repertoire.pdf"'
def repertoire_pdf(request):
"""Generate a PDF containing the full repertoire with scores."""
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = 'attachment; filename="Repertoire.pdf"'
# Create the PDF object, using the response object as its "file.
# Create the PDF object, using the response object as its "file"
doc = RepertoireDocTemplate(response, pagesize=pagesizes.A4)
elements = []
elements.append( Bookmark("Inhaltsverzeichnis", "Contents") )
#TOC
elements.append(Bookmark("Inhaltsverzeichnis", "Contents"))
# Table of Contents
data = []
for piece in Piece.getRepertoire():
data.append( [ "%d %s" % ( piece.repertoire_nr, piece.title ) ] )
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
for piece in Piece.get_repertoire():
data.append([f"{piece.repertoire_nr} {piece.title}"])
if data:
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
elements.append( NextPageTemplate('FullPage') )
elements.append(NextPageTemplate("FullPage"))
elements.append(PageBreak())
pagesize=pagesizes.A4
for piece in Piece.getRepertoire():
score = piece.get_score_for_user( request.user )
pagesize = pagesizes.A4
for piece in Piece.get_repertoire():
score = piece.get_score_for_user(request.user)
if score is None:
continue
filename = score.file
bookmarkTitle = "%02d %s - %s " % ( piece.repertoire_nr, piece.title, score.score_type )
elements.append( Bookmark( bookmarkTitle, str(piece.id) ) )
#TODO support also multiple pages!!
image_flowable = PdfImage( filename, 0, width=pagesize[0]*0.98, height=pagesize[1] *0.98 )
image_flowable.hAlign = "CENTER"
elements.append( image_flowable )
bookmark_title = (
f"{piece.repertoire_nr:02d} {piece.title} - {score.score_type}"
)
elements.append(Bookmark(bookmark_title, str(piece.id)))
try:
# TODO: support multiple pages
image_flowable = PdfImage(
filename, 0, width=pagesize[0] * 0.98, height=pagesize[1] * 0.98
)
image_flowable.hAlign = "CENTER"
elements.append(image_flowable)
except Exception as e:
# Skip this score if PDF processing fails
print(f"Error processing PDF for {piece.title}: {e}")
continue
doc.build(elements)
return response
def repertoire_toc(request):
"""Generate a PDF containing only the table of contents."""
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = 'attachment; filename="Inhaltsverzeichnis.pdf"'
def repertoire_toc( request ):
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="Inhaltsverzeichnis.pdf"'
# Create the PDF object, using the response object as its "file.
doc = RepertoireDocTemplate(response,pagesize=pagesizes.A4 )
# Create the PDF object, using the response object as its "file"
doc = RepertoireDocTemplate(response, pagesize=pagesizes.A4)
elements = []
data = []
for piece in Piece.getRepertoire():
data.append( [ "%d %s" % ( piece.repertoire_nr, piece.title ) ] )
for piece in Piece.get_repertoire():
data.append([f"{piece.repertoire_nr} {piece.title}"])
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
if data:
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
doc.build(elements)
return response

View File

@@ -1,133 +1,130 @@
{% extends "website/base.html" %} {% load sekizai_tags staticfiles %}
{% extends "website/base.html" %} {% load sekizai_tags static %} {% block content %} {% addtoblock "css" %}
<style>
.pic-with-border {
border: 7px solid rgb(255, 255, 255);
border-radius: 5px;
box-shadow: 1px 1px 2px 0px rgb(207, 207, 207);
}
.box {
padding-bottom: 30px;
overflow: hidden;
margin-bottom: 30px;
border-bottom: 1px solid rgb(216, 216, 216);
}
{% block content %}
{% addtoblock "css" %}
<style>
.pic-with-border {
border: 7px solid rgb(255, 255, 255);
border-radius: 5px;
box-shadow: 1px 1px 2px 0px rgb(207, 207, 207);
}
.box {
padding-bottom: 30px;
overflow: hidden;
margin-bottom: 30px;
border-bottom: 1px solid rgb(216, 216, 216);
}
.sidebar {
margin: 0px;
padding: 40px 19px 40px 28px;
background: none repeat scroll 0% 0% rgb(255, 255, 255);
border-radius: 4px;
box-shadow: 1px 1px 2px 0px rgb(207, 207, 207);
float: right;
margin-bottom: 40px;
}
.sidebar_menu {
margin: 0px;
padding-top: 30px;
padding-left: 10px;
list-style: none outside none;
}
.sidebar_menu ul{
margin: 0;
padding: 0;
list-style: none;
}
.sidebar .sidebar_menu li {
margin-bottom: 6px;
}
a.inverse_color {
color: #333;
font-size: 14px;
-webkit-transition: color .2s;
-moz-transition: color .2s;
-ms-transition: color .2s;
transition: color .2s;
}
a.inverse_color:hover {
color: #1187D8;
}
.sidebar {
margin: 0px;
padding: 40px 19px 40px 28px;
background: none repeat scroll 0% 0% rgb(255, 255, 255);
border-radius: 4px;
box-shadow: 1px 1px 2px 0px rgb(207, 207, 207);
float: right;
margin-bottom: 40px;
}
.sidebar_menu {
margin: 0px;
padding-top: 30px;
padding-left: 10px;
list-style: none outside none;
}
.sidebar_menu ul {
margin: 0;
padding: 0;
list-style: none;
}
.sidebar .sidebar_menu li {
margin-bottom: 6px;
}
a.inverse_color {
color: #333;
font-size: 14px;
-webkit-transition: color 0.2s;
-moz-transition: color 0.2s;
-ms-transition: color 0.2s;
transition: color 0.2s;
}
a.inverse_color:hover {
color: #1187d8;
}
</style>
{% endaddtoblock %}
<div class="container">
<div class="row">
<div class="row">
<div class="span8">
<h2>Repertoire</h2>
<div class="container">
<div class="row">
<div class="row">
<div class="span8" >
<h2>Repertoire</h2>
<table class="table table-striped">
<thead>
<tr>
<th> # </th>
<th> Stück </th>
<th></th>
</tr>
</thead>
{% for piece in repertoire %}
<tr>
<td>
{{ piece.repertoire_nr }}
</td>
<td>
<a class="inverse_color" href="{% url 'scoremanager.views.piece_view' piece.pk %}">
{{ piece.title }}
</a>
</td>
<td>
{{ piece.booklocation }}
</td>
</tr>
{% endfor %}
</table>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Stück</th>
<th></th>
</tr>
</thead>
{% for piece in repertoire %}
<tr>
<td>{{ piece.repertoire_nr }}</td>
<td>
<a
class="inverse_color"
href="{% url 'scoremanager:piece_view' piece.pk %}"
>
{{ piece.title }}
</a>
</td>
<td>{{ piece.booklocation }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="span3 offset1 sidebar">
<div class="box">
<h4>Notenverwaltung</h4>
<img
class="pic-with-border"
src="{{STATIC_URL}}/img/scoreSheet.jpg"
/>
<div class="span3 offset1 sidebar">
<div class="box">
<h4>Notenverwaltung</h4>
<img class="pic-with-border" src="{{STATIC_URL}}img/scoreSheet.jpg" />
<ul class="sidebar_menu">
<ul>
<li>
<a class="inverse_color" href="{% url 'scoremanager.pdf_views.repertoire_toc' %}"> Inhaltsverzeichnis herunterladen </a>
<a
class="inverse_color"
href="{% url 'scoremanager:repertoire_toc' %}"
>
Inhaltsverzeichnis herunterladen
</a>
</li>
<li>
<a class="inverse_color" href="{% url 'scoremanager.pdf_views.repertoire_pdf' %}">Repertoire herunterladen</a>
<a
class="inverse_color"
href="{% url 'scoremanager:repertoire_pdf' %}"
>Repertoire herunterladen</a
>
</li>
{% if perms.scoremanager.manage_scores %}
<li>
<a class="inverse_color" href="{% url 'scoremanager.views.manage_repertoire' %}">
<a
class="inverse_color"
href="{% url 'scoremanager:manage_repertoire' %}"
>
Repertoire verwalten
</a>
</li>
{% endif %}
</ul>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -1,284 +1,282 @@
{% extends "website/base.html" %} {% load sekizai_tags staticfiles %}
{% extends "website/base.html" %} {% load sekizai_tags static %} {% block content %} {% addtoblock "js" strip %}
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
{% endaddtoblock %} {% addtoblock "js" strip %}
<script src="{{STATIC_URL}}/js/List.js"></script>
{% endaddtoblock %} {% addtoblock "js" strip %}
<script src="{{STATIC_URL}}/js/List.pagination.js"></script>
{% endaddtoblock %} {% addtoblock "css" strip %}
<link
rel="stylesheet"
href="{{STATIC_URL}}/css/jquery-ui-1.8.21.custom.css"
type="text/css"
media="screen"
/>
{% endaddtoblock %} {% addtoblock "css" %}
<style>
.piecelist {
position: relative;
padding: 45px 15px 15px;
margin: 0 -15px 15px;
background-color: #fafafa;
box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.05);
border-color: #e5e5e5 #eee #eee;
border-style: solid;
border-width: 1px 0;
min-height: 500px;
}
.piecelist:after {
content: "Stückliste";
position: absolute;
top: 15px;
left: 15px;
font-size: 12px;
font-weight: bold;
color: #bbb;
text-transform: uppercase;
letter-spacing: 1px;
}
.piecelist + .highlight {
margin: -15px -15px 15px;
border-radius: 0;
border-width: 0 0 1px;
}
@media (min-width: 768px) {
.piecelist {
margin-left: 0;
margin-right: 0;
background-color: #fff;
border-width: 1px;
border-color: #ddd;
border-radius: 4px 4px 0 0;
box-shadow: none;
}
.piecelist + .highlight {
margin-top: -16px;
margin-left: 0;
margin-right: 0;
border-width: 1px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
}
{% block content %}
input[type="text"].pieceSearch {
float: right;
z-index: 2;
margin-top: 15px;
margin-right: 15px;
font-size: 0.8em;
height: 12px;
width: 165px;
position: relative;
}
ul.piecelist {
list-style-type: none;
}
{% addtoblock "js" strip %} <script src="{{STATIC_URL}}js/jquery-ui-1.12.1.min.js"></script> {% endaddtoblock %}
{% addtoblock "js" strip %} <script src="{{STATIC_URL}}js/List.js"></script> {% endaddtoblock %}
{% addtoblock "js" strip %} <script src="{{STATIC_URL}}js/List.pagination.js"></script> {% endaddtoblock %}
{% addtoblock "css" strip %} <link rel="stylesheet" href="{{STATIC_URL}}css/jquery-ui-1.12.1.min.css" type="text/css" media="screen" /> {% endaddtoblock %}
ol.piecelist {
font-size: 16px;
font-weight: bold;
margin-bottom: 0;
margin-left: 0px; /* Since 'ul,ol{}' setting in line 108 affects this selector, 'margin-left' is redefined. */
list-style-position: inside; /* This will place the number on top and inside of the current color of li */
}
.piecelist .item {
color: #333;
font-size: 14px;
font-weight: normal;
height: 30px;
display: inline-block;
vertical-align: text-top;
width: 90%;
}
.piecelist .item img {
height: 40px;
float: left;
}
{% addtoblock "css" %}
<style>
.piecelist {
position: relative;
padding: 45px 15px 15px;
margin: 0 -15px 15px;
background-color: #fafafa;
box-shadow: inset 0 3px 6px rgba(0,0,0,.05);
border-color: #e5e5e5 #eee #eee;
border-style: solid;
border-width: 1px 0;
min-height: 500px;
}
.piecelist > li {
border-radius: 17px;
background-color: #dff0d8;
padding: 10px;
box-shadow: inset 0 1px 0 #fff;
}
.piecelist:after {
content: "Stückliste";
position: absolute;
top: 15px;
left: 15px;
font-size: 12px;
font-weight: bold;
color: #bbb;
text-transform: uppercase;
letter-spacing: 1px;
}
.piecelist + .highlight {
margin: -15px -15px 15px;
border-radius: 0;
border-width: 0 0 1px;
}
@media (min-width: 768px) {
.piecelist {
margin-left: 0;
margin-right: 0;
background-color: #fff;
border-width: 1px;
border-color: #ddd;
border-radius: 4px 4px 0 0;
box-shadow: none;
}
.piecelist + .highlight {
margin-top: -16px;
margin-left: 0;
margin-right: 0;
border-width: 1px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
}
input[type='text'].pieceSearch {
float: right;
z-index: 2;
margin-top: 15px;
margin-right: 15px;
font-size: 0.8em;
height: 12px;
width: 165px;
position: relative;
}
ul.piecelist{
list-style-type: none;
}
ol.piecelist {
font-size: 16px;
font-weight: bold;
margin-bottom:0;
margin-left: 0px; /* Since 'ul,ol{}' setting in line 108 affects this selector, 'margin-left' is redefined. */
list-style-position: inside; /* This will place the number on top and inside of the current color of li */
}
.piecelist .item {
color: #333;
font-size: 14px;
font-weight: normal;
height: 30px;
display: inline-block;
vertical-align:text-top;
width: 90%;
}
.piecelist .item img {
height: 40px;
float:left;
}
.piecelist .title {
font-weight: bold;
font-size: 1.1em;
}
.piecelist > li {
border-radius: 17px;
background-color: #dff0d8;
padding:10px;
box-shadow: inset 0 1px 0 #fff;
}
.piecelist .title {
font-weight: bold;
font-size: 1.1em;
}
.piecelist .composer {
font-style: italic;
font-size: 0.8em;
padding-left: 10px;
}
.piecelist .bookLocation {
font-style: italic;
font-size: 0.8em;
margin-left: 6px;
}
.piecelist li span {
width: 100%;
}
.pagination {
min-height: 30px;
}
.pagination li {
display:inline-block;
padding:5px;
}
#saveButton {
float: right;
margin-top: 20px;
}
</style>
.piecelist .composer {
font-style: italic;
font-size: 0.8em;
padding-left: 10px;
}
.piecelist .bookLocation {
font-style: italic;
font-size: 0.8em;
margin-left: 6px;
}
.piecelist li span {
width: 100%;
}
.pagination {
min-height: 30px;
}
.pagination li {
display: inline-block;
padding: 5px;
}
#saveButton {
float: right;
margin-top: 20px;
}
</style>
{% endaddtoblock %} {% addtoblock "js" %}
<script>
$(function () {
$("#allPieces li").draggable({
connectToSortable: "#repertoire",
appendTo: "parent",
helper: function () {
return $(this).clone().width($(this).width());
},
});
$("#allPieces li.in-repertoire").draggable("disable");
$("#repertoire")
.sortable({
receive: function (ev, ui) {
$(ui.item).draggable("disable");
$("#saveButton").removeAttr("disabled");
},
update: function (event, ui) {
$("#saveButton").removeAttr("disabled");
},
})
.disableSelection();
$("#allPieces").droppable({
accept: "#repertoire li",
hoverClass: "ui-state-hover",
drop: function (ev, ui) {
var id = $(ui.draggable).data("pieceid");
$("#allPieces li[data-pieceid=" + id + "]").draggable("enable");
ui.draggable.remove();
$("#saveButton").removeAttr("disabled");
},
});
$("#saveButton").click(function () {
var result = {};
$("#repertoire li").each(function (index, value) {
var id = parseInt($(this).data("pieceid"));
result[id] = index + 1;
});
$.ajax({
url: "{% url 'scoremanager:manage_repertoire_ajax_save' %} ",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(result),
dataType: "text",
success: function (result) {
$("#saveButton").attr("disabled", "disabled");
},
});
});
var options = {
valueNames: ["title", "composer"],
page: 5,
plugins: [
ListPagination({ paginationClass: "paginationListMarker" }),
],
};
var pieceList = new List("allPiecesList", options);
});
</script>
{% endaddtoblock %}
{% addtoblock "js" %}
<script>
$(function() {
$( "#allPieces li" ).draggable({
connectToSortable: "#repertoire" ,
appendTo: "parent",
helper: function(){
return $(this).clone().width($(this).width());
}
});
$( "#allPieces li.in-repertoire" ).draggable("disable");
$( "#repertoire" ).sortable( {
receive: function(ev, ui) {
$(ui.item).draggable("disable");
$('#saveButton').removeAttr('disabled');
},
update: function( event, ui ) {
$('#saveButton').removeAttr('disabled');
}
}).disableSelection();
$("#allPieces").droppable({
accept: "#repertoire li",
hoverClass: "ui-state-hover",
drop: function(ev, ui) {
var id = $(ui.draggable).data("pieceid");
$("#allPieces li[data-pieceid=" + id + "]" ).draggable("enable")
ui.draggable.remove();
$('#saveButton').removeAttr('disabled');
}
});
$( "#saveButton").click( function() {
var result = {};
$("#repertoire li").each( function(index,value) {
var id = parseInt( $(this).data("pieceid") );
result[id] = index +1;
});
$.ajax({
url: "{% url 'scoremanager.views.manage_repertoire_ajax_save' %} " ,
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(result),
dataType: 'text',
success: function(result) {
$('#saveButton').attr('disabled','disabled');
}
});
});
var options = {
valueNames : [ 'title', 'composer' ],
page : 5,
plugins : [ ListPagination({ paginationClass: 'paginationListMarker'}) ]
};
var pieceList = new List('allPiecesList', options);
});
</script>
{% endaddtoblock %}
<div class="container">
<div class="row">
<div class="row-fluid">
<div class="span12">
<h2>Repertoire Manager</h2>
</div>
</div>
<div class="row">
<div class="span6" id="allPiecesList">
<h4>Alle Stücke</h4>
<input class="search pieceSearch" type="text" placeholder="Suchen" />
<ul id="allPieces" class="piecelist list">
{% for piece in allPieces %}
<li data-pieceid="{{piece.pk}}" {% if not piece.repertoire_nr %} {% else %}class="in-repertoire"{% endif %}>
<p class="item">
<img src="{{STATIC_URL}}img/score-icon.png" />
<span class="title"> {{ piece.title }} </span>
<br/>
<span class="composer">{{ piece.composer}} </span>
{% if piece.booklocation %} <span class="bookLocation"> ( {{piece.booklocation}} ) </span> {% endif %}
</p>
</li>
{% endfor %}
</ul>
<div class="pagination">
<ul class="paginationListMarker"></ul>
</div>
</div>
<div class="span6">
<h4>Repertoire</h4>
<ol id="repertoire" class="piecelist" >
{% for piece in repertoire %}
<li data-pieceid="{{piece.pk}}">
<p class="item">
<img src="{{STATIC_URL}}img/score-icon.png" />
<span class="title"> {{ piece.title }} </span>
<br/>
<span class="composer">{{ piece.composer}} </span>
{% if piece.booklocation %} <span class="bookLocation"> ( {{piece.booklocation}} ) </span> {% endif %}
</p>
</li>
{% endfor %}
</ol>
<div class="row">
<div class="row-fluid">
<div class="span12">
<h2>Repertoire Manager</h2>
</div>
</div>
<div class="row">
<div class="span6" id="allPiecesList">
<h4>Alle Stücke</h4>
<button id="saveButton" class="btn btn-primary" disabled="true">Speichern</button>
<input
class="search pieceSearch"
type="text"
placeholder="Suchen"
/>
<ul id="allPieces" class="piecelist list">
{% for piece in allPieces %}
<li data-pieceid="{{piece.pk}}"{% if piece.repertoire_nr %} class="in-repertoire"{% endif %}>
<p class="item">
<img src="{{STATIC_URL}}/img/score-icon.png" />
<span class="title"> {{ piece.title }} </span>
<br />
<span class="composer">{{ piece.composer}} </span>
{% if piece.booklocation %}
<span class="bookLocation">
( {{piece.booklocation}} )
</span>
{% endif %}
</p>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="pagination">
<ul class="paginationListMarker"></ul>
</div>
</div>
<div class="span6">
<h4>Repertoire</h4>
<ol id="repertoire" class="piecelist">
{% for piece in repertoire %}
<li data-pieceid="{{piece.pk}}">
<p class="item">
<img src="{{STATIC_URL}}/img/score-icon.png" />
<span class="title"> {{ piece.title }} </span>
<br />
<span class="composer">{{ piece.composer}} </span>
{% if piece.booklocation %}
<span class="bookLocation">
( {{piece.booklocation}} )
</span>
{% endif %}
</p>
</li>
{% endfor %}
</ol>
<button id="saveButton" class="btn btn-primary" disabled="true">
Speichern
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "website/base.html" %}
{% load sekizai_tags staticfiles %}
{% load sekizai_tags static %}
{% block content %}
@@ -72,7 +72,7 @@
}
#section2 {
background: url("{{STATIC_URL}}img/backgrounds/aqua.jpg") no-repeat scroll 0% 0% / cover transparent;
background: url("{{STATIC_URL}}/img/backgrounds/aqua.jpg") no-repeat scroll 0% 0% / cover transparent;
display: block;
padding-top:50px;
padding-bottom:20px;
@@ -107,7 +107,7 @@
$("a.score-label").click( function() {
var id = $(this).data("id");
$.ajax( {
url: "{% url 'scoremanager.views.score_user_mapping_save' %}",
url: "{% url 'scoremanager:score_user_mapping_save' %}",
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: ' { "myScore" : ' + id + ' } ',
@@ -120,7 +120,7 @@
$(".delete-button").click(function() {
var id = $(this).data("id");
var scoreUrl = "{% url 'scoremanager.views.score' '4242' %}"
var scoreUrl = "{% url 'scoremanager:score' '4242' %}"
if (confirm('Diesen Notensatz wirklich löschen?')) {
$.ajax( {
@@ -141,7 +141,7 @@
"pieceId": "{{ piece.id }}" };
$.ajax( {
url: "{% url 'scoremanager.views.youtube_link' %}",
url: "{% url 'scoremanager:youtube_link' %}",
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data),
@@ -154,7 +154,7 @@
$(".youtubeDeleteButton").click( function() {
var id = $(this).data("id");
$.ajax( {
url: "{% url 'scoremanager.views.youtube_link' %}",
url: "{% url 'scoremanager:youtube_link' %}",
type: 'DELETE',
contentType: 'application/json; charset=utf-8',
data: ' { "linkid" : ' + id + ' } ',
@@ -178,7 +178,7 @@
<div class="span7">
{% if pictureScore %}
<a href="{{MEDIA_URL}}/{{pictureScore.file}}">
<img class="piece-pic img-responsive" src="{% url 'scoremanager.views.score' pictureScore.id %}"> </img>
<img class="piece-pic img-responsive" src="{% url 'scoremanager:score' pictureScore.id %}"> </img>
</a>
{% endif %}
@@ -307,4 +307,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -1,16 +1,25 @@
from django.conf.urls import url
from django.urls import path
import scoremanager.views
import scoremanager.pdf_views
from . import pdf_views, views
app_name = "scoremanager"
urlpatterns = [
url(r'^repertoireManager$', scoremanager.views.manage_repertoire),
url(r'^repertoireAjaxSave$$', scoremanager.views.manage_repertoire_ajax_save),
url(r'^$', scoremanager.views.list_repertoire),
url(r'^piece/(?P<pk>\d+)', scoremanager.views.piece_view),
url(r'^score_usermapping_save', scoremanager.views.score_user_mapping_save),
url(r'^score/(?P<pk>\d+)', scoremanager.views.score),
url(r'^youtubeLink$', scoremanager.views.youtube_link),
url(r'^inhaltsverzeichnis.pdf$', scoremanager.pdf_views.repertoire_toc),
url(r'^repertoire.pdf', scoremanager.pdf_views.repertoire_pdf),
path("", views.list_repertoire, name="list_repertoire"),
path("repertoireManager/", views.manage_repertoire, name="manage_repertoire"),
path(
"repertoireAjaxSave/",
views.manage_repertoire_ajax_save,
name="manage_repertoire_ajax_save",
),
path("piece/<int:pk>/", views.piece_view, name="piece_view"),
path(
"score_usermapping_save/",
views.score_user_mapping_save,
name="score_user_mapping_save",
),
path("score/<int:pk>/", views.score, name="score"),
path("youtubeLink/", views.youtube_link, name="youtube_link"),
path("inhaltsverzeichnis.pdf", pdf_views.repertoire_toc, name="repertoire_toc"),
path("repertoire.pdf", pdf_views.repertoire_pdf, name="repertoire_pdf"),
]

View File

@@ -1,154 +1,211 @@
"""
Views for the score manager application.
"""
from django.shortcuts import render
from django.http import HttpResponse
from django import forms
from django.conf import settings
from django.utils.translation import ugettext as _
from django.core.exceptions import PermissionDenied
from scoremanager.models import Piece, Score, Recording, ScoreUserMapping, YoutubeRecording
import json
import os
def manage_repertoire( request ):
context = {}
context['repertoire'] = Piece.getRepertoire()
context['allPieces' ] = Piece.objects.all().order_by( 'title' )
return render ( request, 'scoremanager/manage_repertoire.html', context )
def manage_repertoire_ajax_save( request ):
if request.is_ajax():
if request.method == 'POST':
result = json.loads( request.body )
parsedResult = { int(key): value for ( key,value ) in result.items() }
Piece.objects.all().update( repertoire_nr = None)
for piece in Piece.objects.all():
if piece.pk in parsedResult:
piece.repertoire_nr = parsedResult[piece.pk]
piece.save()
return HttpResponse("OK")
def list_repertoire( request ):
context = { 'repertoire': Piece.getRepertoire() }
return render( request, 'scoremanager/list_repertoire.html' , context )
from django import forms
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from .models import Piece, Recording, Score, ScoreUserMapping, YoutubeRecording
def manage_repertoire(request):
"""View for managing the repertoire order and contents."""
context = {
"repertoire": Piece.get_repertoire(),
"allPieces": Piece.objects.all().order_by("title"),
}
return render(request, "scoremanager/manage_repertoire.html", context)
def manage_repertoire_ajax_save(request):
"""AJAX endpoint for saving repertoire order changes."""
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
if request.method == "POST":
try:
result = json.loads(request.body)
parsed_result = {int(key): value for (key, value) in result.items()}
# Clear all repertoire numbers first
Piece.objects.all().update(repertoire_nr=None)
# Set new repertoire numbers
for piece in Piece.objects.all():
if piece.pk in parsed_result:
piece.repertoire_nr = parsed_result[piece.pk]
piece.save()
except (json.JSONDecodeError, ValueError) as e:
return HttpResponse(f"Error: {e}", status=400)
return HttpResponse("OK")
def list_repertoire(request):
"""View for listing the current repertoire."""
context = {"repertoire": Piece.get_repertoire()}
return render(request, "scoremanager/list_repertoire.html", context)
# ----------------------------- Piece View + Ajax views + Forms ------------------------------------------
def score_user_mapping_save( request ):
if request.is_ajax():
if request.method == 'POST':
result = json.loads( request.body )
print ( "Result " + str( result ) )
print ( "Resulting score: " + str( result['myScore'] ) )
result = int(result['myScore'])
ScoreUserMapping.add_user_score_mapping( user=request.user, score= Score.objects.get(pk=result) )
def score_user_mapping_save(request):
"""AJAX endpoint for saving a user's preferred score for a piece."""
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
if request.method == "POST":
try:
result = json.loads(request.body)
score_id = int(result["myScore"])
score = Score.objects.get(pk=score_id)
ScoreUserMapping.add_user_score_mapping(user=request.user, score=score)
except (
json.JSONDecodeError,
KeyError,
ValueError,
Score.DoesNotExist,
) as e:
return HttpResponse(f"Error: {e}", status=400)
return HttpResponse("OK")
def score( request, pk ):
requestedScore = Score.objects.get(pk=pk)
if request.method == 'GET':
imageFile = requestedScore.get_image_file()
image_data = open( settings.MEDIA_ROOT + imageFile, "rb").read()
return HttpResponse(image_data, content_type="image/jpeg")
def score(request, pk):
"""View for serving or deleting a score's preview image."""
try:
requested_score = Score.objects.get(pk=pk)
except Score.DoesNotExist:
return HttpResponse("Score not found", status=404)
if request.method == "GET":
image_file = requested_score.get_image_file()
image_path = os.path.join(settings.MEDIA_ROOT, image_file)
try:
with open(image_path, "rb") as f:
image_data = f.read()
return HttpResponse(image_data, content_type="image/jpeg")
except FileNotFoundError:
return HttpResponse("Image not found", status=404)
if request.method == "DELETE":
if requestedScore.uploaded_by != request.user or request.user.has_perm('scoremanager.manage_scores'):
if not (
requested_score.uploaded_by == request.user
or request.user.has_perm("scoremanager.manage_scores")
):
raise PermissionDenied
requestedScore.delete()
requested_score.delete()
return HttpResponse("OK")
return HttpResponse("Method not allowed", status=405)
def youtube_link( request ):
result = json.loads( request.body )
print("Youtube link: " + str(result) )
if request.method == 'DELETE':
linkId = int( result['linkid'] )
youtubeRecording = YoutubeRecording.objects.get( pk = linkId )
if youtubeRecording.uploaded_by == request.user or request.user.has_perm('scoremanager.manage_scores'):
youtubeRecording.delete()
else:
raise PermissionDenied
def youtube_link(request):
"""AJAX endpoint for adding or deleting YouTube links."""
try:
result = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponse("Invalid JSON", status=400)
return HttpResponse("OK")
if request.method == "DELETE":
try:
link_id = int(result["linkid"])
youtube_recording = YoutubeRecording.objects.get(pk=link_id)
elif request.method == 'POST':
link = str( result['link'] )
pieceId = int( result['pieceId'] )
if youtube_recording.uploaded_by == request.user or request.user.has_perm(
"scoremanager.manage_scores"
):
youtube_recording.delete()
else:
raise PermissionDenied
newRecording = YoutubeRecording(piece=Piece.objects.get(pk=pieceId), link=link)
newRecording.uploaded_by = request.user
newRecording.save()
return HttpResponse("OK")
return HttpResponse("OK")
except (KeyError, ValueError, YoutubeRecording.DoesNotExist) as e:
return HttpResponse(f"Error: {e}", status=400)
elif request.method == "POST":
try:
link = str(result["link"])
piece_id = int(result["pieceId"])
piece = Piece.objects.get(pk=piece_id)
new_recording = YoutubeRecording(piece=piece, link=link)
new_recording.uploaded_by = request.user
new_recording.save()
return HttpResponse("OK")
except (KeyError, ValueError, Piece.DoesNotExist) as e:
return HttpResponse(f"Error: {e}", status=400)
return HttpResponse("Method not allowed", status=405)
class UploadFileForm(forms.Form):
file = forms.FileField( max_length="80")
"""Form for uploading score and recording files."""
file = forms.FileField(max_length=80)
def clean_file(self):
f = self.cleaned_data['file']
extension = os.path.splitext( f.name )[1]
if extension != ".pdf" and extension !=".mp3" and extension != ".zip":
raise forms.ValidationError( _("Unknown extension. Allowed extension are mp3, pdf and zip"))
f = self.cleaned_data["file"]
extension = os.path.splitext(f.name)[1].lower()
allowed_extensions = [".pdf", ".mp3", ".zip"]
if extension not in allowed_extensions:
raise forms.ValidationError(
_("Unknown extension. Allowed extensions are mp3, pdf and zip")
)
return f
def piece_view( request, pk ):
currentPiece = Piece.objects.get( pk=pk )
context = {'piece': currentPiece }
def piece_view(request, pk):
"""View for displaying a piece's details and handling file uploads."""
try:
current_piece = Piece.objects.get(pk=pk)
except Piece.DoesNotExist:
return HttpResponse("Piece not found", status=404)
for score in currentPiece.scores.all():
if score.is_active_score( request.user ):
context['activeScore'] = score
context['pictureScore'] = score
context = {"piece": current_piece}
# Find active score for user
for score_obj in current_piece.scores.all():
if score_obj.is_active_score(request.user):
context["activeScore"] = score_obj
context["pictureScore"] = score_obj
break
if not 'pictureScore' in context.keys() and len( currentPiece.scores.all()) > 0:
context['pictureScore'] = currentPiece.scores.all()[0]
# Fall back to first score if no active score
if "pictureScore" not in context and current_piece.scores.exists():
context["pictureScore"] = current_piece.scores.first()
if request.method == 'POST':
if request.method == "POST":
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
f = request.FILES['file']
[basename,extension] = os.path.splitext( f.name )
print ("extension " + extension + " basename " + basename)
if extension == ".mp3":
print("Uploaded Recording")
recording = Recording( piece=currentPiece, artist=basename, file=f )
recording.uploaded_by=request.user
recording.save()
elif extension == ".pdf":
print("Uploaded Score")
score = Score( piece=currentPiece, score_type=basename, file=f )
score.uploaded_by=request.user
score.save()
elif extension == ".zip":
#TODO
print ("uploaded zip - not yet supported")
f = request.FILES["file"]
basename, extension = os.path.splitext(f.name)
extension = extension.lower()
if extension == ".mp3":
recording = Recording(piece=current_piece, artist=basename, file=f)
recording.uploaded_by = request.user
recording.save()
elif extension == ".pdf":
score_obj = Score(piece=current_piece, score_type=basename, file=f)
score_obj.uploaded_by = request.user
score_obj.save()
elif extension == ".zip":
# TODO: Handle zip file uploads
pass
else:
form = UploadFileForm()
context['form'] = form
return render( request, 'scoremanager/piece_view.html', context )
context["form"] = form
return render(request, "scoremanager/piece_view.html", context)