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