From 39071dba065b8daf243e7133b7d8ed6432fea8f8 Mon Sep 17 00:00:00 2001
From: Johann Jacobsohn <j.jacobsohn@satzmedia.de>
Date: Sun, 9 Aug 2020 12:51:27 +0200
Subject: [PATCH] add loading indicator and info popup for papers with data
 from doi.org

---
 papersurfer.py | 163 +++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 130 insertions(+), 33 deletions(-)

diff --git a/papersurfer.py b/papersurfer.py
index 94493a5..5197284 100644
--- a/papersurfer.py
+++ b/papersurfer.py
@@ -13,12 +13,11 @@ import subprocess
 from dataclasses import dataclass
 import re
 from functools import partial
-import xml.etree.ElementTree as ET
+import json
 import requests
 from mattermostdriver import Driver
 import urwid
 import configargparse
-from pybtex.database import BibliographyData, Entry
 
 URL = "mattermost.cen.uni-hamburg.de"
 CHANNEL = "n5myem9yc7fyzb9am7ym5o41ry"
@@ -37,9 +36,11 @@ class PostDTO:
 class PaperDTO:
     """"Encapsulate Mattermost Posts."""
     author: str
+    authors: str
     title: str
     journal: str
     year: int
+    abstract: str
     doi: str
 
 
@@ -59,21 +60,39 @@ class Doi:
 
     def load_doi_data(self, doi):
         headers = {
-            'Accept': 'application/vnd.crossref.unixsd+xml',
+            'Accept': 'application/json',
         }
         return requests.get(f'http://dx.doi.org/{doi}',
                             headers=headers).content
 
