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.

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 +6 -6
  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.post1.dist-info/METADATA +59 -0
  59. qcanvas-1.0.3.post1.dist-info/RECORD +64 -0
  60. {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post1.dist-info}/WHEEL +1 -1
  61. qcanvas-1.0.3.post1.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 -338
  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 -251
  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 -48
  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.7a0.dist-info/METADATA +0 -21
  113. qcanvas-0.0.5.7a0.dist-info/RECORD +0 -62
  114. /qcanvas/{net → ui/main_ui}/__init__.py +0 -0
@@ -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)
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(["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)
@@ -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()