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.
- 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.post1.dist-info/METADATA +59 -0
- qcanvas-1.0.3.post1.dist-info/RECORD +64 -0
- {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post1.dist-info}/WHEEL +1 -1
- qcanvas-1.0.3.post1.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/ui/setup_dialog.py
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import traceback
|
|
2
|
-
from threading import Semaphore
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
from PySide6.QtCore import Slot, QUrl
|
|
6
|
-
from PySide6.QtGui import QDesktopServices
|
|
7
|
-
from PySide6.QtWidgets import QDialog, QWidget, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, \
|
|
8
|
-
QDialogButtonBox, QGridLayout, QMessageBox
|
|
9
|
-
from PySide6.QtWidgets import QProgressBar
|
|
10
|
-
from qasync import asyncSlot
|
|
11
|
-
|
|
12
|
-
from qcanvas.net.canvas import CanvasClient
|
|
13
|
-
from qcanvas.util.app_settings import settings
|
|
14
|
-
|
|
15
|
-
tutorial_url = "https://www.iorad.com/player/2053777/Canvas---How-to-generate-an-access-token-"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def row(name: str) -> QWidget:
|
|
19
|
-
widget = QWidget()
|
|
20
|
-
layout = QHBoxLayout()
|
|
21
|
-
|
|
22
|
-
layout.addWidget(QLabel(name))
|
|
23
|
-
layout.addWidget(QLineEdit())
|
|
24
|
-
|
|
25
|
-
widget.setLayout(layout)
|
|
26
|
-
|
|
27
|
-
return widget
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class SetupDialog(QDialog):
|
|
31
|
-
"""
|
|
32
|
-
The dialog shown to the user when the canvas api key/url is invalid, such as the first time the user is opening the application.
|
|
33
|
-
The dialog asks for an api key and canvas url then verifies them before saving them to the primary app settings file.
|
|
34
|
-
"""
|
|
35
|
-
def __init__(self, parent: Optional[QWidget] = None, allow_cancel: bool = True):
|
|
36
|
-
super().__init__(parent)
|
|
37
|
-
|
|
38
|
-
self.setWindowTitle("Setup")
|
|
39
|
-
self._row_counter = 0
|
|
40
|
-
self._operation_sem = Semaphore()
|
|
41
|
-
self.allow_cancel = allow_cancel
|
|
42
|
-
|
|
43
|
-
# Progress bar used to indicate that an operation is underway and the application has not frozen
|
|
44
|
-
self.operation_activity_indicator = QProgressBar()
|
|
45
|
-
self.operation_activity_indicator.setMaximum(0)
|
|
46
|
-
self.operation_activity_indicator.setMinimum(0)
|
|
47
|
-
self.operation_activity_indicator.setValue(0)
|
|
48
|
-
self.operation_activity_indicator.hide()
|
|
49
|
-
|
|
50
|
-
self.grid = QGridLayout()
|
|
51
|
-
|
|
52
|
-
# Line edits for the different properties
|
|
53
|
-
self.canvas_url = QLineEdit(settings.canvas_url or "")
|
|
54
|
-
self.panopto_url = QLineEdit()
|
|
55
|
-
self.canvas_api_key = QLineEdit(settings.api_key or "")
|
|
56
|
-
|
|
57
|
-
# Add the line edits to the dialog
|
|
58
|
-
self._row("Canvas URL", self.canvas_url)
|
|
59
|
-
self._row("Painopto URL", self.panopto_url)
|
|
60
|
-
self._row("Canvas API key", self.canvas_api_key)
|
|
61
|
-
|
|
62
|
-
# Add the activity indicator to the dialog
|
|
63
|
-
self.grid.addWidget(self.operation_activity_indicator, self._row_counter, 0, 1, 2)
|
|
64
|
-
|
|
65
|
-
# Setup the rest of the layout
|
|
66
|
-
grid_widget = QWidget()
|
|
67
|
-
grid_widget.setLayout(self.grid)
|
|
68
|
-
|
|
69
|
-
stack = QVBoxLayout()
|
|
70
|
-
stack.addWidget(grid_widget)
|
|
71
|
-
stack.addWidget(self._setup_button_box())
|
|
72
|
-
|
|
73
|
-
self.setLayout(stack)
|
|
74
|
-
self.resize(500, 200)
|
|
75
|
-
|
|
76
|
-
def _setup_button_box(self) -> QDialogButtonBox:
|
|
77
|
-
button_box = QDialogButtonBox(
|
|
78
|
-
QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel if self.allow_cancel else QDialogButtonBox.StandardButton.Save)
|
|
79
|
-
# Add a help button to show the user how to get a canvas api key
|
|
80
|
-
button_box.addButton("How to get a canvas API key?", QDialogButtonBox.ButtonRole.HelpRole)
|
|
81
|
-
# Connect signals
|
|
82
|
-
button_box.helpRequested.connect(self._show_help)
|
|
83
|
-
button_box.accepted.connect(self._verify)
|
|
84
|
-
button_box.rejected.connect(lambda: self.reject())
|
|
85
|
-
|
|
86
|
-
return button_box
|
|
87
|
-
|
|
88
|
-
@asyncSlot()
|
|
89
|
-
async def _verify(self) -> None:
|
|
90
|
-
"""
|
|
91
|
-
Verifies the user's inputs before saving them
|
|
92
|
-
"""
|
|
93
|
-
if self._operation_sem.acquire(False):
|
|
94
|
-
try:
|
|
95
|
-
canvas_url_text = self.ensure_protocol(self.canvas_url.text().strip())
|
|
96
|
-
panopto_url_text = self.ensure_protocol(self.panopto_url.text().strip())
|
|
97
|
-
canvas_api_key_text = self.canvas_api_key.text().strip()
|
|
98
|
-
|
|
99
|
-
if not (len(canvas_url_text) > 0 and QUrl(canvas_url_text).isValid()):
|
|
100
|
-
self._show_invalid_msgbox("Canvas URL")
|
|
101
|
-
return
|
|
102
|
-
if not (len(panopto_url_text) > 0 and QUrl(panopto_url_text).isValid()):
|
|
103
|
-
self._show_invalid_msgbox("Panopto URL")
|
|
104
|
-
return
|
|
105
|
-
elif not len(canvas_api_key_text) > 0:
|
|
106
|
-
self._show_invalid_msgbox("API key")
|
|
107
|
-
elif not (await self._verify_canvas_config(canvas_url_text, canvas_api_key_text)):
|
|
108
|
-
# Show message box saying that either the url or api key is incorrect
|
|
109
|
-
QMessageBox(
|
|
110
|
-
QMessageBox.Icon.Critical,
|
|
111
|
-
"Error",
|
|
112
|
-
f"The canvas URL or API key is invalid.\nPlease check you entered them correctly.",
|
|
113
|
-
parent=self
|
|
114
|
-
).show()
|
|
115
|
-
else:
|
|
116
|
-
# If nothing was wrong, everything should be fine
|
|
117
|
-
# Save the url and api key
|
|
118
|
-
settings.canvas_url = canvas_url_text
|
|
119
|
-
settings.panopto_url = panopto_url_text
|
|
120
|
-
settings.api_key = canvas_api_key_text
|
|
121
|
-
|
|
122
|
-
self.accept()
|
|
123
|
-
finally:
|
|
124
|
-
self._operation_sem.release()
|
|
125
|
-
else:
|
|
126
|
-
QMessageBox(QMessageBox.Icon.Critical, "Error", "An operation is already in progress", parent=self).show()
|
|
127
|
-
|
|
128
|
-
def _show_invalid_msgbox(self, field_name: str) -> None:
|
|
129
|
-
"""
|
|
130
|
-
Shows a message box saying that that specified field is invalid
|
|
131
|
-
"""
|
|
132
|
-
QMessageBox(
|
|
133
|
-
QMessageBox.Icon.Critical,
|
|
134
|
-
"Error",
|
|
135
|
-
f"{field_name} is invalid",
|
|
136
|
-
parent=self
|
|
137
|
-
).show()
|
|
138
|
-
|
|
139
|
-
@staticmethod
|
|
140
|
-
def ensure_protocol(url: str) -> str:
|
|
141
|
-
# Check if the url is blank/empty so we can tell if the user didn't input anything
|
|
142
|
-
if len(url) > 0 and not (url.startswith("http://") or url.startswith("https://")):
|
|
143
|
-
return "https://" + url
|
|
144
|
-
else:
|
|
145
|
-
return url
|
|
146
|
-
|
|
147
|
-
async def _verify_canvas_config(self, canvas_url: str, api_key: str) -> bool:
|
|
148
|
-
"""
|
|
149
|
-
Makes a network request to canvas to ensure the provided url and api key are correct
|
|
150
|
-
|
|
151
|
-
Returns
|
|
152
|
-
-------
|
|
153
|
-
bool
|
|
154
|
-
True if valid, False if invalid
|
|
155
|
-
"""
|
|
156
|
-
self.operation_activity_indicator.show()
|
|
157
|
-
|
|
158
|
-
try:
|
|
159
|
-
return await CanvasClient.verify_config(canvas_url, api_key)
|
|
160
|
-
except:
|
|
161
|
-
traceback.print_exc()
|
|
162
|
-
return False
|
|
163
|
-
finally:
|
|
164
|
-
self.operation_activity_indicator.hide()
|
|
165
|
-
|
|
166
|
-
@Slot()
|
|
167
|
-
def _show_help(self) -> None:
|
|
168
|
-
msg = QMessageBox(
|
|
169
|
-
QMessageBox.Icon.Information,
|
|
170
|
-
"Help",
|
|
171
|
-
"""An interactive tutorial will open in your browser when you click OK.
|
|
172
|
-
|
|
173
|
-
Note that the "purpose" text doesn't matter and you can enter anything you want.
|
|
174
|
-
|
|
175
|
-
You should also leave the "expires" item blank if you want the key to last forever.
|
|
176
|
-
|
|
177
|
-
Don't share this key. You can revoke it at any time.""",
|
|
178
|
-
parent=self
|
|
179
|
-
)
|
|
180
|
-
msg.accepted.connect(lambda: QDesktopServices.openUrl(tutorial_url))
|
|
181
|
-
msg.show()
|
|
182
|
-
|
|
183
|
-
def _row(self, name: str, widget: QWidget) -> None:
|
|
184
|
-
"""
|
|
185
|
-
Shortcut to add a field with a label to the dialog
|
|
186
|
-
"""
|
|
187
|
-
self.grid.addWidget(QLabel(name), self._row_counter, 0)
|
|
188
|
-
self.grid.addWidget(widget, self._row_counter, 1)
|
|
189
|
-
|
|
190
|
-
self._row_counter += 1
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from PySide6.QtWidgets import QStatusBar, QProgressBar
|
|
4
|
-
|
|
5
|
-
from qcanvas.util.progress_reporter import ProgressReporter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class StatusBarReporter(ProgressReporter):
|
|
9
|
-
def __init__(self, status_bar: QStatusBar):
|
|
10
|
-
self.status_bar = status_bar
|
|
11
|
-
self.section_name: None | str = None
|
|
12
|
-
self.progress_bar: QProgressBar | None = None
|
|
13
|
-
|
|
14
|
-
def section_started(self, section_name: str, total_progress: int) -> None:
|
|
15
|
-
self.section_name = section_name
|
|
16
|
-
self.status_bar.showMessage(section_name)
|
|
17
|
-
|
|
18
|
-
if self.progress_bar is None:
|
|
19
|
-
self.progress_bar = QProgressBar(self.status_bar)
|
|
20
|
-
self.progress_bar.setMaximumHeight(self.status_bar.height())
|
|
21
|
-
self.progress_bar.setMinimum(0)
|
|
22
|
-
self.status_bar.addPermanentWidget(self.progress_bar)
|
|
23
|
-
|
|
24
|
-
self.progress_bar.setValue(0)
|
|
25
|
-
self.progress_bar.setMaximum(total_progress)
|
|
26
|
-
|
|
27
|
-
def progress(self, current_progress: int, total: int) -> None:
|
|
28
|
-
if self.progress_bar is not None:
|
|
29
|
-
self.progress_bar.setValue(current_progress)
|
|
30
|
-
|
|
31
|
-
def finished(self) -> None:
|
|
32
|
-
self.status_bar.removeWidget(self.progress_bar)
|
|
33
|
-
self.progress_bar = None
|
|
34
|
-
self.status_bar.showMessage("Finished", 5000)
|
|
35
|
-
|
|
36
|
-
def errored(self, context: Any) -> None:
|
|
37
|
-
if self.status_bar.parent() is not None:
|
|
38
|
-
self.status_bar.removeWidget(self.progress_bar)
|
|
39
|
-
self.progress_bar = None
|
|
40
|
-
self.status_bar.showMessage("Synchronisation error!!", 5000)
|
qcanvas/ui/viewer/__init__.py
DELETED
|
File without changes
|
qcanvas/ui/viewer/course_list.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from typing import Sequence
|
|
3
|
-
|
|
4
|
-
from PySide6.QtCore import QItemSelection, Slot, Signal, QObject
|
|
5
|
-
from PySide6.QtGui import QStandardItemModel, QStandardItem
|
|
6
|
-
from PySide6.QtWidgets import QTreeView
|
|
7
|
-
from qasync import asyncSlot
|
|
8
|
-
|
|
9
|
-
import qcanvas.db as db
|
|
10
|
-
from qcanvas.util.course_indexer import DataManager
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class CourseNode(QStandardItem, QObject):
|
|
14
|
-
name_changed = Signal(db.Course, str)
|
|
15
|
-
|
|
16
|
-
def __init__(self, course: db.Course):
|
|
17
|
-
QObject.__init__(self)
|
|
18
|
-
QStandardItem.__init__(self, course.preferences.local_name or course.name)
|
|
19
|
-
self.course = course
|
|
20
|
-
|
|
21
|
-
def setData(self, value, role=...):
|
|
22
|
-
if isinstance(value, str):
|
|
23
|
-
value = value.strip()
|
|
24
|
-
|
|
25
|
-
if len(value) == 0:
|
|
26
|
-
super().setData(self.course.name, role)
|
|
27
|
-
self.name_changed.emit(self.course, None)
|
|
28
|
-
else:
|
|
29
|
-
super().setData(value, role)
|
|
30
|
-
self.name_changed.emit(self.course, value)
|
|
31
|
-
else:
|
|
32
|
-
super().setData(value, role)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class CourseList(QTreeView):
|
|
36
|
-
course_selected = Signal(db.Course)
|
|
37
|
-
|
|
38
|
-
def __init__(self, data_manager: DataManager):
|
|
39
|
-
super().__init__()
|
|
40
|
-
self.data_manager = data_manager
|
|
41
|
-
self.model = QStandardItemModel()
|
|
42
|
-
self.setModel(self.model)
|
|
43
|
-
self.selectionModel().selectionChanged.connect(self._on_selection_changed)
|
|
44
|
-
|
|
45
|
-
@asyncSlot(db.Course, str)
|
|
46
|
-
async def course_name_changed(self, course: db.Course, name: str):
|
|
47
|
-
course.preferences.local_name = name
|
|
48
|
-
await self.data_manager.update_item(course.preferences)
|
|
49
|
-
|
|
50
|
-
def load_course_list(self, courses: Sequence[db.Course]):
|
|
51
|
-
self.model.clear()
|
|
52
|
-
self.model.setHorizontalHeaderLabels(["Canvas Courses"])
|
|
53
|
-
|
|
54
|
-
courses_root = self.model.invisibleRootItem()
|
|
55
|
-
|
|
56
|
-
for term, courses in self.group_courses_by_term(courses):
|
|
57
|
-
term_node = QStandardItem(term.name)
|
|
58
|
-
term_node.setEditable(False)
|
|
59
|
-
term_node.setSelectable(False)
|
|
60
|
-
|
|
61
|
-
for course in courses:
|
|
62
|
-
course_node = CourseNode(course)
|
|
63
|
-
course_node.name_changed.connect(self.course_name_changed)
|
|
64
|
-
term_node.appendRow(course_node)
|
|
65
|
-
|
|
66
|
-
courses_root.appendRow(term_node)
|
|
67
|
-
|
|
68
|
-
self.expandAll()
|
|
69
|
-
|
|
70
|
-
@staticmethod
|
|
71
|
-
def group_courses_by_term(courses: Sequence[db.Course]):
|
|
72
|
-
courses_grouped_by_term: dict[db.Term, list[db.Course]] = {}
|
|
73
|
-
|
|
74
|
-
# Put courses into groups in the above dict
|
|
75
|
-
for course in courses:
|
|
76
|
-
if course.term in courses_grouped_by_term:
|
|
77
|
-
courses_grouped_by_term[course.term].append(course)
|
|
78
|
-
else:
|
|
79
|
-
courses_grouped_by_term[course.term] = [course]
|
|
80
|
-
|
|
81
|
-
# Convert the dict item list into a mutable list
|
|
82
|
-
pairs = list(courses_grouped_by_term.items())
|
|
83
|
-
# Sort them by start date, with most recent terms at the start
|
|
84
|
-
pairs.sort(key=lambda x: x[0].start_at or datetime.min, reverse=True)
|
|
85
|
-
|
|
86
|
-
return pairs
|
|
87
|
-
|
|
88
|
-
@Slot(QItemSelection, QItemSelection)
|
|
89
|
-
def _on_selection_changed(self, selected: QItemSelection, deselected: QItemSelection):
|
|
90
|
-
if len(self.selectedIndexes()) == 0:
|
|
91
|
-
self.course_selected.emit(None)
|
|
92
|
-
else:
|
|
93
|
-
item = self.model.itemFromIndex(self.selectedIndexes()[0])
|
|
94
|
-
|
|
95
|
-
if isinstance(item, CourseNode):
|
|
96
|
-
self.course_selected.emit(item.course)
|
qcanvas/ui/viewer/file_list.py
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
from typing import Sequence
|
|
2
|
-
|
|
3
|
-
from PySide6.QtCore import QObject, Slot, Qt
|
|
4
|
-
from PySide6.QtWidgets import QStyledItemDelegate, QStyleOptionProgressBar, QApplication, QStyle, \
|
|
5
|
-
QHeaderView, QStyleOptionViewItem, QTreeWidget, QTreeWidgetItem
|
|
6
|
-
|
|
7
|
-
import qcanvas.db.database as db
|
|
8
|
-
from qcanvas.util.download_pool import DownloadPool
|
|
9
|
-
from qcanvas.util.helpers import file_icon_helper
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# https://code.whatever.social/questions/1094841/get-human-readable-version-of-file-size#1094933
|
|
13
|
-
def sizeof_fmt(num, suffix="B"):
|
|
14
|
-
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
|
|
15
|
-
if abs(num) < 1024.0:
|
|
16
|
-
return f"{num:3.1f}{unit}{suffix}"
|
|
17
|
-
num /= 1024.0
|
|
18
|
-
return f"{num:.1f}Yi{suffix}"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class FileColumnDelegate(QStyledItemDelegate):
|
|
22
|
-
def __init__(self, tree: QTreeWidget, download_pool: DownloadPool, parent, *args, **kwargs):
|
|
23
|
-
super().__init__(parent, *args, **kwargs)
|
|
24
|
-
self.download_pool = download_pool
|
|
25
|
-
self.tree = tree
|
|
26
|
-
|
|
27
|
-
def paint(self, painter, option, index) -> None:
|
|
28
|
-
item = self.tree.itemFromIndex(index)
|
|
29
|
-
|
|
30
|
-
# Check that the item is actually a file row and not already downloaded
|
|
31
|
-
if not isinstance(item, FileRow) or item.resource.state == db.ResourceState.DOWNLOADED:
|
|
32
|
-
return super().paint(painter, option, index)
|
|
33
|
-
|
|
34
|
-
resource = item.resource
|
|
35
|
-
progress = item.download_progres
|
|
36
|
-
|
|
37
|
-
if progress == resource.file_size:
|
|
38
|
-
return super().paint(painter, option, index)
|
|
39
|
-
|
|
40
|
-
# Check that the file is not currently downloading (signaled by progress = -1)
|
|
41
|
-
if progress < 0:
|
|
42
|
-
# Show the state of the file instead (downloaded, failed, not downloaded)
|
|
43
|
-
view_item = QStyleOptionViewItem(option)
|
|
44
|
-
self.initStyleOption(view_item, index)
|
|
45
|
-
view_item.text = db.ResourceState.human_readable(resource.state)
|
|
46
|
-
|
|
47
|
-
return QApplication.style().drawControl(QStyle.CE_ItemViewItem, view_item, painter, view_item.widget)
|
|
48
|
-
else:
|
|
49
|
-
# otherwise show progress bar
|
|
50
|
-
progress_bar = QStyleOptionProgressBar()
|
|
51
|
-
progress_bar.rect = option.rect
|
|
52
|
-
progress_bar.minimum = 0
|
|
53
|
-
progress_bar.maximum = resource.file_size
|
|
54
|
-
progress_bar.progress = progress
|
|
55
|
-
# Display download progress as a percentage
|
|
56
|
-
progress_bar.text = "{0:.0f}%".format((progress / resource.file_size) * 100)
|
|
57
|
-
progress_bar.textVisible = True
|
|
58
|
-
|
|
59
|
-
QApplication.style().drawControl(QStyle.CE_ProgressBar, progress_bar, painter)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class FileRow(QTreeWidgetItem):
|
|
63
|
-
def __init__(self, resource: db.Resource):
|
|
64
|
-
# Set column names
|
|
65
|
-
super().__init__(
|
|
66
|
-
[
|
|
67
|
-
resource.file_name,
|
|
68
|
-
resource.date_discovered.strftime("%Y-%m-%d"),
|
|
69
|
-
sizeof_fmt(resource.file_size),
|
|
70
|
-
]
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Set the icon for this file, displayed in the name column
|
|
74
|
-
self.setIcon(0, file_icon_helper.icon_for_filename(resource.file_name))
|
|
75
|
-
|
|
76
|
-
self.resource = resource
|
|
77
|
-
# This is used to pass the download progress value from download_progress_updated to FileColumnDelegate. This way
|
|
78
|
-
# it means we don't have to get the download progress from the DownloadPool a second time with an async function
|
|
79
|
-
# (which is undesirable from an item delegate...). Use -1 to show that the file is not currently downloading.
|
|
80
|
-
self.download_progres = -1
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class FileList(QTreeWidget):
|
|
84
|
-
def __init__(self, download_pool: DownloadPool, parent: QObject | None = None):
|
|
85
|
-
super().__init__(parent)
|
|
86
|
-
self._setup_header()
|
|
87
|
-
|
|
88
|
-
# Keep a map of file ids to rows, so we can actually update the download column using a file id.
|
|
89
|
-
# This has to be a list or else files that are in multiple modules/pages will not be updated properly on the tree
|
|
90
|
-
self.row_id_map: dict[str, list[FileRow]] = {}
|
|
91
|
-
self.download_pool = download_pool
|
|
92
|
-
self.setAlternatingRowColors(True)
|
|
93
|
-
# Add the download column renderer
|
|
94
|
-
self.setItemDelegateForColumn(3, FileColumnDelegate(tree=self, download_pool=download_pool, parent=None))
|
|
95
|
-
|
|
96
|
-
# Connect download update events
|
|
97
|
-
self.download_pool.download_progress_updated.connect(self._file_download_progress_update)
|
|
98
|
-
self.download_pool.download_failed.connect(self._file_download_failed_update)
|
|
99
|
-
|
|
100
|
-
@Slot(str, int)
|
|
101
|
-
def _file_download_progress_update(self, resource_id: str, progress: int):
|
|
102
|
-
# Ignore files that aren't in this tree
|
|
103
|
-
if resource_id not in self.row_id_map:
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
rows = self.row_id_map[resource_id]
|
|
107
|
-
|
|
108
|
-
# Update each row that maps to this file.
|
|
109
|
-
# Note that a file may belong to multiple modules or pages
|
|
110
|
-
for row in rows:
|
|
111
|
-
# Update the progress
|
|
112
|
-
row.download_progres = progress
|
|
113
|
-
|
|
114
|
-
# Get the download column for the relevant item
|
|
115
|
-
index = self.indexFromItem(row, 3)
|
|
116
|
-
|
|
117
|
-
# Update the download column
|
|
118
|
-
self.model().dataChanged.emit(index, index, Qt.ItemDataRole.DisplayRole)
|
|
119
|
-
|
|
120
|
-
@Slot(str)
|
|
121
|
-
def _file_download_failed_update(self, resource_id: str):
|
|
122
|
-
if resource_id not in self.row_id_map:
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
rows = self.row_id_map[resource_id]
|
|
126
|
-
|
|
127
|
-
for row in rows:
|
|
128
|
-
# Get the download column for the relevant item
|
|
129
|
-
index = self.indexFromItem(row, 3)
|
|
130
|
-
|
|
131
|
-
# Update the download column
|
|
132
|
-
self.model().dataChanged.emit(index, index, Qt.ItemDataRole.DisplayRole)
|
|
133
|
-
|
|
134
|
-
def _setup_header(self):
|
|
135
|
-
self.setColumnCount(4)
|
|
136
|
-
self.setHeaderLabels(["Name", "Date Found", "Size", "Download"])
|
|
137
|
-
|
|
138
|
-
header = self.header()
|
|
139
|
-
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
140
|
-
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
141
|
-
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
|
142
|
-
# Make the download progress column not try to take up all remaining space in the tree, as it just looks weird
|
|
143
|
-
# and decreases room for more important data
|
|
144
|
-
header.setStretchLastSection(0)
|
|
145
|
-
|
|
146
|
-
def load_items(self, items: Sequence[db.ModuleItem | db.Module]):
|
|
147
|
-
self.clear()
|
|
148
|
-
|
|
149
|
-
groups = []
|
|
150
|
-
|
|
151
|
-
for item in items:
|
|
152
|
-
# Get the resources depending on the type of the item
|
|
153
|
-
if isinstance(item, (db.PageLike, db.ModuleItem)):
|
|
154
|
-
resources = item.resources
|
|
155
|
-
elif isinstance(item, db.Module):
|
|
156
|
-
resources = []
|
|
157
|
-
|
|
158
|
-
for module_item in item.items:
|
|
159
|
-
resources.extend(module_item.resources)
|
|
160
|
-
else:
|
|
161
|
-
continue
|
|
162
|
-
|
|
163
|
-
# Avoid adding groups with no files in them, it just adds clutter
|
|
164
|
-
if len(resources) == 0:
|
|
165
|
-
continue
|
|
166
|
-
|
|
167
|
-
# Create the group node for this item
|
|
168
|
-
group_node = QTreeWidgetItem([item.name])
|
|
169
|
-
|
|
170
|
-
# fixme this does not remove duplicate files e.g. when on module groups
|
|
171
|
-
|
|
172
|
-
for resource in resources:
|
|
173
|
-
row = FileRow(resource)
|
|
174
|
-
|
|
175
|
-
# Put the file into the row id map so we can use the file id provided by download_progress_updated
|
|
176
|
-
# and such to update the download progress in the tree
|
|
177
|
-
if resource.id not in self.row_id_map:
|
|
178
|
-
self.row_id_map[resource.id] = [row]
|
|
179
|
-
else:
|
|
180
|
-
self.row_id_map[resource.id].append(row)
|
|
181
|
-
|
|
182
|
-
# Add the row to the group
|
|
183
|
-
group_node.addChild(row)
|
|
184
|
-
|
|
185
|
-
# Add the module/page to the list
|
|
186
|
-
groups.append(group_node)
|
|
187
|
-
|
|
188
|
-
# Add all the items and expand them
|
|
189
|
-
self.insertTopLevelItems(0, groups)
|
|
190
|
-
self.expandAll()
|
|
191
|
-
|
|
192
|
-
def clear(self):
|
|
193
|
-
super().clear()
|
|
194
|
-
self._setup_header()
|
|
195
|
-
self.row_id_map.clear()
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from typing import Sequence
|
|
2
|
-
|
|
3
|
-
from PySide6.QtWidgets import * # QWidget, QTreeView, QGroupBox, QBoxLayout, QHeaderView, QHBoxLayout, QComboBox, QLabel
|
|
4
|
-
|
|
5
|
-
import qcanvas.db as db
|
|
6
|
-
from qcanvas.ui.viewer.file_list import FileList
|
|
7
|
-
from qcanvas.util.constants import default_assignments_module_names
|
|
8
|
-
from qcanvas.util.download_pool import DownloadPool
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class FileColumn(QGroupBox):
|
|
12
|
-
def __init__(self, column_name, download_pool: DownloadPool):
|
|
13
|
-
super().__init__(title=column_name)
|
|
14
|
-
|
|
15
|
-
self.tree = FileList(download_pool)
|
|
16
|
-
self.setLayout(QBoxLayout(QBoxLayout.Direction.TopToBottom))
|
|
17
|
-
self.layout().addWidget(self.tree)
|
|
18
|
-
|
|
19
|
-
def load_items(self, items: Sequence[db.ModuleItem | db.Module]):
|
|
20
|
-
self.tree.load_items(items)
|
|
21
|
-
|
|
22
|
-
def clear(self):
|
|
23
|
-
self.tree.clear()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class FileViewTab(QWidget):
|
|
27
|
-
|
|
28
|
-
def __init__(self, download_pool: DownloadPool):
|
|
29
|
-
super().__init__()
|
|
30
|
-
|
|
31
|
-
self.files_column = FileColumn("Files", download_pool)
|
|
32
|
-
self.assignment_files_column = FileColumn("Assignment files", download_pool)
|
|
33
|
-
|
|
34
|
-
layout = QHBoxLayout()
|
|
35
|
-
layout.addWidget(self.files_column)
|
|
36
|
-
layout.addWidget(self.assignment_files_column)
|
|
37
|
-
|
|
38
|
-
self.setLayout(layout)
|
|
39
|
-
|
|
40
|
-
def load_course_files(self, course: db.Course):
|
|
41
|
-
module_items: list[db.ModuleItem] = []
|
|
42
|
-
assignment_items: list[db.ModuleItem] = []
|
|
43
|
-
|
|
44
|
-
for module_item in course.module_items:
|
|
45
|
-
if module_item.module.name.lower() in default_assignments_module_names:
|
|
46
|
-
assignment_items.append(module_item)
|
|
47
|
-
else:
|
|
48
|
-
module_items.append(module_item)
|
|
49
|
-
|
|
50
|
-
if course.preferences.files_group_by_preference == db.GroupByPreference.GROUP_BY_MODULES:
|
|
51
|
-
exclude_assignments_module = list(
|
|
52
|
-
filter(lambda x: x.name.lower() not in default_assignments_module_names, course.modules))
|
|
53
|
-
|
|
54
|
-
self.files_column.load_items(exclude_assignments_module)
|
|
55
|
-
else:
|
|
56
|
-
self.files_column.load_items(module_items)
|
|
57
|
-
|
|
58
|
-
self.assignment_files_column.load_items(assignment_items + course.assignments)
|
|
59
|
-
|
|
60
|
-
def clear(self):
|
|
61
|
-
self.files_column.clear()
|
|
62
|
-
self.assignment_files_column.clear()
|