qcanvas 0.0.5.7a0__py3-none-any.whl → 1.0.3.post1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of qcanvas might be problematic. Click here for more details.

Files changed (114) hide show
  1. qcanvas/app_start/__init__.py +47 -0
  2. qcanvas/backend_connectors/__init__.py +2 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
  4. qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
  5. qcanvas/icons/__init__.py +6 -0
  6. qcanvas/icons/file-download-failed.svg +6 -0
  7. qcanvas/icons/file-downloaded.svg +6 -0
  8. qcanvas/icons/file-not-downloaded.svg +6 -0
  9. qcanvas/icons/file-unknown.svg +6 -0
  10. qcanvas/icons/icons.qrc +4 -0
  11. qcanvas/icons/main_icon.svg +7 -7
  12. qcanvas/icons/rc_icons.py +580 -214
  13. qcanvas/icons/sync.svg +6 -6
  14. qcanvas/run.py +29 -0
  15. qcanvas/ui/course_viewer/__init__.py +2 -0
  16. qcanvas/ui/course_viewer/content_tree.py +123 -0
  17. qcanvas/ui/course_viewer/course_tree.py +93 -0
  18. qcanvas/ui/course_viewer/course_viewer.py +62 -0
  19. qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
  21. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
  22. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
  23. qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
  24. qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
  25. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
  26. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
  27. qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
  28. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
  30. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
  31. qcanvas/ui/course_viewer/tabs/util.py +1 -0
  32. qcanvas/ui/main_ui/course_viewer_container.py +52 -0
  33. qcanvas/ui/main_ui/options/__init__.py +3 -0
  34. qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
  35. qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
  36. qcanvas/ui/main_ui/qcanvas_window.py +192 -0
  37. qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
  38. qcanvas/ui/memory_tree/__init__.py +2 -0
  39. qcanvas/ui/memory_tree/_tree_memory.py +66 -0
  40. qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
  41. qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
  42. qcanvas/ui/setup/__init__.py +2 -0
  43. qcanvas/ui/setup/setup_checker.py +17 -0
  44. qcanvas/ui/setup/setup_dialog.py +212 -0
  45. qcanvas/util/__init__.py +2 -0
  46. qcanvas/util/basic_fonts.py +12 -0
  47. qcanvas/util/fe_resource_manager.py +23 -0
  48. qcanvas/util/html_cleaner.py +25 -0
  49. qcanvas/util/layouts.py +52 -0
  50. qcanvas/util/logs.py +6 -0
  51. qcanvas/util/paths.py +41 -0
  52. qcanvas/util/settings/__init__.py +9 -0
  53. qcanvas/util/settings/_client_settings.py +29 -0
  54. qcanvas/util/settings/_mapped_setting.py +63 -0
  55. qcanvas/util/settings/_ui_settings.py +34 -0
  56. qcanvas/util/ui_tools.py +41 -0
  57. qcanvas/util/url_checker.py +13 -0
  58. qcanvas-1.0.3.post1.dist-info/METADATA +59 -0
  59. qcanvas-1.0.3.post1.dist-info/RECORD +64 -0
  60. {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post1.dist-info}/WHEEL +1 -1
  61. qcanvas-1.0.3.post1.dist-info/entry_points.txt +3 -0
  62. qcanvas/__main__.py +0 -155
  63. qcanvas/db/__init__.py +0 -5
  64. qcanvas/db/database.py +0 -338
  65. qcanvas/db/db_converter_helper.py +0 -81
  66. qcanvas/net/canvas/__init__.py +0 -2
  67. qcanvas/net/canvas/canvas_client.py +0 -209
  68. qcanvas/net/canvas/legacy_canvas_types.py +0 -124
  69. qcanvas/net/custom_httpx_async_transport.py +0 -34
  70. qcanvas/net/self_authenticating.py +0 -108
  71. qcanvas/queries/__init__.py +0 -4
  72. qcanvas/queries/all_courses.gql +0 -7
  73. qcanvas/queries/all_courses.py +0 -108
  74. qcanvas/queries/canvas_course_data.gql +0 -51
  75. qcanvas/queries/canvas_course_data.py +0 -143
  76. qcanvas/ui/container_item.py +0 -11
  77. qcanvas/ui/main_ui.py +0 -251
  78. qcanvas/ui/menu_bar/__init__.py +0 -0
  79. qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
  80. qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
  81. qcanvas/ui/setup_dialog.py +0 -190
  82. qcanvas/ui/status_bar_reporter.py +0 -40
  83. qcanvas/ui/viewer/__init__.py +0 -0
  84. qcanvas/ui/viewer/course_list.py +0 -96
  85. qcanvas/ui/viewer/file_list.py +0 -195
  86. qcanvas/ui/viewer/file_view_tab.py +0 -62
  87. qcanvas/ui/viewer/page_list_viewer.py +0 -150
  88. qcanvas/util/app_settings.py +0 -98
  89. qcanvas/util/constants.py +0 -5
  90. qcanvas/util/course_indexer/__init__.py +0 -1
  91. qcanvas/util/course_indexer/conversion_helpers.py +0 -78
  92. qcanvas/util/course_indexer/data_manager.py +0 -447
  93. qcanvas/util/course_indexer/resource_helpers.py +0 -191
  94. qcanvas/util/download_pool.py +0 -58
  95. qcanvas/util/helpers/__init__.py +0 -0
  96. qcanvas/util/helpers/canvas_sanitiser.py +0 -47
  97. qcanvas/util/helpers/file_icon_helper.py +0 -34
  98. qcanvas/util/helpers/qaction_helper.py +0 -25
  99. qcanvas/util/helpers/theme_helper.py +0 -48
  100. qcanvas/util/link_scanner/__init__.py +0 -2
  101. qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
  102. qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
  103. qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
  104. qcanvas/util/link_scanner/resource_scanner.py +0 -69
  105. qcanvas/util/progress_reporter.py +0 -101
  106. qcanvas/util/self_updater.py +0 -55
  107. qcanvas/util/task_pool.py +0 -253
  108. qcanvas/util/tree_util/__init__.py +0 -3
  109. qcanvas/util/tree_util/expanding_tree.py +0 -165
  110. qcanvas/util/tree_util/model_helpers.py +0 -36
  111. qcanvas/util/tree_util/tree_model.py +0 -85
  112. qcanvas-0.0.5.7a0.dist-info/METADATA +0 -21
  113. qcanvas-0.0.5.7a0.dist-info/RECORD +0 -62
  114. /qcanvas/{net → ui/main_ui}/__init__.py +0 -0
