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
qcanvas/icons/sync.svg
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
2
|
-
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
-
<path d="M18.43 4.25C18.2319 4.25259 18.0426 4.33244 17.9025 4.47253C17.7625 4.61263 17.6826 4.80189 17.68 5V7.43L16.84 6.59C15.971 5.71363 14.8924 5.07396 13.7067 4.73172C12.5209 4.38948 11.2673 4.35604 10.065 4.63458C8.86267 4.91312 7.7515 5.49439 6.83703 6.32318C5.92255 7.15198 5.23512 8.20078 4.84001 9.37C4.79887 9.46531 4.77824 9.56821 4.77947 9.67202C4.7807 9.77583 4.80375 9.87821 4.84714 9.97252C4.89052 10.0668 4.95326 10.151 5.03129 10.2194C5.10931 10.2879 5.20087 10.3392 5.30001 10.37C5.38273 10.3844 5.4673 10.3844 5.55001 10.37C5.70646 10.3684 5.85861 10.3186 5.98568 10.2273C6.11275 10.136 6.20856 10.0078 6.26001 9.86C6.53938 9.0301 7.00847 8.27681 7.63001 7.66C8.70957 6.58464 10.1713 5.98085 11.695 5.98085C13.2188 5.98085 14.6805 6.58464 15.76 7.66L16.6 8.5H14.19C13.9911 8.5 13.8003 8.57902 13.6597 8.71967C13.519 8.86032 13.44 9.05109 13.44 9.25C13.44 9.44891 13.519 9.63968 13.6597 9.78033C13.8003 9.92098 13.9911 10 14.19 10H18.43C18.5289 10.0013 18.627 9.98286 18.7186 9.94565C18.8102 9.90844 18.8934 9.85324 18.9633 9.78333C19.0333 9.71341 19.0885 9.6302 19.1257 9.5386C19.1629 9.44699 19.1814 9.34886 19.18 9.25V5C19.18 4.80109 19.101 4.61032 18.9603 4.46967C18.8197 4.32902 18.6289 4.25 18.43 4.25Z"
|
|
4
|
-
fill="#000000"/>
|
|
5
|
-
<path d="M18.68 13.68C18.5837 13.6422 18.4808 13.6244 18.3774 13.6277C18.274 13.6311 18.1724 13.6555 18.0787 13.6995C17.9851 13.7435 17.9015 13.8062 17.8329 13.8836C17.7643 13.9611 17.7123 14.0517 17.68 14.15C17.4006 14.9799 16.9316 15.7332 16.31 16.35C15.2305 17.4254 13.7688 18.0291 12.245 18.0291C10.7213 18.0291 9.25957 17.4254 8.18001 16.35L7.34001 15.51H9.81002C10.0089 15.51 10.1997 15.431 10.3403 15.2903C10.481 15.1497 10.56 14.9589 10.56 14.76C10.56 14.5611 10.481 14.3703 10.3403 14.2297C10.1997 14.089 10.0089 14.01 9.81002 14.01H5.57001C5.47115 14.0086 5.37302 14.0271 5.28142 14.0643C5.18982 14.1016 5.1066 14.1568 5.03669 14.2267C4.96677 14.2966 4.91158 14.3798 4.87436 14.4714C4.83715 14.563 4.81867 14.6611 4.82001 14.76V19C4.82001 19.1989 4.89903 19.3897 5.03968 19.5303C5.18034 19.671 5.3711 19.75 5.57001 19.75C5.76893 19.75 5.95969 19.671 6.10034 19.5303C6.241 19.3897 6.32001 19.1989 6.32001 19V16.57L7.16001 17.41C8.02901 18.2864 9.10761 18.926 10.2934 19.2683C11.4791 19.6105 12.7327 19.6439 13.935 19.3654C15.1374 19.0869 16.2485 18.5056 17.163 17.6768C18.0775 16.848 18.7649 15.7992 19.16 14.63C19.1926 14.5362 19.2061 14.4368 19.1995 14.3377C19.1929 14.2386 19.1664 14.1418 19.1216 14.0532C19.0768 13.9645 19.0146 13.8858 18.9387 13.8217C18.8629 13.7576 18.7749 13.7094 18.68 13.68Z"
|
|
6
|
-
fill="#000000"/>
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
2
|
+
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M18.43 4.25C18.2319 4.25259 18.0426 4.33244 17.9025 4.47253C17.7625 4.61263 17.6826 4.80189 17.68 5V7.43L16.84 6.59C15.971 5.71363 14.8924 5.07396 13.7067 4.73172C12.5209 4.38948 11.2673 4.35604 10.065 4.63458C8.86267 4.91312 7.7515 5.49439 6.83703 6.32318C5.92255 7.15198 5.23512 8.20078 4.84001 9.37C4.79887 9.46531 4.77824 9.56821 4.77947 9.67202C4.7807 9.77583 4.80375 9.87821 4.84714 9.97252C4.89052 10.0668 4.95326 10.151 5.03129 10.2194C5.10931 10.2879 5.20087 10.3392 5.30001 10.37C5.38273 10.3844 5.4673 10.3844 5.55001 10.37C5.70646 10.3684 5.85861 10.3186 5.98568 10.2273C6.11275 10.136 6.20856 10.0078 6.26001 9.86C6.53938 9.0301 7.00847 8.27681 7.63001 7.66C8.70957 6.58464 10.1713 5.98085 11.695 5.98085C13.2188 5.98085 14.6805 6.58464 15.76 7.66L16.6 8.5H14.19C13.9911 8.5 13.8003 8.57902 13.6597 8.71967C13.519 8.86032 13.44 9.05109 13.44 9.25C13.44 9.44891 13.519 9.63968 13.6597 9.78033C13.8003 9.92098 13.9911 10 14.19 10H18.43C18.5289 10.0013 18.627 9.98286 18.7186 9.94565C18.8102 9.90844 18.8934 9.85324 18.9633 9.78333C19.0333 9.71341 19.0885 9.6302 19.1257 9.5386C19.1629 9.44699 19.1814 9.34886 19.18 9.25V5C19.18 4.80109 19.101 4.61032 18.9603 4.46967C18.8197 4.32902 18.6289 4.25 18.43 4.25Z"
|
|
4
|
+
fill="#000000"/>
|
|
5
|
+
<path d="M18.68 13.68C18.5837 13.6422 18.4808 13.6244 18.3774 13.6277C18.274 13.6311 18.1724 13.6555 18.0787 13.6995C17.9851 13.7435 17.9015 13.8062 17.8329 13.8836C17.7643 13.9611 17.7123 14.0517 17.68 14.15C17.4006 14.9799 16.9316 15.7332 16.31 16.35C15.2305 17.4254 13.7688 18.0291 12.245 18.0291C10.7213 18.0291 9.25957 17.4254 8.18001 16.35L7.34001 15.51H9.81002C10.0089 15.51 10.1997 15.431 10.3403 15.2903C10.481 15.1497 10.56 14.9589 10.56 14.76C10.56 14.5611 10.481 14.3703 10.3403 14.2297C10.1997 14.089 10.0089 14.01 9.81002 14.01H5.57001C5.47115 14.0086 5.37302 14.0271 5.28142 14.0643C5.18982 14.1016 5.1066 14.1568 5.03669 14.2267C4.96677 14.2966 4.91158 14.3798 4.87436 14.4714C4.83715 14.563 4.81867 14.6611 4.82001 14.76V19C4.82001 19.1989 4.89903 19.3897 5.03968 19.5303C5.18034 19.671 5.3711 19.75 5.57001 19.75C5.76893 19.75 5.95969 19.671 6.10034 19.5303C6.241 19.3897 6.32001 19.1989 6.32001 19V16.57L7.16001 17.41C8.02901 18.2864 9.10761 18.926 10.2934 19.2683C11.4791 19.6105 12.7327 19.6439 13.935 19.3654C15.1374 19.0869 16.2485 18.5056 17.163 17.6768C18.0775 16.848 18.7649 15.7992 19.16 14.63C19.1926 14.5362 19.2061 14.4368 19.1995 14.3377C19.1929 14.2386 19.1664 14.1418 19.1216 14.0532C19.0768 13.9645 19.0146 13.8858 18.9387 13.8217C18.8629 13.7576 18.7749 13.7094 18.68 13.68Z"
|
|
6
|
+
fill="#000000"/>
|
|
7
7
|
</svg>
|
qcanvas/run.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging import DEBUG, INFO, WARNING
|
|
3
|
+
|
|
4
|
+
import qcanvas.app_start
|
|
5
|
+
from qcanvas.util import logs, paths
|
|
6
|
+
|
|
7
|
+
paths.data_storage().mkdir(parents=True, exist_ok=True)
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(
|
|
10
|
+
filename=paths.data_storage() / "debug.log",
|
|
11
|
+
level="WARN",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logs.set_levels(
|
|
15
|
+
{
|
|
16
|
+
"qcanvas": INFO,
|
|
17
|
+
"qcanvas.ui": WARNING,
|
|
18
|
+
"qcanvas_backend": INFO,
|
|
19
|
+
"qcanvas.ui.main_ui.status_bar_progress_display": DEBUG,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
qcanvas.app_start.launch()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == '__main__':
|
|
29
|
+
main()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import *
|
|
4
|
+
|
|
5
|
+
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
6
|
+
from qtpy.QtCore import Signal, Slot
|
|
7
|
+
from qtpy.QtWidgets import *
|
|
8
|
+
|
|
9
|
+
from qcanvas.ui.memory_tree import MemoryTreeWidget, MemoryTreeWidgetItem
|
|
10
|
+
from qcanvas.util.basic_fonts import bold_font, normal_font
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContentTree(MemoryTreeWidget, Generic[T]):
|
|
18
|
+
item_selected = Signal(object)
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
tree_name: str,
|
|
23
|
+
*,
|
|
24
|
+
emit_selection_signal_for_type: Type,
|
|
25
|
+
):
|
|
26
|
+
super().__init__(tree_name)
|
|
27
|
+
self._reloading = False
|
|
28
|
+
self._last_selected_id: Optional[str] = None
|
|
29
|
+
self._target_data_type = emit_selection_signal_for_type
|
|
30
|
+
|
|
31
|
+
self.selectionModel().selectionChanged.connect(self._selection_changed)
|
|
32
|
+
|
|
33
|
+
def ui_setup(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
header_text: str | Sequence[str],
|
|
37
|
+
indentation: int = 20,
|
|
38
|
+
max_width: int,
|
|
39
|
+
min_width: int,
|
|
40
|
+
) -> None:
|
|
41
|
+
if not isinstance(header_text, str) and isinstance(header_text, Sequence):
|
|
42
|
+
self.setHeaderLabels(header_text)
|
|
43
|
+
else:
|
|
44
|
+
self.setHeaderLabel(header_text)
|
|
45
|
+
|
|
46
|
+
self.setIndentation(indentation)
|
|
47
|
+
self.setMaximumWidth(max_width)
|
|
48
|
+
self.setMinimumWidth(min_width)
|
|
49
|
+
|
|
50
|
+
def reload(self, data: T, *, sync_receipt: Optional[SyncReceipt]) -> None:
|
|
51
|
+
self._reloading = True
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self.clear()
|
|
55
|
+
self.addTopLevelItems(self.create_tree_items(data, sync_receipt))
|
|
56
|
+
self.reexpand()
|
|
57
|
+
finally:
|
|
58
|
+
self._reloading = False
|
|
59
|
+
|
|
60
|
+
self.reselect()
|
|
61
|
+
|
|
62
|
+
def reselect(self) -> None:
|
|
63
|
+
if self._last_selected_id is not None:
|
|
64
|
+
if not self.select_ids([self._last_selected_id]):
|
|
65
|
+
self._clear_selection()
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def create_tree_items(
|
|
69
|
+
self, data: T, sync_receipt: Optional[SyncReceipt]
|
|
70
|
+
) -> Sequence[MemoryTreeWidgetItem]: ...
|
|
71
|
+
|
|
72
|
+
@Slot()
|
|
73
|
+
def _selection_changed(self) -> None:
|
|
74
|
+
if self._reloading:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if len(self.selectedItems()) > 0:
|
|
78
|
+
selected = self.selectedItems()[0]
|
|
79
|
+
else:
|
|
80
|
+
self._clear_selection()
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if self.is_unseen(selected):
|
|
84
|
+
self.mark_as_seen(selected)
|
|
85
|
+
|
|
86
|
+
if not isinstance(selected, MemoryTreeWidgetItem):
|
|
87
|
+
self._clear_selection()
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
data = selected.extra_data
|
|
91
|
+
|
|
92
|
+
if isinstance(data, self._target_data_type):
|
|
93
|
+
if hasattr(data, "id"):
|
|
94
|
+
_logger.debug("id=%s selected", data.id)
|
|
95
|
+
self._last_selected_id = data.id
|
|
96
|
+
self.item_selected.emit(data)
|
|
97
|
+
else:
|
|
98
|
+
raise AttributeError(
|
|
99
|
+
f"Expected {self._target_data_type.__name__} to have an id attribute"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
logging.warning(
|
|
103
|
+
"Expected type %s, got %s instead, ignoring",
|
|
104
|
+
self._target_data_type.__name__,
|
|
105
|
+
type(data).__name__,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self._clear_selection()
|
|
109
|
+
|
|
110
|
+
def _clear_selection(self) -> None:
|
|
111
|
+
_logger.debug("Clearing selection")
|
|
112
|
+
|
|
113
|
+
self._last_selected_id = None
|
|
114
|
+
self.item_selected.emit(None)
|
|
115
|
+
|
|
116
|
+
def is_unseen(self, item: QTreeWidgetItem) -> bool:
|
|
117
|
+
return item.font(0).bold()
|
|
118
|
+
|
|
119
|
+
def mark_as_unseen(self, item: QTreeWidgetItem) -> None:
|
|
120
|
+
item.setFont(0, bold_font)
|
|
121
|
+
|
|
122
|
+
def mark_as_seen(self, item: QTreeWidgetItem) -> None:
|
|
123
|
+
item.setFont(0, normal_font)
|
|
@@ -0,0 +1,93 @@
|
|
|
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.QtCore import QObject, Qt, Signal, Slot
|
|
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 _CourseTreeItem(MemoryTreeWidgetItem, QObject):
|
|
15
|
+
renamed = Signal(db.Course, str)
|
|
16
|
+
|
|
17
|
+
def __init__(self, course: db.Course):
|
|
18
|
+
MemoryTreeWidgetItem.__init__(
|
|
19
|
+
self,
|
|
20
|
+
id=course.id,
|
|
21
|
+
data=course,
|
|
22
|
+
strings=[course.configuration.nickname or course.name],
|
|
23
|
+
)
|
|
24
|
+
QObject.__init__(self)
|
|
25
|
+
|
|
26
|
+
self._course = course
|
|
27
|
+
|
|
28
|
+
self.setFlags(
|
|
29
|
+
Qt.ItemFlag.ItemIsEditable
|
|
30
|
+
| Qt.ItemFlag.ItemIsSelectable
|
|
31
|
+
| Qt.ItemFlag.ItemIsEnabled
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def setData(self, column: int, role: int, value: Any):
|
|
35
|
+
if column != 0 or not isinstance(value, str):
|
|
36
|
+
return super().setData(column, role, value)
|
|
37
|
+
|
|
38
|
+
value = value.strip()
|
|
39
|
+
|
|
40
|
+
if len(value) == 0:
|
|
41
|
+
super().setData(column, role, self._course.name)
|
|
42
|
+
self.renamed.emit(self._course, None)
|
|
43
|
+
else:
|
|
44
|
+
super().setData(column, role, value)
|
|
45
|
+
self.renamed.emit(self._course, value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CourseTree(ContentTree[Sequence[db.Term]]):
|
|
49
|
+
course_renamed = Signal(db.Course, str)
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
super().__init__("course_tree", emit_selection_signal_for_type=db.Course)
|
|
53
|
+
|
|
54
|
+
self.ui_setup(max_width=250, min_width=150, header_text="Courses")
|
|
55
|
+
|
|
56
|
+
def create_tree_items(
|
|
57
|
+
self, terms: List[db.Term], sync_receipt: Optional[SyncReceipt]
|
|
58
|
+
) -> Sequence[MemoryTreeWidgetItem]:
|
|
59
|
+
widgets = []
|
|
60
|
+
|
|
61
|
+
for term in reversed(terms):
|
|
62
|
+
term_widget = self._create_term_widget(term)
|
|
63
|
+
|
|
64
|
+
for course in term.courses:
|
|
65
|
+
course_widget = self._create_course_widget(course, sync_receipt)
|
|
66
|
+
term_widget.addChild(course_widget)
|
|
67
|
+
|
|
68
|
+
widgets.append(term_widget)
|
|
69
|
+
|
|
70
|
+
return widgets
|
|
71
|
+
|
|
72
|
+
def _create_course_widget(
|
|
73
|
+
self, course: db.Course, sync_receipt: Optional[SyncReceipt]
|
|
74
|
+
) -> MemoryTreeWidgetItem:
|
|
75
|
+
course_widget = _CourseTreeItem(course)
|
|
76
|
+
course_widget.renamed.connect(self._on_course_renamed)
|
|
77
|
+
|
|
78
|
+
is_new = sync_receipt is not None and course.id in sync_receipt.updated_courses
|
|
79
|
+
|
|
80
|
+
if is_new:
|
|
81
|
+
self.mark_as_unseen(course_widget)
|
|
82
|
+
|
|
83
|
+
return course_widget
|
|
84
|
+
|
|
85
|
+
def _create_term_widget(self, term: db.Term) -> MemoryTreeWidgetItem:
|
|
86
|
+
term_widget = MemoryTreeWidgetItem(id=term.id, data=term, strings=[term.name])
|
|
87
|
+
term_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
88
|
+
|
|
89
|
+
return term_widget
|
|
90
|
+
|
|
91
|
+
@Slot()
|
|
92
|
+
def _on_course_renamed(self, course: db.Course, new_name: str) -> None:
|
|
93
|
+
self.course_renamed.emit(course, new_name)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
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.assignment_tab import AssignmentTab
|
|
10
|
+
from qcanvas.ui.course_viewer.tabs.mail_tab import MailTab
|
|
11
|
+
from qcanvas.ui.course_viewer.tabs.page_tab import PageTab
|
|
12
|
+
from qcanvas.util.basic_fonts import bold_font
|
|
13
|
+
from qcanvas.util.layouts import layout
|
|
14
|
+
from qcanvas.util.ui_tools import make_truncatable
|
|
15
|
+
|
|
16
|
+
_logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CourseViewer(QWidget):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
course: db.Course,
|
|
23
|
+
downloader: ResourceManager,
|
|
24
|
+
*,
|
|
25
|
+
initial_sync_receipt: Optional[SyncReceipt] = None
|
|
26
|
+
):
|
|
27
|
+
super().__init__()
|
|
28
|
+
# todo this is a mess. there are several other messes like this too, do they all have to be a mess?
|
|
29
|
+
self._course_label = QLabel(course.name)
|
|
30
|
+
self._course_label.setFont(bold_font)
|
|
31
|
+
make_truncatable(self._course_label)
|
|
32
|
+
self._pages_tab = PageTab.create_from_receipt(
|
|
33
|
+
course=course,
|
|
34
|
+
downloader=downloader,
|
|
35
|
+
sync_receipt=initial_sync_receipt,
|
|
36
|
+
)
|
|
37
|
+
self._assignments_tab = AssignmentTab.create_from_receipt(
|
|
38
|
+
course=course,
|
|
39
|
+
downloader=downloader,
|
|
40
|
+
sync_receipt=initial_sync_receipt,
|
|
41
|
+
)
|
|
42
|
+
self._mail_tab = MailTab.create_from_receipt(
|
|
43
|
+
course=course,
|
|
44
|
+
downloader=downloader,
|
|
45
|
+
sync_receipt=initial_sync_receipt,
|
|
46
|
+
)
|
|
47
|
+
self._tabs = QTabWidget()
|
|
48
|
+
|
|
49
|
+
self._tabs.addTab(self._pages_tab, "Pages")
|
|
50
|
+
self._tabs.addTab(self._assignments_tab, "Assignments")
|
|
51
|
+
self._tabs.addTab(self._mail_tab, "Mail")
|
|
52
|
+
# todo: move back to first when implemented
|
|
53
|
+
self._tabs.addTab(QLabel("Not implemented"), "Files")
|
|
54
|
+
# self._tabs.addTab(QLabel("Not implemented"), "Panopto") The meme lives on!
|
|
55
|
+
|
|
56
|
+
self.setLayout(layout(QVBoxLayout, self._course_label, self._tabs))
|
|
57
|
+
|
|
58
|
+
def reload(self, course: db.Course, *, sync_receipt: Optional[SyncReceipt]) -> None:
|
|
59
|
+
# self._tabs.setTabText(1, "*Pages")
|
|
60
|
+
self._pages_tab.reload(course, sync_receipt=sync_receipt)
|
|
61
|
+
self._assignments_tab.reload(course, sync_receipt=sync_receipt)
|
|
62
|
+
self._mail_tab.reload(course, sync_receipt=sync_receipt)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .assignment_tab import AssignmentTab
|
|
@@ -0,0 +1,168 @@
|
|
|
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.assignment_tab.assignment_tree import AssignmentTree
|
|
10
|
+
from qcanvas.ui.course_viewer.tabs.content_tab import ContentTab
|
|
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
|
+
class AssignmentTab(ContentTab):
|
|
19
|
+
@staticmethod
|
|
20
|
+
def create_from_receipt(
|
|
21
|
+
*,
|
|
22
|
+
course: db.Course,
|
|
23
|
+
sync_receipt: Optional[SyncReceipt],
|
|
24
|
+
downloader: ResourceManager,
|
|
25
|
+
) -> "AssignmentTab":
|
|
26
|
+
return AssignmentTab(
|
|
27
|
+
course=course, sync_receipt=sync_receipt, downloader=downloader
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
course: db.Course,
|
|
34
|
+
sync_receipt: Optional[SyncReceipt],
|
|
35
|
+
downloader: ResourceManager,
|
|
36
|
+
):
|
|
37
|
+
super().__init__(
|
|
38
|
+
explorer=AssignmentTree.create_from_receipt(
|
|
39
|
+
course, sync_receipt=sync_receipt
|
|
40
|
+
),
|
|
41
|
+
title_placeholder_text="No assignment selected",
|
|
42
|
+
downloader=downloader,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self._due_date_label = QLabel("")
|
|
46
|
+
self._score_label = QLabel("")
|
|
47
|
+
|
|
48
|
+
self.enable_info_grid()
|
|
49
|
+
|
|
50
|
+
def setup_info_grid(self) -> QGridLayout:
|
|
51
|
+
grid = grid_layout(
|
|
52
|
+
[
|
|
53
|
+
[
|
|
54
|
+
bold_label("Due:"),
|
|
55
|
+
self._due_date_label,
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
bold_label("Score:"),
|
|
59
|
+
self._score_label,
|
|
60
|
+
],
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
grid.setColumnStretch(0, 0)
|
|
65
|
+
grid.setColumnStretch(1, 1)
|
|
66
|
+
|
|
67
|
+
return grid
|
|
68
|
+
|
|
69
|
+
def update_info_grid(self, assignment: db.Assignment) -> None:
|
|
70
|
+
if assignment.due_date is not None:
|
|
71
|
+
due_text = assignment.due_date.strftime(date_strftime_format)
|
|
72
|
+
else:
|
|
73
|
+
due_text = "?"
|
|
74
|
+
|
|
75
|
+
self._due_date_label.setText(due_text)
|
|
76
|
+
|
|
77
|
+
self._score_label.setText(
|
|
78
|
+
f"{assignment.mark or '?'}/{assignment.max_mark_possible or '?'}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# def __init__(
|
|
82
|
+
# self,
|
|
83
|
+
# course: db.Course,
|
|
84
|
+
# resource_manager: ResourceManager,
|
|
85
|
+
# *,
|
|
86
|
+
# initial_sync_receipt: Optional[SyncReceipt] = None,
|
|
87
|
+
# ):
|
|
88
|
+
# super().__init__()
|
|
89
|
+
# self._assignment_tree = AssignmentTree(
|
|
90
|
+
# course, sync_receipt=initial_sync_receipt
|
|
91
|
+
# )
|
|
92
|
+
# self._placeholder_assignment_title = "No assignment selected"
|
|
93
|
+
# self._assignment_label = QLabel(self._placeholder_assignment_title)
|
|
94
|
+
# self._assignment_label.setFont(bold_font)
|
|
95
|
+
# self._due_date_label = QLabel("")
|
|
96
|
+
# self._score_label = QLabel("")
|
|
97
|
+
# self._info_grid = self._setup_info_grid()
|
|
98
|
+
# make_truncatable(self._assignment_label)
|
|
99
|
+
# self._viewer = ResourceRichBrowser(resource_manager)
|
|
100
|
+
# self.setLayout(self._setup_layout())
|
|
101
|
+
# self._assignment_tree.assignment_selected.connect(self._assignment_selected)
|
|
102
|
+
# self._assignment_tree.reexpand()
|
|
103
|
+
#
|
|
104
|
+
# def _setup_info_grid(self) -> QWidget:
|
|
105
|
+
# layout = grid_layout(
|
|
106
|
+
# [
|
|
107
|
+
# [
|
|
108
|
+
# _bold_label("Due:"),
|
|
109
|
+
# self._due_date_label,
|
|
110
|
+
# ],
|
|
111
|
+
# [
|
|
112
|
+
# _bold_label("Score:"),
|
|
113
|
+
# self._score_label,
|
|
114
|
+
# ],
|
|
115
|
+
# ]
|
|
116
|
+
# )
|
|
117
|
+
# layout.setColumnStretch(0, 0)
|
|
118
|
+
# layout.setColumnStretch(1, 1)
|
|
119
|
+
#
|
|
120
|
+
# widget = QWidget()
|
|
121
|
+
# widget.setLayout(layout)
|
|
122
|
+
#
|
|
123
|
+
# return widget
|
|
124
|
+
#
|
|
125
|
+
# def _setup_layout(self) -> QHBoxLayout:
|
|
126
|
+
# h_box = QHBoxLayout()
|
|
127
|
+
# h_box.addWidget(self._assignment_tree, 1)
|
|
128
|
+
# h_box.addLayout(
|
|
129
|
+
# layout(QVBoxLayout, self._assignment_label, self._info_grid, self._viewer),
|
|
130
|
+
# 2,
|
|
131
|
+
# )
|
|
132
|
+
#
|
|
133
|
+
# return h_box
|
|
134
|
+
#
|
|
135
|
+
# def reload(self, course: db.Course, *, sync_receipt: Optional[SyncReceipt]) -> None:
|
|
136
|
+
# self._assignment_tree.reload(course, sync_receipt=sync_receipt)
|
|
137
|
+
#
|
|
138
|
+
# @Slot()
|
|
139
|
+
# def _assignment_selected(self, assignment: db.Assignment) -> None:
|
|
140
|
+
# if assignment is not None:
|
|
141
|
+
# _logger.debug(
|
|
142
|
+
# "Show assignment %s (id='%s')", assignment.name, assignment.id
|
|
143
|
+
# )
|
|
144
|
+
# self._viewer.show_content(assignment)
|
|
145
|
+
# self._update_info(assignment)
|
|
146
|
+
# else:
|
|
147
|
+
# self._assignment_label.setText(self._placeholder_assignment_title)
|
|
148
|
+
# self._show_blank()
|
|
149
|
+
#
|
|
150
|
+
# def _update_info(self, assignment: db.Assignment) -> None:
|
|
151
|
+
# _logger.debug(assignment.course.name)
|
|
152
|
+
# _logger.debug("Due %s", assignment.due_date)
|
|
153
|
+
# _logger.debug("Score %s/%s", assignment.mark, assignment.max_mark_possible)
|
|
154
|
+
# self._assignment_label.setText(assignment.name)
|
|
155
|
+
#
|
|
156
|
+
# if assignment.due_date is not None:
|
|
157
|
+
# self._due_date_label.setText(
|
|
158
|
+
# assignment.due_date.strftime("%A %Y-%m-%d %H:%M:%S")
|
|
159
|
+
# )
|
|
160
|
+
# else:
|
|
161
|
+
# self._due_date_label.setText("?")
|
|
162
|
+
# self._score_label.setText(
|
|
163
|
+
# f"{assignment.mark or '?'}/{assignment.max_mark_possible or '?'}"
|
|
164
|
+
# )
|
|
165
|
+
#
|
|
166
|
+
# def _show_blank(self) -> None:
|
|
167
|
+
# self._viewer.show_blank()
|
|
168
|
+
# self._info_grid.hide()
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
from qtpy.QtWidgets import QHeaderView
|
|
8
|
+
|
|
9
|
+
from qcanvas.ui.course_viewer.content_tree import ContentTree
|
|
10
|
+
from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AssignmentTree(ContentTree[db.Course]):
|
|
16
|
+
@staticmethod
|
|
17
|
+
def create_from_receipt(
|
|
18
|
+
course: db.Course, *, sync_receipt: Optional[SyncReceipt]
|
|
19
|
+
) -> "AssignmentTree":
|
|
20
|
+
tree = AssignmentTree(course.id)
|
|
21
|
+
tree.reload(course, sync_receipt=sync_receipt)
|
|
22
|
+
return tree
|
|
23
|
+
|
|
24
|
+
def __init__(self, course_id: str):
|
|
25
|
+
super().__init__(
|
|
26
|
+
tree_name=f"course.{course_id}.assignment_groups",
|
|
27
|
+
emit_selection_signal_for_type=db.Assignment,
|
|
28
|
+
)
|
|
29
|
+
self.ui_setup(
|
|
30
|
+
header_text=["Assignments", "Weight"],
|
|
31
|
+
indentation=15,
|
|
32
|
+
max_width=350,
|
|
33
|
+
min_width=150,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self._adjust_header()
|
|
37
|
+
|
|
38
|
+
def _adjust_header(self) -> None:
|
|
39
|
+
header = self.header()
|
|
40
|
+
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
41
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
42
|
+
header.setStretchLastSection(False)
|
|
43
|
+
|
|
44
|
+
def create_tree_items(
|
|
45
|
+
self, course: db.Course, sync_receipt: Optional[SyncReceipt]
|
|
46
|
+
) -> Sequence[MemoryTreeWidgetItem]:
|
|
47
|
+
widgets = []
|
|
48
|
+
|
|
49
|
+
for assignment_group in course.assignment_groups: # type: db.AssignmentGroup
|
|
50
|
+
assignment_group_widget = self._create_assignment_group_widget(
|
|
51
|
+
assignment_group, sync_receipt
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
widgets.append(assignment_group_widget)
|
|
55
|
+
for assignment in assignment_group.assignments: # type: db.Assignment
|
|
56
|
+
assignment_widget = self._create_assignment_widget(
|
|
57
|
+
assignment, sync_receipt
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assignment_group_widget.addChild(assignment_widget)
|
|
61
|
+
|
|
62
|
+
return widgets
|
|
63
|
+
|
|
64
|
+
def _create_assignment_group_widget(
|
|
65
|
+
self, assignment_group: db.AssignmentGroup, sync_receipt: SyncReceipt
|
|
66
|
+
) -> MemoryTreeWidgetItem:
|
|
67
|
+
assignment_group_widget = MemoryTreeWidgetItem(
|
|
68
|
+
id=assignment_group.id,
|
|
69
|
+
data=assignment_group,
|
|
70
|
+
strings=[assignment_group.name, f"{assignment_group.group_weight}%"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assignment_group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
74
|
+
|
|
75
|
+
is_new = (
|
|
76
|
+
sync_receipt is not None
|
|
77
|
+
and assignment_group.id in sync_receipt.updated_assignment_groups
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if is_new:
|
|
81
|
+
self.mark_as_unseen(assignment_group_widget)
|
|
82
|
+
|
|
83
|
+
return assignment_group_widget
|
|
84
|
+
|
|
85
|
+
def _create_assignment_widget(
|
|
86
|
+
self, assignment: db.Assignment, sync_receipt: SyncReceipt
|
|
87
|
+
) -> MemoryTreeWidgetItem:
|
|
88
|
+
assignment_widget = MemoryTreeWidgetItem(
|
|
89
|
+
id=assignment.id, data=assignment, strings=[assignment.name]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assignment_widget.setFlags(
|
|
93
|
+
Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
is_new = (
|
|
97
|
+
sync_receipt is not None
|
|
98
|
+
and assignment.id in sync_receipt.updated_assignments
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if is_new:
|
|
102
|
+
self.mark_as_unseen(assignment_widget)
|
|
103
|
+
|
|
104
|
+
return assignment_widget
|