qcanvas 0.0.3.4a0__py3-none-any.whl → 0.0.4.1a0__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/QtVersionHelper/QtGui/__init__.py +1 -1
- qcanvas/QtVersionHelper/__init__.py +2 -1
- qcanvas/__main__.py +5 -9
- qcanvas/db/__init__.py +2 -1
- qcanvas/db/database.py +11 -4
- qcanvas/icons/__init__.py +1 -1
- qcanvas/icons/icons.qrc +5 -4
- qcanvas/icons/main_icon.svg +217 -204
- qcanvas/icons/rc_icons.py +3 -0
- qcanvas/net/canvas/__init__.py +1 -1
- qcanvas/net/canvas/canvas_client.py +4 -2
- qcanvas/net/canvas/legacy_canvas_types.py +2 -2
- qcanvas/net/custom_httpx_async_transport.py +2 -3
- qcanvas/net/self_authenticating.py +1 -0
- qcanvas/queries/all_courses.gql +3 -3
- qcanvas/queries/all_courses.py +2 -2
- qcanvas/queries/canvas_course_data.py +2 -2
- qcanvas/ui/container_item.py +0 -6
- qcanvas/ui/main_ui.py +34 -18
- qcanvas/ui/menu_bar/__init__.py +0 -0
- qcanvas/ui/menu_bar/grouping_preferences_menu.py +61 -0
- qcanvas/ui/menu_bar/theme_selection_menu.py +36 -0
- qcanvas/ui/setup_dialog.py +3 -3
- qcanvas/ui/viewer/course_list.py +1 -3
- qcanvas/ui/viewer/file_list.py +1 -3
- qcanvas/ui/viewer/file_view_tab.py +3 -64
- qcanvas/util/__init__.py +1 -1
- qcanvas/util/app_settings.py +28 -2
- qcanvas/util/constants.py +1 -1
- qcanvas/util/course_indexer/__init__.py +1 -1
- qcanvas/util/course_indexer/data_manager.py +8 -4
- qcanvas/util/course_indexer/resource_helpers.py +2 -1
- qcanvas/util/file_icon_helper.py +2 -1
- qcanvas/util/linkscanner/canvas_media_object_scanner.py +2 -2
- qcanvas/util/linkscanner/resource_scanner.py +1 -1
- qcanvas/util/self_updater.py +6 -6
- qcanvas/util/task_pool.py +1 -1
- qcanvas/util/tree_util/model_helpers.py +1 -1
- qcanvas/util/tree_util/tree_model.py +1 -0
- {qcanvas-0.0.3.4a0.dist-info → qcanvas-0.0.4.1a0.dist-info}/METADATA +1 -1
- qcanvas-0.0.4.1a0.dist-info/RECORD +60 -0
- qcanvas-0.0.3.4a0.dist-info/RECORD +0 -57
- {qcanvas-0.0.3.4a0.dist-info → qcanvas-0.0.4.1a0.dist-info}/WHEEL +0 -0
qcanvas/icons/rc_icons.py
CHANGED
|
@@ -230,10 +230,13 @@ qt_resource_struct = b"\
|
|
|
230
230
|
\x00\x00\x01\x8d\xcbE['\
|
|
231
231
|
"
|
|
232
232
|
|
|
233
|
+
|
|
233
234
|
def qInitResources():
|
|
234
235
|
QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
|
|
235
236
|
|
|
237
|
+
|
|
236
238
|
def qCleanupResources():
|
|
237
239
|
QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
|
|
238
240
|
|
|
241
|
+
|
|
239
242
|
qInitResources()
|
qcanvas/net/canvas/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from .canvas_client import CanvasClient
|
|
2
|
-
from .legacy_canvas_types import LegacyFile, LegacyPage
|
|
2
|
+
from .legacy_canvas_types import LegacyFile, LegacyPage
|
|
@@ -99,7 +99,8 @@ class CanvasClient(SelfAuthenticating):
|
|
|
99
99
|
async def get_page(self, page_id: str | int, course_id: str | int) -> LegacyPage:
|
|
100
100
|
async with self._net_op_sem:
|
|
101
101
|
response = detect_ratelimit_and_raise(
|
|
102
|
-
await self.client.get(self.canvas_url.join(f"api/v1/courses/{course_id}/pages/{page_id}"),
|
|
102
|
+
await self.client.get(self.canvas_url.join(f"api/v1/courses/{course_id}/pages/{page_id}"),
|
|
103
|
+
**self.get_headers()))
|
|
103
104
|
|
|
104
105
|
return LegacyPage.from_dict(json.loads(response.text))
|
|
105
106
|
|
|
@@ -155,7 +156,8 @@ class CanvasClient(SelfAuthenticating):
|
|
|
155
156
|
retries = 0
|
|
156
157
|
|
|
157
158
|
while retries < self.max_retries:
|
|
158
|
-
async with self.client.stream(method='GET', url=resource.url, cookies=self.client.cookies,
|
|
159
|
+
async with self.client.stream(method='GET', url=resource.url, cookies=self.client.cookies,
|
|
160
|
+
follow_redirects=True) as resp:
|
|
159
161
|
if await self.reauthenticate_if_needed(resp):
|
|
160
162
|
retries += 1
|
|
161
163
|
self._logger.warning("Retrying download of %s", resource.url)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Any, TypeVar, Type, cast
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
T = TypeVar("T")
|
|
5
4
|
|
|
6
5
|
|
|
@@ -66,7 +65,8 @@ class LegacyFile:
|
|
|
66
65
|
hidden_for_user: bool
|
|
67
66
|
locked_for_user: bool
|
|
68
67
|
|
|
69
|
-
def __init__(self, id: int, uuid: str, display_name: str, filename: str, url: str, size: int, locked: bool,
|
|
68
|
+
def __init__(self, id: int, uuid: str, display_name: str, filename: str, url: str, size: int, locked: bool,
|
|
69
|
+
hidden: bool, hidden_for_user: bool, locked_for_user: bool) -> None:
|
|
70
70
|
self.id = id
|
|
71
71
|
self.uuid = uuid
|
|
72
72
|
self.display_name = display_name
|
|
@@ -12,7 +12,8 @@ class CustomHTTPXAsyncTransport(HTTPXAsyncTransport):
|
|
|
12
12
|
The transport uses the httpx library with anyio.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
def __init__(self, client: httpx.AsyncClient, url: Union[str, httpx.URL], headers: dict[str, Any] | None = None,
|
|
15
|
+
def __init__(self, client: httpx.AsyncClient, url: Union[str, httpx.URL], headers: dict[str, Any] | None = None,
|
|
16
|
+
**kwargs):
|
|
16
17
|
super().__init__(url=url, **kwargs)
|
|
17
18
|
self.client = client
|
|
18
19
|
self.headers = headers or {}
|
|
@@ -31,5 +32,3 @@ class CustomHTTPXAsyncTransport(HTTPXAsyncTransport):
|
|
|
31
32
|
result["headers"] = self.headers
|
|
32
33
|
|
|
33
34
|
return result
|
|
34
|
-
|
|
35
|
-
|
|
@@ -5,6 +5,7 @@ from asyncio import Lock, Event
|
|
|
5
5
|
class AuthenticationException(Exception):
|
|
6
6
|
pass
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
# httpx does have an authentication flow mechanism that allows you to also make other requests but I don't know if it
|
|
9
10
|
# will behave the same way as this does. I also finished this before I found out that existed.
|
|
10
11
|
|
qcanvas/queries/all_courses.gql
CHANGED
qcanvas/queries/all_courses.py
CHANGED
qcanvas/ui/container_item.py
CHANGED
qcanvas/ui/main_ui.py
CHANGED
|
@@ -8,8 +8,10 @@ from qasync import asyncSlot
|
|
|
8
8
|
|
|
9
9
|
import qcanvas.db.database as db
|
|
10
10
|
from qcanvas.QtVersionHelper.QtCore import Slot, Signal, Qt, QUrl
|
|
11
|
-
from qcanvas.QtVersionHelper.QtGui import QDesktopServices
|
|
11
|
+
from qcanvas.QtVersionHelper.QtGui import QDesktopServices, QAction
|
|
12
12
|
from qcanvas.QtVersionHelper.QtWidgets import *
|
|
13
|
+
from qcanvas.ui.menu_bar.grouping_preferences_menu import GroupingPreferencesMenu
|
|
14
|
+
from qcanvas.ui.menu_bar.theme_selection_menu import ThemeSelectionMenu
|
|
13
15
|
from qcanvas.ui.viewer.course_list import CourseList
|
|
14
16
|
from qcanvas.ui.viewer.file_list import FileRow
|
|
15
17
|
from qcanvas.ui.viewer.file_view_tab import FileViewTab
|
|
@@ -20,14 +22,19 @@ from qcanvas.util.course_indexer import DataManager
|
|
|
20
22
|
|
|
21
23
|
_aux_settings = AppSettings.auxiliary
|
|
22
24
|
|
|
25
|
+
|
|
23
26
|
class AppMainWindow(QMainWindow):
|
|
24
27
|
logger = logging.getLogger()
|
|
25
28
|
loaded = Signal()
|
|
29
|
+
files_grouping_preference_changed = Signal(db.GroupByPreference)
|
|
26
30
|
operation_lock = Event()
|
|
27
31
|
|
|
28
32
|
def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
|
|
29
33
|
super().__init__(parent)
|
|
30
34
|
|
|
35
|
+
self.group_by_pages_action: QAction | None = None
|
|
36
|
+
self.group_by_modules_action: QAction | None = None
|
|
37
|
+
self.file_grouping_menu: QMenu | None = None
|
|
31
38
|
self.selected_course: db.Course | None = None
|
|
32
39
|
self.courses: Sequence[db.Course] = []
|
|
33
40
|
self.resources: dict[str, db.Resource] = {}
|
|
@@ -50,8 +57,6 @@ class AppMainWindow(QMainWindow):
|
|
|
50
57
|
self.pages_viewer.viewer.anchorClicked.connect(self.viewer_link_clicked)
|
|
51
58
|
|
|
52
59
|
self.file_viewer = FileViewTab(data_manager.download_pool)
|
|
53
|
-
self.file_viewer.group_by_preference_changed.connect(self.course_file_group_by_preference_changed)
|
|
54
|
-
|
|
55
60
|
self.file_viewer.files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
|
|
56
61
|
self.file_viewer.assignment_files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
|
|
57
62
|
|
|
@@ -73,17 +78,36 @@ class AppMainWindow(QMainWindow):
|
|
|
73
78
|
widget.setLayout(v_layout)
|
|
74
79
|
self.setCentralWidget(widget)
|
|
75
80
|
|
|
81
|
+
self.setup_menu_bar()
|
|
82
|
+
|
|
83
|
+
self.files_grouping_preference_changed.connect(self.on_grouping_preference_changed)
|
|
84
|
+
|
|
76
85
|
self.loaded.connect(self.load_course_list)
|
|
77
86
|
self.loaded.connect(self.check_for_update)
|
|
78
87
|
self.loaded.emit()
|
|
79
88
|
|
|
80
|
-
self.
|
|
89
|
+
self.restore_window_position()
|
|
90
|
+
|
|
91
|
+
def setup_menu_bar(self):
|
|
92
|
+
menu_bar = self.menuBar()
|
|
93
|
+
|
|
94
|
+
menu_bar.addMenu(ThemeSelectionMenu())
|
|
95
|
+
view_menu = menu_bar.addMenu("View")
|
|
96
|
+
|
|
97
|
+
view_menu.addMenu(self.setup_group_by_menu())
|
|
98
|
+
|
|
99
|
+
def setup_group_by_menu(self) -> QMenu:
|
|
100
|
+
file_grouping_menu = GroupingPreferencesMenu(self.data_manager)
|
|
101
|
+
self.course_list.course_selected.connect(file_grouping_menu.course_changed)
|
|
102
|
+
file_grouping_menu.preference_changed.connect(self.on_grouping_preference_changed)
|
|
103
|
+
|
|
104
|
+
return file_grouping_menu
|
|
81
105
|
|
|
82
106
|
def closeEvent(self, event):
|
|
83
107
|
_aux_settings.setValue("geometry", self.saveGeometry())
|
|
84
108
|
_aux_settings.setValue("windowState", self.saveState())
|
|
85
109
|
|
|
86
|
-
def
|
|
110
|
+
def restore_window_position(self):
|
|
87
111
|
self.restoreGeometry(_aux_settings.value("geometry"))
|
|
88
112
|
self.restoreState(_aux_settings.value("windowState"))
|
|
89
113
|
|
|
@@ -97,14 +121,12 @@ class AppMainWindow(QMainWindow):
|
|
|
97
121
|
await self.data_manager.download_resource(resource)
|
|
98
122
|
QDesktopServices.openUrl(QUrl.fromLocalFile(resource.download_location.absolute()))
|
|
99
123
|
|
|
100
|
-
|
|
101
124
|
@asyncSlot(QTreeWidgetItem, int)
|
|
102
|
-
async def download_file_from_file_pane(self, item: QTreeWidgetItem, _
|
|
125
|
+
async def download_file_from_file_pane(self, item: QTreeWidgetItem, _: int):
|
|
103
126
|
if isinstance(item, FileRow):
|
|
104
127
|
await self.data_manager.download_resource(item.resource)
|
|
105
128
|
QDesktopServices.openUrl(QUrl.fromLocalFile(item.resource.download_location.absolute()))
|
|
106
129
|
|
|
107
|
-
|
|
108
130
|
@asyncSlot()
|
|
109
131
|
async def sync_data(self):
|
|
110
132
|
# # self.operation_lock.
|
|
@@ -118,8 +140,6 @@ class AppMainWindow(QMainWindow):
|
|
|
118
140
|
self.sync_button.setEnabled(True)
|
|
119
141
|
self.sync_button.setText("Synchronize")
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
143
|
@asyncSlot()
|
|
124
144
|
async def load_course_list(self):
|
|
125
145
|
self.courses = (await self.data_manager.get_data())
|
|
@@ -134,13 +154,13 @@ class AppMainWindow(QMainWindow):
|
|
|
134
154
|
@asyncSlot()
|
|
135
155
|
async def check_for_update(self):
|
|
136
156
|
try:
|
|
137
|
-
newer_version = await self_updater.get_newer_version()
|
|
157
|
+
newer_version, installed_version = await self_updater.get_newer_version()
|
|
138
158
|
|
|
139
159
|
if newer_version is not None and newer_version != AppSettings.last_ignored_update:
|
|
140
160
|
msg_box = QMessageBox(
|
|
141
161
|
QMessageBox.Icon.Question,
|
|
142
162
|
"Update available",
|
|
143
|
-
"There is an update available
|
|
163
|
+
f"There is an update available ({installed_version} -> {newer_version})\n do you want to update?\nThe program will close after the update is finished.",
|
|
144
164
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
145
165
|
self
|
|
146
166
|
)
|
|
@@ -180,6 +200,7 @@ class AppMainWindow(QMainWindow):
|
|
|
180
200
|
def on_course_selected(self, course: Optional[db.Course]):
|
|
181
201
|
if course is not None:
|
|
182
202
|
self.selected_course = course
|
|
203
|
+
# todo these should really be slots connected to this signal...
|
|
183
204
|
self.pages_viewer.fill_tree(course)
|
|
184
205
|
self.assignment_viewer.fill_tree(course)
|
|
185
206
|
self.file_viewer.load_course_files(course)
|
|
@@ -187,11 +208,6 @@ class AppMainWindow(QMainWindow):
|
|
|
187
208
|
self.selected_course = None
|
|
188
209
|
self.file_viewer.clear()
|
|
189
210
|
|
|
190
|
-
|
|
191
211
|
@asyncSlot(db.CoursePreferences)
|
|
192
|
-
async def
|
|
193
|
-
self.selected_course.preferences.files_group_by_preference = preference
|
|
194
|
-
await self.data_manager.update_item(self.selected_course.preferences)
|
|
212
|
+
async def on_grouping_preference_changed(self):
|
|
195
213
|
self.file_viewer.load_course_files(self.selected_course)
|
|
196
|
-
|
|
197
|
-
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from qasync import asyncSlot
|
|
2
|
+
|
|
3
|
+
import qcanvas.db as db
|
|
4
|
+
from qcanvas.QtVersionHelper.QtCore import Slot, Signal
|
|
5
|
+
from qcanvas.QtVersionHelper.QtGui import create_qaction
|
|
6
|
+
from qcanvas.QtVersionHelper.QtWidgets import QMenu, QWidget
|
|
7
|
+
from qcanvas.util.course_indexer import DataManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GroupingPreferencesMenu(QMenu):
|
|
11
|
+
_preference_changed_private = Signal(db.GroupByPreference)
|
|
12
|
+
preference_changed = Signal()
|
|
13
|
+
|
|
14
|
+
def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
|
|
15
|
+
super().__init__("Group files by", parent)
|
|
16
|
+
self.setEnabled(False)
|
|
17
|
+
|
|
18
|
+
self.data_manager = data_manager
|
|
19
|
+
self._selected_course: db.Course | None = None
|
|
20
|
+
|
|
21
|
+
self.group_by_modules_action = self._make_action("Modules", db.GroupByPreference.GROUP_BY_MODULES)
|
|
22
|
+
self.group_by_pages_action = self._make_action("Pages", db.GroupByPreference.GROUP_BY_PAGES)
|
|
23
|
+
|
|
24
|
+
self.addActions([self.group_by_pages_action, self.group_by_modules_action])
|
|
25
|
+
|
|
26
|
+
self._preference_changed_private.connect(self._on_preference_changed)
|
|
27
|
+
|
|
28
|
+
def _make_action(self, text: str, preference_value: db.GroupByPreference):
|
|
29
|
+
return create_qaction(
|
|
30
|
+
name=text,
|
|
31
|
+
parent=self,
|
|
32
|
+
triggered=lambda: self._preference_changed_private.emit(preference_value),
|
|
33
|
+
checkable=True,
|
|
34
|
+
checked=False
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@Slot()
|
|
38
|
+
def course_changed(self, course: db.Course | None):
|
|
39
|
+
if course is not None:
|
|
40
|
+
self._selected_course = course
|
|
41
|
+
self._update_actions()
|
|
42
|
+
self.setEnabled(True)
|
|
43
|
+
else:
|
|
44
|
+
self._selected_course = None
|
|
45
|
+
self.setEnabled(False)
|
|
46
|
+
|
|
47
|
+
@asyncSlot()
|
|
48
|
+
async def _on_preference_changed(self, preference: db.GroupByPreference):
|
|
49
|
+
if self._selected_course is not None:
|
|
50
|
+
self._selected_course.preferences.files_group_by_preference = preference
|
|
51
|
+
self._update_actions()
|
|
52
|
+
# todo not sure if this should go in main_ui or here...
|
|
53
|
+
await self.data_manager.update_item(self._selected_course.preferences)
|
|
54
|
+
|
|
55
|
+
self.preference_changed.emit()
|
|
56
|
+
|
|
57
|
+
def _update_actions(self):
|
|
58
|
+
preference = self._selected_course.preferences.files_group_by_preference
|
|
59
|
+
|
|
60
|
+
self.group_by_pages_action.setChecked(preference == db.GroupByPreference.GROUP_BY_PAGES)
|
|
61
|
+
self.group_by_modules_action.setChecked(preference == db.GroupByPreference.GROUP_BY_MODULES)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from qcanvas.QtVersionHelper.QtGui import QActionGroup
|
|
2
|
+
from qcanvas.QtVersionHelper.QtGui import create_qaction
|
|
3
|
+
from qcanvas.QtVersionHelper.QtWidgets import QMenu, QWidget
|
|
4
|
+
from qcanvas.util import AppSettings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def change_theme(theme_name: str):
|
|
8
|
+
AppSettings.theme = theme_name
|
|
9
|
+
AppSettings.apply_selected_theme()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThemeSelectionMenu(QMenu):
|
|
13
|
+
def __init__(self, parent: QWidget | None = None):
|
|
14
|
+
super().__init__("Theme", parent)
|
|
15
|
+
|
|
16
|
+
action_group = QActionGroup(self)
|
|
17
|
+
|
|
18
|
+
light_theme = self._create_action("Light", "light")
|
|
19
|
+
dark_theme = self._create_action("Dark", "dark")
|
|
20
|
+
native_theme = self._create_action("Native (requires restart)", "native")
|
|
21
|
+
|
|
22
|
+
actions = [light_theme, dark_theme, native_theme]
|
|
23
|
+
|
|
24
|
+
self.addActions(actions)
|
|
25
|
+
|
|
26
|
+
for theme in actions:
|
|
27
|
+
action_group.addAction(theme)
|
|
28
|
+
|
|
29
|
+
def _create_action(self, text: str, theme_name: str):
|
|
30
|
+
return create_qaction(
|
|
31
|
+
name=text,
|
|
32
|
+
parent=self,
|
|
33
|
+
triggered=lambda: change_theme(theme_name),
|
|
34
|
+
checkable=True,
|
|
35
|
+
checked=AppSettings.theme == theme_name
|
|
36
|
+
)
|
qcanvas/ui/setup_dialog.py
CHANGED
|
@@ -14,6 +14,7 @@ from qcanvas.util import AppSettings
|
|
|
14
14
|
|
|
15
15
|
tutorial_url = "https://www.iorad.com/player/2053777/Canvas---How-to-generate-an-access-token-"
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
def row(name: str) -> QWidget:
|
|
18
19
|
widget = QWidget()
|
|
19
20
|
layout = QHBoxLayout()
|
|
@@ -78,7 +79,7 @@ class SetupDialog(QDialog):
|
|
|
78
79
|
"Error",
|
|
79
80
|
f"{name} is invalid",
|
|
80
81
|
parent=self
|
|
81
|
-
|
|
82
|
+
)
|
|
82
83
|
msg.show()
|
|
83
84
|
|
|
84
85
|
def ensure_protocol(url: str):
|
|
@@ -104,7 +105,7 @@ class SetupDialog(QDialog):
|
|
|
104
105
|
"Error",
|
|
105
106
|
f"The canvas URL or API key is invalid.\nPlease check you entered them correctly.",
|
|
106
107
|
parent=self
|
|
107
|
-
|
|
108
|
+
)
|
|
108
109
|
msg.show()
|
|
109
110
|
else:
|
|
110
111
|
AppSettings.canvas_url = canvas_url_text
|
|
@@ -144,7 +145,6 @@ Don't share this key. You can revoke it at any time.""",
|
|
|
144
145
|
msg.accepted.connect(lambda: QDesktopServices.openUrl(tutorial_url))
|
|
145
146
|
msg.show()
|
|
146
147
|
|
|
147
|
-
|
|
148
148
|
def _row(self, name: str, widget: QWidget):
|
|
149
149
|
self.grid.addWidget(QLabel(name), self._row_counter, 0)
|
|
150
150
|
self.grid.addWidget(widget, self._row_counter, 1)
|
qcanvas/ui/viewer/course_list.py
CHANGED
|
@@ -18,7 +18,7 @@ class CourseNode(QStandardItem, QObject):
|
|
|
18
18
|
QStandardItem.__init__(self, course.preferences.local_name or course.name)
|
|
19
19
|
self.course = course
|
|
20
20
|
|
|
21
|
-
def setData(self, value, role
|
|
21
|
+
def setData(self, value, role=...):
|
|
22
22
|
if isinstance(value, str):
|
|
23
23
|
value = value.strip()
|
|
24
24
|
|
|
@@ -93,5 +93,3 @@ class CourseList(QTreeView):
|
|
|
93
93
|
|
|
94
94
|
if isinstance(item, CourseNode):
|
|
95
95
|
self.course_selected.emit(item.course)
|
|
96
|
-
|
|
97
|
-
|
qcanvas/ui/viewer/file_list.py
CHANGED
|
@@ -130,7 +130,6 @@ class FileList(QTreeWidget):
|
|
|
130
130
|
# Update the download column
|
|
131
131
|
self.model().dataChanged.emit(index, index, Qt.ItemDataRole.DisplayRole)
|
|
132
132
|
|
|
133
|
-
|
|
134
133
|
def _setup_header(self):
|
|
135
134
|
self.setColumnCount(4)
|
|
136
135
|
self.setHeaderLabels(["Name", "Date Found", "Size", "Download"])
|
|
@@ -167,7 +166,7 @@ class FileList(QTreeWidget):
|
|
|
167
166
|
# Create the group node for this item
|
|
168
167
|
group_node = QTreeWidgetItem([item.name])
|
|
169
168
|
|
|
170
|
-
#fixme this does not remove duplicate files e.g. when on module groups
|
|
169
|
+
# fixme this does not remove duplicate files e.g. when on module groups
|
|
171
170
|
|
|
172
171
|
for resource in resources:
|
|
173
172
|
row = FileRow(resource)
|
|
@@ -193,4 +192,3 @@ class FileList(QTreeWidget):
|
|
|
193
192
|
super().clear()
|
|
194
193
|
self._setup_header()
|
|
195
194
|
self.row_id_map.clear()
|
|
196
|
-
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from typing import Sequence
|
|
2
2
|
|
|
3
3
|
import qcanvas.db as db
|
|
4
|
-
from qcanvas.QtVersionHelper.QtCore import Signal, Qt, Slot, QPoint
|
|
5
|
-
from qcanvas.QtVersionHelper.QtGui import QActionGroup, create_qaction
|
|
6
4
|
from qcanvas.QtVersionHelper.QtWidgets import * # QWidget, QTreeView, QGroupBox, QBoxLayout, QHeaderView, QHBoxLayout, QComboBox, QLabel
|
|
7
5
|
from qcanvas.ui.viewer.file_list import FileList
|
|
8
6
|
from qcanvas.util.constants import default_assignments_module_names
|
|
@@ -25,7 +23,6 @@ class FileColumn(QGroupBox):
|
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
class FileViewTab(QWidget):
|
|
28
|
-
group_by_preference_changed = Signal(db.GroupByPreference)
|
|
29
26
|
|
|
30
27
|
def __init__(self, download_pool: DownloadPool):
|
|
31
28
|
super().__init__()
|
|
@@ -33,37 +30,13 @@ class FileViewTab(QWidget):
|
|
|
33
30
|
self.files_column = FileColumn("Files", download_pool)
|
|
34
31
|
self.assignment_files_column = FileColumn("Assignment files", download_pool)
|
|
35
32
|
|
|
36
|
-
self.group_by_combobox = QComboBox()
|
|
37
|
-
|
|
38
|
-
self.group_by_preference: db.GroupByPreference | None = None
|
|
39
|
-
self.group_preference_layout = QHBoxLayout()
|
|
40
|
-
self.group_preference_layout.addWidget(QLabel("Group By:"))
|
|
41
|
-
self.group_preference_layout.addWidget(self.group_by_combobox)
|
|
42
|
-
self.group_preference_layout.setStretch(1, 1)
|
|
43
|
-
|
|
44
|
-
widget = QWidget()
|
|
45
|
-
widget.setLayout(self.group_preference_layout)
|
|
46
|
-
|
|
47
|
-
# fixme this can't stay here
|
|
48
|
-
self.files_column.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
49
|
-
self.files_column.customContextMenuRequested.connect(self.files_column_context_menu)
|
|
50
|
-
|
|
51
|
-
self.group_by_preference_changed.connect(self.update_group_by_preferences)
|
|
52
|
-
|
|
53
33
|
layout = QHBoxLayout()
|
|
54
34
|
layout.addWidget(self.files_column)
|
|
55
35
|
layout.addWidget(self.assignment_files_column)
|
|
56
36
|
|
|
57
37
|
self.setLayout(layout)
|
|
58
38
|
|
|
59
|
-
@Slot(db.GroupByPreference)
|
|
60
|
-
def update_group_by_preferences(self, preference : db.GroupByPreference):
|
|
61
|
-
if self.group_by_preference is not None:
|
|
62
|
-
self.group_by_preference = preference
|
|
63
|
-
|
|
64
39
|
def load_course_files(self, course: db.Course):
|
|
65
|
-
self.group_by_preference = course.preferences.files_group_by_preference
|
|
66
|
-
|
|
67
40
|
module_items: list[db.ModuleItem] = []
|
|
68
41
|
assignment_items: list[db.ModuleItem] = []
|
|
69
42
|
|
|
@@ -73,8 +46,9 @@ class FileViewTab(QWidget):
|
|
|
73
46
|
else:
|
|
74
47
|
module_items.append(module_item)
|
|
75
48
|
|
|
76
|
-
if
|
|
77
|
-
exclude_assignments_module = list(
|
|
49
|
+
if course.preferences.files_group_by_preference == db.GroupByPreference.GROUP_BY_MODULES:
|
|
50
|
+
exclude_assignments_module = list(
|
|
51
|
+
filter(lambda x: x.name.lower() not in default_assignments_module_names, course.modules))
|
|
78
52
|
|
|
79
53
|
self.files_column.load_items(exclude_assignments_module)
|
|
80
54
|
else:
|
|
@@ -83,40 +57,5 @@ class FileViewTab(QWidget):
|
|
|
83
57
|
self.assignment_files_column.load_items(assignment_items + course.assignments)
|
|
84
58
|
|
|
85
59
|
def clear(self):
|
|
86
|
-
self.group_by_preference = None
|
|
87
60
|
self.files_column.clear()
|
|
88
61
|
self.assignment_files_column.clear()
|
|
89
|
-
|
|
90
|
-
@Slot(QPoint)
|
|
91
|
-
def files_column_context_menu(self, pos: QPoint):
|
|
92
|
-
if self.group_by_preference is None:
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
menu = QMenu(self.files_column)
|
|
96
|
-
|
|
97
|
-
group_by_menu = menu.addMenu("Group by")
|
|
98
|
-
|
|
99
|
-
select_group_preference_modules = create_qaction(
|
|
100
|
-
name="Modules",
|
|
101
|
-
checkable=True,
|
|
102
|
-
checked=self.group_by_preference == db.GroupByPreference.GROUP_BY_MODULES,
|
|
103
|
-
triggered=lambda: self.group_by_preference_changed.emit(db.GroupByPreference.GROUP_BY_MODULES)
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
select_group_preference_pages = create_qaction(
|
|
107
|
-
name="Pages",
|
|
108
|
-
checkable=True,
|
|
109
|
-
checked=self.group_by_preference == db.GroupByPreference.GROUP_BY_PAGES,
|
|
110
|
-
triggered=lambda: self.group_by_preference_changed.emit(db.GroupByPreference.GROUP_BY_PAGES)
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
action_group = QActionGroup(menu)
|
|
114
|
-
action_group.addAction(select_group_preference_modules)
|
|
115
|
-
action_group.addAction(select_group_preference_pages)
|
|
116
|
-
|
|
117
|
-
group_by_menu.addAction(select_group_preference_pages)
|
|
118
|
-
group_by_menu.addAction(select_group_preference_modules)
|
|
119
|
-
|
|
120
|
-
menu.exec(self.files_column.mapToGlobal(pos))
|
|
121
|
-
|
|
122
|
-
|
qcanvas/util/__init__.py
CHANGED
qcanvas/util/app_settings.py
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import qdarktheme
|
|
2
|
+
|
|
1
3
|
from qcanvas.QtVersionHelper.QtCore import QSettings, QUrl
|
|
2
4
|
|
|
3
5
|
|
|
6
|
+
def ensure_theme_is_valid(theme: str) -> str:
|
|
7
|
+
if theme not in ["auto", "light", "dark", "native"]:
|
|
8
|
+
return "light"
|
|
9
|
+
else:
|
|
10
|
+
return theme
|
|
11
|
+
|
|
12
|
+
|
|
4
13
|
class _AppSettings:
|
|
5
14
|
def __init__(self):
|
|
6
15
|
self.settings = QSettings("QCanvas", "client")
|
|
@@ -8,6 +17,7 @@ class _AppSettings:
|
|
|
8
17
|
self._canvas_url = self.settings.value("canvas_url", None)
|
|
9
18
|
self._api_key = self.settings.value("api_key", defaultValue=None)
|
|
10
19
|
self._ignored_update = self.settings.value("ignored_update", defaultValue=None)
|
|
20
|
+
self._theme = ensure_theme_is_valid(self.settings.value("theme", defaultValue="light"))
|
|
11
21
|
|
|
12
22
|
@property
|
|
13
23
|
def canvas_url(self) -> str | None:
|
|
@@ -37,7 +47,23 @@ class _AppSettings:
|
|
|
37
47
|
self.settings.setValue("ignored_update", value)
|
|
38
48
|
|
|
39
49
|
@property
|
|
40
|
-
def
|
|
41
|
-
return self.
|
|
50
|
+
def theme(self) -> str | None:
|
|
51
|
+
return self._theme
|
|
42
52
|
|
|
53
|
+
@theme.setter
|
|
54
|
+
def theme(self, value: str):
|
|
55
|
+
value = ensure_theme_is_valid(value)
|
|
56
|
+
self._theme = value
|
|
57
|
+
self.settings.setValue("theme", value)
|
|
43
58
|
|
|
59
|
+
# fixme should this really be here
|
|
60
|
+
def apply_selected_theme(self):
|
|
61
|
+
if self.theme != "native":
|
|
62
|
+
qdarktheme.setup_theme(
|
|
63
|
+
self.theme,
|
|
64
|
+
custom_colors={"primary": "FF804F"}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_set(self):
|
|
69
|
+
return self.canvas_url is not None and QUrl(self.canvas_url).isValid() and self.canvas_url is not None
|
qcanvas/util/constants.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from .data_manager import DataManager
|
|
1
|
+
from .data_manager import DataManager
|
|
@@ -33,7 +33,8 @@ class TransientModulePage:
|
|
|
33
33
|
position: int
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def _prepare_out_of_date_pages_for_loading(g_courses: Sequence[queries.Course], pages: Sequence[db.ModuleItem]) -> list[
|
|
36
|
+
def _prepare_out_of_date_pages_for_loading(g_courses: Sequence[queries.Course], pages: Sequence[db.ModuleItem]) -> list[
|
|
37
|
+
TransientModulePage]:
|
|
37
38
|
"""
|
|
38
39
|
Removes pages that are up-to-date from the pages list by comparing the last update time of the pages from the query
|
|
39
40
|
to the last update time of the pages in the database.
|
|
@@ -70,6 +71,7 @@ def _prepare_out_of_date_pages_for_loading(g_courses: Sequence[queries.Course],
|
|
|
70
71
|
|
|
71
72
|
return result
|
|
72
73
|
|
|
74
|
+
|
|
73
75
|
# todo make this reusable and add some way of refreshing only a list of pages or one page or one course or something
|
|
74
76
|
# todo use logger instead of print and put some signals around the place for useful things, e.g. indexing progress
|
|
75
77
|
class DataManager:
|
|
@@ -111,7 +113,7 @@ class DataManager:
|
|
|
111
113
|
self._add_resources_and_pages_to_taskpool(existing_pages=existing_pages,
|
|
112
114
|
existing_resources=existing_resources)
|
|
113
115
|
|
|
114
|
-
async def _download_resource_helper(self, link_handler
|
|
116
|
+
async def _download_resource_helper(self, link_handler: ResourceScanner, resource: db.Resource):
|
|
115
117
|
try:
|
|
116
118
|
async for progress in link_handler.download(resource):
|
|
117
119
|
yield progress
|
|
@@ -263,7 +265,8 @@ class DataManager:
|
|
|
263
265
|
self._moduleitem_pool.add_values({page.id: page for page in existing_pages})
|
|
264
266
|
self._resource_pool.add_values({resource.id: resource for resource in existing_resources})
|
|
265
267
|
# Add downloaded resources to the resource pool so we don't download them again
|
|
266
|
-
self.download_pool.add_values(
|
|
268
|
+
self.download_pool.add_values(
|
|
269
|
+
{resource.id: None for resource in existing_resources if resource.state == db.ResourceState.DOWNLOADED})
|
|
267
270
|
|
|
268
271
|
async def _load_page_content(self, pages: Sequence[TransientModulePage]) -> list[db.ModuleItem]:
|
|
269
272
|
"""
|
|
@@ -282,7 +285,8 @@ class DataManager:
|
|
|
282
285
|
content = page.page
|
|
283
286
|
|
|
284
287
|
if isinstance(content, queries.File):
|
|
285
|
-
task = asyncio.create_task(
|
|
288
|
+
task = asyncio.create_task(
|
|
289
|
+
self._load_module_file(content, page.course_id, page.module_id, page.position))
|
|
286
290
|
tasks.append(task)
|
|
287
291
|
elif isinstance(content, queries.Page):
|
|
288
292
|
tasks.append(
|