generating output markdown files works
authorJacob Lifshay <programmerjake@gmail.com>
Fri, 18 Sep 2020 06:26:03 +0000 (23:26 -0700)
committerJacob Lifshay <programmerjake@gmail.com>
Fri, 18 Sep 2020 06:26:03 +0000 (23:26 -0700)
budget-sync-config.toml
src/budget_sync/budget_graph.py
src/budget_sync/test/test_write_budget_markdown.py
src/budget_sync/write_budget_markdown.py

index 9366f2f263b35f195aa96e9e11e7a5eecc1c6269..fce36e3e13de1bb101f6cd9be3c7b34e6d216a6a 100644 (file)
@@ -13,7 +13,7 @@ output_markdown_file = "lkcl.mdwn"
 [people."Samuel A. Falvo II"]
 email = "kc5tja@arrl.net"
 aliases = ["kc5tja", "samuel", "Samuel", "Samuel Falvo II", "sam.falvo"]
-output_markdown_file = "samuel_falvo_ii.mdwn"
+output_markdown_file = "Samuel_A_Falvo_II.mdwn"
 
 [people."Vivek Pandya"]
 email = "vivekvpandya@gmail.com"
@@ -25,6 +25,71 @@ email = "florent@enjoy-digital.fr"
 aliases = ["florent", "Florent"]
 output_markdown_file = "florent_kermarrec.mdwn"
 
+[people."Michael Nolan"]
+email = "mtnolan2640@gmail.com"
+aliases = ["michael", "Michael", "mtnolan", "mtnolan2640"]
+output_markdown_file = "michael_nolan.mdwn"
+
+[people."Alain D D Williams"]
+email = "addw@phcomp.co.uk"
+aliases = ["alain", "Alain", "Alain Williams", "addw"]
+output_markdown_file = "addw.mdwn"
+
+[people."Jock Tanner"]
+email = "tanner.of.kha@gmail.com"
+aliases = []
+output_markdown_file = "jock_tanner.mdwn"
+
+[people."R Veera Kumar"]
+email = "vklr@vkten.in"
+aliases = ["Veera", "veera", "Veera Kumar"]
+output_markdown_file = "veera.mdwn"
+
+[people."Jean-Paul Chaput"]
+email = "Jean-Paul.Chaput@lip6.fr"
+aliases = ["Jean Paul Chaput", "Jean-Paul", "jean-paul", "jean paul"]
+output_markdown_file = "jean-paul_chaput.mdwn"
+
+[people."Staf Verhaegen"]
+email = "staf@fibraservi.eu"
+aliases = ["Staf", "staf"]
+output_markdown_file = "staf_verhaegen.mdwn"
+
+[people."Lauri Kasanen"]
+email = "cand@gmx.com"
+aliases = ["Lauri", "lauri"]
+output_markdown_file = "lauri_kasanen.mdwn"
+
+[people."Yehowshua Immanuel"]
+email = "yimmanuel3@gatech.edu"
+aliases = ["Yehowshua", "yehowshua"]
+output_markdown_file = "yehowshua_immanuel.mdwn"
+
+[people."whitequark"]
+email = "whitequark@whitequark.org"
+aliases = []
+output_markdown_file = "whitequark.mdwn"
+
+[people."Tobias Platen"]
+email = "libre-soc@platen-software.de"
+aliases = ["Tobias", "tobias", "tplaten"]
+output_markdown_file = "tplaten.mdwn"
+
+[people."Cole Poirier"]
+email = "colepoirier@gmail.com"
+aliases = ["Cole", "cole"]
+output_markdown_file = "cole.mdwn"
+
+[people."Aleksandar Kostovic"]
+email = "alexandar.kostovic@gmail.com"
+aliases = ["alexandar", "aleksandar"]
+output_markdown_file = "aleksandar_kostovic.mdwn"
+
+[people."Cesar Strauss"]
+email = "cestrauss@gmail.com"
+aliases = ["Cesar", "cesar", "cestrauss"]
+output_markdown_file = "cesar_strauss.mdwn"
+
 [milestones]
 "NLnet.2019.02" = { canonical_bug_id = 191 }
 "NLnet.2019.10.Cells" = { canonical_bug_id = 153 }
index 074447d617446a9670d31acf0d11dd0b8f02ff01..dccf4e28deb53dc96b394562d916c30c57eb74a5 100644 (file)
@@ -717,6 +717,13 @@ class BudgetGraph:
                 errors.append(e)
         return errors
 
+    @cached_property
+    def assigned_nodes(self) -> Dict[Person, List[Node]]:
+        retval = {person: [] for person in self.config.people.values()}
+        for node in self.nodes.values():
+            retval[node.assignee].append(node)
+        return retval
+
     @cached_property
     def payments(self) -> Dict[Person, Dict[Milestone, List[Payment]]]:
         retval = {}
