diff --git a/config.py.example b/config.py.example index 4d8d389a2785d2541fa4b2c5f41c524d65af04d7..66052cd5376e4ed5a2b69fd5cc556e791fc87b9f 100644 --- a/config.py.example +++ b/config.py.example @@ -1,18 +1,52 @@ SQLALCHEMY_DATABASE_URI = "postgresql://proto3:@/proto3" SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = "abc" + DEBUG = False + MAIL_ACTIVE = True MAIL_FROM = "protokolle@example.com" MAIL_HOST = "mail.example.com:465" MAIL_USER = "user" MAIL_PASSWORD = "password" MAIL_PREFIX = "protokolle" + CELERY_BROKER_URL = "redis://localhost:6379/0" CELERY_TASK_SERIALIZER = "pickle" CELERY_ACCEPT_CONTENT = ["pickle"] + URL_ROOT = "protokolle.example.com" URL_PROTO = "https" URL_PATH = "/" URL_PARAMS = "" + ERROR_CONTEXT_LINES = 3 + +# choose something nice from fc-list +FONTS = { + "main": { + "regular": "Nimbus Sans", + "bold": "NimbusSans", + "italic": "NimbusSans", + "bolditalic": "NimbusSans" + }, + "roman": { + "regular": "Nimbus Roman", + "bold": "Nimbus Roman", + "italic": "Nimbus Roman", + "bolditalic": "Nimbus Roman" + }, + "sans": { + "regular": "Nimbus Sans", + "bold": "NimbusSans", + "italic": "NimbusSans", + "bolditalic": "NimbusSans" + }, + "mono": { + "regular": "Nimbus Mono PS", + "bold": "Nimbus Mono PS", + "italic": "Nimbus Mono PS", + "bolditalic": "Nimbus Mono PS" + } +} diff --git a/migrations/versions/b114754024fb_.py b/migrations/versions/b114754024fb_.py new file mode 100644 index 0000000000000000000000000000000000000000..a381a071d6d2a82d07d7c70925e71ad03673de2c --- /dev/null +++ b/migrations/versions/b114754024fb_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: b114754024fb +Revises: 97cf1913e60d +Create Date: 2017-02-23 20:33:56.446729 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b114754024fb' +down_revision = '97cf1913e60d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('documents', sa.Column('is_private', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('documents', 'is_private') + # ### end Alembic commands ### diff --git a/models/database.py b/models/database.py index 87b9f1d20eb7a998073ae8c591f0494c088636fe..3bac9b1ed9a7e986eb28d04f89aa4de30f361bbe 100644 --- a/models/database.py +++ b/models/database.py @@ -6,9 +6,10 @@ import math from shared import db from utils import random_string, url_manager -#from models.tables import TexResponsiblesTable, TexSupportersTable +import os -from sqlalchemy.orm import relationship, backref +from sqlalchemy import event +from sqlalchemy.orm import relationship, backref, sessionmaker import config @@ -171,16 +172,28 @@ class Document(db.Model): name = db.Column(db.String) filename = db.Column(db.String, unique=True) is_compiled = db.Column(db.Boolean) + is_private = db.Column(db.Boolean) - def __init__(self, protocol_id, name, filename, is_compiled): + def __init__(self, protocol_id, name, filename, is_compiled, is_private): self.protocol_id = protocol_id self.name = name self.filename = filename self.is_compiled = is_compiled + self.is_private = is_private def __repr__(self): - return "<Document(id={}, protocol_id={}, name={}, filename={}, is_compiled={})>".format( - self.id, self.protocol_id, self.name, self.filename, self.is_compiled) + return "<Document(id={}, protocol_id={}, name={}, filename={}, is_compiled={}, is_private={})>".format( + self.id, self.protocol_id, self.name, self.filename, self.is_compiled, self.is_private) + + def get_filename(self): + return os.path.join(config.DOCUMENTS_PATH, self.filename) + +@event.listens_for(Document, "before_delete") +def on_delete(mapper, connection, document): + if document.filename is not None: + document_path = os.path.join(config.DOCUMENTS_PATH, document.filename) + if os.path.isfile(document_path): + os.remove(document_path) class Todo(db.Model): __tablename__ = "todos" diff --git a/server.py b/server.py index 030978ceffa89c8453c51731f2d5ef08c6682e52..5ca957dcc36b0e32238a20714c64ed6d9a1034cf 100755 --- a/server.py +++ b/server.py @@ -3,6 +3,7 @@ import locale locale.setlocale(locale.LC_TIME, "de_DE.utf8") from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response, send_file +from werkzeug.utils import secure_filename from flask_script import Manager, prompt from flask_migrate import Migrate, MigrateCommand #from flask_socketio import SocketIO @@ -10,13 +11,14 @@ from celery import Celery from functools import wraps import requests from io import StringIO, BytesIO +import os import config from shared import db, date_filter, datetime_filter, date_filter_long, time_filter, ldap_manager, security_manager from utils import is_past, mail_manager, url_manager from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error -from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm -from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable +from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm +from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable app = Flask(__name__) app.config.from_object(config) @@ -335,7 +337,9 @@ def show_protocol(protocol_id): flash("Invalides Protokoll.", "alert-error") return redirect(request.args.get("next") or url_for("index")) errors_table = ErrorsTable(protocol.errors) - return render_template("protocol-show.html", protocol=protocol, errors_table=errors_table) + documents_table = DocumentsTable(protocol.documents) + document_upload_form = DocumentUploadForm() + return render_template("protocol-show.html", protocol=protocol, errors_table=errors_table, documents_table=documents_table, document_upload_form=document_upload_form) @app.route("/protocol/etherpull/<int:protocol_id>") def etherpull_protocol(protocol_id): @@ -345,10 +349,7 @@ def etherpull_protocol(protocol_id): flash("Invalides Protokoll oder keine Berechtigung.", "alert-error") return redirect(request.args.get("next") or url_for("index")) source_req = requests.get(protocol.get_etherpad_source_link()) - #source = source_req.content.decode("utf-8") source = source_req.text - print(source.split("\r")) - #print(source.split("\n")) protocol.source = source db.session.commit() tasks.parse_protocol(protocol) @@ -386,6 +387,49 @@ def list_todos(): todos_table = TodosTable(todos) return render_template("todos-list.html", todos=todos, todos_table=todos_table) +@app.route("/document/download/<int:document_id>") +def download_document(document_id): + user = current_user() + document = Document.query.filter_by(id=document_id).first() + if document is None: + flash("Invalides Dokument.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + if ((document.is_private + and not document.protocol.protocoltype.has_private_view_right(user)) + or (not document.is_private + and not document.protocol.protocoltype.has_public_view_right(user))): + flash("Keine Berechtigung.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + with open(document.get_filename(), "rb") as file: + file_like = BytesIO(file.read()) + return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename=document.name) + +@app.route("/document/upload/<int:protocol_id>", methods=["POST"]) +@login_required +def upload_document(protocol_id): + user = current_user() + protocol = Protocol.query.filter_by(id=protocol_id).first() + if protocol is None or not protocol.protocoltype.has_modify_right(user): + flash("Insufficient permissions.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + form = DocumentUploadForm() + print(form, form.document.data, form.private.data) + print(request.files) + if form.document.data is None: + flash("No file has been selected.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + file = form.document.data + if file.filename == "": + flash("No file has been selected.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + if file: + filename = secure_filename(file.filename) + internal_filename = "{}-{}".format(protocol.id, filename) + file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename)) + document = Document(protocol.id, filename, internal_filename, False, form.private.data) + db.session.add(document) + db.session.commit() + return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) @app.route("/login", methods=["GET", "POST"]) diff --git a/shared.py b/shared.py index a54521eee1ae8eca6e2cce0effb93acb096db4aa..aa22cf5fd695ba07facecfbab504fbb9022f36cd 100644 --- a/shared.py +++ b/shared.py @@ -69,6 +69,8 @@ def datetime_filter(date): return date.strftime("%d. %B %Y, %H:%M") def date_filter_long(date): return date.strftime("%A, %d.%m.%Y, Kalenderwoche %W") +def date_filter_short(date): + return date.strftime("%d.%m.%Y") def time_filter(time): return time.strftime("%H:%m") diff --git a/tasks.py b/tasks.py index eb30dba5839e4079ae367327562d57fc54991fd8..96aa4b138e7a82965c761fdd13d432853f90e001 100644 --- a/tasks.py +++ b/tasks.py @@ -7,7 +7,7 @@ import tempfile from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP from server import celery, app -from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, time_filter, class_filter +from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, class_filter from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs from parser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork @@ -27,6 +27,7 @@ texenv.lstrip_blocks = True texenv.filters["url_complete"] = url_manager.complete texenv.filters["datify"] = date_filter texenv.filters["datify_long"] = date_filter_long +texenv.filters["datify_short"] = date_filter_short texenv.filters["datetimify"] = datetime_filter texenv.filters["timify"] = time_filter texenv.filters["class"] = class_filter @@ -182,7 +183,9 @@ def compile_async(content, protocol_id): log_filename = "protocol.log" with open(os.path.join(compile_dir, protocol_source_filename), "w") as source_file: source_file.write(content) - shutil.copy("static/tex/protokoll2.cls", compile_dir) + protocol2_class_source = texenv.get_template("protokoll2.cls").render(fonts=config.FONTS) + with open(os.path.join(compile_dir, "protokoll2.cls"), "w") as protocol2_class_file: + protocol2_class_file.write(protocol2_class_source) os.chdir(compile_dir) command = [ "/usr/bin/xelatex", @@ -196,12 +199,12 @@ def compile_async(content, protocol_id): for old_document in [document for document in protocol.documents if document.is_compiled]: protocol.documents.remove(old_document) db.session.commit() - document = Document(protocol.id, name="protokoll_{}_{}.pdf".format(protocol.protocoltype.short_name, date_filter(protocol.date)), filename="", is_compiled=True) + document = Document(protocol.id, name="protokoll_{}_{}.pdf".format(protocol.protocoltype.short_name, date_filter_short(protocol.date)), filename="", is_compiled=True, is_private=False) db.session.add(document) db.session.commit() target_filename = "compiled-{}.pdf".format(document.id) document.filename = target_filename - shutil.copy(os.path.join(compile_dir, protocol_target_filename), os.path.join("documents", target_filename)) + shutil.copy(os.path.join(compile_dir, protocol_target_filename), os.path.join(config.DOCUMENTS_PATH, target_filename)) db.session.commit() shutil.copy(os.path.join(compile_dir, log_filename), "/tmp") except subprocess.SubprocessError: diff --git a/templates/macros.html b/templates/macros.html index 86a89896936fd7bbde88232d6c3a0a83bad4d219..11d3f2c76a8ed34da13f0366cfdb6f6bfa996a21 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -87,9 +87,9 @@ to not render a label for the CRSFTokenField --> action_text - text of submit button class_ - sets a class for form #} -{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-default') -%} +{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-default', enctype=None) -%} - <form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}"> + <form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}"{% if enctype is not none %}enctype="{{enctype}}"{% endif %}> {{ form.hidden_tag() if form.hidden_tag }} {% if caller %} {{ caller() }} diff --git a/templates/protocol-show.html b/templates/protocol-show.html index 55f9311ca29c469a806a52c59f70bf9b61eefd63..98aa0f0192c4921255b351a4e93ccea14766fdc1 100644 --- a/templates/protocol-show.html +++ b/templates/protocol-show.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% from "macros.html" import render_table %} +{% from "macros.html" import render_table, render_form %} {% block title %}Protokoll{% endblock %} {% block content %} @@ -71,8 +71,10 @@ {{render_table(errors_table)}} {% endif %} {% if protocol.documents|length > 0 %} - <h3>Anhang</h3> - {# TODO: render documents table here #} + {{render_table(documents_table)}} + {% endif %} + {% if protocol.is_done() %} + {{render_form(document_upload_form, action_url=url_for("upload_document", protocol_id=protocol.id, next=url_for("show_protocol", protocol_id=protocol.id)), action_text="Hochladen", enctype="multipart/form-data")}} {% endif %} </div> </div> diff --git a/static/tex/protokoll2.cls b/templates/protokoll2.cls similarity index 85% rename from static/tex/protokoll2.cls rename to templates/protokoll2.cls index d5f32ea6c16a33f5489f374f84696ea77bb30260..be443f0764032592e11c6c3b585020b1c35d6814 100644 --- a/static/tex/protokoll2.cls +++ b/templates/protokoll2.cls @@ -14,37 +14,41 @@ \LoadClass[a4paper]{article} % so ein paar sachen die wir eh immer brauchen -%\RequirePackage[T1]{fontenc} -%\RequirePackage{ngerman} \RequirePackage[ngerman]{babel} -%\RequirePackage[utf8]{inputenc} \RequirePackage[vmargin=1.5cm,hmargin={1.5cm,1.2cm},bindingoffset=8mm]{geometry} %\RequirePackage{lineno} \RequirePackage{longtable} \RequirePackage{framed} \RequirePackage{eurosym} \RequirePackage[babel]{csquotes} -%\RequirePackage{fontspec} \RequirePackage{polyglossia} \setmainlanguage[babelshorthands=true]{german} \RequirePackage{fontspec} -%\setromanfont[Scale=0.925]{DejaVu Serif} -%\setsansfont[Scale=0.925]{DejaVu Sans} -%\setmonofont[Scale=0.925]{DejaVu Sans Mono} -%\setmainfont[Scale=0.925]{DejaVu Sans} -\setromanfont{Nimbus Roman} -\setsansfont{Nimbus Sans} -\setmonofont{Nimbus Mono PS} +\setromanfont[ + BoldFont={\VAR{fonts.roman.bold}}, + ItalicFont={\VAR{fonts.roman.italic}}, + BoldItalicFont={\VAR{fonts.roman.bolditalic}} +]{\VAR{fonts.roman.regular}} +\setsansfont[ + BoldFont={\VAR{fonts.sans.bold}}, + ItalicFont={\VAR{fonts.sans.italic}}, + BoldItalicFont={\VAR{fonts.sans.bolditalic}} +]{\VAR{fonts.sans.regular}} +\setmonofont[ + BoldFont={\VAR{fonts.mono.bold}}, + ItalicFont={\VAR{fonts.mono.italic}}, + BoldItalicFont={\VAR{fonts.mono.bolditalic}} +]{\VAR{fonts.mono.regular}} \setmainfont[ - BoldFont={NimbusSans}, - ItalicFont={NimbusSans}, - BoldItalicFont={NimbusSans} -]{Nimbus Sans} % TODO: make configurable + BoldFont={\VAR{fonts.main.bold}}, + ItalicFont={\VAR{fonts.main.italic}}, + BoldItalicFont={\VAR{fonts.main.bolditalic}} +]{\VAR{fonts.main.regular}} -% nicht einr�cken und benutzerinnendefinierte kopfzeile +% nicht einrücken und benutzerinnendefinierte kopfzeile \setlength{\parindent}{0cm} \setlength{\parskip}{1ex} \pagestyle{myheadings} diff --git a/views/forms.py b/views/forms.py index 6f6bb5840ed37cc5bf9674808d2a9f95e0f17a3c..48e4fa8ee53c60f2561dfab6299070f765f7ad10 100644 --- a/views/forms.py +++ b/views/forms.py @@ -1,5 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField +from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField from wtforms.validators import InputRequired class LoginForm(FlaskForm): @@ -32,3 +32,7 @@ class NewProtocolForm(FlaskForm): def __init__(self, protocoltypes, **kwargs): super().__init__(**kwargs) self.protocoltype.choices = [(protocoltype.id, protocoltype.short_name) for protocoltype in protocoltypes] + +class DocumentUploadForm(FlaskForm): + document = FileField("Datei", validators=[InputRequired("Du musst eine Datei angeben.")]) + private = BooleanField("Intern") diff --git a/views/tables.py b/views/tables.py index 1fcc0a991ea226839f45a4a0dce1798d1ff2006d..8f81e0b7e974333866a62446f0620b2f08812745 100644 --- a/views/tables.py +++ b/views/tables.py @@ -1,7 +1,7 @@ # coding: utf-8 from flask import Markup, url_for, request from models.database import Protocol, ProtocolType, DefaultTOP, TOP, Todo, Decision -from shared import date_filter, datetime_filter +from shared import date_filter, datetime_filter, date_filter_short class Table: def __init__(self, title, values, newlink=None, newtext=None): @@ -174,3 +174,17 @@ class TodosTable(Table): todo.who, todo.description ] + +class DocumentsTable(Table): + def __init__(self, documents): + super().__init__("Anhang", documents) + + def headers(self): + return ["ID", "Name", ""] + + def row(self, document): + return [ + document.id, + Table.link(url_for("download_document", document_id=document.id), document.name), + "" + ]