@@ -0,0 +1,96 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ import qcanvas_backend.database.types as db
5
+ from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
+ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
+ from qtpy.QtWidgets import *
8
+
9
+ from qcanvas.ui.course_viewer.content_tree import ContentTree
10
+ from qcanvas.ui.course_viewer.tabs.resource_rich_browser import ResourceRichBrowser
11
+ from qcanvas.util.basic_fonts import bold_font
12
+ from qcanvas.util.ui_tools import make_truncatable
13
+
14
+ _logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ContentTab(QWidget):
18
+ def __init__(
19
+ self,
20
+ *,
21
+ explorer: ContentTree[db.Course],
22
+ title_placeholder_text: str,
23
+ downloader: ResourceManager,
24
+ ):
25
+ super().__init__()
26
+ self._content_vbox = QVBoxLayout()
27
+ self._placeholder_text = title_placeholder_text
28
+ self._title_label = self._create_title_label()
29
+ self._info_grid: Optional[QWidget] = None
30
+ self._viewer = ResourceRichBrowser(downloader=downloader)
31
+ self._explorer = explorer
32
+
33
+ self._setup_layout()
34
+ self._explorer.item_selected.connect(self._item_selected)
35
+
36
+ def enable_info_grid(self) -> None:
37
+ # Info grid needs to be a widget, so it can be hidden/shown
38
+ grid_layout = self.setup_info_grid()
39
+
40
+ grid_widget = QWidget()
41
+ grid_widget.setLayout(grid_layout)
42
+ grid_widget.hide()
43
+
44
+ self._info_grid = grid_widget
45
+ self._content_vbox.insertWidget(1, grid_widget)
46
+
47
+ def _create_title_label(self) -> QLabel:
48
+ title_label = QLabel(self._placeholder_text)
49
+ title_label.setFont(bold_font)
50
+ make_truncatable(title_label)
51
+ return title_label
52
+
53
+ def setup_info_grid(self) -> QGridLayout:
54
+ """
55
+ Override this if you need an info grid
56
+ """
57
+ raise NotImplementedError()
58
+
59
+ def _setup_layout(self) -> None:
60
+ parent_layout = QHBoxLayout()
61
+ parent_layout.addWidget(self._explorer)
62
+
63
+ self._content_vbox.addWidget(self._title_label)
64
+ self._content_vbox.addWidget(self._viewer)
65
+
66
+ parent_layout.addLayout(self._content_vbox)
67
+
68
+ self.setLayout(parent_layout)
69
+
70
+ def reload(self, course: db.Course, *, sync_receipt: Optional[SyncReceipt]) -> None:
71
+ self._explorer.reload(course, sync_receipt=sync_receipt)
72
+
73
+ def _item_selected(self, item: object) -> None:
74
+ if isinstance(item, db.CourseContentItem):
75
+ _logger.debug("Show %s", item.name)
76
+ self._show_content(item)
77
+ else:
78
+ self._show_blank()
79
+
80
+ def _show_content(self, item: db.CourseContentItem) -> None:
81
+ self._title_label.setText(item.name)
82
+ self._viewer.show_content(item)
83
+
84
+ if self._info_grid is not None:
85
+ self._info_grid.show()
86
+ self.update_info_grid(item)
87
+
88
+ def update_info_grid(self, content: db.CourseContentItem) -> None:
89
+ raise NotImplementedError()
90
+
91
+ def _show_blank(self) -> None:
92
+ self._title_label.setText(self._placeholder_text)
93
+ self._viewer.show_blank(completely_blank=True)
94
+
95
+ if self._info_grid is not None:
96
+ self._info_grid.hide()
@@ -0,0 +1 @@
1
+ from .mail_tab import MailTab
@@ -0,0 +1,68 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ import qcanvas_backend.database.types as db
5
+ from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
+ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
+ from qtpy.QtWidgets import *
8
+
9
+ from qcanvas.ui.course_viewer.tabs.content_tab import ContentTab
10
+ from qcanvas.ui.course_viewer.tabs.mail_tab.mail_tree import MailTree
11
+ from qcanvas.ui.course_viewer.tabs.util import date_strftime_format
12
+ from qcanvas.util.basic_fonts import bold_label
13
+ from qcanvas.util.layouts import grid_layout
14
+
15
+ _logger = logging.getLogger(__name__)
16
+
17
+
18
+ # todo maybe update has_been_read? probably not the responsibility of this class though
19
+ class MailTab(ContentTab):
20
+ @staticmethod
21
+ def create_from_receipt(
22
+ *,
23
+ course: db.Course,
24
+ sync_receipt: Optional[SyncReceipt],
25
+ downloader: ResourceManager,
26
+ ) -> "MailTab":
27
+ return MailTab(course=course, sync_receipt=sync_receipt, downloader=downloader)
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ course: db.Course,
33
+ sync_receipt: Optional[SyncReceipt],
34
+ downloader: ResourceManager,
35
+ ):
36
+ super().__init__(
37
+ explorer=MailTree.create_from_receipt(course, sync_receipt=sync_receipt),
38
+ title_placeholder_text="No mail selected",
39
+ downloader=downloader,
40
+ )
41
+
42
+ self._date_sent_label = QLabel("")
43
+ self._sender_label = QLabel("")
44
+
45
+ self.enable_info_grid()
46
+
47
+ def setup_info_grid(self) -> QGridLayout:
48
+ grid = grid_layout(
49
+ [
50
+ [
51
+ bold_label("From:"),
52
+ self._sender_label,
53
+ ],
54
+ [
55
+ bold_label("Date:"),
56
+ self._date_sent_label,
57
+ ],
58
+ ]
59
+ )
60
+
61
+ grid.setColumnStretch(0, 0)
62
+ grid.setColumnStretch(1, 1)
63
+
64
+ return grid
65
+
66
+ def update_info_grid(self, mail: db.CourseMessage) -> None:
67
+ self._date_sent_label.setText(mail.creation_date.strftime(date_strftime_format))
68
+ self._sender_label.setText(mail.sender_name)
@@ -0,0 +1,70 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ import qcanvas_backend.database.types as db
5
+ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
+ from qtpy.QtWidgets import *
7
+
8
+ from qcanvas.ui.course_viewer.content_tree import ContentTree
9
+ from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class MailTree(ContentTree[db.Course]):
15
+ @staticmethod
16
+ def create_from_receipt(
17
+ course: db.Course, *, sync_receipt: Optional[SyncReceipt]
18
+ ) -> "MailTree":
19
+ tree = MailTree(course.id)
20
+ tree.reload(course, sync_receipt=sync_receipt)
21
+ return tree
22
+
23
+ def __init__(self, course_id: str):
24
+ super().__init__(
25
+ tree_name=f"course.{course_id}.mail",
26
+ emit_selection_signal_for_type=db.CourseMessage,
27
+ )
28
+ self.ui_setup(
29
+ header_text=["Subject", "Sender"],
30
+ max_width=500,
31
+ min_width=300,
32
+ indentation=20,
33
+ )
34
+
35
+ self._adjust_header()
36
+
37
+ def _adjust_header(self) -> None:
38
+ header = self.header()
39
+ header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
40
+ header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
41
+ header.setStretchLastSection(False)
42
+
43
+ def create_tree_items(
44
+ self, course: db.Course, sync_receipt: Optional[SyncReceipt]
45
+ ) -> Sequence[MemoryTreeWidgetItem]:
46
+ widgets = []
47
+
48
+ for message in course.messages: # type: db.CourseMessage
49
+ message_widget = self._create_mail_widget(message, sync_receipt)
50
+ widgets.append(message_widget)
51
+
52
+ return widgets
53
+
54
+ def _create_mail_widget(
55
+ self, message: db.CourseMessage, sync_receipt: Optional[SyncReceipt]
56
+ ) -> MemoryTreeWidgetItem:
57
+ message_widget = MemoryTreeWidgetItem(
58
+ id=message.id,
59
+ data=message,
60
+ strings=[message.name, message.sender_name],
61
+ )
62
+
63
+ is_new = (
64
+ sync_receipt is not None and message.id in sync_receipt.updated_messages
65
+ )
66
+
67
+ if is_new:
68
+ self.mark_as_unseen(message_widget)
69
+
70
+ return message_widget
@@ -0,0 +1 @@
1
+ from .page_tab import PageTab
@@ -0,0 +1,36 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ import qcanvas_backend.database.types as db
5
+ from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
+ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
+
8
+ from qcanvas.ui.course_viewer.tabs.content_tab import ContentTab
9
+ from qcanvas.ui.course_viewer.tabs.page_tab.page_tree import PageTree
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PageTab(ContentTab):
15
+
16
+ @staticmethod
17
+ def create_from_receipt(
18
+ *,
19
+ course: db.Course,
20
+ sync_receipt: Optional[SyncReceipt],
21
+ downloader: ResourceManager,
22
+ ) -> "PageTab":
23
+ return PageTab(course=course, sync_receipt=sync_receipt, downloader=downloader)
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ course: db.Course,
29
+ sync_receipt: Optional[SyncReceipt],
30
+ downloader: ResourceManager,
31
+ ):
32
+ super().__init__(
33
+ explorer=PageTree.create_from_receipt(course, sync_receipt=sync_receipt),
34
+ title_placeholder_text="No page selected",
35
+ downloader=downloader,
36
+ )
@@ -0,0 +1,74 @@
1
+ import logging
2
+ from typing import Optional, Sequence
3
+
4
+ import qcanvas_backend.database.types as db
5
+ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
+ from qtpy.QtGui import Qt
7
+
8
+ from qcanvas.ui.course_viewer.content_tree import ContentTree
9
+ from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PageTree(ContentTree[db.Course]):
15
+ @staticmethod
16
+ def create_from_receipt(
17
+ course: db.Course, *, sync_receipt: Optional[SyncReceipt]
18
+ ) -> "PageTree":
19
+ tree = PageTree(course.id)
20
+ tree.reload(course, sync_receipt=sync_receipt)
21
+ return tree
22
+
23
+ def __init__(self, course_id: str):
24
+ super().__init__(
25
+ tree_name=f"course.{course_id}.modules",
26
+ emit_selection_signal_for_type=db.ModulePage,
27
+ )
28
+ self.ui_setup(
29
+ header_text="Content", indentation=15, max_width=300, min_width=150
30
+ )
31
+
32
+ def create_tree_items(
33
+ self, course: db.Course, sync_receipt: Optional[SyncReceipt]
34
+ ) -> Sequence[MemoryTreeWidgetItem]:
35
+ widgets = []
36
+
37
+ for module in course.modules: # type: db.Module
38
+ module_widget = self._create_module_widget(module, sync_receipt)
39
+ widgets.append(module_widget)
40
+
41
+ for page in module.pages: # type: db.ModulePage
42
+ page_widget = self._create_page_widget(page, sync_receipt)
43
+ module_widget.addChild(page_widget)
44
+
45
+ return widgets
46
+
47
+ def _create_module_widget(
48
+ self, module: db.Module, sync_receipt: Optional[SyncReceipt]
49
+ ) -> MemoryTreeWidgetItem:
50
+ module_widget = MemoryTreeWidgetItem(
51
+ id=module.id, data=module, strings=[module.name]
52
+ )
53
+ module_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
54
+
55
+ # todo add some helpers to SyncReceipt to make this less shit, and maybe use an empty syncreceipt instead of None
56
+ is_new = sync_receipt is not None and module.id in sync_receipt.updated_modules
57
+
58
+ if is_new:
59
+ self.mark_as_unseen(module_widget)
60
+
61
+ return module_widget
62
+
63
+ def _create_page_widget(
64
+ self, page: db.ModulePage, sync_receipt: Optional[SyncReceipt]
65
+ ) -> MemoryTreeWidgetItem:
66
+ page_widget = MemoryTreeWidgetItem(id=page.id, data=page, strings=[page.name])
67
+ page_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
68
+
69
+ is_new = sync_receipt is not None and page.id in sync_receipt.updated_pages
70
+
71
+ if is_new:
72
+ self.mark_as_unseen(page_widget)
73
+
74
+ return page_widget
@@ -0,0 +1,176 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import qcanvas_backend.database.types as db
5
+ from bs4 import BeautifulSoup, Tag
6
+ from qasync import asyncSlot
7
+ from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
8
+ from qcanvas_backend.net.resources.extracting.no_extractor_error import NoExtractorError
9
+ from qcanvas_backend.net.resources.scanning.resource_scanner import ResourceScanner
10
+ from qtpy.QtCore import QUrl, Slot
11
+ from qtpy.QtGui import QDesktopServices
12
+ from qtpy.QtWidgets import QTextBrowser
13
+
14
+ from qcanvas import icons
15
+ from qcanvas.backend_connectors import FrontendResourceManager
16
+ from qcanvas.util.html_cleaner import clean_up_html
17
+
18
+ _logger = logging.getLogger(__name__)
19
+
20
+
21
+ # class _DarkListener(QObject):
22
+ # theme_changed = Signal(str)
23
+ #
24
+ # def __init__(self):
25
+ # super().__init__()
26
+ #
27
+ # self._thread = threading.Thread(target=darkdetect.listener, args=(self._emit,))
28
+ # self._thread.daemon = True
29
+ # self._thread.start()
30
+ #
31
+ # def _emit(self, theme: str) -> None:
32
+ # self.theme_changed.emit(theme)
33
+ #
34
+ # _dark_listener = _DarkListener()
35
+
36
+
37
+ class ResourceRichBrowser(QTextBrowser):
38
+ def __init__(self, downloader: ResourceManager):
39
+ super().__init__()
40
+ self._downloader = downloader
41
+ self._content: Optional[db.CourseContentItem] = None
42
+ self._current_content_resources: dict[str, db.Resource] = {}
43
+ self._extractors = downloader.extractors
44
+ self.setMinimumWidth(300)
45
+ self.setOpenLinks(False)
46
+ self.anchorClicked.connect(self._open_url)
47
+
48
+ if isinstance(self._downloader, FrontendResourceManager):
49
+ self._downloader.download_finished.connect(self._download_updated)
50
+ self._downloader.download_failed.connect(self._download_updated)
51
+
52
+ # _dark_listener.theme_changed.connect(self._theme_changed)
53
+
54
+ # @Slot()
55
+ # def _theme_changed(self, theme: str) -> None:
56
+ # print(theme)
57
+
58
+ def show_blank(self, completely_blank: bool = False) -> None:
59
+ if completely_blank:
60
+ self.clear()
61
+ else:
62
+ self.setPlainText("No content")
63
+
64
+ self._content = None
65
+ self._current_content_resources.clear()
66
+
67
+ def show_content(self, page: db.CourseContentItem) -> None:
68
+ if page.body is None:
69
+ self.show_blank()
70
+ else:
71
+ self._collect_resources(page)
72
+ self._show_page_content(page)
73
+
74
+ def _collect_resources(self, page: db.CourseContentItem):
75
+ self._current_content_resources = {
76
+ resource.id: resource for resource in page.resources
77
+ }
78
+
79
+ def _show_page_content(self, page: db.CourseContentItem):
80
+ self._content = page
81
+ html = clean_up_html(page.body)
82
+ html = self._substitute_links(html)
83
+ self.setHtml(html)
84
+
85
+ def _substitute_links(self, html: str) -> str:
86
+ doc = BeautifulSoup(html, "html.parser")
87
+
88
+ for resource_link in doc.find_all(self._extractors.tag_whitelist):
89
+ try:
90
+ extractor = self._extractors.extractor_for_tag(resource_link)
91
+ resource_id = extractor.resource_id_from_tag(resource_link)
92
+
93
+ # FIXME private method
94
+ if ResourceScanner._is_link_invisible(resource_link):
95
+ _logger.debug("Found dead link for %s, removing", resource_id)
96
+ resource_link.decompose()
97
+ continue
98
+ elif resource_id not in self._current_content_resources:
99
+ _logger.debug(
100
+ "%s not found in page resources, ignoring", resource_id
101
+ )
102
+ continue
103
+
104
+ file_link_tag = self._create_resource_link_tag(doc, resource_id)
105
+ resource_link.replace_with(file_link_tag)
106
+ except NoExtractorError:
107
+ pass
108
+
109
+ return str(doc)
110
+
111
+ def _create_resource_link_tag(self, doc: BeautifulSoup, resource_id: str) -> Tag:
112
+ resource = self._current_content_resources[resource_id]
113
+
114
+ file_link_tag = doc.new_tag(
115
+ "a",
116
+ attrs={
117
+ "href": f"data:{resource_id}",
118
+ },
119
+ )
120
+
121
+ file_link_tag.append(self._file_icon_tag(doc, resource.download_state))
122
+ file_link_tag.append("\N{NO-BREAK SPACE}" + resource.file_name)
123
+
124
+ _logger.debug(str(file_link_tag))
125
+
126
+ return file_link_tag
127
+
128
+ def _file_icon_tag(
129
+ self, document: BeautifulSoup, download_state: db.ResourceDownloadState
130
+ ) -> Tag:
131
+ return document.new_tag(
132
+ "img",
133
+ attrs={
134
+ "src": self._download_state_icon(download_state),
135
+ "style": "vertical-align:middle",
136
+ "width": 18,
137
+ },
138
+ )
139
+
140
+ def _download_state_icon(self, download_state: db.ResourceDownloadState) -> str:
141
+ match download_state:
142
+ case db.ResourceDownloadState.DOWNLOADED:
143
+ return icons.file_downloaded
144
+ case db.ResourceDownloadState.NOT_DOWNLOADED:
145
+ return icons.file_not_downloaded
146
+ case db.ResourceDownloadState.FAILED:
147
+ return icons.file_download_failed
148
+ case _:
149
+ raise ValueError()
150
+
151
+ @asyncSlot()
152
+ async def _open_url(self, url: QUrl) -> None:
153
+ if url.scheme() == "data":
154
+ await self._open_resource_from_link(url)
155
+ else:
156
+ QDesktopServices.openUrl(url)
157
+
158
+ async def _open_resource_from_link(self, url) -> None:
159
+ resource_id = url.path()
160
+ resource = self._current_content_resources[resource_id]
161
+
162
+ try:
163
+ await self._downloader.download(resource)
164
+ except Exception as e:
165
+ _logger.warning(
166
+ "Download of resource id=%s failed", resource_id, exc_info=e
167
+ )
168
+ return
169
+
170
+ resource_path = self._downloader.resource_download_location(resource)
171
+ QDesktopServices.openUrl(QUrl.fromLocalFile(resource_path.absolute()))
172
+
173
+ @Slot()
174
+ def _download_updated(self, resource: db.Resource) -> None:
175
+ if self._content is not None and resource.id in self._current_content_resources:
176
+ self._show_page_content(self._content)
@@ -0,0 +1 @@
1
+ date_strftime_format = "%A, %Y-%m-%d, %H:%M:%S"
@@ -0,0 +1,52 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ import qcanvas_backend.database.types as db
5
+ from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
+ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
+ from qtpy.QtCore import Qt
8
+ from qtpy.QtWidgets import *
9
+
10
+ from qcanvas.ui.course_viewer.course_viewer import CourseViewer
11
+
12
+ _logger = logging.getLogger(__name__)
13
+
14
+
15
+ class CourseViewerContainer(QStackedWidget):
16
+ def __init__(self, downloader: ResourceManager):
17
+ super().__init__()
18
+ self._course_viewers: dict[str, CourseViewer] = {}
19
+ self._downloader = downloader
20
+ self._last_course_id: Optional[str] = None
21
+ self._last_sync_receipt: Optional[SyncReceipt] = None
22
+ self._placeholder = QLabel("No Course Selected")
23
+ self._placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
24
+ self.addWidget(self._placeholder)
25
+
26
+ def show_blank(self) -> None:
27
+ self._last_course_id = None
28
+ self.setCurrentWidget(self._placeholder)
29
+
30
+ def load_course(self, course: db.Course) -> None:
31
+ if course.id not in self._course_viewers:
32
+ viewer = CourseViewer(
33
+ course=course,
34
+ downloader=self._downloader,
35
+ initial_sync_receipt=self._last_sync_receipt,
36
+ )
37
+ self._course_viewers[course.id] = viewer
38
+ self.addWidget(viewer)
39
+ else:
40
+ viewer = self._course_viewers[course.id]
41
+
42
+ self.setCurrentWidget(viewer)
43
+ self._last_course_id = course.id
44
+
45
+ async def reload_all(
46
+ self, courses: Sequence[db.Course], *, sync_receipt: Optional[SyncReceipt]
47
+ ) -> None:
48
+ self._last_sync_receipt = sync_receipt
49
+ for course in courses:
50
+ if course.id in self._course_viewers:
51
+ viewer = self._course_viewers[course.id]
52
+ viewer.reload(course, sync_receipt=sync_receipt)
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ _logger = logging.getLogger(__name__)
@@ -0,0 +1,25 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ from qtpy.QtCore import Slot
5
+ from qtpy.QtGui import QAction
6
+ from qtpy.QtWidgets import QMenu
7
+
8
+ from qcanvas.util import settings
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ class QuickSyncOption(QAction):
14
+ def __init__(self, parent: Optional[QMenu] = None):
15
+ super().__init__("Ignore old courses", parent)
16
+ self.setToolTip(
17
+ "When this option is selected, old courses will not be synchronised. This will only effect the first sync."
18
+ )
19
+ self.setCheckable(True)
20
+ self.setChecked(settings.client.quick_sync_enabled)
21
+ self.triggered.connect(self._triggered)
22
+
23
+ @Slot()
24
+ def _triggered(self) -> None:
25
+ settings.client.quick_sync_enabled = self.isChecked()
@@ -0,0 +1,25 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ from qtpy.QtCore import Slot
5
+ from qtpy.QtGui import QAction
6
+ from qtpy.QtWidgets import QMenu
7
+
8
+ from qcanvas.util import settings
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SyncOnStartOption(QAction):
14
+ def __init__(self, parent: Optional[QMenu] = None):
15
+ super().__init__("Sync on start", parent)
16
+ self.setToolTip(
17
+ "When this option is selected, synchronisation will be started automatically when the app starts."
18
+ )
19
+ self.setCheckable(True)
20
+ self.setChecked(settings.client.sync_on_start)
21
+ self.triggered.connect(self._triggered)
22
+
23
+ @Slot()
24
+ def _triggered(self) -> None:
25
+ settings.client.sync_on_start = self.isChecked()