diff --git a/.gitignore b/.gitignore
index b9c5eaac..02eec5cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -137,3 +137,6 @@ dmypy.json
qtribu/gui/*.db
*.qm
*.zip
+
+# jb stuff
+.idea
diff --git a/qtribu/gui/dlg_contents.py b/qtribu/gui/dlg_contents.py
new file mode 100644
index 00000000..04a3f238
--- /dev/null
+++ b/qtribu/gui/dlg_contents.py
@@ -0,0 +1,253 @@
+from pathlib import Path
+from typing import Callable, Dict, List
+
+from qgis.core import QgsApplication
+from qgis.PyQt import QtCore, QtWidgets, uic
+from qgis.PyQt.QtGui import QIcon
+from qgis.PyQt.QtWidgets import QDialog, QTreeWidgetItem, QWidget
+
+from qtribu.__about__ import DIR_PLUGIN_ROOT
+from qtribu.gui.form_article import ArticleForm
+from qtribu.gui.form_rdp_news import RdpNewsForm
+from qtribu.logic import RssItem
+from qtribu.logic.json_feed import JsonFeedClient
+from qtribu.toolbelt import PlgLogger, PlgOptionsManager
+from qtribu.toolbelt.commons import open_url_in_browser, open_url_in_webviewer
+
+MARKER_VALUE = "---"
+
+
+class GeotribuContentsDialog(QDialog):
+ contents: Dict[int, List[RssItem]] = {}
+
+ def __init__(self, parent: QWidget = None):
+ """
+ QDialog for geotribu contents
+
+ :param parent: parent widget or application
+ :type parent: QgsDockWidget
+ """
+ super().__init__(parent)
+ self.log = PlgLogger().log
+ self.plg_settings = PlgOptionsManager()
+ self.json_feed_client = JsonFeedClient()
+ uic.loadUi(Path(__file__).parent / f"{Path(__file__).stem}.ui", self)
+ self.setWindowIcon(
+ QIcon(str(DIR_PLUGIN_ROOT / "resources/images/logo_green_no_text.svg"))
+ )
+
+ # buttons actions
+ self.form_article = None
+ self.form_rdp_news = None
+ self.submit_article_button.clicked.connect(self.submit_article)
+ self.submit_article_button.setIcon(
+ QgsApplication.getThemeIcon("mActionEditTable.svg")
+ )
+ self.submit_news_button.clicked.connect(self.submit_news)
+ self.submit_news_button.setIcon(
+ QgsApplication.getThemeIcon("mActionAllEdits.svg")
+ )
+ self.donate_button.clicked.connect(self.donate)
+ self.donate_button.setIcon(
+ QgsApplication.getThemeIcon("mIconCertificateTrusted.svg")
+ )
+
+ # search actions
+ self.search_line_edit.textChanged.connect(self.on_search_text_changed)
+
+ # authors combobox
+ self.authors_combobox.addItem(MARKER_VALUE)
+ for author in self.json_feed_client.authors():
+ self.authors_combobox.addItem(author)
+ self.authors_combobox.currentTextChanged.connect(self.on_author_changed)
+
+ # categories combobox
+ self.categories_combobox.addItem(MARKER_VALUE)
+ for cat in self.json_feed_client.categories():
+ self.categories_combobox.addItem(cat)
+ self.categories_combobox.currentTextChanged.connect(self.on_category_changed)
+
+ # tree widget initialization
+ self.contents_tree_widget.setHeaderLabels(
+ [
+ self.tr("Date"),
+ self.tr("Title"),
+ self.tr("Author(s)"),
+ self.tr("Categories"),
+ ]
+ )
+ self.contents_tree_widget.itemClicked.connect(self.on_tree_view_item_click)
+
+ self.refresh_list(lambda: self.search_line_edit.text())
+ self.contents_tree_widget.expandAll()
+
+ def submit_article(self) -> None:
+ """
+ Submit article action
+ Usually launched when clicking on button
+ """
+ self.log("Opening form to submit an article")
+ if not self.form_article:
+ self.form_article = ArticleForm()
+ self.form_article.setModal(True)
+ self.form_article.finished.connect(self._post_form_article)
+ self.form_article.show()
+
+ def _post_form_article(self, dialog_result: int) -> None:
+ """Perform actions after the article form has been closed.
+
+ :param dialog_result: dialog's result code. Accepted (1) or Rejected (0)
+ :type dialog_result: int
+ """
+ if self.form_article:
+ # if accept button, save user inputs
+ if dialog_result == 1:
+ self.form_article.wdg_author.save_settings()
+ # clean up
+ self.form_article.deleteLater()
+ self.form_article = None
+
+ def submit_news(self) -> None:
+ """
+ Submit RDP news action
+ Usually launched when clicking on button
+ """
+ self.log("Opening form to submit a news")
+ if not self.form_rdp_news:
+ self.form_rdp_news = RdpNewsForm()
+ self.form_rdp_news.setModal(True)
+ self.form_rdp_news.finished.connect(self._post_form_rdp_news)
+ self.form_rdp_news.show()
+
+ def _post_form_rdp_news(self, dialog_result: int) -> None:
+ """Perform actions after the GeoRDP news form has been closed.
+
+ :param dialog_result: dialog's result code. Accepted (1) or Rejected (0)
+ :type dialog_result: int
+ """
+ if self.form_rdp_news:
+ # if accept button, save user inputs
+ if dialog_result == 1:
+ self.form_rdp_news.wdg_author.save_settings()
+ # clean up
+ self.form_rdp_news.deleteLater()
+ self.form_rdp_news = None
+
+ def donate(self) -> None:
+ """
+ Donate action
+ Usually launched when clicking on button
+ """
+ open_url_in_browser("https://geotribu.fr/team/sponsoring/")
+
+ def refresh_list(self, query_action: Callable[[], str]) -> None:
+ """
+ Refresh content list as well as treewidget that list all contents
+
+ :param query_action: action to call for potentially filtering contents
+ :type query_action: Callable[[], str]
+ """
+ # fetch last RSS items using JSONFeed
+ rss_contents = self.json_feed_client.fetch(query=query_action())
+ years = sorted(set([c.date_pub.year for c in rss_contents]), reverse=True)
+ self.contents = {
+ y: [c for c in rss_contents if c.date_pub.year == y] for y in years
+ }
+
+ # clean treeview items
+ self.contents_tree_widget.clear()
+
+ # populate treewidget
+ items = []
+ for i, year in enumerate(years):
+ # create root item for year
+ item = QTreeWidgetItem([str(year)])
+ # create contents items
+ for content in self.contents[year]:
+ child = self._build_tree_widget_item_from_content(content)
+ item.addChild(child)
+ items.append(item)
+ self.contents_tree_widget.insertTopLevelItems(0, items)
+ self.contents_tree_widget.expandAll()
+
+ @QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem, int)
+ def on_tree_view_item_click(self, item: QTreeWidgetItem, column: int):
+ """
+ Method called when a content item is clicked
+
+ :param item: item that is clicked by user
+ :type item: QTreeWidgetItem
+ :param column: column that is clicked by user
+ :type column: int
+ """
+ # open URL of content (in column at index 4 which is not displayed)
+ url, title = item.text(4), item.text(1)
+ open_url_in_webviewer(url, title)
+
+ def on_search_text_changed(self) -> None:
+ """
+ Method called when search box is changed
+ Should get search
+ """
+ # do nothing if text is too small
+ current = self.search_line_edit.text()
+ if current == "":
+ self.refresh_list(lambda: current)
+ return
+ if len(current) < 3:
+ return
+ self.refresh_list(lambda: current)
+
+ def on_author_changed(self, value: str) -> None:
+ """
+ Function triggered when author combobox is changed
+
+ :param value: text value of the selected author
+ :type value: str
+ """
+ self.search_line_edit.setText("")
+ if value == MARKER_VALUE:
+ self.refresh_list(lambda: self.search_line_edit.text())
+ return
+ self.refresh_list(lambda: value)
+
+ def on_category_changed(self, value: str) -> None:
+ """
+ Function triggered when category/tag combobox is changed
+
+ :param value: text value of the selected category
+ :type value: str
+ """
+ self.search_line_edit.setText("")
+ if value == MARKER_VALUE:
+ self.refresh_list(lambda: self.search_line_edit.text())
+ return
+ self.refresh_list(lambda: value)
+
+ @staticmethod
+ def _build_tree_widget_item_from_content(content: RssItem) -> QTreeWidgetItem:
+ """
+ Builds a QTreeWidgetItem from a RSS content item
+
+ :param content: content to generate item for
+ :type content: RssItem
+ """
+ item = QTreeWidgetItem(
+ [
+ content.date_pub.strftime("%d %B"),
+ content.title,
+ ",".join(content.author),
+ ",".join(content.categories),
+ content.url,
+ ]
+ )
+ for i in range(4):
+ item.setToolTip(i, content.abstract)
+ icon_file = (
+ "logo_orange_no_text"
+ if "Revue de presse" in content.title
+ else "logo_green_no_text"
+ )
+ icon = QIcon(str(DIR_PLUGIN_ROOT / f"resources/images/{icon_file}.svg"))
+ item.setIcon(1, icon)
+ return item
diff --git a/qtribu/gui/dlg_contents.ui b/qtribu/gui/dlg_contents.ui
new file mode 100644
index 00000000..a29f4327
--- /dev/null
+++ b/qtribu/gui/dlg_contents.ui
@@ -0,0 +1,250 @@
+
+
+ geotribu_toolbox
+
+
+
+ 0
+ 0
+ 1080
+ 640
+
+
+
+
+ 0
+ 0
+
+
+
+ Geotribu Contents
+
+
+ -
+
+
+ 5
+
+
+ 5
+
+
+ 10
+
+
+ 5
+
+
+ 10
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Actions
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Submit Article
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Submit News
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Donate
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 3
+
+
+
+
+ 0
+ 5
+
+
+
+ Contents
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Search :
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 255
+
+
+ true
+
+
+
+ -
+
+
+ Filter by author :
+
+
+
+ -
+
+
+ -
+
+
+ Filter by category :
+
+
+
+ -
+
+
+
+
+ -
+
+
+ true
+
+
+ true
+
+
+ 4
+
+
+ true
+
+
+ true
+
+
+ 64
+
+
+ true
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QgsCollapsibleGroupBox
+ QGroupBox
+
+ 1
+
+
+
+ contents_groupbox
+ submit_news_button
+
+
+
+
diff --git a/qtribu/gui/dlg_settings.py b/qtribu/gui/dlg_settings.py
index d5ba3ed1..be572971 100644
--- a/qtribu/gui/dlg_settings.py
+++ b/qtribu/gui/dlg_settings.py
@@ -12,8 +12,7 @@
from qgis.core import QgsApplication
from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory
from qgis.PyQt import uic
-from qgis.PyQt.Qt import QUrl
-from qgis.PyQt.QtGui import QDesktopServices, QIcon
+from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QButtonGroup
# project
@@ -25,6 +24,7 @@
__version__,
)
from qtribu.toolbelt import PlgLogger, PlgOptionsManager
+from qtribu.toolbelt.commons import open_url_in_browser
from qtribu.toolbelt.preferences import PlgSettingsStructure
# ############################################################################
@@ -61,15 +61,13 @@ def __init__(self, parent=None):
# customization
self.btn_help.setIcon(QIcon(QgsApplication.iconPath("mActionHelpContents.svg")))
- self.btn_help.pressed.connect(
- partial(QDesktopServices.openUrl, QUrl(__uri_homepage__))
- )
+ self.btn_help.pressed.connect(partial(open_url_in_browser, __uri_homepage__))
self.btn_report.setIcon(
QIcon(QgsApplication.iconPath("console/iconSyntaxErrorConsole.svg"))
)
self.btn_report.pressed.connect(
- partial(QDesktopServices.openUrl, QUrl(f"{__uri_tracker__}new/choose"))
+ partial(open_url_in_browser, f"{__uri_tracker__}new/choose")
)
self.btn_reset_read_history.setIcon(
diff --git a/qtribu/gui/form_article.py b/qtribu/gui/form_article.py
new file mode 100644
index 00000000..37ca580b
--- /dev/null
+++ b/qtribu/gui/form_article.py
@@ -0,0 +1,190 @@
+#! python3 # noqa: E265
+
+"""
+ Form to submit a news for a GeoRDP.
+TODO: markdown highlight https://github.com/rupeshk/MarkdownHighlighter/blob/master/editor.py
+https://github.com/baudren/NoteOrganiser/blob/devel/noteorganiser/syntax.py
+"""
+
+# standard
+from functools import partial
+from pathlib import Path
+
+# PyQGIS
+from qgis.PyQt import uic
+from qgis.PyQt.QtCore import Qt
+from qgis.PyQt.QtGui import QIcon
+from qgis.PyQt.QtWidgets import QDialog
+
+# plugin
+from qtribu.__about__ import DIR_PLUGIN_ROOT
+from qtribu.constants import GEORDP_NEWS_ICONS, GeotribuImage
+from qtribu.toolbelt import NetworkRequestsManager, PlgLogger, PlgOptionsManager
+from qtribu.toolbelt.commons import open_url_in_browser
+
+
+class ArticleForm(QDialog):
+ """QDialog form to submit an article."""
+
+ LOCAL_CDN_PATH: Path = Path().home() / ".geotribu/cdn/"
+
+ def __init__(self, parent=None):
+ """Constructor.
+
+ :param parent: parent widget or application
+ :type parent: QWidget
+ """
+ super().__init__(parent)
+ uic.loadUi(Path(__file__).parent / f"{Path(__file__).stem}.ui", self)
+
+ self.log = PlgLogger().log
+ self.plg_settings = PlgOptionsManager()
+ self.qntwk = NetworkRequestsManager()
+
+ # custom icon
+ self.setWindowIcon(QIcon(str(DIR_PLUGIN_ROOT / "resources/images/news.png")))
+
+ # icon combobox
+ self.cbb_icon_populate()
+ self.cbb_icon.textActivated.connect(self.cbb_icon_selected)
+
+ # publication
+ self.chb_license.setChecked(
+ self.plg_settings.get_value_from_key(
+ key="license_global_accept", exp_type=bool
+ )
+ )
+
+ # connect help button
+ self.btn_box.helpRequested.connect(
+ partial(
+ open_url_in_browser,
+ "https://contribuer.geotribu.fr/rdp/add_news/",
+ )
+ )
+
+ def cbb_icon_populate(self) -> None:
+ """Populate combobox of article icons."""
+ # save current index
+ current_item_idx = self.cbb_icon.currentIndex()
+
+ # clear
+ self.cbb_icon.clear()
+
+ # populate
+ self.cbb_icon.addItem("", None)
+ for rdp_icon in GEORDP_NEWS_ICONS:
+ if rdp_icon.kind != "icon":
+ continue
+
+ if rdp_icon.local_path().is_file():
+ self.cbb_icon.addItem(
+ QIcon(str(rdp_icon.local_path().resolve())), rdp_icon.name, rdp_icon
+ )
+ else:
+ self.cbb_icon.addItem(rdp_icon.name, rdp_icon)
+
+ # icon tooltip
+ self.cbb_icon.setItemData(
+ GEORDP_NEWS_ICONS.index(rdp_icon) + 1,
+ rdp_icon.description,
+ Qt.ToolTipRole,
+ )
+
+ # restore current index
+ self.cbb_icon.setCurrentIndex(current_item_idx)
+
+ def cbb_icon_selected(self) -> None:
+ """Download selected icon locally if it doesn't exist already."""
+ selected_icon: GeotribuImage = self.cbb_icon.currentData()
+ if not selected_icon:
+ return
+
+ icon_local_path = selected_icon.local_path()
+ if not icon_local_path.is_file():
+ self.log(
+ message=f"Icon doesn't exist locally: {icon_local_path}", log_level=4
+ )
+ icon_local_path.parent.mkdir(parents=True, exist_ok=True)
+ self.qntwk.download_file(
+ remote_url=selected_icon.url,
+ local_path=str(icon_local_path.resolve()),
+ )
+ # repopulate combobx to get updated items icons
+ self.cbb_icon_populate()
+
+ def accept(self) -> bool:
+ """Auto-connected to the OK button (within the button box), i.e. the `accepted`
+ signal. Check if required form fields are correctly filled.
+
+ :return: False if some check fails. True and emit accepted() signal if everything is ok.
+ :rtype: bool
+ """
+ invalid_fields = []
+ error_message = ""
+
+ # check title
+ if len(self.lne_title.text()) < 3:
+ invalid_fields.append(self.lne_title)
+ error_message += self.tr(
+ "- A title is required, with at least 3 characters.\n"
+ )
+
+ # check description
+ if len(self.txt_description.toPlainText()) < 25:
+ invalid_fields.append(self.txt_description)
+ error_message += self.tr(
+ "- Description is not long enough (25 characters at least).\n"
+ )
+ if len(self.txt_description.toPlainText()) > 160:
+ invalid_fields.append(self.txt_description)
+ error_message += self.tr(
+ "- Description is too long (160 characters at least).\n"
+ )
+
+ # check license
+ if not self.chb_license.isChecked():
+ invalid_fields.append(self.chb_license)
+ error_message += self.tr("- License must be accepted.\n")
+
+ # check author firstname
+ if len(self.wdg_author.lne_firstname.text()) < 2:
+ invalid_fields.append(self.wdg_author.lne_firstname)
+ error_message += self.tr(
+ "- For attribution purpose, author's firstname is required.\n"
+ )
+
+ # check author lastname
+ if len(self.wdg_author.lne_lastname.text()) < 2:
+ invalid_fields.append(self.wdg_author.lne_lastname)
+ error_message += self.tr(
+ "- For attribution purpose, author's lastname is required.\n"
+ )
+
+ # check author email
+ if len(self.wdg_author.lne_email.text()) < 5:
+ invalid_fields.append(self.wdg_author.lne_email)
+ error_message += self.tr(
+ "- For attribution purpose, author's email is required.\n"
+ )
+
+ # inform
+ if len(invalid_fields):
+ self.log(
+ message=self.tr("Some of required fields are incorrectly filled."),
+ push=True,
+ log_level=2,
+ duration=20,
+ button=True,
+ button_label=self.tr("See details..."),
+ button_more_text=self.tr(
+ "Fields in bold must be filled. Missing fields:\n"
+ )
+ + error_message,
+ )
+ for wdg in invalid_fields:
+ wdg.setStyleSheet("border: 1px solid red;")
+ return False
+ else:
+ super().accept()
+ return True
diff --git a/qtribu/gui/form_article.ui b/qtribu/gui/form_article.ui
new file mode 100644
index 00000000..698ff427
--- /dev/null
+++ b/qtribu/gui/form_article.ui
@@ -0,0 +1,292 @@
+
+
+ dlg_form_rdp_news
+
+
+
+ 0
+ 0
+ 763
+ 1060
+
+
+
+ GeoRDP - News Form
+
+
+ 0.98
+
+
+
+
+
+ true
+
+
+ true
+
+
+ -
+
+
+
+ 11
+
+
+
+ The news
+
+
+ false
+
+
+
-
+
+
+
+ 11
+ 75
+ true
+
+
+
+ The title must be concise and avoid some special characters, especially at the end
+
+
+ Title:
+
+
+
+ -
+
+
+ -
+
+
+ Icon:
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Keywords:
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+
+ 11
+ 75
+ true
+
+
+
+ Description:
+
+
+
+ -
+
+
+ IBeamCursor
+
+
+ QFrame::StyledPanel
+
+
+
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Publication
+
+
+
-
+
+
+
+ 11
+ 75
+ true
+
+
+
+ License:
+
+
+
+ -
+
+
+ I accept that my contribution is published under the CC BY-NC-SA 4.0
+
+
+ false
+
+
+
+ -
+
+
+ 300
+
+
+
+ -
+
+
+ Comment:
+
+
+
+ -
+
+
+ Transparency:
+
+
+
+ -
+
+
+ I'm not related to the published content. If not, I give some details in the comment area.
+
+
+
+
+
+
+ -
+
+
+
+ 10
+ 0
+
+
+
+ QFrame::Plain
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ AuthoringWidget
+ QWidget
+
+ 1
+
+
+
+
+
+ btn_box
+ accepted()
+ dlg_form_rdp_news
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ btn_box
+ rejected()
+ dlg_form_rdp_news
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/qtribu/gui/form_rdp_news.py b/qtribu/gui/form_rdp_news.py
index 7ac35d96..be99df22 100644
--- a/qtribu/gui/form_rdp_news.py
+++ b/qtribu/gui/form_rdp_news.py
@@ -14,14 +14,15 @@
# PyQGIS
from qgis.core import QgsApplication
from qgis.PyQt import uic
-from qgis.PyQt.QtCore import Qt, QUrl
-from qgis.PyQt.QtGui import QDesktopServices, QIcon
+from qgis.PyQt.QtCore import Qt
+from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QDialog
# plugin
from qtribu.__about__ import DIR_PLUGIN_ROOT
from qtribu.constants import GEORDP_NEWS_CATEGORIES, GEORDP_NEWS_ICONS, GeotribuImage
from qtribu.toolbelt import NetworkRequestsManager, PlgLogger, PlgOptionsManager
+from qtribu.toolbelt.commons import open_url_in_browser
class RdpNewsForm(QDialog):
@@ -77,8 +78,8 @@ def __init__(self, parent=None):
# connect help button
self.btn_box.helpRequested.connect(
partial(
- QDesktopServices.openUrl,
- QUrl("https://contribuer.geotribu.fr/rdp/add_news/"),
+ open_url_in_browser,
+ "https://contribuer.geotribu.fr/rdp/add_news/",
)
)
@@ -189,7 +190,7 @@ def accept(self) -> bool:
if len(self.txt_body.toPlainText()) < 25:
invalid_fields.append(self.txt_body)
error_message += self.tr(
- "- News is not long enougth (25 characters at least).\n"
+ "- News is not long enough (25 characters at least).\n"
)
# check license
diff --git a/qtribu/gui/form_rdp_news.ui b/qtribu/gui/form_rdp_news.ui
index d5c08425..3004e22a 100644
--- a/qtribu/gui/form_rdp_news.ui
+++ b/qtribu/gui/form_rdp_news.ui
@@ -14,7 +14,7 @@
GeoRDP - News Form
- 0.900000000000000
+ 0.98
diff --git a/qtribu/logic/json_feed.py b/qtribu/logic/json_feed.py
new file mode 100644
index 00000000..a49aba9a
--- /dev/null
+++ b/qtribu/logic/json_feed.py
@@ -0,0 +1,123 @@
+from datetime import datetime
+from typing import Any, List, Optional
+
+import requests
+from requests import Response
+
+from qtribu.__about__ import __title__, __version__
+from qtribu.logic import RssItem
+from qtribu.toolbelt import PlgLogger, PlgOptionsManager
+
+HEADERS: dict = {
+ b"Accept": b"application/json",
+ b"User-Agent": bytes(f"{__title__}/{__version__}", "utf8"),
+}
+
+FETCH_UPDATE_INTERVAL_SECONDS = 7200
+
+
+class JsonFeedClient:
+ """
+ Class representing a Geotribu's JSON feed client
+ """
+
+ items: Optional[List[RssItem]] = None
+ last_fetch_date: Optional[datetime] = None
+
+ def __init__(
+ self, url: str = PlgOptionsManager.get_plg_settings().json_feed_source
+ ):
+ """Class initialization."""
+ self.log = PlgLogger().log
+ self.url = url
+
+ def fetch(self, query: str = "") -> list[RssItem]:
+ """
+ Fetch RSS feed items using JSON Feed
+
+ :param query: filter to look for items matching this query
+ :type query: str
+
+ :return: list of RssItem objects matching the query filter
+ """
+ if not self.items or (
+ self.last_fetch_date
+ and (datetime.now() - self.last_fetch_date).total_seconds()
+ > FETCH_UPDATE_INTERVAL_SECONDS
+ ):
+ r: Response = requests.get(self.url, headers=HEADERS)
+ r.raise_for_status()
+ self.items = [self._map_item(i) for i in r.json()["items"]]
+ self.last_fetch_date = datetime.now()
+ return [i for i in self.items if self._matches(query, i)]
+
+ def authors(self) -> list[str]:
+ """
+ Get a list of authors available in the RSS feed
+
+ :return: list of authors
+ """
+ authors = []
+ for content in self.fetch():
+ for ca in content.author:
+ authors.append(" ".join([a.title() for a in ca.split(" ")]))
+ return sorted(set(authors))
+
+ def categories(self) -> list[str]:
+ """
+ Get a list of all categories available in the RSS feed
+
+ :return: list of categories available in the RSS feed
+ """
+ tags = []
+ for content in self.fetch():
+ tags.extend([c.lower() for c in content.categories])
+ return sorted(set(tags))
+
+ @staticmethod
+ def _map_item(item: dict[str, Any]) -> RssItem:
+ """
+ Map raw JSON object coming from JSON feed to an RssItem object
+
+ :param item: raw JSON object
+ :type item: dict[str, Any]
+
+ :return: RssItem
+ """
+ return RssItem(
+ abstract=item.get("content_html"),
+ author=[i["name"] for i in item.get("authors")],
+ categories=item.get("tags", []),
+ date_pub=datetime.fromisoformat(item.get("date_published")),
+ guid=item.get("id"),
+ image_length=666,
+ image_type=item.get("image"),
+ image_url=item.get("image"),
+ title=item.get("title"),
+ url=item.get("url"),
+ )
+
+ @staticmethod
+ def _matches(query: str, item: RssItem) -> bool:
+ """
+ Check if item matches given query
+
+ :param query: filter to look for items matching this query
+ :type query: str
+ :param item: RssItem to check
+ :type item: RssItem
+
+ :return: True if item matches given query, False if not
+ """
+ words = query.split(" ")
+ if len(words) > 1:
+ return all([JsonFeedClient._matches(w, item) for w in words])
+ return (
+ query.upper() in item.abstract.upper()
+ or query.upper() in ",".join(item.author).upper()
+ or query.upper() in ",".join(item.categories).upper()
+ or query.upper() in item.date_pub.isoformat().upper()
+ or query.upper() in item.image_url.upper()
+ or query.upper() in item.title.upper()
+ or query.upper() in item.url.upper()
+ )
diff --git a/qtribu/logic/rss_reader.py b/qtribu/logic/rss_reader.py
index 985fcd4f..2bfbb468 100644
--- a/qtribu/logic/rss_reader.py
+++ b/qtribu/logic/rss_reader.py
@@ -13,7 +13,7 @@
import logging
import xml.etree.ElementTree as ET
from email.utils import parsedate
-from typing import Optional
+from typing import List, Optional
# QGIS
from qgis.core import Qgis, QgsSettings
@@ -117,6 +117,23 @@ def latest_item(self) -> RssItem:
return self.FEED_ITEMS[0]
+ def latest_items(self, count: int = 36) -> List[RssItem]:
+ """Returns the latest feed items.
+ :param count: number of items to fetch
+ :type count: int
+ :return: latest feed items
+ :rtype: List[RssItem]
+ """
+ if count <= 0:
+ raise ValueError("Number of RSS items to get must be > 0")
+ if not self.FEED_ITEMS:
+ logger.warning(
+ "Feed has not been loaded, so it's impossible to "
+ "return the latest item."
+ )
+ return []
+ return self.FEED_ITEMS[:count]
+
@property
def has_new_content(self) -> bool:
"""Compare the saved item guid (in plugin settings) with feed latest item to \
@@ -206,3 +223,17 @@ def tr(self, message: str) -> str:
:rtype: str
"""
return QCoreApplication.translate(self.__class__.__name__, message)
+
+
+class RssArticlesMiniReader(RssMiniReader):
+ PATTERN_INCLUDE: list = ["articles/"]
+
+ def __init__(self):
+ super().__init__()
+
+
+class RssRdpMiniReader(RssMiniReader):
+ PATTERN_INCLUDE: list = ["rdp/"]
+
+ def __init__(self):
+ super().__init__()
diff --git a/qtribu/logic/web_viewer.py b/qtribu/logic/web_viewer.py
index 29f8bd7d..5acb9023 100644
--- a/qtribu/logic/web_viewer.py
+++ b/qtribu/logic/web_viewer.py
@@ -85,6 +85,9 @@ def display_web_page(self, url: str):
push=True,
)
+ def set_window_title(self, title: str) -> None:
+ self.wdg_web.setWindowTitle(title)
+
def tr(self, message: str) -> str:
"""Translation method.
diff --git a/qtribu/plugin_main.py b/qtribu/plugin_main.py
index af2c9bf9..3ae3412a 100644
--- a/qtribu/plugin_main.py
+++ b/qtribu/plugin_main.py
@@ -11,16 +11,18 @@
# PyQGIS
from qgis.core import Qgis, QgsApplication, QgsSettings
from qgis.gui import QgisInterface
-from qgis.PyQt.QtCore import QCoreApplication, QLocale, QTranslator, QUrl
-from qgis.PyQt.QtGui import QDesktopServices, QIcon
+from qgis.PyQt.QtCore import QCoreApplication, QLocale, QTranslator
+from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction
# project
from qtribu.__about__ import DIR_PLUGIN_ROOT, __icon_path__, __title__, __uri_homepage__
+from qtribu.gui.dlg_contents import GeotribuContentsDialog
from qtribu.gui.dlg_settings import PlgOptionsFactory
from qtribu.gui.form_rdp_news import RdpNewsForm
-from qtribu.logic import RssMiniReader, SplashChanger, WebViewer
+from qtribu.logic import RssMiniReader, SplashChanger
from qtribu.toolbelt import NetworkRequestsManager, PlgLogger, PlgOptionsManager
+from qtribu.toolbelt.commons import open_url_in_browser, open_url_in_webviewer
# ############################################################################
# ########## Classes ###############
@@ -67,7 +69,6 @@ def __init__(self, iface: QgisInterface):
# sub-modules
self.rss_rdr = RssMiniReader()
self.splash_chgr = SplashChanger(self)
- self.web_viewer = WebViewer()
def initGui(self):
"""Set up plugin UI elements."""
@@ -78,6 +79,7 @@ def initGui(self):
# -- Forms
self.form_rdp_news = None
+ self.form_contents = None
# -- Actions
self.action_run = QAction(
@@ -85,9 +87,18 @@ def initGui(self):
self.tr("Newest article"),
self.iface.mainWindow(),
)
+
self.action_run.setToolTip(self.tr("Newest article"))
self.action_run.triggered.connect(self.run)
+ self.action_contents = QAction(
+ QgsApplication.getThemeIcon("mActionOpenTableVisible.svg"),
+ self.tr("Contents"),
+ self.iface.mainWindow(),
+ )
+ self.action_contents.setToolTip(self.tr("Contents"))
+ self.action_contents.triggered.connect(self.contents)
+
self.action_rdp_news = QAction(
QIcon(QgsApplication.iconPath("mActionHighlightFeature.svg")),
self.tr("Propose a news to the next GeoRDP"),
@@ -101,7 +112,7 @@ def initGui(self):
self.iface.mainWindow(),
)
self.action_help.triggered.connect(
- partial(QDesktopServices.openUrl, QUrl(__uri_homepage__))
+ partial(open_url_in_browser, __uri_homepage__)
)
self.action_settings = QAction(
@@ -118,6 +129,7 @@ def initGui(self):
# -- Menu
self.iface.addPluginToWebMenu(__title__, self.action_run)
+ self.iface.addPluginToWebMenu(__title__, self.action_contents)
self.iface.addPluginToWebMenu(__title__, self.action_rdp_news)
self.iface.addPluginToWebMenu(__title__, self.action_splash)
self.iface.addPluginToWebMenu(__title__, self.action_settings)
@@ -131,8 +143,8 @@ def initGui(self):
)
self.action_geotribu.triggered.connect(
partial(
- QDesktopServices.openUrl,
- QUrl("https://geotribu.fr"),
+ open_url_in_browser,
+ "https://geotribu.fr",
)
)
@@ -142,8 +154,8 @@ def initGui(self):
)
self.action_georezo.triggered.connect(
partial(
- QDesktopServices.openUrl,
- QUrl("https://georezo.net/forum/viewforum.php?id=55"),
+ open_url_in_browser,
+ "https://georezo.net/forum/viewforum.php?id=55",
)
)
self.action_osgeofr = QAction(
@@ -152,8 +164,8 @@ def initGui(self):
)
self.action_osgeofr.triggered.connect(
partial(
- QDesktopServices.openUrl,
- QUrl("https://www.osgeo.fr/"),
+ open_url_in_browser,
+ "https://www.osgeo.fr/",
)
)
self.iface.helpMenu().addAction(self.action_georezo)
@@ -162,6 +174,7 @@ def initGui(self):
# -- Toolbar
self.iface.addToolBarIcon(self.action_run)
+ self.iface.addToolBarIcon(self.action_contents)
self.iface.addToolBarIcon(self.action_rdp_news)
# -- Post UI initialization
@@ -173,6 +186,7 @@ def unload(self):
self.iface.removePluginWebMenu(__title__, self.action_help)
self.iface.removePluginWebMenu(__title__, self.action_rdp_news)
self.iface.removePluginWebMenu(__title__, self.action_run)
+ self.iface.removePluginWebMenu(__title__, self.action_contents)
self.iface.removePluginWebMenu(__title__, self.action_settings)
self.iface.removePluginWebMenu(__title__, self.action_splash)
@@ -182,6 +196,7 @@ def unload(self):
# -- Clean up toolbar
self.iface.removeToolBarIcon(self.action_run)
+ self.iface.removeToolBarIcon(self.action_contents)
self.iface.removeToolBarIcon(self.action_rdp_news)
# -- Clean up preferences panel in QGIS settings
@@ -274,7 +289,9 @@ def run(self):
if not self.rss_rdr.latest_item:
self.post_ui_init()
- self.web_viewer.display_web_page(url=self.rss_rdr.latest_item.url)
+ open_url_in_webviewer(
+ self.rss_rdr.latest_item.url, self.rss_rdr.latest_item.title
+ )
self.action_run.setIcon(
QIcon(str(DIR_PLUGIN_ROOT / "resources/images/logo_green_no_text.svg"))
)
@@ -284,8 +301,19 @@ def run(self):
key="latest_content_guid", value=self.rss_rdr.latest_item.guid
)
except Exception as err:
+ self.log(
+ message=self.tr(f"Michel, we've got a problem: {err}"),
+ log_level=2,
+ push=True,
+ )
raise err
+ def contents(self):
+ """Action to open contents dialog"""
+ if not self.form_contents:
+ self.form_contents = GeotribuContentsDialog()
+ self.form_contents.show()
+
def open_form_rdp_news(self) -> None:
"""Open the form to create a GeoRDP news."""
if not self.form_rdp_news:
diff --git a/qtribu/toolbelt/commons.py b/qtribu/toolbelt/commons.py
new file mode 100644
index 00000000..36fac1fe
--- /dev/null
+++ b/qtribu/toolbelt/commons.py
@@ -0,0 +1,31 @@
+from qgis.PyQt.QtCore import QUrl
+from qgis.PyQt.QtGui import QDesktopServices
+
+from qtribu.logic import WebViewer
+
+web_viewer = WebViewer()
+
+
+def open_url_in_browser(url: str) -> bool:
+ """Opens an url in a browser using user's desktop environment
+
+ :param url: url to open
+ :type url: str
+
+ :return: true if successful otherwise false
+ :rtype: bool
+ """
+ return QDesktopServices.openUrl(QUrl(url))
+
+
+def open_url_in_webviewer(url: str, window_title: str) -> None:
+ """Opens an url in Geotribu's webviewer
+
+ :param url: url to open
+ :type url: str
+
+ :param window_title: title to give to the webviewer window
+ :type window_title: str
+ """
+ web_viewer.display_web_page(url)
+ web_viewer.set_window_title(window_title)
diff --git a/qtribu/toolbelt/preferences.py b/qtribu/toolbelt/preferences.py
index dccf1750..2872be26 100644
--- a/qtribu/toolbelt/preferences.py
+++ b/qtribu/toolbelt/preferences.py
@@ -29,6 +29,7 @@ class PlgSettingsStructure:
# RSS feed
rss_source: str = "https://geotribu.fr/feed_rss_created.xml"
+ json_feed_source: str = "https://geotribu.fr/feed_json_created.json"
# usage
browser: int = 1