Commit 2f5c086f authored by Robin Sonnabend's avatar Robin Sonnabend
Browse files

Add optional decision categories

Categories can be set per protocoltype.
They are specified as another semicolon-separated argument to
[beschluss;…].
Decisions can be search for their category.
/close #53
parent 481dcd7d
"""empty message
Revision ID: 4b813bbbd8ef
Revises: 8fdd381e6a2a
Create Date: 2017-03-15 02:18:24.986531
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4b813bbbd8ef'
down_revision = '8fdd381e6a2a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('decisioncategories',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('protocoltype_id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['protocoltype_id'], ['protocoltypes.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('decisions', sa.Column('category_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'decisions', 'decisioncategories', ['category_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'decisions', type_='foreignkey')
op.drop_column('decisions', 'category_id')
op.drop_table('decisioncategories')
# ### end Alembic commands ###
...@@ -72,6 +72,7 @@ class ProtocolType(DatabaseModel): ...@@ -72,6 +72,7 @@ class ProtocolType(DatabaseModel):
reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.days_before") reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.days_before")
todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id") todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id")
metas = relationship("DefaultMeta", backref=backref("protocoltype"), cascade="all, delete-orphan") metas = relationship("DefaultMeta", backref=backref("protocoltype"), cascade="all, delete-orphan")
decisioncategories = relationship("DecisionCategory", backref=backref("protocoltype"), cascade="all, delete-orphan")
def get_latest_protocol(self): def get_latest_protocol(self):
candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True) candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True)
...@@ -540,12 +541,25 @@ class Decision(DatabaseModel): ...@@ -540,12 +541,25 @@ class Decision(DatabaseModel):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id")) protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
content = db.Column(db.String) content = db.Column(db.String)
category_id = db.Column(db.Integer, db.ForeignKey("decisioncategories.id"), nullable=True)
document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False) document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False)
def get_parent(self): def get_parent(self):
return self.protocol return self.protocol
class DecisionCategory(DatabaseModel):
__tablename__ = "decisioncategories"
__model_name__ = "decisioncategory"
id = db.Column(db.Integer, primary_key=True)
protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
name = db.Column(db.String)
decisions = relationship("Decision", backref=backref("category"), order_by="Decision.id")
def get_parent(self):
return self.protocoltype
class MeetingReminder(DatabaseModel): class MeetingReminder(DatabaseModel):
__tablename__ = "meetingreminders" __tablename__ = "meetingreminders"
__model_name__ = "meetingreminder" __model_name__ = "meetingreminder"
...@@ -621,6 +635,6 @@ class Meta(DatabaseModel): ...@@ -621,6 +635,6 @@ class Meta(DatabaseModel):
ALL_MODELS = [ ALL_MODELS = [
ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument, ProtocolType, Protocol, DefaultTOP, TOP, Document, DecisionDocument,
Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta Todo, Decision, MeetingReminder, Error, DefaultMeta, Meta, DecisionCategory
] ]
...@@ -23,9 +23,9 @@ import config ...@@ -23,9 +23,9 @@ import config
from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, time_filter_short, ldap_manager, security_manager, current_user, check_login, login_required, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter, indent_tab_filter from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, time_filter_short, ldap_manager, security_manager, current_user, check_login, login_required, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter, indent_tab_filter
from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg
from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_admin_right from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_admin_right
from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory
from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, DecisionSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable
from legacy import import_old_todos, import_old_protocols, import_old_todomails from legacy import import_old_todos, import_old_protocols, import_old_todomails
app = Flask(__name__) app = Flask(__name__)
...@@ -242,7 +242,8 @@ def show_type(protocoltype): ...@@ -242,7 +242,8 @@ def show_type(protocoltype):
default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype) default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype) reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype)
metas_table = DefaultMetasTable(protocoltype.metas, protocoltype) metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table, metas_table=metas_table, reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE) categories_table = DecisionCategoriesTable(protocoltype.decisioncategories, protocoltype)
return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table, metas_table=metas_table, reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE, categories_table=categories_table)
@app.route("/type/delete/<int:protocoltype_id>") @app.route("/type/delete/<int:protocoltype_id>")
@login_required @login_required
...@@ -900,16 +901,30 @@ def list_decisions(): ...@@ -900,16 +901,30 @@ def list_decisions():
user = current_user() user = current_user()
protocoltype = None protocoltype = None
protocoltype_id = None protocoltype_id = None
decisioncategory = None
decisioncategory_id = None
try: try:
protocoltype_id = int(request.args.get("protocoltype_id")) protocoltype_id = int(request.args.get("protocoltype_id"))
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
try:
decisioncategory_id = int(request.args.get("decisioncategory_id"))
except (ValueError, TypeError):
pass
search_term = request.args.get("search") search_term = request.args.get("search")
protocoltypes = ProtocolType.get_public_protocoltypes(user) protocoltypes = ProtocolType.get_public_protocoltypes(user)
search_form = SearchForm(protocoltypes) decisioncategories = [
category
for protocoltype in protocoltypes
for category in protocoltype.decisioncategories
]
search_form = DecisionSearchForm(protocoltypes, decisioncategories)
if protocoltype_id is not None: if protocoltype_id is not None:
search_form.protocoltype_id.data = protocoltype_id search_form.protocoltype_id.data = protocoltype_id
protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first() protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
if decisioncategory_id is not None:
search_form.decisioncategory_id.data = decisioncategory_id
decisioncategory = DecisionCategory.query.filter_by(id=decisioncategory_id).first()
if search_term is not None: if search_term is not None:
search_form.search.data = search_term search_form.search.data = search_term
decisions = [ decisions = [
...@@ -921,6 +936,12 @@ def list_decisions(): ...@@ -921,6 +936,12 @@ def list_decisions():
decision for decision in decisions decision for decision in decisions
if decision.protocol.protocoltype.id == protocoltype_id if decision.protocol.protocoltype.id == protocoltype_id
] ]
if decisioncategory_id is not None and decisioncategory_id != -1:
decisions = [
decision for decision in decisions
if decision.category is not None
and decision.category.id == decisioncategory_id
]
if search_term is not None and len(search_term.strip()) > 0: if search_term is not None and len(search_term.strip()) > 0:
decisions = [ decisions = [
decision for decision in decisions decision for decision in decisions
...@@ -1126,7 +1147,47 @@ def delete_defaultmeta(defaultmeta): ...@@ -1126,7 +1147,47 @@ def delete_defaultmeta(defaultmeta):
type_id = defaultmeta.protocoltype.id type_id = defaultmeta.protocoltype.id
db.session.delete(meta) db.session.delete(meta)
db.session.commit() db.session.commit()
flash("Metadatenfeld '{}' gelöscht.".format(name), "alert-error") flash("Metadatenfeld '{}' gelöscht.".format(name), "alert-success")
return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=type_id))
@app.route("/decisioncategory/new/<int:protocoltype_id>", methods=["GET", "POST"])
@login_required
@db_lookup(ProtocolType)
@require_modify_right()
def new_decisioncategory(protocoltype):
form = DecisionCategoryForm()
if form.validate_on_submit():
category = DecisionCategory(protocoltype_id=protocoltype.id)
form.populate_obj(category)
db.session.add(category)
db.session.commit()
flash("Beschlusskategorie hinzugefügt.", "alert-success")
return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=protocoltype.id))
return render_template("decisioncategory-new.html", form=form, protocoltype=protocoltype)
@app.route("/decisioncategory/edit/<int:decisioncategory_id>", methods=["GET", "POST"])
@login_required
@db_lookup(DecisionCategory)
@require_modify_right()
def edit_decisioncategory(decisioncategory):
form = DecisionCategoryForm(obj=decisioncategory)
if form.validate_on_submit():
form.populate_obj(decisioncategory)
db.session.commit()
return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=decisioncategory.protocoltype.id))
return render_template("decisioncategory-edit.html", form=form, decisioncategory=decisioncategory)
@app.route("/decisioncategory/delete/<int:decisioncategory_id>")
@login_required
@group_required(config.ADMIN_GROUP)
@db_lookup(DecisionCategory)
@require_modify_right()
def delete_decisioncategory(decisioncategory):
name = decisioncategory.name
type_id = decisioncategory.protocoltype.id
db.session.delete(decisioncategory)
db.session.commit()
flash("Beschlusskategorie {} gelöscht.".format(name), "alert-success")
return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=type_id)) return redirect(request.args.get("next") or url_for("show_type", protocoltype_id=type_id))
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
......
...@@ -8,7 +8,7 @@ from datetime import datetime ...@@ -8,7 +8,7 @@ from datetime import datetime
import traceback import traceback
from copy import copy from copy import copy
from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder, TodoMail, DecisionDocument, TodoState, OldTodo from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder, TodoMail, DecisionDocument, TodoState, OldTodo, DecisionCategory
from models.errors import DateNotMatchingException from models.errors import DateNotMatchingException
from server import celery, app from server import celery, app
from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, class_filter, KNOWN_KEYS from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, class_filter, KNOWN_KEYS
...@@ -281,7 +281,32 @@ def parse_protocol_async_inner(protocol, encoded_kwargs): ...@@ -281,7 +281,32 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
db.session.add(error) db.session.add(error)
db.session.commit() db.session.commit()
return return
decision = Decision(protocol_id=protocol.id, content=decision_tag.values[0]) decision_content = decision_tag.values[0]
decision_category_id = None
if len(decision_tag.values) > 1:
decision_category_name = decision_tag.values[1]
decision_category = DecisionCategory.query.filter_by(protocoltype_id=protocol.protocoltype.id, name=decision_category_name).first()
if decision_category is None:
category_candidates = DecisionCategory.query.filter_by(protocoltype_id=protocol.protocoltype.id).all()
category_names = [
"'{}'".format(category.name)
for category in category_candidates
]
error = protocol.create_error("Parsing",
"Unknown decision category",
"The decision in line {} has the category {}, "
"but there is no such category. "
"Known categories are {}".format(
decision_tag.linenumber,
decision_category_name,
", ".join(category_names)))
db.session.add(error)
db.session.commit()
return
else:
decision_category_id = decision_category.id
decision = Decision(protocol_id=protocol.id,
content=decision_content, category_id=decision_category_id)
db.session.add(decision) db.session.add(decision)
db.session.commit() db.session.commit()
decision_top = decision_tag.fork.get_top() decision_top = decision_tag.fork.get_top()
......
{% extends "layout.html" %}
{% from "macros.html" import render_form %}
{% block title %}Beschlusskategorie ändern{% endblock %}
{% block content %}
<div class="container">
{{render_form(form, action_url=url_for("edit_decisioncategory", decisioncategory_id=decisioncategory.id, next=url_for("show_type", protocoltype_id=decisioncategory.protocoltype.id)), action_text="Ändern")}}
</div>
{% endblock %}
{% extends "layout.html" %}
{% from "macros.html" import render_form %}
{% block title %}Beschlusskategorie hinzufügen{% endblock %}
{% block content %}
<div class="container">
{{render_form(form, action_url=url_for("new_decisioncategory", protocoltype_id=protocoltype.id, next=url_for("show_type", protocoltype_id=protocoltype.id)), action_text="Anlegen")}}
</div>
{% endblock %}
...@@ -11,5 +11,6 @@ ...@@ -11,5 +11,6 @@
{{render_table(reminders_table)}} {{render_table(reminders_table)}}
{% endif %} {% endif %}
{{render_table(metas_table)}} {{render_table(metas_table)}}
{{render_table(categories_table)}}
</div> </div>
{% endblock %} {% endblock %}
...@@ -18,7 +18,17 @@ def get_protocoltype_choices(protocoltypes, add_all=True): ...@@ -18,7 +18,17 @@ def get_protocoltype_choices(protocoltypes, add_all=True):
in sorted(protocoltypes, key=lambda t: t.short_name) in sorted(protocoltypes, key=lambda t: t.short_name)
] ]
if add_all: if add_all:
choices.insert(0, (-1, "Alle")) choices.insert(0, (-1, "Alle Typen"))
return choices
def get_category_choices(categories, add_all=True):
choices = [
(category.id, category.name)
for category
in sorted(categories, key=lambda c: c.name)
]
if add_all:
choices.insert(0, (-1, "Alle Kategorien"))
return choices return choices
def get_todostate_choices(): def get_todostate_choices():
...@@ -188,6 +198,13 @@ class SearchForm(FlaskForm): ...@@ -188,6 +198,13 @@ class SearchForm(FlaskForm):
super().__init__(**kwargs) super().__init__(**kwargs)
self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes) self.protocoltype_id.choices = get_protocoltype_choices(protocoltypes)
class DecisionSearchForm(SearchForm):
decisioncategory_id = SelectField("Kategorie", choices=[], coerce=int)
def __init__(self, protocoltypes, categories, **kwargs):
super().__init__(protocoltypes=protocoltypes, **kwargs)
self.decisioncategory_id.choices = get_category_choices(categories)
class NewTodoForm(FlaskForm): class NewTodoForm(FlaskForm):
protocoltype_id = SelectField("Typ", choices=[], coerce=int) protocoltype_id = SelectField("Typ", choices=[], coerce=int)
who = StringField("Person", validators=[InputRequired("Bitte gib an, wer das Todo erledigen soll.")]) who = StringField("Person", validators=[InputRequired("Bitte gib an, wer das Todo erledigen soll.")])
...@@ -222,6 +239,9 @@ class DefaultMetaForm(FlaskForm): ...@@ -222,6 +239,9 @@ class DefaultMetaForm(FlaskForm):
key = StringField("Key", validators=[InputRequired("Bitte gib den Protokoll-Syntax-Schlüssel der Metadaten an.")]) key = StringField("Key", validators=[InputRequired("Bitte gib den Protokoll-Syntax-Schlüssel der Metadaten an.")])
name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Metadaten an.")]) name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Metadaten an.")])
class DecisionCategoryForm(FlaskForm):
name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Kategorie an.")])
class MergeTodosForm(FlaskForm): class MergeTodosForm(FlaskForm):
todo1 = IntegerField("todo 1", validators=[InputRequired()]) todo1 = IntegerField("todo 1", validators=[InputRequired()])
todo2 = IntegerField("todo 2", validators=[InputRequired()]) todo2 = IntegerField("todo 2", validators=[InputRequired()])
......
...@@ -373,21 +373,36 @@ class TodoTable(SingleValueTable): ...@@ -373,21 +373,36 @@ class TodoTable(SingleValueTable):
class DecisionsTable(Table): class DecisionsTable(Table):
def __init__(self, decisions): def __init__(self, decisions):
super().__init__("Beschlüsse", decisions) super().__init__("Beschlüsse", decisions)
self.category_present = len([
decision for decision in decisions
if decision.category is not None
]) > 0
def headers(self): def headers(self):
return ["Sitzung", "Beschluss", ""] content_part = ["Sitzung", "Beschluss"]
category_part = ["Kategorie"]
if not self.category_present:
category_part = []
action_part = [""]
return content_part + category_part + action_part
def row(self, decision): def row(self, decision):
user = current_user() user = current_user()
return [ content_part = [
Table.link(url_for("show_protocol", protocol_id=decision.protocol.id), decision.protocol.get_identifier()), Table.link(url_for("show_protocol", protocol_id=decision.protocol.id), decision.protocol.get_identifier()),
decision.content, decision.content
]
category_part = [decision.category.name if decision.category is not None else ""]
if not self.category_present:
category_part = []
action_part = [
Table.link(url_for("print_decision", decisiondocument_id=decision.document.id), "Drucken") Table.link(url_for("print_decision", decisiondocument_id=decision.document.id), "Drucken")
if config.PRINTING_ACTIVE if config.PRINTING_ACTIVE
and decision.protocol.protocoltype.has_modify_right(user) and decision.protocol.protocoltype.has_modify_right(user)
and decision.document is not None and decision.document is not None
else "" else ""
] ]
return content_part + category_part + action_part
class DocumentsTable(Table): class DocumentsTable(Table):
def __init__(self, documents): def __init__(self, documents):
...@@ -453,3 +468,26 @@ class DefaultMetasTable(Table): ...@@ -453,3 +468,26 @@ class DefaultMetasTable(Table):
link_part = [Table.concat(links)] link_part = [Table.concat(links)]
return general_part + link_part return general_part + link_part
class DecisionCategoriesTable(Table):
def __init__(self, categories, protocoltype):
print(categories)
super().__init__(
"Beschlusskategorien",
categories,
url_for("new_decisioncategory", protocoltype_id=protocoltype.id)
)
def headers(self):
return ["Name", ""]
def row(self, category):
user = current_user()
general_part = [category.name]
action_part = [
Table.concat([
Table.link(url_for("edit_decisioncategory", decisioncategory_id=category.id), "Ändern"),
Table.link(url_for("delete_decisioncategory", decisioncategory_id=category.id), "Löschen", confirm="Bist du dir sicher, dass du die Beschlusskategorie {} löschen willst?".format(category.name))
])
]
return general_part + action_part
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment