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 +
qgscollapsiblegroupbox.h
+ 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 +
qtribu.gui.wdg_authoring
+ 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