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

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):
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")
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):
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):
id = db.Column(db.Integer, primary_key=True)
protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
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)
def get_parent(self):
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):
__tablename__ = "meetingreminders"
__model_name__ = "meetingreminder"
......@@ -621,6 +635,6 @@ class Meta(DatabaseModel):
ALL_MODELS = [
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
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 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 views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm
from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable
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, 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, DecisionCategoriesTable
from legacy import import_old_todos, import_old_protocols, import_old_todomails
app = Flask(__name__)
......@@ -242,7 +242,8 @@ def show_type(protocoltype):
default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
reminders_table = MeetingRemindersTable(protocoltype.reminders, 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>")
@login_required
......@@ -900,16 +901,30 @@ def list_decisions():
user = current_user()
protocoltype = None
protocoltype_id = None
decisioncategory = None
decisioncategory_id = None
try:
protocoltype_id = int(request.args.get("protocoltype_id"))
except (ValueError, TypeError):
pass
try:
decisioncategory_id = int(request.args.get("decisioncategory_id"))
except (ValueError, TypeError):
pass
search_term = request.args.get("search")
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:
search_form.protocoltype_id.data = protocoltype_id
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:
search_form.search.data = search_term
decisions = [
......@@ -921,6 +936,12 @@ def list_decisions():
decision for decision in decisions
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:
decisions = [
decision for decision in decisions
......@@ -1126,7 +1147,47 @@ def delete_defaultmeta(defaultmeta):
type_id = defaultmeta.protocoltype.id
db.session.delete(meta)
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))
@app.route("/login", methods=["GET", "POST"])
......
......@@ -8,7 +8,7 @@ from datetime import datetime
import traceback
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 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
......@@ -281,7 +281,32 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
db.session.add(error)
db.session.commit()
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.commit()
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 @@
{{render_table(reminders_table)}}
{% endif %}
{{render_table(metas_table)}}
{{render_table(categories_table)}}
</div>
{% endblock %}
......@@ -18,7 +18,17 @@ def get_protocoltype_choices(protocoltypes, add_all=True):
in sorted(protocoltypes, key=lambda t: t.short_name)
]
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
def get_todostate_choices():
......@@ -188,6 +198,13 @@ class SearchForm(FlaskForm):
super().__init__(**kwargs)
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):
protocoltype_id = SelectField("Typ", choices=[], coerce=int)
who = StringField("Person", validators=[InputRequired("Bitte gib an, wer das Todo erledigen soll.")])
......@@ -222,6 +239,9 @@ class DefaultMetaForm(FlaskForm):
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.")])
class DecisionCategoryForm(FlaskForm):
name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Kategorie an.")])
class MergeTodosForm(FlaskForm):
todo1 = IntegerField("todo 1", validators=[InputRequired()])
todo2 = IntegerField("todo 2", validators=[InputRequired()])
......
......@@ -373,21 +373,36 @@ class TodoTable(SingleValueTable):
class DecisionsTable(Table):
def __init__(self, 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):
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):
user = current_user()
return [
content_part = [
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")
if config.PRINTING_ACTIVE
and decision.protocol.protocoltype.has_modify_right(user)
and decision.document is not None
else ""
]
return content_part + category_part + action_part
class DocumentsTable(Table):
def __init__(self, documents):
......@@ -453,3 +468,26 @@ class DefaultMetasTable(Table):
link_part = [Table.concat(links)]
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
Markdown is supported
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