diff --git a/jm2l/__init__.py b/jm2l/__init__.py index 40e9f00..ce22a61 100644 --- a/jm2l/__init__.py +++ b/jm2l/__init__.py @@ -147,6 +147,9 @@ def main(global_config, **settings): ## Users config.add_route('pict_user', '/user_picture') config.add_route('show_user', '/user/{user_slug:([\w-]+)?}') + config.add_route('badge_user', '/user/{user_slug:([\w-]+)?}/badge') + config.add_route('badge_user1', '/user/{user_slug:([\w-]+)?}/badge1') + config.add_route('badge_user2', '/user/{user_slug:([\w-]+)?}/badge2') # HTML Routes - Logged #config.add_route('profil', 'MesJM2L') diff --git a/jm2l/badge.py b/jm2l/badge.py new file mode 100644 index 0000000..7f620e2 --- /dev/null +++ b/jm2l/badge.py @@ -0,0 +1,561 @@ +# -*- coding: utf8 -*- +from pyramid.httpexceptions import HTTPNotFound +from pyramid.response import Response +import cStringIO as StringIO +from pyramid.view import view_config +from .models import User + +from reportlab.pdfgen import canvas +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.lib.units import mm +import qrcode + +# Create PDF container +WIDTH = 85 * mm +HEIGHT = 60 * mm +ICONSIZE = 8 * mm + +def Ribbon35(DispUser, canvas): + canvas.saveState() + canvas.rotate(35) + offset_u=0 + if DispUser.Staff: + # Staff + canvas.setFillColorRGB(1,.2,.2) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(1,1,1) + canvas.setFont('Liberation', 20) + canvas.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+5, "STAFF") + elif DispUser.is_Intervenant: + # Intervenant + canvas.setFillColorRGB(.3,.3,1) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(1,1,1) + canvas.setFont('Liberation', 15) + canvas.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+10, "Intervenant") + else: + # Visiteur + canvas.setFillColorRGB(.8,.8,.8) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(0,0,0) + canvas.setFont('Liberation', 12) + canvas.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+7, "Visiteur") + canvas.restoreState() + return canvas + +def Ribbon45(DispUser, canvas): + canvas.saveState() + canvas.rotate(45) + offset_u=0 + if DispUser.Staff: + # Staff + canvas.setFillColorRGB(1,.2,.2) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(1,1,1) + canvas.setFont('Liberation', 20) + canvas.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+5, "STAFF") + elif DispUser.is_Intervenant: + # Intervenant + canvas.setFillColorRGB(.3,.3,1) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(1,1,1) + canvas.setFont('Liberation', 15) + canvas.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+10, "Intervenant") + else: + # Visiteur + canvas.setFillColorRGB(.8,.8,.8) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(0,0,0) + canvas.setFont('Liberation', 12) + canvas.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+7, "Visiteur") + canvas.restoreState() + return canvas + +def Ribbon90(DispUser, canvas): + canvas.saveState() + canvas.rotate(90) + offset_u=0 + if DispUser.Staff: + # Staff + canvas.setFillColorRGB(1,.2,.2) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(1,1,1) + canvas.setFont('Liberation', 20) + canvas.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+5, "STAFF") + elif DispUser.is_Intervenant: + # Intervenant + canvas.setFillColorRGB(.3,.3,1) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(1,1,1) + canvas.setFont('Liberation', 15) + canvas.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+10, "Intervenant") + else: + # Visiteur + canvas.setFillColorRGB(.8,.8,.8) + canvas.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + canvas.setFillColorRGB(0,0,0) + canvas.setFont('Liberation', 12) + canvas.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+7, "Visiteur") + canvas.restoreState() + return canvas + +def JM2L_Logo(canvas, Position="Up"): + # Import font + ttfFile_Logo = "jm2l/static/fonts/PWTinselLetters.ttf" + pdfmetrics.registerFont(TTFont("Logo", ttfFile_Logo)) + + if Position=="Up": + logoobject = canvas.beginText() + logoobject.setFont('Logo', 52) + logoobject.setFillColorRGB(.83,0,.33) + logoobject.setTextOrigin(55, HEIGHT-50) + logoobject.textLines("JM2L") + canvas.drawText(logoobject) + + yearobject = canvas.beginText() + yearobject.setFont("Helvetica-Bold", 15) + yearobject.setFillColorRGB(1,1,1) + yearobject.setTextOrigin(67, HEIGHT-20) + yearobject.setWordSpace(21) + yearobject.textLines("2 0 1 5") + canvas.drawText(yearobject) + elif Position=="Down": + logoobject = canvas.beginText() + logoobject.setFont('Logo', 32) + logoobject.setFillColorRGB(.83,0,.33) + logoobject.setTextOrigin(2, 17) + logoobject.textLines("JM2L") + canvas.drawText(logoobject) + + yearobject = canvas.beginText() + yearobject.setFont("Helvetica-Bold", 10) + yearobject.setFillColorRGB(1,1,1) + #yearobject.setLineWidth(.1) + #yearobject.setStrokeColorRGB(.5,.5,.5) + yearobject.setTextRenderMode(0) + #yearobject.setStrokeOverprint(.2) + yearobject.setTextOrigin(9 , 35) + yearobject.setWordSpace(13) + yearobject.textLines("2 0 1 5") + canvas.drawText(yearobject) + +def Tiers_Logo(canvas, DispUser, LWidth=4, StartPos=None): + Border = 1 + if StartPos is None: + StartPos = ( WIDTH -70, 0 ) + StartX, StartY = StartPos + num = 0 + canvas.setStrokeColorRGB(0.5,0.5,0.5) + Logos = filter(lambda x:x.ThumbLinks, DispUser.tiers) + for tiers in Logos: + FileName = tiers.ThumbLinks.pop().split("/")[-1] + ImagePath = "jm2l/upload/images/tiers/%s/%s" % (tiers.slug, FileName) + # Should We compute a better positionning for logos ? + #PosX = StartX + (( (2.0*num+1) / 2*len(Logos) )*(ICONSIZE+Border) - ((ICONSIZE+Border)/2)) + PosX = StartX + (num % LWidth) * (ICONSIZE+Border) + if len(Logos)<=LWidth: + # Middle of line + PosY = StartY + 0.5 * (ICONSIZE+Border) + else: + # in two or more lines + PosY = StartY + (num / LWidth) * (ICONSIZE+Border) + canvas.setLineWidth(.1) + canvas.drawImage(ImagePath, + PosX, PosY, ICONSIZE, ICONSIZE,\ + preserveAspectRatio=True, + anchor='c', + mask=[0,3,0,3,0,3] + ) + canvas.roundRect(PosX, PosY, ICONSIZE, ICONSIZE, radius=2, stroke=True) + num+=1 + +def QRCode(DispUser): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=2, + ) + # Data of QR code + qr.add_data('http://jm2l.linux-azur.org/user/%s' % DispUser.slug) + qr.make(fit=True) + + return qr.make_image() + +@view_config(route_name='badge_user') +def badge_user(request): + user_slug = request.matchdict.get('user_slug', None) + if user_slug is None or len(user_slug)==0: + raise HTTPNotFound(u"Cet utilisateur n'a pas été reconnu") + # Query database + DispUser = User.by_slug(user_slug) + if DispUser is None: + raise HTTPNotFound() + + # Ok let's generate a PDF Badge + + # Register LiberationMono font + ttfFile = "jm2l/static/fonts/LiberationMono-Regular.ttf" + pdfmetrics.registerFont(TTFont("Liberation", ttfFile)) + + pdf = StringIO.StringIO() + + c = canvas.Canvas( pdf, pagesize=(WIDTH, HEIGHT) ) + c.translate(mm, mm) + + # Feed some metadata + c.setCreator("linux-azur.org") + c.setTitle("Badge") + + c.saveState() + # Logo on Top + JM2L_Logo(c, "Down") + + if DispUser.Staff: + # Staff + c.setFillColorRGB(.83,0,.33) + c.rect(-3, HEIGHT-30, WIDTH, HEIGHT, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 30) + c.drawCentredString(WIDTH/2, HEIGHT-26, "STAFF") + elif DispUser.is_Intervenant: + # Intervenant + c.setFillColorRGB(.3,.3,1) + c.rect(-3, HEIGHT-30, WIDTH, HEIGHT, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 30) + c.drawCentredString(WIDTH/2, HEIGHT-26, "Intervenant") + else: + # Visiteur + c.setFillColorRGB(.8,.8,.8) + c.rect(-3, HEIGHT-30, WIDTH, HEIGHT, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 30) + c.drawCentredString(WIDTH/2, HEIGHT-26, "Visiteur") + + c.restoreState() + + c.setFont('Liberation', 18) + c.setStrokeColorRGB(0,0,0) + c.setFillColorRGB(0,0,0) + # Feed Name and SurName + if len(DispUser.prenom) + len(DispUser.nom)>18: + if DispUser.pseudo: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 4 * mm , "%s" % DispUser.prenom ) + c.setFont('Courier', 17) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 2 * mm , "%s" % DispUser.nom ) + else: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 2 * mm , "%s" % DispUser.prenom ) + c.setFont('Courier', 17) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 6 * mm , "%s" % DispUser.nom ) + else: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 0 * mm , "%s %s" % (DispUser.prenom, DispUser.nom) ) + + if DispUser.pseudo: + c.setFont("Helvetica-Oblique", 14) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 8 * mm , "%s" % DispUser.pseudo ) + + # Put QR code to user profile + c.drawInlineImage(QRCode(DispUser), \ + WIDTH - 20 * mm - 7, 0, \ + 20 * mm, 20 * mm, \ + preserveAspectRatio=True, \ + anchor='s') + + Tiers_Logo(c, DispUser, 4, ( 30 * mm, 2 )) + + c.showPage() + c.save() + pdf.seek(0) + return Response(app_iter=pdf, content_type = 'application/pdf' ) + +@view_config(route_name='badge_user1') +def badge_user1(request): + user_slug = request.matchdict.get('user_slug', None) + if user_slug is None or len(user_slug)==0: + raise HTTPNotFound(u"Cet utilisateur n'a pas été reconnu") + # Query database + DispUser = User.by_slug(user_slug) + if DispUser is None: + raise HTTPNotFound() + + # Ok let's generate a PDF Badge + ttfFile = "jm2l/static/fonts/LiberationMono-Regular.ttf" + ttfFile_Logo = "jm2l/static/fonts/PWTinselLetters.ttf" + pdfmetrics.registerFont(TTFont("Liberation", ttfFile)) + pdfmetrics.registerFont(TTFont("Logo", ttfFile_Logo)) + pdf = StringIO.StringIO() + + c = canvas.Canvas( pdf, pagesize=(WIDTH, HEIGHT) ) + c.translate(mm, mm) + + # Feed some metadata + c.setCreator("linux-azur.org") + c.setTitle("Badge") + + c.saveState() + + logoobject = c.beginText() + logoobject.setFont('Logo', 52) + logoobject.setFillColorRGB(.83,0,.33) + logoobject.setTextOrigin(55, HEIGHT-50) + logoobject.textLines("JM2L") + c.drawText(logoobject) + + + yearobject = c.beginText() + yearobject.setFont("Helvetica-Bold", 15) + yearobject.setFillColorRGB(1,1,1) + yearobject.setTextOrigin(67, HEIGHT-20) + yearobject.setWordSpace(21) + yearobject.textLines("2 0 1 5") + c.drawText(yearobject) + + num = 0 + for tiers in DispUser.tiers: + if tiers.ThumbLinks: + FileName = tiers.ThumbLinks.pop().split("/")[-1] + num+=1 + ImagePath = "jm2l/upload/images/tiers/%s/%s" % (tiers.slug, FileName) + if num<6: + c.drawImage(ImagePath, + WIDTH -5 - num * (ICONSIZE+5), 5, ICONSIZE, ICONSIZE,\ + preserveAspectRatio=True, + anchor='c', + mask=[0,3,0,3,0,3]) + else: + c.drawImage(ImagePath, + WIDTH -5 - (num-5) * (ICONSIZE+5), 5 + ICONSIZE, ICONSIZE, ICONSIZE,\ + preserveAspectRatio=True, + anchor='c', + mask=[0,3,0,3,0,3]) + + + + + if 1: + Ribbon90(DispUser, c) + + if 0: + c.rotate(90) + offset_u=111 + if DispUser.Staff: + # Staff + c.setFillColorRGB(1,.2,.2) + c.rect(-5, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 30) + c.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+5, "STAFF") + elif DispUser.is_Intervenant: + # Intervenant + c.setFillColorRGB(.3,.3,1) + c.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 15) + c.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+10, "Intervenant") + else: + # Visiteur + c.setFillColorRGB(.8,.8,.8) + c.rect(-5, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(0,0,0) + c.setFont('Liberation', 19) + c.drawCentredString(WIDTH/2, HEIGHT/2-offset_u+7, "Visiteur") + c.restoreState() + + c.setFont('Courier', 18) + c.setStrokeColorRGB(0,0,0) + c.setFillColorRGB(0,0,0) + # Feed Name and SurName + if len(DispUser.prenom) + len(DispUser.nom)>18: + if DispUser.pseudo: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 4 * mm , "%s" % DispUser.prenom ) + c.setFont('Courier', 17) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 2 * mm , "%s" % DispUser.nom ) + else: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 2 * mm , "%s" % DispUser.prenom ) + c.setFont('Courier', 17) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 6 * mm , "%s" % DispUser.nom ) + else: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 0 * mm , "%s %s" % (DispUser.prenom, DispUser.nom) ) + #c.drawCentredString(WIDTH/2, HEIGHT - 22 * mm, ) + if DispUser.pseudo: + #c.setFont('Liberation', 14) + c.setFont("Helvetica-Oblique", 14) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 8 * mm , "\"%s\"" % DispUser.pseudo ) + #c.restoreState() + + # Put QR code to user profile + c.drawInlineImage(QRCode(DispUser), \ + WIDTH - 20 * mm - 7, 0, \ + 20 * mm, 20 * mm, \ + preserveAspectRatio=True, \ + anchor='s') + + c.showPage() + c.save() + pdf.seek(0) + return Response(app_iter=pdf, content_type = 'application/pdf' ) + + +@view_config(route_name='badge_user2') +def badge_user2(request): + user_slug = request.matchdict.get('user_slug', None) + if user_slug is None or len(user_slug)==0: + raise HTTPNotFound(u"Cet utilisateur n'a pas été reconnu") + # Query database + DispUser = User.by_slug(user_slug) + if DispUser is None: + raise HTTPNotFound() + + # Ok let's generate a PDF Badge + ttfFile = "jm2l/static/fonts/LiberationMono-Regular.ttf" + ttfFile_Logo = "jm2l/static/fonts/PWTinselLetters.ttf" + pdfmetrics.registerFont(TTFont("Liberation", ttfFile)) + pdfmetrics.registerFont(TTFont("Logo", ttfFile_Logo)) + pdf = StringIO.StringIO() + + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=2, + ) + # Data of QR code + qr.add_data('http://jm2l.linux-azur.org/user/%s' % DispUser.slug) + qr.make(fit=True) + + img = qr.make_image() + # Create PDF container + WIDTH = 85 * mm + HEIGHT = 60 * mm + ICONSIZE = 10 * mm + c = canvas.Canvas( pdf, pagesize=(WIDTH, HEIGHT) ) + c.translate(mm, mm) + + # Feed some metadata + c.setCreator("linux-azur.org") + c.setTitle("Badge") + + c.saveState() + + logoobject = c.beginText() + logoobject.setFont('Logo', 52) + logoobject.setFillColorRGB(.83,0,.33) + logoobject.setTextOrigin(55, HEIGHT-50) + logoobject.textLines("JM2L") + c.drawText(logoobject) + + + yearobject = c.beginText() + yearobject.setFont("Helvetica-Bold", 15) + yearobject.setFillColorRGB(1,1,1) + yearobject.setTextOrigin(67, HEIGHT-20) + yearobject.setWordSpace(21) + yearobject.textLines("2 0 1 5") + c.drawText(yearobject) + + num = 0 + for tiers in DispUser.tiers: + if tiers.ThumbLinks: + FileName = tiers.ThumbLinks.pop().split("/")[-1] + num+=1 + ImagePath = "jm2l/upload/images/tiers/%s/%s" % (tiers.slug, FileName) + if num<6: + c.drawImage(ImagePath, + WIDTH -5 - num * (ICONSIZE+5), 5, ICONSIZE, ICONSIZE,\ + preserveAspectRatio=True, + anchor='c', + mask=[0,3,0,3,0,3]) + else: + c.drawImage(ImagePath, + WIDTH -5 - (num-5) * (ICONSIZE+5), 5 + ICONSIZE, ICONSIZE, ICONSIZE,\ + preserveAspectRatio=True, + anchor='c', + mask=[0,3,0,3,0,3]) + + + + + if 1: + c.rotate(35) + offset_u=0 + if DispUser.Staff: + # Staff + c.setFillColorRGB(1,.2,.2) + c.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 20) + c.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+5, "STAFF") + elif DispUser.is_Intervenant: + # Intervenant + c.setFillColorRGB(.3,.3,1) + c.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 15) + c.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+10, "Intervenant") + else: + # Visiteur + c.setFillColorRGB(.8,.8,.8) + c.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(0,0,0) + c.setFont('Liberation', 12) + c.drawCentredString(WIDTH/2-10, HEIGHT/2-offset_u+7, "Visiteur") + + if 0: + c.rotate(90) + offset_u=111 + if DispUser.Staff: + # Staff + c.setFillColorRGB(1,.2,.2) + c.rect(-5, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 30) + c.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+5, "STAFF") + elif DispUser.is_Intervenant: + # Intervenant + c.setFillColorRGB(.3,.3,1) + c.rect(0, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(1,1,1) + c.setFont('Liberation', 15) + c.drawCentredString(WIDTH/2-15, HEIGHT/2-offset_u+10, "Intervenant") + else: + # Visiteur + c.setFillColorRGB(.8,.8,.8) + c.rect(-5, HEIGHT/2-offset_u, WIDTH, 10*mm, fill=1) + c.setFillColorRGB(0,0,0) + c.setFont('Liberation', 19) + c.drawCentredString(WIDTH/2, HEIGHT/2-offset_u+7, "Visiteur") + + + c.restoreState() + + c.setFont('Courier', 18) + c.setStrokeColorRGB(0,0,0) + c.setFillColorRGB(0,0,0) + # Feed Name and SurName + if len(DispUser.prenom) + len(DispUser.nom)>18: + if DispUser.pseudo: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 4 * mm , "%s" % DispUser.prenom ) + c.setFont('Courier', 17) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 2 * mm , "%s" % DispUser.nom ) + else: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 2 * mm , "%s" % DispUser.prenom ) + c.setFont('Courier', 17) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 6 * mm , "%s" % DispUser.nom ) + else: + c.drawCentredString(WIDTH/2, HEIGHT/2 + 0 * mm , "%s %s" % (DispUser.prenom, DispUser.nom) ) + #c.drawCentredString(WIDTH/2, HEIGHT - 22 * mm, ) + if DispUser.pseudo: + #c.setFont('Liberation', 14) + c.setFont("Helvetica-Oblique", 14) + c.drawCentredString(WIDTH/2, HEIGHT/2 - 8 * mm , "\"%s\"" % DispUser.pseudo ) + #c.restoreState() + + # Put QR code to user profile + c.drawInlineImage(img, WIDTH - 20 * mm - 7, 0, 20 * mm, 20 * mm, preserveAspectRatio=True, anchor='s') + + c.showPage() + c.save() + pdf.seek(0) + return Response(app_iter=pdf, content_type = 'application/pdf' ) diff --git a/jm2l/blenderthumbnailer.py b/jm2l/blenderthumbnailer.py new file mode 100644 index 0000000..32d5567 --- /dev/null +++ b/jm2l/blenderthumbnailer.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +""" +Thumbnailer runs with python 2.7 and 3.x. +To run automatically with a file manager such as Nautilus, save this file +in a directory that is listed in PATH environment variable, and create +blender.thumbnailer file in ${HOME}/.local/share/thumbnailers/ directory +with the following contents: + +[Thumbnailer Entry] +TryExec=blender-thumbnailer.py +Exec=blender-thumbnailer.py %u %o +MimeType=application/x-blender; +""" + +import struct + + +def open_wrapper_get(): + """ wrap OS spesific read functionality here, fallback to 'open()' + """ + + class GFileWrapper: + __slots__ = ("mode", "g_file") + + def __init__(self, url, mode='r'): + self.mode = mode # used in gzip module + self.g_file = Gio.File.parse_name(url).read(None) + + def read(self, size): + return self.g_file.read_bytes(size, None).get_data() + + def seek(self, offset, whence=0): + self.g_file.seek(offset, [1, 0, 2][whence], None) + return self.g_file.tell() + + def tell(self): + return self.g_file.tell() + + def close(self): + self.g_file.close(None) + + def open_local_url(url, mode='r'): + o = urlparse(url) + if o.scheme == '': + path = o.path + elif o.scheme == 'file': + path = unquote(o.path) + else: + raise(IOError('URL scheme "%s" needs gi.repository.Gio module' % o.scheme)) + return open(path, mode) + + try: + from gi.repository import Gio + return GFileWrapper + except ImportError: + try: + # Python 3 + from urllib.parse import urlparse, unquote + except ImportError: + # Python 2 + from urlparse import urlparse + from urllib import unquote + return open_local_url + + +def blend_extract_thumb(path): + import os + open_wrapper = open_wrapper_get() + + # def MAKE_ID(tag): ord(tag[0])<<24 | ord(tag[1])<<16 | ord(tag[2])<<8 | ord(tag[3]) + REND = 1145980242 # MAKE_ID(b'REND') + TEST = 1414743380 # MAKE_ID(b'TEST') + + blendfile = open_wrapper(path, 'rb') + + head = blendfile.read(12) + + if head[0:2] == b'\x1f\x8b': # gzip magic + import gzip + blendfile.close() + blendfile = gzip.GzipFile('', 'rb', 0, open_wrapper(path, 'rb')) + head = blendfile.read(12) + + if not head.startswith(b'BLENDER'): + blendfile.close() + return None, 0, 0 + + is_64_bit = (head[7] == b'-'[0]) + + # true for PPC, false for X86 + is_big_endian = (head[8] == b'V'[0]) + + # blender pre 2.5 had no thumbs + if head[9:11] <= b'24': + blendfile.close() + return None, 0, 0 + + sizeof_bhead = 24 if is_64_bit else 20 + int_endian_pair = '>ii' if is_big_endian else ' ") + else: + file_in = sys.argv[-2] + + buf, width, height = blend_extract_thumb(file_in) + + if buf: + file_out = sys.argv[-1] + + f = open(file_out, "wb") + f.write(write_png(buf, width, height)) + f.close() + + +if __name__ == '__main__': + main() diff --git a/jm2l/captcha.py b/jm2l/captcha.py index 65ab5db..f7dd768 100644 --- a/jm2l/captcha.py +++ b/jm2l/captcha.py @@ -3,7 +3,7 @@ import random from PIL import Image, ImageDraw, ImageFont, ImageFilter -import StringIO +import cStringIO as StringIO import math from pyramid.view import view_config from .words import TabMots diff --git a/jm2l/models.py b/jm2l/models.py index 52402ac..9e3db09 100644 --- a/jm2l/models.py +++ b/jm2l/models.py @@ -169,6 +169,13 @@ class User(Base): return u return None + @property + def is_Intervenant(self, year=2015): + """ This property will return if User do an event on specified year """ + return DBSession.query(Event).join(User_Event) \ + .filter(User_Event.user_uid==self.uid) \ + .filter(Event.for_year==year).count() + @property def my_hash(self): m = hashlib.sha1() diff --git a/jm2l/static/fonts/PWTinselLetters.ttf b/jm2l/static/fonts/PWTinselLetters.ttf new file mode 100644 index 0000000..7dfd99e Binary files /dev/null and b/jm2l/static/fonts/PWTinselLetters.ttf differ diff --git a/jm2l/static/img/Blender_Thumb_Stamp.png b/jm2l/static/img/Blender_Thumb_Stamp.png new file mode 100644 index 0000000..4eeb37e Binary files /dev/null and b/jm2l/static/img/Blender_Thumb_Stamp.png differ diff --git a/jm2l/templates/Participant/list.mako b/jm2l/templates/Participant/list.mako index 16c99c8..46eebb5 100644 --- a/jm2l/templates/Participant/list.mako +++ b/jm2l/templates/Participant/list.mako @@ -162,7 +162,10 @@ now = datetime.datetime.now() - % endif + % endif + + + diff --git a/jm2l/upload.py b/jm2l/upload.py index 9c4c7e9..d68cc4e 100644 --- a/jm2l/upload.py +++ b/jm2l/upload.py @@ -2,8 +2,7 @@ from pyramid.view import view_config, view_defaults from pyramid.response import Response from pyramid.exceptions import NotFound -from pyramid.httpexceptions import HTTPNotFound -from pyramid.request import Request +from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest from PIL import Image import re, os, shutil from os import path @@ -13,6 +12,7 @@ import subprocess import cStringIO as StringIO # Database access imports from .models import User, Place, Tiers, Event, SallePhy +from .blenderthumbnailer import blend_extract_thumb, write_png CurrentYear = 2015 MIN_FILE_SIZE = 1 # bytes @@ -27,7 +27,8 @@ ACCEPTED_MIMES = ['application/pdf', 'application/vnd.oasis.opendocument.presentation-template', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet-template', - 'image/svg+xml' + 'image/svg+xml', + 'application/x-blender' ] @@ -136,6 +137,38 @@ class MediaPath(): if mime=='application/pdf': return "/img/PDF.png" + def check_blend_file(self, fileobj): + head = fileobj.read(12) + fileobj.seek(0) + + if head[:2] == b'\x1f\x8b': # gzip magic + import zlib + head = zlib.decompress(fileobj.read(), 31)[:12] + fileobj.seek(0) + + if head.startswith(b'BLENDER'): + return True + + def get_mimetype_from_file(self, fileobj): + mimetype = magic.from_buffer(fileobj.read(1024), mime=True) + fileobj.seek(0) + + # Check if the binary file is a blender file + if ( mimetype == "application/octet-stream" or mimetype == "application/x-gzip" ) and self.check_blend_file(fileobj): + return "application/x-blender", True + else: + return mimetype, False + + + def get_mimetype(self, name): + """ This function return the mime-type based on .type file """ + try: + with open(self.mediapath(name) + '.type', 'r', 16) as f: + mime = f.read() + return mime + except IOError: + return None + @view_defaults(route_name='media_upload') class MediaUpload(MediaPath): @@ -152,19 +185,31 @@ class MediaUpload(MediaPath): return self.get_mediapath(self.media_table, self.linked_id, name) def validate(self, result, filecontent): - # let's say we don't trust the uploader - RealMime = magic.from_buffer( filecontent.read(1024), mime=True) + """ Do some basic check to the uploaded file before to accept it """ + # Try to determine mime type from content uploaded + found_mime = magic.from_buffer(filecontent.read(1024), mime=True) filecontent.seek(0) - if RealMime!=result['type']: - result['error'] = 'l\'extension du fichier ne correspond pas à son contenu - ' - result['error'] += "( %s vs %s )" % (RealMime, result['type']) + + # Do a special statement for specific detected mime type + if found_mime in ["application/octet-stream", "application/x-gzip"]: + # Lets see if it's a bender file + if self.check_blend_file(filecontent): + found_mime = "application/x-blender" + # MonKey Patch of content type + result['type'] = found_mime + + # Reject mime type that don't match + if found_mime!=result['type']: + result['error'] = 'L\'extension du fichier ne correspond pas à son contenu - ' + result['error'] += "( %s vs %s )" % (found_mime, result['type']) return False + # Accept images and mime types listed - if not RealMime in ACCEPTED_MIMES: - if not (IMAGE_TYPES.match(RealMime)): + if not found_mime in ACCEPTED_MIMES: + if not (IMAGE_TYPES.match(found_mime)): result['error'] = 'Ce type fichier n\'est malheureusement pas supporté. ' - result['error'] += 'Les fichiers acceptées sont les images et pdf.' return False + if result['size'] < MIN_FILE_SIZE: result['error'] = 'le fichier est trop petit' elif result['size'] > MAX_FILE_SIZE: @@ -173,12 +218,13 @@ class MediaUpload(MediaPath): # file['error'] = u'les type de fichiers acceptés sont png, jpg et gif' else: return True + return False - def get_file_size(self, file): - file.seek(0, 2) # Seek to the end of the file - size = file.tell() # Get the position of EOF - file.seek(0) # Reset the file position to the beginning + def get_file_size(self, fileobj): + fileobj.seek(0, 2) # Seek to the end of the file + size = fileobj.tell() # Get the position of EOF + fileobj.seek(0) # Reset the file position to the beginning return size def thumbnailurl(self,name): @@ -254,9 +300,6 @@ class MediaUpload(MediaPath): def docthumbnail(self, filename): TargetFileName = self.thumbnailpath(filename) - # unoconv need a libre office server to be up - Command = ["unoconv", "-f", "pdf", "-e", "PageRange=1", "--output=%s" % TargetFileName, \ - "%s[0]" % self.mediapath(filename) ] # let's take the thumbnail generated inside the document Command = ["unzip", "-p", self.mediapath(filename), "Thumbnails/thumbnail.png"] ThumbBytes = subprocess.check_output(Command) @@ -284,6 +327,40 @@ class MediaUpload(MediaPath): timage.convert('RGB').save( TargetFileName+".jpg", 'JPEG') return self.thumbnailurl( os.path.basename(TargetFileName+".jpg") ) + def blendthumbnail(self, filename): + blendfile = self.mediapath(filename) + # Extract Thumb + if 0: + head = fileobj.read(12) + fileobj.seek(0) + + if head[:2] == b'\x1f\x8b': # gzip magic + import zlib + head = zlib.decompress(fileobj.read(), 31)[:12] + fileobj.seek(0) + + buf, width, height = blend_extract_thumb(blendfile) + if buf: + png = write_png(buf, width, height) + TargetFileName = self.thumbnailpath(filename) + image = Image.open(StringIO.StringIO(png)) + blender_indicator = Image.open( "jm2l/static/img/Blender_Thumb_Stamp.png" ) + image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE), Image.ANTIALIAS) + timage = Image.new('RGBA', (THUMBNAIL_SIZE, THUMBNAIL_SIZE), (255, 255, 255, 0)) + # Add thumbnail + timage.paste( + image, + ((THUMBNAIL_SIZE - image.size[0]) / 2, (THUMBNAIL_SIZE - image.size[1]) / 2)) + # Stamp with Blender file type + timage.paste( + blender_indicator, + (timage.size[0]-30, timage.size[1]-30), + blender_indicator, + ) + timage.save( TargetFileName+".png") + return self.thumbnailurl( os.path.basename(TargetFileName+".png") ) + return self.ExtMimeIcon('application/x-blender') + def fileinfo(self,name): filename = self.mediapath(name) f, ext = os.path.splitext(name) @@ -295,13 +372,17 @@ class MediaUpload(MediaPath): name=name, media_table=self.media_table, uid=self.linked_id) - mime = mimetypes.types_map.get(ext) - if (IMAGE_TYPES.match(mime)): + + mime = self.get_mimetype(name) + if IMAGE_TYPES.match(mime): info['thumbnailUrl'] = self.thumbnailurl(name) elif mime in ACCEPTED_MIMES: thumb = self.thumbnailpath("%s%s" % (f, ext)) - if os.path.exists( thumb +'.jpg' ): - info['thumbnailUrl'] = self.thumbnailurl(name)+'.jpg' + thumbext = ".jpg" + if mime == "application/x-blender": + thumbext = ".png" + if os.path.exists( thumb + thumbext ): + info['thumbnailUrl'] = self.thumbnailurl(name)+thumbext else: info['thumbnailUrl'] = self.ExtMimeIcon(mime) else: @@ -343,7 +424,6 @@ class MediaUpload(MediaPath): @view_config(request_method='DELETE', xhr=True, accept="application/json", renderer='json') def delete(self): - import json filename = self.request.matchdict.get('name') try: os.remove(self.mediapath(filename) + '.type') @@ -381,11 +461,17 @@ class MediaUpload(MediaPath): result['name'] = os.path.basename(fieldStorage.filename) result['type'] = fieldStorage.type result['size'] = self.get_file_size(fieldStorage.file) + if self.validate(result, fieldStorage.file): - with open( self.mediapath(result['name'] + '.type'), 'w') as f: + # Keep mime-type in .type file + with open( self.mediapath( result['name'] ) + '.type', 'w') as f: f.write(result['type']) - with open( self.mediapath(result['name']), 'wb') as f: + + # Store uploaded file + fieldStorage.file.seek(0) + with open( self.mediapath(result['name'] ), 'wb') as f: shutil.copyfileobj( fieldStorage.file , f) + if re.match(IMAGE_TYPES, result['type']): result['thumbnailUrl'] = self.createthumbnail(result['name']) elif result['type']=='application/pdf': @@ -394,8 +480,11 @@ class MediaUpload(MediaPath): result['thumbnailUrl'] = self.svgthumbnail(result['name']) elif result['type'].startswith('application/vnd'): result['thumbnailUrl'] = self.docthumbnail(result['name']) + elif result['type']=='application/x-blender': + result['thumbnailUrl'] = self.blendthumbnail(result['name']) else: result['thumbnailUrl'] = self.ExtMimeIcon(result['type']) + result['deleteType'] = DELETEMETHOD result['deleteUrl'] = self.request.route_url('media_upload', sep='', @@ -425,12 +514,8 @@ class MediaView(MediaPath): @view_config(request_method='GET', http_cache = (EXPIRATION_TIME, {'public':True})) def get(self): name = self.request.matchdict.get('name') - try: - with open( self.mediapath( os.path.basename(name) ) + '.type', 'r', 16) as f: - test = f.read() - self.request.response.content_type = test - except IOError: - pass + self.request.response.content_type = self.get_mimetype(name) + try: self.request.response.body_file = open( self.mediapath(name), 'rb', 10000) except IOError: diff --git a/setup.py b/setup.py index 8bd9095..5d0df3d 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,9 @@ requires = [ 'pyramid_exclog', 'repoze.sendmail==4.1', 'pyramid_mailer', - 'apscheduler' + 'apscheduler', + 'qrcode', + 'reportlab' ] setup(name='JM2L',