port to new django, AI automated
This commit is contained in:
95
scoremanager/migrations/0001_initial.py
Normal file
95
scoremanager/migrations/0001_initial.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
scoremanager/migrations/__init__.py
Normal file
0
scoremanager/migrations/__init__.py
Normal 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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user