From 023b130cde91f4e38c719cb20828cceda7460597 Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Tue, 13 Jun 2017 19:34:16 +0200
Subject: [PATCH] Footnotes and small fixes

/close #132
/close #130
---
 parser.py               | 22 ++++++++++++++-
 server.py               |  3 ++-
 tasks.py                | 59 ++++++++++++++++++++++++++++++++---------
 templates/protocol.html |  7 +++++
 templates/protocol.wiki |  4 +++
 utils.py                |  3 +++
 6 files changed, 83 insertions(+), 15 deletions(-)

diff --git a/parser.py b/parser.py
index 9e7285a..12703d1 100644
--- a/parser.py
+++ b/parser.py
@@ -4,6 +4,7 @@ from collections import OrderedDict
 from enum import Enum
 
 from shared import escape_tex
+from utils import footnote_hash
 
 import config
 
@@ -223,6 +224,8 @@ class Tag:
                         r"\textit{{({})}}".format(self.decision.get_categories_str())
                     )
                 return " ".join(parts)
+            elif self.name == "footnote":
+                return r"\footnote{{{}}}".format(self.values[0])
             return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(";".join(self.values)))
         elif render_type == RenderType.plaintext:
             if self.name == "url":
@@ -231,6 +234,8 @@ class Tag:
                 if not show_private:
                     return ""
                 return self.values[0]
+            elif self.name == "footnote":
+                return "[^]({})".format(self.values[0])
             return "{}: {}".format(self.name.capitalize(), ";".join(self.values))
         elif render_type == RenderType.wikitext:
             if self.name == "url":
@@ -239,6 +244,8 @@ class Tag:
                 if not show_private:
                     return ""
                 return self.todo.render_wikitext(current_protocol=protocol)
+            elif self.name == "footnote":
+                return "<ref>{}</ref>".format(self.values[0])
             return "'''{}:''' {}".format(self.name.capitalize(), ";".join(self.values))
         elif render_type == RenderType.html:
             if self.name == "url":
@@ -259,6 +266,9 @@ class Tag:
                     return " ".join(parts)
                 else:
                     return "<b>Beschluss:</b> {}".format(self.values[0])
+            elif self.name == "footnote":
+                return '<sup id="#fnref{0}"><a href="#fn{0}">Fn</a></sup>'.format(
+                    footnote_hash(self.values[0]))
         else:
             raise _not_implemented(self, render_type)
 
@@ -284,7 +294,7 @@ class Tag:
     # v3: also match [] without semicolons inbetween, as there is not other use for that
     PATTERN = r"\[(?<content>[^\]]*)\]"
 
-    KNOWN_TAGS = ["todo", "url", "beschluss"]
+    KNOWN_TAGS = ["todo", "url", "beschluss", "footnote"]
 
 
 class Empty(Element):
@@ -495,6 +505,16 @@ class Fork(Element):
         else:
             return 1
 
+    def get_visible_elements(self, show_private, elements=None):
+        if elements is None:
+            elements = set()
+        if show_private or not self.test_private(self.name):
+            for child in self.children:
+                elements.add(child)
+                if isinstance(child, Fork):
+                    child.get_visible_elements(show_private, elements)
+        return elements
+
     @staticmethod
     def create_root():
         return Fork(None, None, None, 0)
diff --git a/server.py b/server.py
index 0ca5a9a..cfb636c 100755
--- a/server.py
+++ b/server.py
@@ -21,7 +21,7 @@ import mimetypes
 
 import config
 from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, time_filter_short, user_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, fancy_join
+from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg, fancy_join, footnote_hash
 from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_publish_right, require_admin_right
 from models.database import ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory, Like
 from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, generate_protocol_form, TopForm, LocalTopForm, SearchForm, DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm, DocumentEditForm
@@ -60,6 +60,7 @@ app.jinja_env.filters["todo_get_name"] = todostate_name_filter
 app.jinja_env.filters["code"] = code_filter
 app.jinja_env.filters["indent_tab"] = indent_tab_filter
 app.jinja_env.filters["fancy_join"] = fancy_join
+app.jinja_env.filters["footnote_hash"] = footnote_hash
 app.jinja_env.tests["auth_valid"] = security_manager.check_user
 app.jinja_env.tests["needs_date"] = needs_date_test
 
diff --git a/tasks.py b/tasks.py
index ef4d1ba..f393309 100644
--- a/tasks.py
+++ b/tasks.py
@@ -12,7 +12,7 @@ from models.database import Document, Protocol, Error, Todo, Decision, TOP, Defa
 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
-from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs, add_line_numbers, set_etherpad_text, get_etherpad_text
+from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs, add_line_numbers, set_etherpad_text, get_etherpad_text, footnote_hash
 from parser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork, RenderType
 from wiki import WikiClient, WikiException
 from calendarpush import Client as CalendarClient, CalendarException
@@ -168,6 +168,8 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
         return
     # tags 
     tags = tree.get_tags()