index 1691edc6f6d2bc23da72c6659ce65b38d0833ff7..b035a39c64b10da687ec67e5838596bada06f953 100644 (file)
@@ -4,10 +4,22 @@ from budget_sync.test.mock_bug import MockBug
 from budget_sync.test.mock_path import MockPath, DIR
 from budget_sync.test.test_mock_path import make_filesystem_and_report_if_error
 from budget_sync.budget_graph import BudgetGraph
-from budget_sync.write_budget_markdown import write_budget_markdown
+from budget_sync.write_budget_markdown import (
+    write_budget_markdown, DisplayStatus, markdown_escape)
+from budget_sync.util import BugStatus
 
 
 class TestWriteBudgetMarkdown(unittest.TestCase):
+    maxDiff = None
+
+    def test_display_status(self):
+        for status in BugStatus:
+            DisplayStatus.from_status(status)
+
+    def test_markdown_escape(self):
+        self.assertEqual(markdown_escape("abc * def_k < &k"),
+                         r"abc \* def\_k &lt; &amp;k")
+
     def test(self):
         config = Config.from_str(
             """
@@ -37,10 +49,13 @@ class TestWriteBudgetMarkdown(unittest.TestCase):
                 "/": DIR,
                 "/output_dir": DIR,
                 '/output_dir/person1.mdwn': b'<!-- autogenerated by '
-                b'budget-sync -->\n# person1\n\n# Status Tracking\n',
+                b'budget-sync -->\n# person1\n\n# Status Tracking\n## Not yet '
+                b'started\n* [Bug #1](https://bugzilla.example.com/show_bug.c'
+                b'gi?id=1): \n',
                 '/output_dir/person2.mdwn': b'<!-- autogenerated by '
                 b'budget-sync -->\n# person2\n\n# Status Tracking\n',
             }, filesystem.files)
+    # TODO: add more test cases
 
 
 if __name__ == "__main__":
index 6780a10325ee7b42fc64076f47650a963fea5ae9..aefc04a931ade4e44522f4fbf74b46d41b0ba04e 100644 (file)
@@ -1,8 +1,10 @@
 from pathlib import Path
-from typing import Dict, List, Any
+from typing import Dict, List, Any, Optional
 from io import StringIO
+import enum
 from budget_sync.budget_graph import BudgetGraph, Node, Payment, PayeeState
 from budget_sync.config import Person, Milestone, Config
+from budget_sync.util import BugStatus
 
 
 def _markdown_escape_char(char: str) -> str:
@@ -15,34 +17,150 @@ def _markdown_escape_char(char: str) -> str:
     return char
 
 
-def _markdown_escape(v: Any) -> str:
-    return "".join([char for char in str(v)])
+def markdown_escape(v: Any) -> str:
+    return "".join([_markdown_escape_char(char) for char in str(v)])
+
+
+class DisplayStatus(enum.Enum):
+    Hidden = "Hidden"
+    NotYetStarted = "Not yet started"
+    InProgress = "Currently working on"
+    Completed = "Completed but not yet added to payees list"
+
+    @staticmethod
+    def from_status(status: BugStatus) -> "DisplayStatus":
+        return _DISPLAY_STATUS_MAP[status]
+
+
+_DISPLAY_STATUS_MAP = {
+    BugStatus.UNCONFIRMED: DisplayStatus.Hidden,
+    BugStatus.CONFIRMED: DisplayStatus.NotYetStarted,
+    BugStatus.IN_PROGRESS: DisplayStatus.InProgress,
+    BugStatus.DEFERRED: DisplayStatus.Hidden,
+    BugStatus.RESOLVED: DisplayStatus.Completed,
+    BugStatus.VERIFIED: DisplayStatus.Completed,
+    BugStatus.PAYMENTPENDING: DisplayStatus.Completed,
+}
+
+
+class MarkdownWriter:
+    last_headers: List[str]
+
+    def __init__(self):
+        self.buffer = StringIO()
+        self.last_headers = []
+
+    def write_headers(self, headers: List[str]):
+        if headers == self.last_headers:
+            return
+        for i in range(len(headers)):
+            if i >= len(self.last_headers):
+                print(headers[i], file=self.buffer)
+                self.last_headers.append(headers[i])
+            elif headers[i] != self.last_headers[i]:
+                del self.last_headers[i:]
+                print(headers[i], file=self.buffer)
+                self.last_headers.append(headers[i])
+        if len(self.last_headers) > len(headers):
+            raise ValueError("tried to go from deeper header scope stack to "
+                             "ancestor scope without starting a new header, "
+                             "which is not supported by markdown",
+                             self.last_headers, headers)
+        assert headers == self.last_headers
+
+    def write_node(self,
+                   headers: List[str],
+                   node: Node,
+                   payment: Optional[Payment]):
+        self.write_headers(headers)
+        summary = markdown_escape(node.bug.summary)
+        print(f"* [Bug #{node.bug.id}]({node.bug_url}): {summary}",
+              file=self.buffer)
+        if payment is not None:
+            if node.fixed_budget_excluding_subtasks \
+                    != node.budget_excluding_subtasks:
+                total = (f"&euro;{node.fixed_budget_excluding_subtasks} ("
+                         f"total is fixed from amount appearing in bug report,"
+                         f" which is &euro;{node.budget_excluding_subtasks})")
+            else:
+                total = f"&euro;{node.fixed_budget_excluding_subtasks}"
+            if payment.amount != node.fixed_budget_excluding_subtasks \
+                    or payment.amount != node.budget_excluding_subtasks:
+                print(f"    * &euro;{payment.amount} out of total of {total}",
+                      file=self.buffer)
+            else:
+                print(f"    * &euro;{payment.amount} which is the total amount",
+                      file=self.buffer)
 
 
 def _markdown_for_person(person: Person,
-                         payments_dict: Dict[Milestone, List[Payment]]) -> str:
-    buffer = StringIO()
-    print(f"<!-- autogenerated by budget-sync -->", file=buffer)
-    print(f"# {person.identifier}", file=buffer)
-    print(file=buffer)
-    print(f"# Status Tracking", file=buffer)
-    for milestone, payments_list in payments_dict.items():
-        if len(payments_list) == 0:
+                         payments_dict: Dict[Milestone, List[Payment]],
+                         assigned_nodes: List[Node]) -> str:
+    writer = MarkdownWriter()
+    print(f"<!-- autogenerated by budget-sync -->", file=writer.buffer)
+    writer.write_headers([f"# {person.identifier}"])
+    print(file=writer.buffer)
+    status_tracking_header = "# Status Tracking"
+    writer.write_headers([status_tracking_header])
+    displayed_nodes_dict: Dict[DisplayStatus, List[Node]]
+    displayed_nodes_dict = {i: [] for i in DisplayStatus}
+    for node in assigned_nodes:
+        display_status = DisplayStatus.from_status(node.status)
+        displayed_nodes_dict[display_status].append(node)
+
+    def write_display_status_chunk(display_status: DisplayStatus):
+        display_status_header = f"## {display_status.value}"
+        for node in displayed_nodes_dict[display_status]:
+            if display_status == DisplayStatus.Completed:
+                payment_found = False
+                for payment in node.payments.values():
+                    if payment.payee == person:
+                        payment_found = True
+                        break
+                if payment_found:
+                    continue
+                if len(node.payments) == 0 \
+                        and node.budget_excluding_subtasks == 0 \
+                        and node.budget_including_subtasks == 0:
+                    continue
+            writer.write_node(
+                headers=[status_tracking_header, display_status_header],
+                node=node, payment=None)
+
+    for display_status in DisplayStatus:
+        if display_status == DisplayStatus.Hidden \
+                or display_status == DisplayStatus.NotYetStarted:
             continue
-        print(f"## {milestone.identifier}", file=buffer)
-        for payment in payments_list:
-            # TODO: finish
-            summary = _markdown_escape(payment.node.bug.summary)
-            print(f"* [Bug #{payment.node.bug.id}]({payment.node.bug_url}): "
-                  f"{summary}",
-                  file=buffer)
-    return buffer.getvalue()
+        write_display_status_chunk(display_status)
+
+    for payee_state in PayeeState:
+        if payee_state == PayeeState.NotYetSubmitted:
+            display_status_header = f"## Completed but not yet paid"
+        elif payee_state == PayeeState.Submitted:
+            display_status_header = f"## Submitted to NLNet but not yet paid"
+        else:
+            assert payee_state == PayeeState.Paid
+            display_status_header = f"## Paid by NLNet"
+        for milestone, payments_list in payments_dict.items():
+            milestone_header = f"### {milestone.identifier}"
+            for payment in payments_list:
+                if payment.state == payee_state:
+                    writer.write_node(headers=[status_tracking_header,
+                                               display_status_header,
+                                               milestone_header],
+                                      node=payment.node, payment=payment)
+
+    write_display_status_chunk(DisplayStatus.NotYetStarted)
+
+    return writer.buffer.getvalue()
 
 
 def write_budget_markdown(budget_graph: BudgetGraph,
                           output_dir: Path):
     output_dir.mkdir(parents=True, exist_ok=True)
     for person, payments_dict in budget_graph.payments.items():
-        output_dir.joinpath(person.output_markdown_file) \
-            .write_text(_markdown_for_person(person, payments_dict),
-                        encoding="utf-8")
+        markdown = _markdown_for_person(person,
+                                        payments_dict,
+                                        budget_graph.assigned_nodes[person])
+        output_file = output_dir.joinpath(person.output_markdown_file)
+        output_file.write_text(markdown, encoding="utf-8")