Scoremanager: pdf_views

This commit is contained in:
Martin Bauer 2014-02-15 18:07:00 +01:00 committed by Martin Bauer
parent 7ad5ec21fc
commit 5a3d739a9b
16 changed files with 1453 additions and 4 deletions

View File

@ -160,7 +160,7 @@ INSTALLED_APPS = (
'eventplanner', # Event Management
'simpleforum', # Messages ( Forum )
'location_field', # custom location field used in Event Management
'scoremanager', # manager of scores, repertoire etc.
#'imagestore',
#'sorl.thumbnail',
#'tagging'

View File

@ -8,7 +8,7 @@ import eventplanner.urls
import musicians.urls
#import imagestore.urls
import website.urls
import scoremanager.urls
import settings
from django.conf.urls.static import static
@ -19,6 +19,7 @@ urlpatterns = patterns('',
url(r'^', include(website.urls) ),
url(r'^events/', include( eventplanner.urls.urlpatterns) ),
url(r'^musicians/', include( musicians.urls.urlpatterns) ),
url(r'^scores/', include( scoremanager.urls.urlpatterns) ),
url(r'^messages/$', simpleforum.views.message_view ),
url(r'^admin/', include(admin.site.urls) ),
url(r'^location_field/', include('location_field.urls')),

View File

@ -48,7 +48,7 @@ class LocationWidget(widgets.TextInput):
}
# Use schemaless URL so it works with both, http and https websites
js = (
'//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', # jquery
'//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js', # jquery
'//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js',
'//maps.google.com/maps/api/js?sensor=false&language=de',
'/static/js/bindWithDelay.js',

123
scoremanager/PdfImage.py Normal file
View File

@ -0,0 +1,123 @@
#!/usr/bin/env python
# encoding: utf-8
"""matplotlib_example.py
An simple example of how to insert matplotlib generated figures
into a ReportLab platypus document.
"""
import matplotlib
matplotlib.use('PDF')
import matplotlib.pyplot as plt
import cStringIO
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
from reportlab.platypus import Flowable
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch
PAGE_HEIGHT=defaultPageSize[1]; PAGE_WIDTH=defaultPageSize[0]
styles = getSampleStyleSheet()
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/)"""
def __init__(self, filename_or_object, width=None, height=None, kind='direct'):
from reportlab.lib.units import inch
# If using StringIO buffer, set pointer to begining
if hasattr(filename_or_object, 'read'):
filename_or_object.seek(0)
page = PdfReader(filename_or_object, decompress=False).pages[0]
self.xobj = pagexobj(page)
self.imageWidth = width
self.imageHeight = height
x1, y1, x2, y2 = self.xobj.BBox
self._w, self._h = x2 - x1, y2 - y1
if not self.imageWidth:
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.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
def wrap(self, aW, aH):
return self.drawWidth, self.drawHeight
def drawOn(self, canv, x, y, _sW=0):
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):
x += _sW
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
x -= xobj.BBox[0] * xscale
y -= xobj.BBox[1] * yscale
canv.saveState()
canv.translate(x, y)
canv.scale(xscale, yscale)
canv.doForm(xobj_name)
canv.restoreState()
Title = "Hello world"
pageinfo = "platypus example"
def myFirstPage(canvas, doc):
canvas.saveState()
canvas.setFont('Times-Bold',16)
canvas.drawCentredString(PAGE_WIDTH/2.0, PAGE_HEIGHT-108, Title)
canvas.setFont('Times-Roman',9)
canvas.drawString(inch, 0.75 * inch, "First Page / %s" % pageinfo)
canvas.restoreState()
def myLaterPages(canvas, doc):
canvas.saveState()
canvas.setFont('Times-Roman',9)
canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, pageinfo))
canvas.restoreState()
def go():
fig = plt.figure(figsize=(4, 3))
plt.plot([1,2,3,4])
plt.ylabel('some numbers')
imgdata = cStringIO.StringIO()
fig.savefig(imgdata,format='PDF')
doc = SimpleDocTemplate("document.pdf")
Story = [Spacer(1,2*inch)]
style = styles["Normal"]
for i in range(5):
bogustext = ("This is Paragraph number %s. " % i) *20
p = Paragraph(bogustext, style)
Story.append(p)
Story.append(Spacer(1,0.2*inch))
pi = PdfImage(imgdata)
Story.append(pi)
Story.append(Spacer(1,0.2*inch))
doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)
if __name__ == '__main__':
go()

0
scoremanager/__init__.py Normal file
View File

22
scoremanager/admin.py Normal file
View File

@ -0,0 +1,22 @@
from django.contrib import admin
from scoremanager.models import Piece, Score, Recording, BookLocation
class ScoreInline( admin.StackedInline ):
model = Score
extra = 1
class RecordingInline( admin.StackedInline ):
model = Recording
extra = 1
class BookLocationInline( admin.StackedInline ):
model = BookLocation
extra = 1
class PieceAdmin (admin.ModelAdmin):
inlines = (BookLocationInline, ScoreInline, RecordingInline )
admin.site.register( Piece, PieceAdmin )

183
scoremanager/models.py Normal file
View File

@ -0,0 +1,183 @@
import os
import re
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
############################# Helper Functions #######################################################
def space_to_camelcase(value):
def camelcase():
yield type(value).lower
while True:
yield type(value).capitalize
c = camelcase()
return "".join(c.next()(x) if x else '_' for x in value.split() )
def score_filename(score,original_name):
fileExtension = os.path.splitext(original_name)[1]
filename = "scores/"
filename += space_to_camelcase( score.piece.title ) + "/"
filename += space_to_camelcase( score.score_type )
filename += fileExtension
return filename
def recordingFileName( recording, originalName ):
fileExtension = os.path.splitext(originalName)[1]
filename = "recordings/"
filename += space_to_camelcase( recording.piece.title ) + "/"
filename += space_to_camelcase( recording.artist )
filename += fileExtension
return filename
_
#######################################################################################################
class Piece (models.Model):
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' )
class Meta:
permissions = (
("manage_scores", "Administrate and manage scores"),
)
class BookLocation ( models.Model ):
piece = models.ForeignKey ( 'Piece' )
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 Score( models.Model ):
piece = ForeignKey('Piece', related_name="scores")
score_type = models.CharField(max_length=100, verbose_name="score type" ) # for example partitur, unterstimmen ...
file = models.FileField(upload_to=score_filename, verbose_name = _("file") )
uploaded_by = ForeignKey( User, verbose_name=_("uploaded_by") )
@property
def image_file_name(self):
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
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
return ret
def get_image_file(self):
from django.conf import settings
inputFile = settings.MEDIA_ROOT + "/" + str(self.file)
cacheFile = 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 )
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']
class Recording( models.Model ):
piece = ForeignKey( 'Piece', 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") )
class Meta:
unique_together = (("piece", "artist"),)
ordering = ['artist']
class YoutubeRecording( models.Model ):
piece = models.ForeignKey('Piece', related_name="youtubeLinks" )
link = models.CharField( max_length = 300, blank=False)
uploaded_by = ForeignKey( User, verbose_name=_("uploaded_by") )
youtubeRegex = re.compile( u'(?:https://)?(?:http://)?www.youtube.(?:com|de)/watch\?v=(?P<videoID>[-\w]*)' )
@property
def embed_html( self ):
replacement = """
<div class="embed-container"><iframe src="//www.youtube.de/embed/\g<videoID>" frameborder="0" allowfullscreen></iframe> </div>
"""
return mark_safe( YoutubeRecording.youtubeRegex.sub( replacement, self.link ) )
class Meta:
unique_together = (("link", "piece" ))
class ScoreUserMapping( models.Model):
score = ForeignKey( 'Score' )
user = models.OneToOneField( User )
piece = ForeignKey( 'Piece' )
@staticmethod
def add_user_score_mapping( score, user ):
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" ))

221
scoremanager/pdf_views.py Normal file
View File

@ -0,0 +1,221 @@
# ----------------------------- 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
import os
class RepertoireDocTemplate( BaseDocTemplate ):
def __init__(self, *args, **kwargs ):
BaseDocTemplate.__init__(self,*args, **kwargs)
self.pagesize = kwargs['pagesize']
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 )
leftColumn = Frame( leftBorder, bottomBorder, frameWidth, frameHeight, showBoundary=0 )
rightColumn = Frame( leftBorder+frameWidth , bottomBorder, frameWidth, frameHeight, showBoundary=0 )
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.addPageTemplates( [ tocTemplate, fullPageTemplate ] )
@staticmethod
def _drawHeader(canvas,document):
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)]
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] )
# 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) )
# 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 )
# 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]
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/)"""
def __init__(self, filename_or_object, page=0, width=None, height=None, kind='direct'):
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
# If using StringIO buffer, set pointer to beginning
if hasattr(filename_or_object, 'read'):
filename_or_object.seek(0)
page = PdfReader(filename_or_object, decompress=False).pages[page]
self.xobj = pagexobj(page)
self.imageWidth = width
self.imageHeight = height
x1, y1, x2, y2 = self.xobj.BBox
self._w, self._h = x2 - x1, y2 - y1
if not self.imageWidth:
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.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
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
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):
x += _sW
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
x -= xobj.BBox[0] * xscale
y -= xobj.BBox[1] * yscale
canv.saveState()
canv.translate(x, y)
canv.scale(xscale, yscale)
canv.doForm(xobj_name)
canv.restoreState()
class Bookmark(Flowable):
""" 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. """
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
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"'
# Create the PDF object, using the response object as its "file.
doc = RepertoireDocTemplate(response, pagesize=pagesizes.A4)
elements = []
elements.append( Bookmark("Inhaltsverzeichnis", "Contents") )
#TOC
data = []
for piece in Piece.getRepertoire():
data.append( [ "%d %s" % ( piece.repertoire_nr, piece.title ) ] )
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
elements.append( NextPageTemplate('FullPage') )
elements.append(PageBreak())
pagesize=pagesizes.A4
for piece in Piece.getRepertoire():
score = piece.get_score_for_user( request.user )
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 )
doc.build(elements)
return response
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 )
elements = []
data = []
for piece in Piece.getRepertoire():
data.append( [ "%d %s" % ( piece.repertoire_nr, piece.title ) ] )
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
doc.build(elements)
return response

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,133 @@
{% extends "website/base.html" %} {% load sekizai_tags staticfiles %}
{% 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;
}
</style>
{% endaddtoblock %}
<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>
<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>
</li>
<li>
<a class="inverse_color" href="{% url 'scoremanager.pdf_views.repertoire_pdf' %}">Repertoire herunterladen</a>
</li>
{% if perms.scoremanager.manage_scores %}
<li>
<a class="inverse_color" href="{% url 'scoremanager.views.manage_repertoire' %}">
Repertoire verwalten
</a>
</li>
{% endif %}
</ul>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,284 @@
{% extends "website/base.html" %} {% load sekizai_tags staticfiles %}
{% 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,.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;
}
}
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 > 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>
{% 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>
<button id="saveButton" class="btn btn-primary" disabled="true">Speichern</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,310 @@
{% extends "website/base.html" %}
{% load sekizai_tags staticfiles %}
{% block content %}
{% addtoblock "css" %}
<style>
.piece-pic {
border: 7px solid rgb(255, 255, 255);
border-radius: 5px;
box-shadow: 1px 1px 2px 1px rgb(207, 207, 207);
margin-top:10px;
margin-bottom:20px;
}
.sidebar {
margin: 10px 0px 0px 0px;
padding: 40px 19px 20px 28px;
background: none repeat scroll 0% 0% rgb(255, 255, 255);
border-radius: 4px;
box-shadow: 0px 1px 3px 1px rgb(207, 207, 207);
float: right;
}
.error {
color: red;
margin:15px;
}
.upload_form {
margin-top:15px;
}
.upload-area {
margin: 15px;
}
.help-text {
font-size: 0.9em;
font-style: italic;
}
.embed-container {
position: relative;
padding-bottom: 56.25%; /* 16/9 ratio */
padding-top: 30px; /* IE6 workaround*/
height: 0;
overflow: hidden;
}
.embed-container iframe,
.embed-container object,
.embed-container embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.youtubeWrapper {
width: 30%;
float: left;
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
}
.youtubeCaption {
text-align: center;
}
#section1 {
padding-bottom:50px;
}
#section2 {
background: url("{{STATIC_URL}}/img/backgrounds/aqua.jpg") no-repeat scroll 0% 0% / cover transparent;
display: block;
padding-top:50px;
padding-bottom:20px;
}
#section2 h4 {
font-size: 38px;
margin-top: 0px;
margin-bottom: 25px;
line-height: 38px;
text-align: center;
color: rgb(255, 255, 255);
}
#section2 .upload {
text-align: center;
display:block;
}
</style>
{% endaddtoblock %}
{% addtoblock "js" %}
<script type="text/javascript">
$(function() {
$(".upload_form input:file").change(function (){
$(".upload_form").submit();
});
$("a.score-label").click( function() {
var id = $(this).data("id");
$.ajax( {
url: "{% url 'scoremanager.views.score_user_mapping_save' %}",
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: ' { "myScore" : ' + id + ' } ',
dataType: 'text',
success: function(result) {
location.reload();
}
});
});
$(".delete-button").click(function() {
var id = $(this).data("id");
var scoreUrl = "{% url 'scoremanager.views.score' '4242' %}"
if (confirm('Diesen Notensatz wirklich löschen?')) {
$.ajax( {
url: scoreUrl.replace( '4242', id.toString() ),
type: 'DELETE',
contentType: 'application/json; charset=utf-8',
success: function(result) {
location.reload();
}
});
}
});
$("#youtubeLinkInput").keypress(function(e){
if (e.keyCode != 13) { return }
e.preventDefault();
var data = { "link": this.value,
"pieceId": "{{ piece.id }}" };
$.ajax( {
url: "{% url 'scoremanager.views.youtube_link' %}",
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data),
success: function(result) {
location.reload();
}
});
});
$(".youtubeDeleteButton").click( function() {
var id = $(this).data("id");
$.ajax( {
url: "{% url 'scoremanager.views.youtube_link' %}",
type: 'DELETE',
contentType: 'application/json; charset=utf-8',
data: ' { "linkid" : ' + id + ' } ',
dataType: 'text',
success: function(result) {
location.reload();
}
});
});
});
</script>
{% endaddtoblock %}
<div id="section1">
<div class="container">
<div class="row">
<h2>{{ piece.title }}</h2>
<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>
</a>
{% endif %}
<h3>Info</h3>
<table class="table table-striped">
{% if piece.composer %}
<tr>
<td>Komponist:</td>
<td>{{piece.composer}} </td>
</tr>
{% endif %}
{% if piece.booklocation %}
<tr>
<td>Buch: </td>
<td>{{piece.booklocation }} </td>
</tr>
{% endif %}
</table>
{% if piece.scores.all|length > 0 %}
<h3>Alle Noten</h3>
<table class="table table-striped">
{% for score in piece.scores.all %}
<tr>
<td>
<a href="{{MEDIA_URL}}/{{score.file}}">
{{ score.score_type }}
</a>
</td>
<td>
{% if score == activeScore %}
<span data-id="{{score.id}}" class="label label-success score-label">Meine Noten</span>
{% else %}
<a data-id="{{score.id}}" class="btn btn-mini score-label">Meine Noten</a>
{% endif %}
</td>
<td>
{% if score.uploaded_by == request.user or perms.scoremanager.manage_scores %}
<a class="btn btn-mini btn-danger delete-button" data-id="{{score.id}}">Löschen</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% if piece.recordings.all|length > 0 %}
<h3>Aufnahmen</h3>
<ul>
{% for recording in piece.recordings.all %}
<li>
<a href="{{MEDIA_URL}}/{{ recording.file }}">
{{ recording.artist }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="span4 sidebar offset1">
<h4>Upload</h4>
<div class="upload-area">
<div class="help-text">
Hier kann jeder Noten oder Aufnahmen hochladen. Der Dateiname wird als Interpret (für Aufnahmen) bzw. Stimme (für Noten)
verwendet. Zip Dateien, die MP3s und/oder PDFs enthalten funktionieren auch.
</div>
<form class="upload_form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if form.file.errors|length > 0 %}
<div class="error">Problem beim Upload. Erlaubte Dateien: mp3,pdf und zip</div>
{% endif %}
{{ form.file }}
<input type="submit" class="hidden" value="Ok"></input>
</form>
</div>
</div>
</div>
</div>
</div>
<div id='section2'>
<div class="container">
<div class="row">
<div class="span12">
<h4>
Youtube<br>
</h4>
<div class="upload">
<input id="youtubeLinkInput" type="text" placeholder="Hier Youtube Link reinkopieren"></input>
</div>
{% if piece.youtubeLinks.all|length > 0 %}
<div>
{% for youtubeLink in piece.youtubeLinks.all %}
<div class="youtubeWrapper">
{{ youtubeLink.embed_html }}
{% if youtubeLink.uploaded_by == request.user or perms.scoremanager.manage_scores %}
<div class="youtubeCaption">
<button class="btn btn-mini btn-danger youtubeDeleteButton" data-id="{{youtubeLink.id}}">
Löschen
</button>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

17
scoremanager/urls.py Normal file
View File

@ -0,0 +1,17 @@
from django.conf.urls import patterns, url
import scoremanager.views
import scoremanager.pdf_views
urlpatterns = patterns('',
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 ),
)