+    elements = tree.get_visible_elements(show_private=True)
+    public_elements = tree.get_visible_elements(show_private=False)
     for tag in tags:
         if tag.name not in Tag.KNOWN_TAGS:
             error = protocol.create_error("Parsing", "Invalid tag",
@@ -304,11 +306,20 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
         db.session.commit()
         todo_tag.todo = todo
     # Decisions
+    decision_tags = [tag for tag in tags if tag.name == "beschluss"]
+    for decision_tag in decision_tags:
+        if decision_tag not in public_elements:
+            error = protocol.create_error("Parsing", "Decision in private context.",
+                "The decision in line {} is in a private context, but decisions are "
+                "and have to be public. Please move it to a public spot.".format(
+                decision_tag.linenumber))
+            db.session.add(error)
+            db.session.commit()
+            return
     old_decisions = list(protocol.decisions)
     for decision in old_decisions:
         protocol.decisions.remove(decision)
     db.session.commit()
-    decision_tags = [tag for tag in tags if tag.name == "beschluss"]
     decisions_to_render = []
     for decision_tag in decision_tags:
         if len(decision_tag.values) == 0:
@@ -352,9 +363,15 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
         decision_top = decision_tag.fork.get_top()
         decision_content = texenv.get_template("decision.tex").render(
             render_type=RenderType.latex, decision=decision,
-            protocol=protocol, top=decision_top, show_private=False)
+            protocol=protocol, top=decision_top, show_private=True)
         maxdepth = decision_top.get_maxdepth()
         compile_decision(decision_content, decision, maxdepth=maxdepth)
+
+    # Footnotes
+    footnote_tags = [tag for tag in tags if tag.name == "footnote"]
+    public_footnote_tags = [tag for tag in footnote_tags if tag in public_elements]
+
+    # TOPs
     old_tops = list(protocol.tops)
     for top in old_tops:
         protocol.tops.remove(top)
@@ -365,31 +382,47 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
         db.session.add(top)
     db.session.commit()
 
-    render_kwargs = {
+    # render
+    private_render_kwargs = {
         "protocol": protocol,
-        "tree": tree
+        "tree": tree,
+        "footnotes": footnote_tags,
     }
+    public_render_kwargs = copy(private_render_kwargs)
+    public_render_kwargs["footnotes"] = public_footnote_tags
+    render_kwargs = {True: private_render_kwargs, False: public_render_kwargs}
+    
     maxdepth = tree.get_maxdepth()
     privacy_states = [False]
-    content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **render_kwargs)
-    content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **render_kwargs)
+    content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **private_render_kwargs)
+    content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **public_render_kwargs)
     if content_private != content_public:
         privacy_states.append(True)
     protocol.content_private = content_private
     protocol.content_public = content_public
     protocol.content_html_private = render_template("protocol.html",
-        render_type=RenderType.html, show_private=True, **render_kwargs)
+        render_type=RenderType.html, show_private=True, **private_render_kwargs)
     protocol.content_html_public = render_template("protocol.html",
-        render_type=RenderType.html, show_private=False, **render_kwargs)
+        render_type=RenderType.html, show_private=False, **public_render_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)
+        latex_source = texenv.get_template("protocol.tex").render(
+            render_type=RenderType.latex,
+            show_private=show_private,
+            **render_kwargs[show_private])
         compile(latex_source, protocol, show_private=show_private, maxdepth=maxdepth)
 
     if protocol.protocoltype.use_wiki:
-        wiki_source = wikienv.get_template("protocol.wiki").render(render_type=RenderType.wikitext, show_private=not protocol.protocoltype.wiki_only_public, **render_kwargs).replace("\n\n\n", "\n\n")
-        wiki_infobox_source = wikienv.get_template("infobox.wiki").render(protocoltype=protocol.protocoltype)
-        push_to_wiki(protocol, wiki_source, wiki_infobox_source, "Automatisch generiert vom Protokollsystem 3.0")
+        show_private = not protocol.protocoltype.wiki_only_public
+        wiki_source = wikienv.get_template("protocol.wiki").render(
+            render_type=RenderType.wikitext,
+            show_private=show_private,
+            **render_kwargs[show_private]
+        ).replace("\n\n\n", "\n\n")
+        wiki_infobox_source = wikienv.get_template("infobox.wiki").render(
+            protocoltype=protocol.protocoltype)
+        push_to_wiki(protocol, wiki_source, wiki_infobox_source,
+            "Automatisch generiert vom Protokollsystem 3.0")
     protocol.done = True
     db.session.commit()
 
diff --git a/templates/protocol.html b/templates/protocol.html
index 08a54d2..391beba 100644
--- a/templates/protocol.html
+++ b/templates/protocol.html
@@ -4,3 +4,10 @@
 
 {% endif %}
 {% endfor %}
+
+{% if footnotes|length > 0 %}
+    <hr />
+    {% for footnote in footnotes %}
+        <p><sup id="fn{{footnote.values[0]|footnote_hash}}">{{loop.index}}. {{footnote.values[0]}} <a href="#fnref{{footnote.values[0]|footnote_hash}}">↩</a></sup></p>
+    {% endfor %}
+{% endif %}
diff --git a/templates/protocol.wiki b/templates/protocol.wiki
index bc5e061..2ab67c9 100644
--- a/templates/protocol.wiki
+++ b/templates/protocol.wiki
@@ -28,4 +28,8 @@
     <env> endif </env>
 <env> endfor </env>
 
+<env> if footnotes|length > 0 </env>
+    <references />
+<env> endif </env>
+
 [[Kategorie:<var>protocol.protocoltype.wiki_category</var>]]
diff --git a/utils.py b/utils.py
index 1ac0c4d..dfc3fa1 100644
--- a/utils.py
+++ b/utils.py
@@ -200,3 +200,6 @@ def fancy_join(values, sep1=" und ", sep2=", "):
     last = values[-1]
     start = values[:-1]
     return "{}{}{}".format(sep2.join(start), sep1, last)
+
+def footnote_hash(text, length=5):
+    return str(sum(ord(c) * i for i, c in enumerate(text)) % 10**length)
-- 
GitLab