qcanvas 0.0.5.7a0__py3-none-any.whl → 1.0.3.post0__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.
- qcanvas/app_start/__init__.py +47 -0
- qcanvas/backend_connectors/__init__.py +2 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
- qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
- qcanvas/icons/__init__.py +6 -0
- qcanvas/icons/file-download-failed.svg +6 -0
- qcanvas/icons/file-downloaded.svg +6 -0
- qcanvas/icons/file-not-downloaded.svg +6 -0
- qcanvas/icons/file-unknown.svg +6 -0
- qcanvas/icons/icons.qrc +4 -0
- qcanvas/icons/main_icon.svg +7 -7
- qcanvas/icons/rc_icons.py +580 -214
- qcanvas/icons/sync.svg +6 -6
- qcanvas/run.py +29 -0
- qcanvas/ui/course_viewer/__init__.py +2 -0
- qcanvas/ui/course_viewer/content_tree.py +123 -0
- qcanvas/ui/course_viewer/course_tree.py +93 -0
- qcanvas/ui/course_viewer/course_viewer.py +62 -0
- qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
- qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
- qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
- qcanvas/ui/course_viewer/tabs/util.py +1 -0
- qcanvas/ui/main_ui/course_viewer_container.py +52 -0
- qcanvas/ui/main_ui/options/__init__.py +3 -0
- qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
- qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
- qcanvas/ui/main_ui/qcanvas_window.py +192 -0
- qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
- qcanvas/ui/memory_tree/__init__.py +2 -0
- qcanvas/ui/memory_tree/_tree_memory.py +66 -0
- qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
- qcanvas/ui/setup/__init__.py +2 -0
- qcanvas/ui/setup/setup_checker.py +17 -0
- qcanvas/ui/setup/setup_dialog.py +212 -0
- qcanvas/util/__init__.py +2 -0
- qcanvas/util/basic_fonts.py +12 -0
- qcanvas/util/fe_resource_manager.py +23 -0
- qcanvas/util/html_cleaner.py +25 -0
- qcanvas/util/layouts.py +52 -0
- qcanvas/util/logs.py +6 -0
- qcanvas/util/paths.py +41 -0
- qcanvas/util/settings/__init__.py +9 -0
- qcanvas/util/settings/_client_settings.py +29 -0
- qcanvas/util/settings/_mapped_setting.py +63 -0
- qcanvas/util/settings/_ui_settings.py +34 -0
- qcanvas/util/ui_tools.py +41 -0
- qcanvas/util/url_checker.py +13 -0
- qcanvas-1.0.3.post0.dist-info/METADATA +61 -0
- qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
- {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
- qcanvas-1.0.3.post0.dist-info/entry_points.txt +3 -0
- qcanvas/__main__.py +0 -155
- qcanvas/db/__init__.py +0 -5
- qcanvas/db/database.py +0 -338
- qcanvas/db/db_converter_helper.py +0 -81
- qcanvas/net/canvas/__init__.py +0 -2
- qcanvas/net/canvas/canvas_client.py +0 -209
- qcanvas/net/canvas/legacy_canvas_types.py +0 -124
- qcanvas/net/custom_httpx_async_transport.py +0 -34
- qcanvas/net/self_authenticating.py +0 -108
- qcanvas/queries/__init__.py +0 -4
- qcanvas/queries/all_courses.gql +0 -7
- qcanvas/queries/all_courses.py +0 -108
- qcanvas/queries/canvas_course_data.gql +0 -51
- qcanvas/queries/canvas_course_data.py +0 -143
- qcanvas/ui/container_item.py +0 -11
- qcanvas/ui/main_ui.py +0 -251
- qcanvas/ui/menu_bar/__init__.py +0 -0
- qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
- qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
- qcanvas/ui/setup_dialog.py +0 -190
- qcanvas/ui/status_bar_reporter.py +0 -40
- qcanvas/ui/viewer/__init__.py +0 -0
- qcanvas/ui/viewer/course_list.py +0 -96
- qcanvas/ui/viewer/file_list.py +0 -195
- qcanvas/ui/viewer/file_view_tab.py +0 -62
- qcanvas/ui/viewer/page_list_viewer.py +0 -150
- qcanvas/util/app_settings.py +0 -98
- qcanvas/util/constants.py +0 -5
- qcanvas/util/course_indexer/__init__.py +0 -1
- qcanvas/util/course_indexer/conversion_helpers.py +0 -78
- qcanvas/util/course_indexer/data_manager.py +0 -447
- qcanvas/util/course_indexer/resource_helpers.py +0 -191
- qcanvas/util/download_pool.py +0 -58
- qcanvas/util/helpers/__init__.py +0 -0
- qcanvas/util/helpers/canvas_sanitiser.py +0 -47
- qcanvas/util/helpers/file_icon_helper.py +0 -34
- qcanvas/util/helpers/qaction_helper.py +0 -25
- qcanvas/util/helpers/theme_helper.py +0 -48
- qcanvas/util/link_scanner/__init__.py +0 -2
- qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
- qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
- qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
- qcanvas/util/link_scanner/resource_scanner.py +0 -69
- qcanvas/util/progress_reporter.py +0 -101
- qcanvas/util/self_updater.py +0 -55
- qcanvas/util/task_pool.py +0 -253
- qcanvas/util/tree_util/__init__.py +0 -3
- qcanvas/util/tree_util/expanding_tree.py +0 -165
- qcanvas/util/tree_util/model_helpers.py +0 -36
- qcanvas/util/tree_util/tree_model.py +0 -85
- qcanvas-0.0.5.7a0.dist-info/METADATA +0 -21
- qcanvas-0.0.5.7a0.dist-info/RECORD +0 -62
- /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,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()
|