qcanvas 0.0.5.6a0__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.

Files changed (114) hide show
  1. qcanvas/app_start/__init__.py +47 -0
  2. qcanvas/backend_connectors/__init__.py +2 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
  4. qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
  5. qcanvas/icons/__init__.py +6 -0
  6. qcanvas/icons/file-download-failed.svg +6 -0
  7. qcanvas/icons/file-downloaded.svg +6 -0
  8. qcanvas/icons/file-not-downloaded.svg +6 -0
  9. qcanvas/icons/file-unknown.svg +6 -0
  10. qcanvas/icons/icons.qrc +4 -0
  11. qcanvas/icons/main_icon.svg +7 -7
  12. qcanvas/icons/rc_icons.py +580 -214
  13. qcanvas/icons/sync.svg +7 -0
  14. qcanvas/run.py +29 -0
  15. qcanvas/ui/course_viewer/__init__.py +2 -0
  16. qcanvas/ui/course_viewer/content_tree.py +123 -0
  17. qcanvas/ui/course_viewer/course_tree.py +93 -0
  18. qcanvas/ui/course_viewer/course_viewer.py +62 -0
  19. qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
  21. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
  22. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
  23. qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
  24. qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
  25. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
  26. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
  27. qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
  28. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
  30. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
  31. qcanvas/ui/course_viewer/tabs/util.py +1 -0
  32. qcanvas/ui/main_ui/course_viewer_container.py +52 -0
  33. qcanvas/ui/main_ui/options/__init__.py +3 -0
  34. qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
  35. qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
  36. qcanvas/ui/main_ui/qcanvas_window.py +192 -0
  37. qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
  38. qcanvas/ui/memory_tree/__init__.py +2 -0
  39. qcanvas/ui/memory_tree/_tree_memory.py +66 -0
  40. qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
  41. qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
  42. qcanvas/ui/setup/__init__.py +2 -0
  43. qcanvas/ui/setup/setup_checker.py +17 -0
  44. qcanvas/ui/setup/setup_dialog.py +212 -0
  45. qcanvas/util/__init__.py +2 -0
  46. qcanvas/util/basic_fonts.py +12 -0
  47. qcanvas/util/fe_resource_manager.py +23 -0
  48. qcanvas/util/html_cleaner.py +25 -0
  49. qcanvas/util/layouts.py +52 -0
  50. qcanvas/util/logs.py +6 -0
  51. qcanvas/util/paths.py +41 -0
  52. qcanvas/util/settings/__init__.py +9 -0
  53. qcanvas/util/settings/_client_settings.py +29 -0
  54. qcanvas/util/settings/_mapped_setting.py +63 -0
  55. qcanvas/util/settings/_ui_settings.py +34 -0
  56. qcanvas/util/ui_tools.py +41 -0
  57. qcanvas/util/url_checker.py +13 -0
  58. qcanvas-1.0.3.post0.dist-info/METADATA +61 -0
  59. qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
  60. {qcanvas-0.0.5.6a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
  61. qcanvas-1.0.3.post0.dist-info/entry_points.txt +3 -0
  62. qcanvas/__main__.py +0 -155
  63. qcanvas/db/__init__.py +0 -5
  64. qcanvas/db/database.py +0 -337
  65. qcanvas/db/db_converter_helper.py +0 -81
  66. qcanvas/net/canvas/__init__.py +0 -2
  67. qcanvas/net/canvas/canvas_client.py +0 -209
  68. qcanvas/net/canvas/legacy_canvas_types.py +0 -124
  69. qcanvas/net/custom_httpx_async_transport.py +0 -34
  70. qcanvas/net/self_authenticating.py +0 -108
  71. qcanvas/queries/__init__.py +0 -4
  72. qcanvas/queries/all_courses.gql +0 -7
  73. qcanvas/queries/all_courses.py +0 -108
  74. qcanvas/queries/canvas_course_data.gql +0 -51
  75. qcanvas/queries/canvas_course_data.py +0 -143
  76. qcanvas/ui/container_item.py +0 -11
  77. qcanvas/ui/main_ui.py +0 -249
  78. qcanvas/ui/menu_bar/__init__.py +0 -0
  79. qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
  80. qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
  81. qcanvas/ui/setup_dialog.py +0 -190
  82. qcanvas/ui/status_bar_reporter.py +0 -40
  83. qcanvas/ui/viewer/__init__.py +0 -0
  84. qcanvas/ui/viewer/course_list.py +0 -96
  85. qcanvas/ui/viewer/file_list.py +0 -195
  86. qcanvas/ui/viewer/file_view_tab.py +0 -62
  87. qcanvas/ui/viewer/page_list_viewer.py +0 -150
  88. qcanvas/util/app_settings.py +0 -98
  89. qcanvas/util/constants.py +0 -5
  90. qcanvas/util/course_indexer/__init__.py +0 -1
  91. qcanvas/util/course_indexer/conversion_helpers.py +0 -78
  92. qcanvas/util/course_indexer/data_manager.py +0 -447
  93. qcanvas/util/course_indexer/resource_helpers.py +0 -191
  94. qcanvas/util/download_pool.py +0 -58
  95. qcanvas/util/helpers/__init__.py +0 -0
  96. qcanvas/util/helpers/canvas_sanitiser.py +0 -47
  97. qcanvas/util/helpers/file_icon_helper.py +0 -34
  98. qcanvas/util/helpers/qaction_helper.py +0 -25
  99. qcanvas/util/helpers/theme_helper.py +0 -45
  100. qcanvas/util/link_scanner/__init__.py +0 -2
  101. qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
  102. qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
  103. qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
  104. qcanvas/util/link_scanner/resource_scanner.py +0 -69
  105. qcanvas/util/progress_reporter.py +0 -101
  106. qcanvas/util/self_updater.py +0 -55
  107. qcanvas/util/task_pool.py +0 -253
  108. qcanvas/util/tree_util/__init__.py +0 -3
  109. qcanvas/util/tree_util/expanding_tree.py +0 -165
  110. qcanvas/util/tree_util/model_helpers.py +0 -36
  111. qcanvas/util/tree_util/tree_model.py +0 -85
  112. qcanvas-0.0.5.6a0.dist-info/METADATA +0 -21
  113. qcanvas-0.0.5.6a0.dist-info/RECORD +0 -61
  114. /qcanvas/{net → ui/main_ui}/__init__.py +0 -0
@@ -1,143 +0,0 @@
1
- """
2
- Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
- """
4
- from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
- from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
- from enum import Enum # noqa: F401 # pylint: disable=W0611
7
- from typing import ( # noqa: F401 # pylint: disable=W0611
8
- Any,
9
- Optional,
10
- Union,
11
- )
12
-
13
- from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
- BaseModel,
15
- Extra,
16
- Field,
17
- Json,
18
- )
19
-
20
- DEFINITION = """
21
- fragment CanvasCourseData on Course {
22
- _id
23
- name
24
- courseNickname
25
- term {
26
- endAt
27
- startAt
28
- name
29
- id
30
- }
31
- assignmentsConnection @include(if: $detailed) {
32
- nodes {
33
- description
34
- courseId
35
- dueAt
36
- createdAt
37
- id
38
- name
39
- position
40
- updatedAt
41
- }
42
- }
43
- modulesConnection @include(if: $detailed) {
44
- nodes {
45
- name
46
- id
47
- moduleItems {
48
- content {
49
- ... on File {
50
- _id
51
- displayName
52
- createdAt
53
- updatedAt
54
- url
55
- size
56
- mimeClass
57
- contentType
58
- }
59
- ... on Page {
60
- _id
61
- title
62
- updatedAt
63
- createdAt
64
- }
65
- }
66
- }
67
- }
68
- }
69
- }
70
- """
71
-
72
-
73
- class ConfiguredBaseModel(BaseModel):
74
- class Config:
75
- smart_union = True
76
- extra = Extra.forbid
77
-
78
-
79
- class Term(ConfiguredBaseModel):
80
- end_at: Optional[datetime] = Field(..., alias="endAt")
81
- start_at: Optional[datetime] = Field(..., alias="startAt")
82
- name: Optional[str] = Field(..., alias="name")
83
- q_id: str = Field(..., alias="id")
84
-
85
-
86
- class Assignment(ConfiguredBaseModel):
87
- description: Optional[str] = Field(..., alias="description")
88
- course_id: Optional[str] = Field(..., alias="courseId")
89
- due_at: Optional[datetime] = Field(..., alias="dueAt")
90
- created_at: Optional[datetime] = Field(..., alias="createdAt")
91
- q_id: str = Field(..., alias="id")
92
- name: Optional[str] = Field(..., alias="name")
93
- position: Optional[int] = Field(..., alias="position")
94
- updated_at: Optional[datetime] = Field(..., alias="updatedAt")
95
-
96
-
97
- class AssignmentConnection(ConfiguredBaseModel):
98
- nodes: Optional[list[Optional[Assignment]]] = Field(..., alias="nodes")
99
-
100
-
101
- class ModuleItemInterface(ConfiguredBaseModel):
102
- ...
103
-
104
-
105
- class File(ModuleItemInterface):
106
- m_id: str = Field(..., alias="_id")
107
- display_name: Optional[str] = Field(..., alias="displayName")
108
- created_at: Optional[datetime] = Field(..., alias="createdAt")
109
- updated_at: Optional[datetime] = Field(..., alias="updatedAt")
110
- url: Optional[str] = Field(..., alias="url")
111
- size: Optional[str] = Field(..., alias="size")
112
- mime_class: Optional[str] = Field(..., alias="mimeClass")
113
- content_type: Optional[str] = Field(..., alias="contentType")
114
-
115
-
116
- class Page(ModuleItemInterface):
117
- m_id: str = Field(..., alias="_id")
118
- title: Optional[str] = Field(..., alias="title")
119
- updated_at: Optional[datetime] = Field(..., alias="updatedAt")
120
- created_at: Optional[datetime] = Field(..., alias="createdAt")
121
-
122
-
123
- class ModuleItem(ConfiguredBaseModel):
124
- content: Optional[Union[File, Page, ModuleItemInterface]] = Field(..., alias="content")
125
-
126
-
127
- class Module(ConfiguredBaseModel):
128
- name: Optional[str] = Field(..., alias="name")
129
- q_id: str = Field(..., alias="id")
130
- module_items: Optional[list[ModuleItem]] = Field(..., alias="moduleItems")
131
-
132
-
133
- class ModuleConnection(ConfiguredBaseModel):
134
- nodes: Optional[list[Optional[Module]]] = Field(..., alias="nodes")
135
-
136
-
137
- class CanvasCourseData(ConfiguredBaseModel):
138
- m_id: str = Field(..., alias="_id")
139
- name: str = Field(..., alias="name")
140
- course_nickname: Optional[str] = Field(..., alias="courseNickname")
141
- term: Optional[Term] = Field(..., alias="term")
142
- assignments_connection: Optional[AssignmentConnection] = Field(None, alias="assignmentsConnection")
143
- modules_connection: Optional[ModuleConnection] = Field(None, alias="modulesConnection")
@@ -1,11 +0,0 @@
1
- from PySide6.QtGui import QStandardItem
2
-
3
- from qcanvas.util import tree_util as tree
4
-
5
-
6
- class ContainerItem(QStandardItem):
7
- def __init__(self, data: tree.HasText):
8
- super().__init__()
9
- self.content = data
10
- self.setEditable(False)
11
- self.setText(data.text)
qcanvas/ui/main_ui.py DELETED
@@ -1,249 +0,0 @@
1
- import logging
2
- import sys
3
- import traceback
4
- from typing import Sequence, Optional
5
-
6
- from PySide6.QtCore import Slot, Signal, Qt, QUrl, QObject
7
- from PySide6.QtGui import QDesktopServices, QKeySequence
8
- from PySide6.QtWidgets import *
9
- from qasync import asyncSlot
10
-
11
- import qcanvas.db.database as db
12
- from qcanvas.ui.menu_bar.grouping_preferences_menu import GroupingPreferencesMenu
13
- from qcanvas.ui.menu_bar.theme_selection_menu import ThemeSelectionMenu
14
- from qcanvas.ui.status_bar_reporter import StatusBarReporter
15
- from qcanvas.ui.viewer.course_list import CourseList
16
- from qcanvas.ui.viewer.file_list import FileRow
17
- from qcanvas.ui.viewer.file_view_tab import FileViewTab
18
- from qcanvas.ui.viewer.page_list_viewer import AssignmentsViewer, PagesViewer, LinkTransformer
19
- from qcanvas.util import self_updater
20
- from qcanvas.util.app_settings import settings
21
- from qcanvas.util.constants import app_name
22
- from qcanvas.util.course_indexer import DataManager
23
- from qcanvas.util.helpers.qaction_helper import create_qaction
24
-
25
- _aux_settings = settings.auxiliary
26
- _no_course_selected_text = "No course selected"
27
-
28
- class AppMainWindow(QMainWindow):
29
- logger = logging.getLogger()
30
- loaded = Signal()
31
- files_grouping_preference_changed = Signal(db.GroupByPreference)
32
-
33
- def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
34
- super().__init__(parent)
35
-
36
- self.selected_course: db.Course | None = None
37
- self.courses: Sequence[db.Course] = []
38
- self.resources: dict[str, db.Resource] = {}
39
- self.data_manager = data_manager
40
- self.link_transformer = LinkTransformer(self.data_manager.link_scanners, self.resources)
41
-
42
- right_splitter = QSplitter()
43
- right_splitter.setOrientation(Qt.Orientation.Vertical)
44
-
45
- self.sync_button = QPushButton("Synchronize")
46
- self.sync_button.clicked.connect(self.sync_data)
47
-
48
- self.course_list = CourseList(self.data_manager)
49
- self.course_list.course_selected.connect(self.on_course_selected)
50
-
51
- self.assignment_viewer = AssignmentsViewer(self.link_transformer)
52
- self.assignment_viewer.viewer.anchorClicked.connect(self.viewer_link_clicked)
53
-
54
- self.pages_viewer = PagesViewer(self.link_transformer)
55
- self.pages_viewer.viewer.anchorClicked.connect(self.viewer_link_clicked)
56
-
57
- self.file_viewer = FileViewTab(data_manager.download_pool)
58
- self.file_viewer.files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
59
- self.file_viewer.assignment_files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
60
-
61
- self.tab_widget = QTabWidget()
62
- self.tab_widget.insertTab(0, self.file_viewer, "Files")
63
- self.tab_widget.insertTab(1, self.assignment_viewer, "Assignments")
64
- self.tab_widget.insertTab(2, self.pages_viewer, "Pages")
65
-
66
- self.course_name_label = QLabel(_no_course_selected_text)
67
- self.course_name_label.setStyleSheet("font-weight: bold;")
68
- course_stack_layout = self.create_layout_and_add_widgets(QVBoxLayout, self.course_name_label, self.tab_widget)
69
-
70
- h_layout = self.create_layout_and_add_widgets(QHBoxLayout, self.course_list, course_stack_layout)
71
- h_layout.setStretch(1, 1)
72
-
73
- widget = QWidget()
74
- widget.setLayout(self.create_layout_and_add_widgets(QVBoxLayout, h_layout, self.sync_button))
75
- self.setCentralWidget(widget)
76
-
77
- self.setup_menu_bar()
78
-
79
- self.files_grouping_preference_changed.connect(self.on_grouping_preference_changed)
80
-
81
- self.loaded.connect(self.load_course_list)
82
- self.loaded.connect(self.check_for_update)
83
- self.loaded.emit()
84
-
85
- self.restore_window_position()
86
-
87
- # Activate the statusbar so it doesn't just appear randomly later
88
- bar: QStatusBar = self.statusBar()
89
- # Set its height so it doesn't get bigger when there's a progress bar in it
90
- bar.setFixedHeight(bar.height())
91
-
92
- @staticmethod
93
- def create_layout_and_add_widgets(layout_type: type, *widgets) -> QLayout:
94
- layout = layout_type()
95
-
96
- for widget in widgets:
97
- if isinstance(widget, QLayout):
98
- layout.addLayout(widget)
99
- else:
100
- layout.addWidget(widget)
101
-
102
- return layout
103
-
104
- def setup_menu_bar(self):
105
- menu_bar = self.menuBar()
106
-
107
- app_menu: QMenu = menu_bar.addMenu("App")
108
- view_menu: QMenu = menu_bar.addMenu("View")
109
-
110
- app_menu.addAction(self.setup_quick_authentication_action(app_menu))
111
- app_menu.addMenu(ThemeSelectionMenu())
112
- view_menu.addMenu(self.setup_group_by_menu())
113
-
114
- def setup_group_by_menu(self) -> QMenu:
115
- file_grouping_menu = GroupingPreferencesMenu(self.data_manager)
116
- self.course_list.course_selected.connect(file_grouping_menu.course_changed)
117
- file_grouping_menu.preference_changed.connect(self.on_grouping_preference_changed)
118
-
119
- return file_grouping_menu
120
-
121
- def setup_quick_authentication_action(self, parent: QObject):
122
- return create_qaction(
123
- name="Quick canvas login",
124
- shortcut=QKeySequence("Ctrl+O"),
125
- triggered=self.open_quick_auth_in_browser,
126
- parent=parent
127
- )
128
-
129
- @asyncSlot()
130
- async def open_quick_auth_in_browser(self):
131
- opening_progress_dialog = QProgressDialog("Opening canvas", None, 0, 0, self)
132
- opening_progress_dialog.setWindowTitle("Please wait")
133
- opening_progress_dialog.show()
134
- QDesktopServices.openUrl(await self.data_manager.client.get_temp_session_link())
135
- opening_progress_dialog.close()
136
-
137
- def closeEvent(self, event):
138
- settings.geometry = self.saveGeometry()
139
- settings.window_state = self.saveState()
140
-
141
- def restore_window_position(self):
142
- self.restoreGeometry(settings.geometry)
143
- self.restoreState(settings.window_state)
144
-
145
- @asyncSlot(QUrl)
146
- async def viewer_link_clicked(self, url: QUrl):
147
- # The url of a transformed link will start with a specific prefix
148
- if url.toString().startswith(LinkTransformer.transformed_url_prefix):
149
- # The rest of the 'url' is just the file id
150
- resource = self.resources[url.toString().removeprefix(LinkTransformer.transformed_url_prefix)]
151
-
152
- await self.data_manager.download_resource(resource)
153
- QDesktopServices.openUrl(QUrl.fromLocalFile(resource.download_location.absolute()))
154
- else:
155
- QDesktopServices.openUrl(url)
156
-
157
- @asyncSlot(QTreeWidgetItem, int)
158
- async def download_file_from_file_pane(self, item: QTreeWidgetItem, _: int):
159
- if isinstance(item, FileRow):
160
- await self.data_manager.download_resource(item.resource)
161
- QDesktopServices.openUrl(QUrl.fromLocalFile(item.resource.download_location.absolute()))
162
-
163
- @asyncSlot()
164
- async def sync_data(self):
165
- self.sync_button.setEnabled(False)
166
- self.sync_button.setText("Synchronizing")
167
- try:
168
- await self.data_manager.synchronize_with_canvas(StatusBarReporter(self.statusBar()))
169
- await self.load_course_list()
170
-
171
- finally:
172
- self.sync_button.setEnabled(True)
173
- self.sync_button.setText("Synchronize")
174
-
175
- @asyncSlot()
176
- async def load_course_list(self):
177
- self.courses = (await self.data_manager.get_data())
178
- self.selected_course = None
179
- self.resources.clear()
180
-
181
- for course in self.courses:
182
- self.resources.update({resource.id: resource for resource in course.resources})
183
-
184
- self.course_list.load_course_list(self.courses)
185
-
186
- @asyncSlot()
187
- async def check_for_update(self):
188
- try:
189
- newer_version, installed_version = await self_updater.get_newer_version()
190
-
191
- if newer_version is not None and newer_version != settings.ignored_update:
192
- msg_box = QMessageBox(
193
- QMessageBox.Icon.Question,
194
- "Update available",
195
- f"There is an update available ({installed_version} -> {newer_version})\nDo you want to update?\nThe program will close after the update is finished.",
196
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
197
- self
198
- )
199
-
200
- def ignore_update():
201
- settings.ignored_update = newer_version
202
-
203
- msg_box.accepted.connect(self.do_self_update)
204
- msg_box.rejected.connect(ignore_update)
205
- msg_box.show()
206
- else:
207
- print("No update available (or skipping this update)")
208
- except BaseException as e:
209
- sys.stderr.write(f"Could not check for updates: {e}\n")
210
- traceback.print_exc()
211
- sys.stderr.write("This can be ignored if in a dev environment\n")
212
-
213
- @asyncSlot()
214
- async def do_self_update(self):
215
- try:
216
- progress_diag = QProgressDialog("Updating", None, 0, 0, self)
217
- progress_diag.setWindowTitle(app_name)
218
- progress_diag.show()
219
- await self_updater.do_update()
220
- self.close()
221
- except BaseException as e:
222
- traceback.print_exc()
223
-
224
- QMessageBox(
225
- QMessageBox.Icon.Critical,
226
- "Error",
227
- "An error occurred during the update",
228
- parent=self
229
- ).show()
230
-
231
- @Slot(db.Course)
232
- def on_course_selected(self, course: Optional[db.Course]):
233
- if course is not None:
234
- self.selected_course = course
235
- # todo these should really be slots connected to this signal...
236
- self.pages_viewer.fill_tree(course)
237
- self.assignment_viewer.fill_tree(course)
238
- self.file_viewer.load_course_files(course)
239
- self.course_name_label.setText(course.name)
240
- else:
241
- self.selected_course = None
242
- self.file_viewer.clear()
243
- self.pages_viewer.clear()
244
- self.assignment_viewer.clear()
245
- self.course_name_label.setText(_no_course_selected_text)
246
-
247
- @asyncSlot(db.CoursePreferences)
248
- async def on_grouping_preference_changed(self):
249
- self.file_viewer.load_course_files(self.selected_course)
File without changes
@@ -1,61 +0,0 @@
1
- from PySide6.QtCore import Slot, Signal
2
- from PySide6.QtWidgets import QMenu, QWidget
3
- from qasync import asyncSlot
4
-
5
- import qcanvas.db as db
6
- from qcanvas.util.course_indexer import DataManager
7
- from qcanvas.util.helpers.qaction_helper import create_qaction
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)
@@ -1,39 +0,0 @@
1
- from PySide6.QtGui import QActionGroup
2
- from PySide6.QtWidgets import QMenu, QWidget
3
-
4
- from qcanvas.util.app_settings import settings
5
- from qcanvas.util.helpers import theme_helper
6
- from qcanvas.util.helpers.qaction_helper import create_qaction
7
-
8
-
9
- def change_theme(theme_name: str):
10
- settings.theme = theme_name
11
- theme_helper.apply_selected_theme()
12
-
13
-
14
- class ThemeSelectionMenu(QMenu):
15
- def __init__(self, parent: QWidget | None = None):
16
- super().__init__("Theme", parent)
17
-
18
- action_group = QActionGroup(self)
19
-
20
- light_theme = self._create_action("Light", "light")
21
- dark_theme = self._create_action("Dark", "dark")
22
- auto_theme = self._create_action("Auto (YMMV)", "auto")
23
- native_theme = self._create_action("Native (requires restart)", "native")
24
-
25
- actions = [light_theme, dark_theme, auto_theme, native_theme]
26
-
27
- self.addActions(actions)
28
-
29
- for theme in actions:
30
- action_group.addAction(theme)
31
-
32
- def _create_action(self, text: str, theme_name: str):
33
- return create_qaction(
34
- name=text,
35
- parent=self,
36
- triggered=lambda: change_theme(theme_name),
37
- checkable=True,
38
- checked=settings.theme == theme_name
39
- )
@@ -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