-    def parse_doi_xml(self, xml):
-        root = ET.fromstring(xml)
-        scheme = ".//{http://www.crossref.org/xschema/1.1}"
-        author = root.find(f"{scheme}surname").text  # fixme
-        title = root.find(f"{scheme}title").text
-        journal = root.find(f"{scheme}full_title").text
-        year = root.find(f"{scheme}year").text
-        doi = root.find(f"{scheme}doi").text
-
-        return PaperDTO(author, title, journal, year, doi)
+    def parse_doi_json(self, jsoncontent):
+        """Tranform doi json to PaperDTO"""
+        info = json.loads(jsoncontent)
+
+        with open("debug.json", "w") as file:
+            file.write(json.dumps(info))
+
+        author = (f"{info['author'][0]['given']} {info['author'][0]['family']}"
+                  if "author" in info
+                  else "Author N/A")
+        authors = (", ".join([f"{a['given']} {a['family']}"
+                              for a in info['author']])
+                   if "author" in info
+                   else "Authors N/A")
+        title = (info['title']
+                 if "title" in info
+                    and isinstance(info['title'], str)
+                 else "Title N/A")
+        journal = (info['publisher']
+                   if "publisher" in info
+                   else "Journal N/A")
+        year = info['created']['date-parts'][0][0]
+        doi = info['DOI']
+        abstract = (info['abstract']
+                    if "abstract" in info
+                    else "Abstract N/A")
+
+        return PaperDTO(author, authors, title, journal, year, abstract, doi)
 
     def get_bibtex(self, doi):
         headers = {
@@ -82,8 +101,8 @@ class Doi:
         return requests.get(f'http://dx.doi.org/{doi}', headers=headers).text
 
     def get_info(self, doi):
-        xml = self.load_doi_data(doi)
-        return self.parse_doi_xml(xml)
+        jsoncontent = self.load_doi_data(doi)
+        return self.parse_doi_json(jsoncontent)
 
     def extract_doi(self, hay):
         """Parse doi from string, or None if not found.
@@ -147,20 +166,10 @@ class PrettyButton(urwid.Button):
     button_left = urwid.Text('[')
     button_right = urwid.Text(']')
 
-    def __init__(self, *args, onclick=lambda *x:x, **kwargs):
+    def __init__(self, *args, onclick=lambda *x: x, **kwargs):
         super(self.__class__, self).__init__(*args, **kwargs)
         urwid.connect_signal(self, 'click', onclick)
 
-    def button(self, label, onclick):
-        """Render a pretty button."""
-        btn = urwid.Button(label)
-        urwid.connect_signal(btn, 'click', onclick)
-
-        wrapper = urwid.AttrMap(btn, '', 'highlight')
-        padding = urwid.Padding(wrapper, left=4, right=4)
-
-        return padding, btn
-
 
 class Papersurfer:
     """Provide UI and interface with mattermost class."""
@@ -173,6 +182,10 @@ class Papersurfer:
             ('I say', 'default,bold', 'default', 'bold'),
             ('needle', 'default, bold, underline', 'default', 'bold'),
             ('highlight', 'black', 'dark blue'),
+            ('banner', 'black', 'light gray'),
+            ('selectable', 'white', 'black'),
+            ('focus', 'black', 'light gray'),
+            ('papertitle', 'default,bold', 'default', 'bold')
         ]
         ask = urwid.Edit(('I say', u"Filter?\n"))
         exitbutton = PrettyButton(u'Exit', onclick=self.on_exit_clicked)
@@ -182,20 +195,87 @@ class Papersurfer:
 
         self.mtm = Mattermost(username, password)
 
-        body = [self.list_item(paper) for paper in self.mtm.retrieve()]
+        body = [urwid.Text("")]
         self.listcontent = urwid.SimpleFocusListWalker(body)
 
         paperlist = urwid.BoxAdapter(urwid.ListBox(self.listcontent),
                                      self.size[1] - 5)
         pile = urwid.Pile([ask, div, paperlist, div,
                            urwid.Columns([exitbutton, self.exportbutton])])
-        top = urwid.Filler(pile, valign='middle')
+        self.top = urwid.Filler(pile, valign='middle')
+        self._pile = urwid.Pile(
+            [
+                self.loading_indicator()
+            ]
+        )
+        self._over = urwid.Overlay(
+            self._pile,
+            self.top,
+            align='center',
+            valign='middle',
+            width=20,
+            height=10
+        )
 
         urwid.connect_signal(ask, 'change', self.onchange)
-
-        self.mainloop = urwid.MainLoop(top, palette)
+        self.main = pile
+        self.mainloop = urwid.MainLoop(self._over, palette)
+        self.mainloop.set_alarm_in(.1, self.load_list)
         self.mainloop.run()
 
+    def load_list(self, _loop, _data):
+        body = [self.list_item(paper) for paper in self.mtm.retrieve()]
+        self.listcontent.clear()
+        self.listcontent.extend(body)
+        self.mainloop.widget = self.top
+
+    def loading_indicator(self):
+        body_text = urwid.Text("Loading...", align='center')
+        body_filler = urwid.Filler(body_text, valign='middle')
+        body_padding = urwid.Padding(
+            body_filler,
+            left=1,
+            right=1
+        )
+
+        return urwid.Frame(body_padding)
+
+    def details_popup(self, paper):
+        header_text = urwid.Text(('banner', 'Paper details'), align='center')
+        header = urwid.AttrMap(header_text, 'banner')
+
+        body_pile = urwid.Pile([
+            urwid.Text(("papertitle", paper.title)),
+            urwid.Text(paper.authors),
+            urwid.Text(paper.journal),
+            urwid.Text(paper.doi),
+            urwid.Text(paper.abstract),
+            urwid.Text(" "),
+            urwid.Text(Bibtex().entry_from_doi(paper.doi)),
+        ])
+        body_filler = urwid.Filler(body_pile, valign='top')
+        body_padding = urwid.Padding(
+            body_filler,
+            left=1,
+            right=1
+        )
+        body = urwid.LineBox(body_padding)
+
+        # Footer
+        footer = urwid.Button('Okay', self.close_details)
+        footer = urwid.AttrWrap(footer, 'selectable', 'focus')
+        footer = urwid.GridFlow([footer], 8, 1, 1, 'center')
+
+        # Layout
+        layout = urwid.Frame(
+            body,
+            header=header,
+            footer=footer,
+            focus_part='footer'
+        )
+
+        return layout
+
     def list_item(self, paper, needle=""):
         """Create highlighted text entry."""
         text_items = []
@@ -210,11 +290,16 @@ class Papersurfer:
 
         title = urwid.Text(text_items)
         discuss_button = PrettyButton("Open Discussion",
-                                       onclick=partial(self.h_open_discussion,
-                                                     paper))
+                                      onclick=partial(self.h_open_discussion,
+                                                      paper))
         doi_button = PrettyButton("Open DOI",
                                   onclick=partial(self.h_open_doi, paper))
-        button_bar = urwid.Columns([discuss_button, doi_button])
+        details_button = PrettyButton("Show details",
+                                      onclick=partial(self.h_show_details,
+                                                      paper))
+
+        button_bar = urwid.Columns([
+            discuss_button, doi_button, details_button])
         pile = urwid.Pile([title, button_bar, urwid.Divider()])
         return pile
 
@@ -264,6 +349,10 @@ class Papersurfer:
         """Handle click/enter on doi button."""
         self.open_doi(post)
 
+    def h_show_details(self, post, _):
+        """Handle click/enter on doi button."""
+        self.show_details(post)
+
     def open_discussion(self, post):
         """Open Mattermost post in browser."""
         link = f"https://mattermost.cen.uni-hamburg.de/ifg/pl/{post.id}"
@@ -273,6 +362,14 @@ class Papersurfer:
         """Open paper page in browser."""
         subprocess.call(["xdg-open", Doi().get_doi_link(post.doi)])
 
+    def show_details(self, post):
+        """Open paper page in browser."""
+        paper = Doi().get_info(post.doi)
+        self.mainloop.widget = self.details_popup(paper)
+
+    def close_details(self, _):
+        self.mainloop.widget = self.top
+
 
 def parse_args():
     """Parse command line arguments and config file."""
-- 
GitLab