From b3f71d2531fa72eb407e7a7a0c1d1b93e4997f91 Mon Sep 17 00:00:00 2001 From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de> Date: Sat, 25 Feb 2017 21:33:32 +0100 Subject: [PATCH] Pushing to Wiki --- config.py.example | 43 +++++++++-- migrations/versions/d8c0c74b88bd_.py | 32 +++++++++ models/database.py | 44 +++++++++--- parser.py | 12 +++- server.py | 4 +- tasks.py | 21 +++++- templates/protocol.wiki | 24 +++++++ views/forms.py | 3 + views/tables.py | 16 +++-- wiki.py | 104 +++++++++++++++++++++++++++ 10 files changed, 281 insertions(+), 22 deletions(-) create mode 100644 migrations/versions/d8c0c74b88bd_.py create mode 100644 templates/protocol.wiki create mode 100644 wiki.py diff --git a/config.py.example b/config.py.example index 66052cd..98a0112 100644 --- a/config.py.example +++ b/config.py.example @@ -1,7 +1,7 @@ -SQLALCHEMY_DATABASE_URI = "postgresql://proto3:@/proto3" -SQLALCHEMY_TRACK_MODIFICATIONS = False +SQLALCHEMY_DATABASE_URI = "postgresql://proto3:@/proto3" # change +SQLALCHEMY_TRACK_MODIFICATIONS = False # do not change -SECRET_KEY = "abc" +SECRET_KEY = "something random" DEBUG = False @@ -13,16 +13,44 @@ MAIL_PASSWORD = "password" MAIL_PREFIX = "protokolle" CELERY_BROKER_URL = "redis://localhost:6379/0" -CELERY_TASK_SERIALIZER = "pickle" -CELERY_ACCEPT_CONTENT = ["pickle"] +CELERY_TASK_SERIALIZER = "pickle" # do not change +CELERY_ACCEPT_CONTENT = ["pickle"] # do not change URL_ROOT = "protokolle.example.com" URL_PROTO = "https" URL_PATH = "/" URL_PARAMS = "" +LDAP_PROVIDER_URL = "ldaps://auth.example.com:389" +LDAP_BASE = "dc=example,dc=example,dc=com" +LDAP_PROTOCOL_VERSION = 3 # do not change + +ETHERPAD_URL = "https://fachschaften.rwth-aachen.de/etherpad" +EMPTY_ETHERPAD = """Welcome to Etherpad! + +This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents! + +Get involved with Etherpad at http://etherpad.org + +""" # do not change + +WIKI_ACTIVE = True +WIKI_API_URL = "https://wiki.example.com/wiki/api.php" +WIKI_ANONYMOUS = False +WIKI_USER = "user" +WIKI_PASSWORD = "password" +WIKI_DOMAIN = "domain" # set to None if not necessary + +SESSION_PROTECTION = "strong" + +SECURITY_KEY = "some other random string" + ERROR_CONTEXT_LINES = 3 +PAGE_LENGTH = 20 +PAGE_DIFF = 3 + + # choose something nice from fc-list FONTS = { "main": { @@ -50,3 +78,8 @@ FONTS = { "bolditalic": "Nimbus Mono PS" } } + +DOCUMENTS_PATH = "documents" + +PRIVATE_KEYWORDS = ["private", "internal", "privat", "intern"] + diff --git a/migrations/versions/d8c0c74b88bd_.py b/migrations/versions/d8c0c74b88bd_.py new file mode 100644 index 0000000..7183f59 --- /dev/null +++ b/migrations/versions/d8c0c74b88bd_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: d8c0c74b88bd +Revises: 495509e8f49a +Create Date: 2017-02-25 20:16:05.371638 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd8c0c74b88bd' +down_revision = '495509e8f49a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('protocoltypes', sa.Column('use_wiki', sa.Boolean(), nullable=True)) + op.add_column('protocoltypes', sa.Column('wiki_category', sa.String(), nullable=True)) + op.add_column('protocoltypes', sa.Column('wiki_only_public', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('protocoltypes', 'wiki_only_public') + op.drop_column('protocoltypes', 'wiki_category') + op.drop_column('protocoltypes', 'use_wiki') + # ### end Alembic commands ### diff --git a/models/database.py b/models/database.py index bb0aa02..89d0921 100644 --- a/models/database.py +++ b/models/database.py @@ -26,6 +26,9 @@ class ProtocolType(db.Model): public_group = db.Column(db.String) private_mail = db.Column(db.String) public_mail = db.Column(db.String) + use_wiki = db.Column(db.Boolean) + wiki_category = db.Column(db.String) + wiki_only_public = db.Column(db.Boolean) protocols = relationship("Protocol", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="Protocol.id") default_tops = relationship("DefaultTOP", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="DefaultTOP.number") @@ -33,7 +36,8 @@ class ProtocolType(db.Model): todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id") def __init__(self, name, short_name, organization, - is_public, private_group, public_group, private_mail, public_mail): + is_public, private_group, public_group, private_mail, public_mail, + use_wiki, wiki_category, wiki_only_public): self.name = name self.short_name = short_name self.organization = organization @@ -42,10 +46,19 @@ class ProtocolType(db.Model): self.public_group = public_group self.private_mail = private_mail self.public_mail = public_mail + self.use_wiki = use_wiki + self.wiki_category = wiki_category + self.wiki_only_public = wiki_only_public def __repr__(self): - return "<ProtocolType(id={}, short_name={}, name={}, organization={}, is_public={}, private_group={}, public_group={})>".format( - self.id, self.short_name, self.name, self.organization, self.is_public, self.private_group, self.public_group) + return ("<ProtocolType(id={}, short_name={}, name={}, " + "organization={}, is_public={}, private_group={}, " + "public_group={}, use_wiki={}, wiki_category='{}', " + "wiki_only_public={})>".format( + self.id, self.short_name, self.name, + self.organization, self.is_public, self.private_group, + self.public_group, self.use_wiki, self.wiki_category, + self.wiki_only_public)) def get_latest_protocol(self): candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True) @@ -153,6 +166,9 @@ class Protocol(db.Model): self.protocoltype.short_name.lower(), self.date.strftime("%y-%m-%d")) + def get_wiki_title(self): + return "Protokoll:{}-{:%Y-%m-%d}".format(self.protocoltype.short_name, self.date) + def get_etherpad_link(self): identifier = self.get_identifier() if identifier is None: @@ -305,8 +321,15 @@ class Todo(db.Model): def get_state(self): return "[Erledigt]" if self.done else "[Offen]" - def get_state_tex(self): + def get_state_plain(self): return "Erledigt" if self.done else "Aktiv" + def get_state_tex(self): + return self.get_state_plain() + + def is_new(self, current_protocol=None): + if current_protocol is not None: + return self.get_first_protocol() == current_protocol + return len(self.protocols) == 1 def render_html(self): parts = [ @@ -317,16 +340,21 @@ class Todo(db.Model): return " ".join(parts) def render_latex(self, current_protocol=None): - is_new = len(self.protocols) == 1 - if current_protocol is not None: - is_new = self.get_first_protocol() == current_protocol return r"\textbf{{{}}}: {}: {} -- {}".format( - "Neuer Todo" if is_new else "Todo", + "Neuer Todo" if self.is_new(current_protocol) else "Todo", self.who, self.description, self.get_state_tex() ) + def render_wikitext(self, current_protocol=None): + return "'''{}:''' {}: {} - {}".format( + "Neuer Todo" if self.is_new(current_protocol) else "Todo", + self.who, + self.description, + self.get_state_plain() + ) + class TodoProtocolAssociation(db.Model): diff --git a/parser.py b/parser.py index f451d2d..0e1a62f 100644 --- a/parser.py +++ b/parser.py @@ -197,6 +197,12 @@ class Tag: return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(self.values[0])) elif render_type == RenderType.plaintext: return "{}: {}".format(self.name.capitalize(), self.values[0]) + elif render_type == RenderType.wikitext: + if self.name == "url": + return "[{0} {0}]".format(self.values[0]) + elif self.name == "todo": + return self.todo.render_wikitext(current_protocol=protocol) + return "'''{}:''' {}".format(self.name.capitalize(), self.values[0]) else: raise _not_implemented(self, render_type) @@ -291,7 +297,7 @@ class Fork(Element): def test_private(self, name): stripped_name = name.replace(":", "").strip() - return stripped_name in config.PRIVATE_KEYS + return stripped_name in config.PRIVATE_KEYWORDS def render(self, render_type, show_private, level, protocol=None): name_line = self.name if self.name is not None and len(self.name) > 0 else "" @@ -317,14 +323,14 @@ class Fork(Element): else: return "\n".join([name_line, begin_line, content_lines, end_line]) elif render_type == RenderType.wikitext: - title_line = "{0}{1}{0}".format("=" * (level + 2), name_line) + title_line = "{0} {1} {0}".format("=" * (level + 2), name_line) content_parts = [] for child in self.children: part = child.render(render_type, show_private, level=level+1, protocol=protocol) if len(part.strip()) == 0: continue content_parts.append(part) - content_lines = "{}\n{}".format(title_line, "\n".join(content_parts)) + content_lines = "{}\n\n{}\n".format(title_line, "\n\n".join(content_parts)) if self.test_private(self.name) and not show_private: return "" else: diff --git a/server.py b/server.py index 05a1cd8..289cdcd 100755 --- a/server.py +++ b/server.py @@ -88,7 +88,9 @@ def new_type(): protocoltype = ProtocolType(form.name.data, form.short_name.data, form.organization.data, form.is_public.data, form.private_group.data, form.public_group.data, - form.private_mail.data, form.public_mail.data) + form.private_mail.data, form.public_mail.data, + form.use_wiki.data, form.wiki_category.data, + form.wiki_only_public.data) db.session.add(protocoltype) db.session.commit() flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success") diff --git a/tasks.py b/tasks.py index a14bf0f..0beb949 100644 --- a/tasks.py +++ b/tasks.py @@ -11,6 +11,7 @@ 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 from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs from parser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork, RenderType +from wiki import WikiClient, WikiException import config @@ -206,10 +207,28 @@ def parse_protocol_async(protocol_id, encoded_kwargs): for show_private in privacy_states: latex_source = texenv.get_template("protocol.tex").render(render_type=RenderType.latex, show_private=show_private, **render_kwargs) compile(latex_source, protocol, show_private=show_private) - # TODO render and push wiki + + if protocol.protocoltype.use_wiki: + wiki_source = render_template("protocol.wiki", render_type=RenderType.wikitext, show_private=not protocol.protocoltype.wiki_only_public, **render_kwargs).replace("\n\n\n", "\n\n") + push_to_wiki(protocol, wiki_source, "Automatisch generiert vom Protokollsystem 3.0") protocol.done = True db.session.commit() +def push_to_wiki(protocol, content, summary): + push_to_wiki_async.delay(protocol.id, content, summary) + +@celery.task +def push_to_wiki_async(protocol_id, content, summary): + with WikiClient() as wiki_client, app.app_context(): + protocol = Protocol.query.filter_by(id=protocol_id).first() + try: + wiki_client.edit_page( + title=protocol.get_wiki_title(), + content=content, + summary="Automatisch generiert vom Protokollsystem 3.") + except WikiException as exc: + error = protocol.create_error("Pushing to Wiki", "Pushing to Wiki failed.", str(exc)) + def compile(content, protocol, show_private): compile_async.delay(content, protocol.id, show_private) diff --git a/templates/protocol.wiki b/templates/protocol.wiki new file mode 100644 index 0000000..3227908 --- /dev/null +++ b/templates/protocol.wiki @@ -0,0 +1,24 @@ +{{'{{'}}Infobox Protokoll +| name = {{protocol.protocoltype.name}} +| datum = {{protocol.date|datify}} +| zeit = von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}} +| protokollant = {{protocol.author}} +| anwesende = {{protocol.participants}} +{{'}}'}} + +== Beschlüsse == +{% if protocol.decisions|length > 0 %} + {% for decision in protocol.decisions %} +* {{decision.content}} + {% endfor %} +{% else %} +* keine Beschlüsse +{% endif %} + +{% for top in tree.children %} + {% if top|class == "Fork" %} +{{top.render(render_type=render_type, level=0, show_private=show_private, protocol=protocol)}} + {% endif %} +{% endfor %} + +[[Kategorie:{{protocol.protocoltype.wiki_category}}]] diff --git a/views/forms.py b/views/forms.py index ebec93e..6abc73b 100644 --- a/views/forms.py +++ b/views/forms.py @@ -15,6 +15,9 @@ class ProtocolTypeForm(FlaskForm): public_group = StringField("Öffentliche Gruppe") private_mail = StringField("Interner Verteiler") public_mail = StringField("Öffentlicher Verteiler") + wiki_category = StringField("Wiki-Kategorie") + use_wiki = BooleanField("Wiki benutzen") + wiki_only_public = BooleanField("Wiki ist öffentlich") class DefaultTopForm(FlaskForm): name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")]) diff --git a/views/tables.py b/views/tables.py index f7f5255..c7372a9 100644 --- a/views/tables.py +++ b/views/tables.py @@ -95,12 +95,16 @@ class ProtocolTypeTable(SingleValueTable): super().__init__(protocoltype.name, protocoltype, newlink=url_for("edit_type", type_id=protocoltype.id)) def headers(self): - return ["Name", "Abkürzung", "Organisation", "Öffentlich", + headers = ["Name", "Abkürzung", "Organisation", "Öffentlich", "Interne Gruppe", "Öffentliche Gruppe", - "Interner Verteiler", "Öffentlicher Verteiler"] + "Interner Verteiler", "Öffentlicher Verteiler", + "Wiki"] + if self.value.use_wiki: + headers.append("Wiki-Kategorie") + return headers def row(self): - return [ + row = [ self.value.name, self.value.short_name, self.value.organization, @@ -108,8 +112,12 @@ class ProtocolTypeTable(SingleValueTable): self.value.private_group, self.value.public_group, self.value.private_mail, - self.value.public_mail + self.value.public_mail, + Table.bool(self.value.use_wiki) + (", " + ("Öffentlich" if self.value.wiki_only_public else "Intern")) if self.value.use_wiki else "" ] + if self.value.use_wiki: + row.append(self.value.wiki_category) + return row class DefaultTOPsTable(Table): def __init__(self, tops, protocoltype=None): diff --git a/wiki.py b/wiki.py new file mode 100644 index 0000000..10cf9b7 --- /dev/null +++ b/wiki.py @@ -0,0 +1,104 @@ +import requests +import json + +import config + +HTTP_STATUS_OK = 200 + +class WikiException(Exception): + pass + +def _filter_params(params): + result = {} + for key, value in sorted(params.items(), key=lambda t: t[0] == "token"): + if isinstance(value, bool): + if value: + result[key] = "" + else: + result[key] = value + return result + +class WikiClient: + def __init__(self, active=None, endpoint=None, anonymous=None, user=None, password=None, domain=None): + self.active = active if active is not None else config.WIKI_ACTIVE + self.endpoint = endpoint if endpoint is not None else config.WIKI_API_URL + self.anonymous = anonymous if anonymous is not None else config.WIKI_ANONYMOUS + self.user = user if user is not None else config.WIKI_USER + self.password = password if password is not None else config.WIKI_PASSWORD + self.domain = domain if domain is not None else config.WIKI_DOMAIN + self.token = None + self.cookies = requests.cookies.RequestsCookieJar() + + def __enter__(self): + if not self.anonymous: + self.login() + return self + + def __exit__(self, type, value, traceback): + if not self.anonymous: + self.logout() + + def is_logged_in(self): + return self.token is not None + + def login(self): + if not self.active: + return + # todo: Change this to the new MediaWiki tokens api once the wiki is updated + token_answer = self.do_action("login", method="post", lgname=self.user) + if "login" not in token_answer or "token" not in token_answer["login"]: + raise WikiException("No token in login answer.") + lgtoken = token_answer["login"]["token"] + login_answer = self.do_action("login", method="post", lgname=self.user, lgpassword=self.password, lgdomain=self.domain, lgtoken=lgtoken) + if ("login" not in login_answer + or "result" not in login_answer["login"] + or login_answer["login"]["result"] != "Success"): + raise WikiException("Login not successful.") + + + def logout(self): + if not self.active: + return + self.do_action("logout") + + def edit_page(self, title, content, summary, recreate=True, createonly=False): + if not self.active: + return + # todo: port to new api once the wiki is updated + prop_answer = self.do_action("query", method="get", prop="info", intoken="edit", titles=title) + if ("query" not in prop_answer + or "pages" not in prop_answer["query"]): + raise WikiException("Can't get token for page {}".format(title)) + pages = prop_answer["query"]["pages"] + edit_token = None + for page in pages.values(): + if page["title"] == title: + edit_token = page["edittoken"] + break + if edit_token is None: + raise WikiException("Can't get token for page {}".format(title)) + edit_answer = self.do_action(action="edit", method="post", data={"text": content}, + token=edit_token, title=title, + summary=summary, recreate=recreate, + createonly=createonly, bot=True) + + def do_action(self, action, method="get", data=None, **kwargs): + if not self.active: + return + kwargs["action"] = action + kwargs["format"] = "json" + params = _filter_params(kwargs) + req = None + if method == "get": + req = requests.get(self.endpoint, cookies=self.cookies, params=params) + elif method == "post": + req = requests.post(self.endpoint, cookies=self.cookies, data=data, params=params) + if req.status_code != HTTP_STATUS_OK: + raise WikiException("HTTP status code {} on action {}.".format(req.status_code, action)) + self.cookies = req.cookies + return req.json() + +def main(): + with WikiClient() as client: + client.edit_page(title="Test", content="This is a very long text.", summary="API client test") + -- GitLab