154
scoremanager/views.py Normal file
View File

@ -0,0 +1,154 @@
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 )
# ----------------------------- 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) )
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, mimetype="image/jpeg")
if request.method == "DELETE":
if requestedScore.uploaded_by != request.user or request.user.has_perm('scoremanager.manage_scores'):
raise PermissionDenied
requestedScore.delete()
return HttpResponse("OK")
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
return HttpResponse("OK")
elif request.method == 'POST':
link = str( result['link'] )
pieceId = int( result['pieceId'] )
newRecording = YoutubeRecording(piece=Piece.objects.get(pk=pieceId), link=link)
newRecording.uploaded_by = request.user
newRecording.save()
return HttpResponse("OK")
class UploadFileForm(forms.Form):
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"))
def piece_view( request, pk ):
currentPiece = Piece.objects.get( pk=pk )
context = {'piece': currentPiece }
for score in currentPiece.scores.all():
if score.is_active_score( request.user ):
context['activeScore'] = score
context['pictureScore'] = score
if not 'pictureScore' in context.keys() and len( currentPiece.scores.all()) > 0:
context['pictureScore'] = currentPiece.scores.all()[0]
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")
else:
form = UploadFileForm()
context['form'] = form
return render( request, 'scoremanager/piece_view.html', context )

View File

@ -37,6 +37,7 @@
<li><a href="/events"> Termine</a></li>
<li><a href="/messages">Forum</a></li>
<li><a href="/musicians">Adressbuch</a></li>
<li><a href="/scores"> Noten</a></li>
{% endblock %}
{% if user.is_authenticated %}
@ -70,7 +71,7 @@
<div class="span12">
<div class="row copyright">
<div class="span12">
© 2013 Blechreiz, Martin Bauer
© 2014 Blechreiz, Martin Bauer
</div>
</div>
</div>