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,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)
File without changes
@@ -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(["Course"])
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)
@@ -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()
@@ -1,150 +0,0 @@
1
- from abc import abstractmethod
2
- from typing import Sequence
3
-
4
- from PySide6.QtCore import QItemSelection, Slot
5
- from PySide6.QtGui import QStandardItemModel
6
- from PySide6.QtWidgets import QWidget, QTextBrowser, QTreeView, QHBoxLayout
7
- from bs4 import BeautifulSoup
8
-
9
- import qcanvas.db as db
10
- from qcanvas.ui.container_item import ContainerItem
11
- from qcanvas.util.constants import default_assignments_module_names
12
- from qcanvas.util.course_indexer import resource_helpers
13
- from qcanvas.util.helpers import canvas_sanitiser
14
- from qcanvas.util.link_scanner import ResourceScanner
15
-
16
-
17
- class LinkTransformer:
18
- # This is used to indicate that a "link" is actually a resource. The resource id is concatenated to this string.
19
- # It just has to be a valid url or qt does not send it to anchorClicked properly
20
- transformed_url_prefix = "data:,"
21
-
22
- def __init__(self, link_scanners: Sequence[ResourceScanner], files: dict[str, db.Resource]):
23
- self.link_scanners = link_scanners
24
- self.files = files
25
-
26
- def transform_links(self, html: str):
27
- doc = BeautifulSoup(html, 'html.parser')
28
-
29
- for element in doc.find_all(resource_helpers.resource_elements):
30
- for scanner in self.link_scanners:
31
- if scanner.accepts_link(element):
32
- resource_id = f"{scanner.name}:{scanner.extract_id(element)}"
33
- # todo make images actually show on the viewer page if they're downloaded
34
- if resource_id in self.files:
35
- file = self.files[resource_id]
36
-
37
- substitute = doc.new_tag(name="a")
38
- # Put the file id on the end of the url so we don't have to use the scanners to extract an id again..
39
- # The actual url doesn't matter
40
- substitute.attrs["href"] = f"{self.transformed_url_prefix}{file.id}"
41
- substitute.string = f"{file.file_name} ({db.ResourceState.human_readable(file.state)})"
42
-
43
- element.replace_with(substitute)
44
- else:
45
- if element.string is not None:
46
- element.string += " (Failed to index)"
47
-
48
- break
49
-
50
- return str(doc)
51
-
52
-
53
- class PageLikeViewer(QWidget):
54
- def __init__(self, header_name: str, link_transformer: LinkTransformer):
55
- super().__init__()
56
- self.viewer = QTextBrowser()
57
- self.viewer.setOpenLinks(False)
58
-
59
- # todo just use QTreeWidget instead
60
- self.tree = QTreeView()
61
- self.model = QStandardItemModel()
62
- self.header_name = header_name
63
- self.link_transformer = link_transformer
64
-
65
- self.tree.setModel(self.model)
66
- self.tree.selectionModel().selectionChanged.connect(self.on_item_clicked)
67
- self.model.setHorizontalHeaderLabels([self.header_name])
68
-
69
- layout = QHBoxLayout()
70
- layout.addWidget(self.tree)
71
- layout.addWidget(self.viewer)
72
- layout.setStretch(1, 1)
73
- self.setLayout(layout)
74
-
75
- def fill_tree(self, data: db.Course):
76
- self.model.clear()
77
- self.viewer.clear()
78
- self._internal_fill_tree(data)
79
- self.model.setHorizontalHeaderLabels([self.header_name])
80
- self.tree.expandAll()
81
-
82
- def clear(self):
83
- self.model.clear()
84
- self.viewer.clear()
85
- self.model.setHorizontalHeaderLabels([self.header_name])
86
-
87
- @abstractmethod
88
- def _internal_fill_tree(self, data: db.Course):
89
- ...
90
-
91
- @Slot(QItemSelection, QItemSelection)
92
- def on_item_clicked(self, selected: QItemSelection, deselected: QItemSelection):
93
- if len(self.tree.selectedIndexes()) == 0:
94
- return
95
-
96
- node = self.model.itemFromIndex(self.tree.selectedIndexes()[0])
97
-
98
- if isinstance(node, ContainerItem):
99
- item = node.content
100
-
101
- if isinstance(item, db.PageLike):
102
- if item.content is None:
103
- return
104
-
105
- # todo when a file is finished downloading it would be nice if the page was refreshed to show the state properly
106
- html = canvas_sanitiser.remove_stylesheets_from_html(item.content)
107
- self.viewer.setHtml(self.link_transformer.transform_links(html))
108
-
109
-
110
- class PagesViewer(PageLikeViewer):
111
- def __init__(self, link_transformer: LinkTransformer):
112
- super().__init__("Pages", link_transformer)
113
-
114
- def _internal_fill_tree(self, course: db.Course):
115
- root = self.model.invisibleRootItem()
116
-
117
- for module in course.modules:
118
- if module.name.lower() in default_assignments_module_names:
119
- continue
120
-
121
- module_node = ContainerItem(module)
122
- module_node.setSelectable(False)
123
-
124
- for module_item in list[db.ModuleItem](module.items):
125
- module_node.appendRow(ContainerItem(module_item))
126
-
127
- root.appendRow(module_node)
128
-
129
-
130
- class AssignmentsViewer(PageLikeViewer):
131
-
132
- def __init__(self, link_transformer: LinkTransformer):
133
- super().__init__("Putting the ASS in assignments", link_transformer)
134
-
135
- def _internal_fill_tree(self, course: db.Course):
136
- root = self.model.invisibleRootItem()
137
-
138
- default_assessments_module = None
139
-
140
- for module in course.modules:
141
- if module.name.lower() in default_assignments_module_names:
142
- default_assessments_module = module
143
- break
144
-
145
- if default_assessments_module is not None:
146
- for module_item in default_assessments_module.items:
147
- root.appendRow(ContainerItem(module_item))
148
-
149
- for assignment in course.assignments:
150
- root.appendRow(ContainerItem(assignment))
@@ -1,98 +0,0 @@
1
- from typing import TypeVar, Generic
2
-
3
- from PySide6.QtCore import QSettings
4
- from packaging.version import Version
5
-
6
- default_theme = "light"
7
-
8
-
9
- def ensure_theme_is_valid(theme: str) -> str:
10
- """
11
- Ensures that a theme name is valid.
12
- If it is invalid, the default theme ("light") is returned
13
- Parameters
14
- ----------
15
- theme
16
- The theme name
17
- Returns
18
- -------
19
- str
20
- A valid theme name
21
- """
22
- if theme not in ["auto", "light", "dark", "native"]:
23
- return default_theme
24
- else:
25
- return theme
26
-
27
-
28
- T = TypeVar("T")
29
-
30
-
31
- class MappedSetting(Generic[T]):
32
- """
33
- Acts as a proxy for a named value in a QSettings object.
34
- Stores the value in memory when initialised and updates it accordingly, to protect it from changes on disk.
35
- """
36
-
37
- def __init__(self, settings_object: QSettings, setting_name: str, default: T | None = None):
38
- self.settings_object = settings_object
39
- self.setting_name = setting_name
40
- self.value = self.settings_object.value(self.setting_name, default)
41
-
42
- def __get__(self, instance, owner):
43
- return self.value
44
-
45
- def __set__(self, instance, value) -> None:
46
- self.value = value
47
- self.settings_object.setValue(self.setting_name, value)
48
-
49
-
50
- class ThemeSetting(MappedSetting):
51
- def __init__(self, settings_object: QSettings):
52
- super().__init__(settings_object, "theme", default_theme)
53
-
54
- def __get__(self, instance, owner):
55
- return ensure_theme_is_valid(super().__get__(instance, owner))
56
-
57
- def __set__(self, instance, value):
58
- super().__set__(instance, ensure_theme_is_valid(value))
59
-
60
-
61
- class _AppSettings:
62
- """
63
- Attributes
64
- ----------
65
- settings : QSettings
66
- Primary settings map for client settings
67
- auxiliary : QSettings
68
- Secondary settings map for settings which aren't related to canvas/panopto client functionality
69
- ignored_update
70
- If there is an update available and the user chooses not to update, then that version will be stored in here and
71
- the user will not be asked to update to that version again
72
- geometry
73
- Used to restore the main window position when it is re-opened
74
- window_state
75
- Used to restore the main window position when it is re-opened
76
- theme
77
- The theme of the app
78
- canvas_url
79
- The canvas url
80
- api_key
81
- The api key for canvas
82
- """
83
-
84
- settings = QSettings("QCanvas", "client")
85
- auxiliary = QSettings("QCanvas", "ui")
86
-
87
- canvas_url: MappedSetting[str] = MappedSetting(settings, "canvas_url")
88
- panopto_url: MappedSetting[str] = MappedSetting(settings, "panopto_url")
89
- api_key: MappedSetting[str] = MappedSetting(settings, "api_key")
90
-
91
- ignored_update: MappedSetting[Version] = MappedSetting(auxiliary, "ignored_update")
92
- theme: ThemeSetting = ThemeSetting(auxiliary)
93
- geometry = MappedSetting(auxiliary, "geometry")
94
- window_state = MappedSetting(auxiliary, "window_state")
95
-
96
-
97
- # Global _AppSettings instance
98
- settings = _AppSettings()
qcanvas/util/constants.py DELETED
@@ -1,5 +0,0 @@
1
- default_assignments_module_names = ["assessments", "assessment"]
2
- app_name = "QCanvas"
3
- package_name = "qcanvas"
4
- # Passed back to the launcher script to indicate that the program should be restarted (after an update)
5
- updated_and_needs_restart_return_code = 144
@@ -1 +0,0 @@
1
- from .data_manager import DataManager