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.
- qcanvas/app_start/__init__.py +47 -0
- qcanvas/backend_connectors/__init__.py +2 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
- qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
- qcanvas/icons/__init__.py +6 -0
- qcanvas/icons/file-download-failed.svg +6 -0
- qcanvas/icons/file-downloaded.svg +6 -0
- qcanvas/icons/file-not-downloaded.svg +6 -0
- qcanvas/icons/file-unknown.svg +6 -0
- qcanvas/icons/icons.qrc +4 -0
- qcanvas/icons/main_icon.svg +7 -7
- qcanvas/icons/rc_icons.py +580 -214
- qcanvas/icons/sync.svg +7 -0
- qcanvas/run.py +29 -0
- qcanvas/ui/course_viewer/__init__.py +2 -0
- qcanvas/ui/course_viewer/content_tree.py +123 -0
- qcanvas/ui/course_viewer/course_tree.py +93 -0
- qcanvas/ui/course_viewer/course_viewer.py +62 -0
- qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
- qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
- qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
- qcanvas/ui/course_viewer/tabs/util.py +1 -0
- qcanvas/ui/main_ui/course_viewer_container.py +52 -0
- qcanvas/ui/main_ui/options/__init__.py +3 -0
- qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
- qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
- qcanvas/ui/main_ui/qcanvas_window.py +192 -0
- qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
- qcanvas/ui/memory_tree/__init__.py +2 -0
- qcanvas/ui/memory_tree/_tree_memory.py +66 -0
- qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
- qcanvas/ui/setup/__init__.py +2 -0
- qcanvas/ui/setup/setup_checker.py +17 -0
- qcanvas/ui/setup/setup_dialog.py +212 -0
- qcanvas/util/__init__.py +2 -0
- qcanvas/util/basic_fonts.py +12 -0
- qcanvas/util/fe_resource_manager.py +23 -0
- qcanvas/util/html_cleaner.py +25 -0
- qcanvas/util/layouts.py +52 -0
- qcanvas/util/logs.py +6 -0
- qcanvas/util/paths.py +41 -0
- qcanvas/util/settings/__init__.py +9 -0
- qcanvas/util/settings/_client_settings.py +29 -0
- qcanvas/util/settings/_mapped_setting.py +63 -0
- qcanvas/util/settings/_ui_settings.py +34 -0
- qcanvas/util/ui_tools.py +41 -0
- qcanvas/util/url_checker.py +13 -0
- qcanvas-1.0.3.post0.dist-info/METADATA +61 -0
- qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
- {qcanvas-0.0.5.6a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
- qcanvas-1.0.3.post0.dist-info/entry_points.txt +3 -0
- qcanvas/__main__.py +0 -155
- qcanvas/db/__init__.py +0 -5
- qcanvas/db/database.py +0 -337
- qcanvas/db/db_converter_helper.py +0 -81
- qcanvas/net/canvas/__init__.py +0 -2
- qcanvas/net/canvas/canvas_client.py +0 -209
- qcanvas/net/canvas/legacy_canvas_types.py +0 -124
- qcanvas/net/custom_httpx_async_transport.py +0 -34
- qcanvas/net/self_authenticating.py +0 -108
- qcanvas/queries/__init__.py +0 -4
- qcanvas/queries/all_courses.gql +0 -7
- qcanvas/queries/all_courses.py +0 -108
- qcanvas/queries/canvas_course_data.gql +0 -51
- qcanvas/queries/canvas_course_data.py +0 -143
- qcanvas/ui/container_item.py +0 -11
- qcanvas/ui/main_ui.py +0 -249
- qcanvas/ui/menu_bar/__init__.py +0 -0
- qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
- qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
- qcanvas/ui/setup_dialog.py +0 -190
- qcanvas/ui/status_bar_reporter.py +0 -40
- qcanvas/ui/viewer/__init__.py +0 -0
- qcanvas/ui/viewer/course_list.py +0 -96
- qcanvas/ui/viewer/file_list.py +0 -195
- qcanvas/ui/viewer/file_view_tab.py +0 -62
- qcanvas/ui/viewer/page_list_viewer.py +0 -150
- qcanvas/util/app_settings.py +0 -98
- qcanvas/util/constants.py +0 -5
- qcanvas/util/course_indexer/__init__.py +0 -1
- qcanvas/util/course_indexer/conversion_helpers.py +0 -78
- qcanvas/util/course_indexer/data_manager.py +0 -447
- qcanvas/util/course_indexer/resource_helpers.py +0 -191
- qcanvas/util/download_pool.py +0 -58
- qcanvas/util/helpers/__init__.py +0 -0
- qcanvas/util/helpers/canvas_sanitiser.py +0 -47
- qcanvas/util/helpers/file_icon_helper.py +0 -34
- qcanvas/util/helpers/qaction_helper.py +0 -25
- qcanvas/util/helpers/theme_helper.py +0 -45
- qcanvas/util/link_scanner/__init__.py +0 -2
- qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
- qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
- qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
- qcanvas/util/link_scanner/resource_scanner.py +0 -69
- qcanvas/util/progress_reporter.py +0 -101
- qcanvas/util/self_updater.py +0 -55
- qcanvas/util/task_pool.py +0 -253
- qcanvas/util/tree_util/__init__.py +0 -3
- qcanvas/util/tree_util/expanding_tree.py +0 -165
- qcanvas/util/tree_util/model_helpers.py +0 -36
- qcanvas/util/tree_util/tree_model.py +0 -85
- qcanvas-0.0.5.6a0.dist-info/METADATA +0 -21
- qcanvas-0.0.5.6a0.dist-info/RECORD +0 -61
- /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)
|
qcanvas/ui/viewer/__init__.py
DELETED
|
File without changes
|
qcanvas/ui/viewer/course_list.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from typing import Sequence
|
|
3
|
-
|
|
4
|
-
from PySide6.QtCore import QItemSelection, Slot, Signal, QObject
|
|
5
|
-
from PySide6.QtGui import QStandardItemModel, QStandardItem
|
|
6
|
-
from PySide6.QtWidgets import QTreeView
|
|
7
|
-
from qasync import asyncSlot
|
|
8
|
-
|
|
9
|
-
import qcanvas.db as db
|
|
10
|
-
from qcanvas.util.course_indexer import DataManager
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class CourseNode(QStandardItem, QObject):
|
|
14
|
-
name_changed = Signal(db.Course, str)
|
|
15
|
-
|
|
16
|
-
def __init__(self, course: db.Course):
|
|
17
|
-
QObject.__init__(self)
|
|
18
|
-
QStandardItem.__init__(self, course.preferences.local_name or course.name)
|
|
19
|
-
self.course = course
|
|
20
|
-
|
|
21
|
-
def setData(self, value, role=...):
|
|
22
|
-
if isinstance(value, str):
|
|
23
|
-
value = value.strip()
|
|
24
|
-
|
|
25
|
-
if len(value) == 0:
|
|
26
|
-
super().setData(self.course.name, role)
|
|
27
|
-
self.name_changed.emit(self.course, None)
|
|
28
|
-
else:
|
|
29
|
-
super().setData(value, role)
|
|
30
|
-
self.name_changed.emit(self.course, value)
|
|
31
|
-
else:
|
|
32
|
-
super().setData(value, role)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class CourseList(QTreeView):
|
|
36
|
-
course_selected = Signal(db.Course)
|
|
37
|
-
|
|
38
|
-
def __init__(self, data_manager: DataManager):
|
|
39
|
-
super().__init__()
|
|
40
|
-
self.data_manager = data_manager
|
|
41
|
-
self.model = QStandardItemModel()
|
|
42
|
-
self.setModel(self.model)
|
|
43
|
-
self.selectionModel().selectionChanged.connect(self._on_selection_changed)
|
|
44
|
-
|
|
45
|
-
@asyncSlot(db.Course, str)
|
|
46
|
-
async def course_name_changed(self, course: db.Course, name: str):
|
|
47
|
-
course.preferences.local_name = name
|
|
48
|
-
await self.data_manager.update_item(course.preferences)
|
|
49
|
-
|
|
50
|
-
def load_course_list(self, courses: Sequence[db.Course]):
|
|
51
|
-
self.model.clear()
|
|
52
|
-
self.model.setHorizontalHeaderLabels(["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)
|
qcanvas/ui/viewer/file_list.py
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
from typing import Sequence
|
|
2
|
-
|
|
3
|
-
from PySide6.QtCore import QObject, Slot, Qt
|
|
4
|
-
from PySide6.QtWidgets import QStyledItemDelegate, QStyleOptionProgressBar, QApplication, QStyle, \
|
|
5
|
-
QHeaderView, QStyleOptionViewItem, QTreeWidget, QTreeWidgetItem
|
|
6
|
-
|
|
7
|
-
import qcanvas.db.database as db
|
|
8
|
-
from qcanvas.util.download_pool import DownloadPool
|
|
9
|
-
from qcanvas.util.helpers import file_icon_helper
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# https://code.whatever.social/questions/1094841/get-human-readable-version-of-file-size#1094933
|
|
13
|
-
def sizeof_fmt(num, suffix="B"):
|
|
14
|
-
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
|
|
15
|
-
if abs(num) < 1024.0:
|
|
16
|
-
return f"{num:3.1f}{unit}{suffix}"
|
|
17
|
-
num /= 1024.0
|
|
18
|
-
return f"{num:.1f}Yi{suffix}"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class FileColumnDelegate(QStyledItemDelegate):
|
|
22
|
-
def __init__(self, tree: QTreeWidget, download_pool: DownloadPool, parent, *args, **kwargs):
|
|
23
|
-
super().__init__(parent, *args, **kwargs)
|
|
24
|
-
self.download_pool = download_pool
|
|
25
|
-
self.tree = tree
|
|
26
|
-
|
|
27
|
-
def paint(self, painter, option, index) -> None:
|
|
28
|
-
item = self.tree.itemFromIndex(index)
|
|
29
|
-
|
|
30
|
-
# Check that the item is actually a file row and not already downloaded
|
|
31
|
-
if not isinstance(item, FileRow) or item.resource.state == db.ResourceState.DOWNLOADED:
|
|
32
|
-
return super().paint(painter, option, index)
|
|
33
|
-
|
|
34
|
-
resource = item.resource
|
|
35
|
-
progress = item.download_progres
|
|
36
|
-
|
|
37
|
-
if progress == resource.file_size:
|
|
38
|
-
return super().paint(painter, option, index)
|
|
39
|
-
|
|
40
|
-
# Check that the file is not currently downloading (signaled by progress = -1)
|
|
41
|
-
if progress < 0:
|
|
42
|
-
# Show the state of the file instead (downloaded, failed, not downloaded)
|
|
43
|
-
view_item = QStyleOptionViewItem(option)
|
|
44
|
-
self.initStyleOption(view_item, index)
|
|
45
|
-
view_item.text = db.ResourceState.human_readable(resource.state)
|
|
46
|
-
|
|
47
|
-
return QApplication.style().drawControl(QStyle.CE_ItemViewItem, view_item, painter, view_item.widget)
|
|
48
|
-
else:
|
|
49
|
-
# otherwise show progress bar
|
|
50
|
-
progress_bar = QStyleOptionProgressBar()
|
|
51
|
-
progress_bar.rect = option.rect
|
|
52
|
-
progress_bar.minimum = 0
|
|
53
|
-
progress_bar.maximum = resource.file_size
|
|
54
|
-
progress_bar.progress = progress
|
|
55
|
-
# Display download progress as a percentage
|
|
56
|
-
progress_bar.text = "{0:.0f}%".format((progress / resource.file_size) * 100)
|
|
57
|
-
progress_bar.textVisible = True
|
|
58
|
-
|
|
59
|
-
QApplication.style().drawControl(QStyle.CE_ProgressBar, progress_bar, painter)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class FileRow(QTreeWidgetItem):
|
|
63
|
-
def __init__(self, resource: db.Resource):
|
|
64
|
-
# Set column names
|
|
65
|
-
super().__init__(
|
|
66
|
-
[
|
|
67
|
-
resource.file_name,
|
|
68
|
-
resource.date_discovered.strftime("%Y-%m-%d"),
|
|
69
|
-
sizeof_fmt(resource.file_size),
|
|
70
|
-
]
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Set the icon for this file, displayed in the name column
|
|
74
|
-
self.setIcon(0, file_icon_helper.icon_for_filename(resource.file_name))
|
|
75
|
-
|
|
76
|
-
self.resource = resource
|
|
77
|
-
# This is used to pass the download progress value from download_progress_updated to FileColumnDelegate. This way
|
|
78
|
-
# it means we don't have to get the download progress from the DownloadPool a second time with an async function
|
|
79
|
-
# (which is undesirable from an item delegate...). Use -1 to show that the file is not currently downloading.
|
|
80
|
-
self.download_progres = -1
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class FileList(QTreeWidget):
|
|
84
|
-
def __init__(self, download_pool: DownloadPool, parent: QObject | None = None):
|
|
85
|
-
super().__init__(parent)
|
|
86
|
-
self._setup_header()
|
|
87
|
-
|
|
88
|
-
# Keep a map of file ids to rows, so we can actually update the download column using a file id.
|
|
89
|
-
# This has to be a list or else files that are in multiple modules/pages will not be updated properly on the tree
|
|
90
|
-
self.row_id_map: dict[str, list[FileRow]] = {}
|
|
91
|
-
self.download_pool = download_pool
|
|
92
|
-
self.setAlternatingRowColors(True)
|
|
93
|
-
# Add the download column renderer
|
|
94
|
-
self.setItemDelegateForColumn(3, FileColumnDelegate(tree=self, download_pool=download_pool, parent=None))
|
|
95
|
-
|
|
96
|
-
# Connect download update events
|
|
97
|
-
self.download_pool.download_progress_updated.connect(self._file_download_progress_update)
|
|
98
|
-
self.download_pool.download_failed.connect(self._file_download_failed_update)
|
|
99
|
-
|
|
100
|
-
@Slot(str, int)
|
|
101
|
-
def _file_download_progress_update(self, resource_id: str, progress: int):
|
|
102
|
-
# Ignore files that aren't in this tree
|
|
103
|
-
if resource_id not in self.row_id_map:
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
rows = self.row_id_map[resource_id]
|
|
107
|
-
|
|
108
|
-
# Update each row that maps to this file.
|
|
109
|
-
# Note that a file may belong to multiple modules or pages
|
|
110
|
-
for row in rows:
|
|
111
|
-
# Update the progress
|
|
112
|
-
row.download_progres = progress
|
|
113
|
-
|
|
114
|
-
# Get the download column for the relevant item
|
|
115
|
-
index = self.indexFromItem(row, 3)
|
|
116
|
-
|
|
117
|
-
# Update the download column
|
|
118
|
-
self.model().dataChanged.emit(index, index, Qt.ItemDataRole.DisplayRole)
|
|
119
|
-
|
|
120
|
-
@Slot(str)
|
|
121
|
-
def _file_download_failed_update(self, resource_id: str):
|
|
122
|
-
if resource_id not in self.row_id_map:
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
rows = self.row_id_map[resource_id]
|
|
126
|
-
|
|
127
|
-
for row in rows:
|
|
128
|
-
# Get the download column for the relevant item
|
|
129
|
-
index = self.indexFromItem(row, 3)
|
|
130
|
-
|
|
131
|
-
# Update the download column
|
|
132
|
-
self.model().dataChanged.emit(index, index, Qt.ItemDataRole.DisplayRole)
|
|
133
|
-
|
|
134
|
-
def _setup_header(self):
|
|
135
|
-
self.setColumnCount(4)
|
|
136
|
-
self.setHeaderLabels(["Name", "Date Found", "Size", "Download"])
|
|
137
|
-
|
|
138
|
-
header = self.header()
|
|
139
|
-
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
140
|
-
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
141
|
-
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
|
142
|
-
# Make the download progress column not try to take up all remaining space in the tree, as it just looks weird
|
|
143
|
-
# and decreases room for more important data
|
|
144
|
-
header.setStretchLastSection(0)
|
|
145
|
-
|
|
146
|
-
def load_items(self, items: Sequence[db.ModuleItem | db.Module]):
|
|
147
|
-
self.clear()
|
|
148
|
-
|
|
149
|
-
groups = []
|
|
150
|
-
|
|
151
|
-
for item in items:
|
|
152
|
-
# Get the resources depending on the type of the item
|
|
153
|
-
if isinstance(item, (db.PageLike, db.ModuleItem)):
|
|
154
|
-
resources = item.resources
|
|
155
|
-
elif isinstance(item, db.Module):
|
|
156
|
-
resources = []
|
|
157
|
-
|
|
158
|
-
for module_item in item.items:
|
|
159
|
-
resources.extend(module_item.resources)
|
|
160
|
-
else:
|
|
161
|
-
continue
|
|
162
|
-
|
|
163
|
-
# Avoid adding groups with no files in them, it just adds clutter
|
|
164
|
-
if len(resources) == 0:
|
|
165
|
-
continue
|
|
166
|
-
|
|
167
|
-
# Create the group node for this item
|
|
168
|
-
group_node = QTreeWidgetItem([item.name])
|
|
169
|
-
|
|
170
|
-
# fixme this does not remove duplicate files e.g. when on module groups
|
|
171
|
-
|
|
172
|
-
for resource in resources:
|
|
173
|
-
row = FileRow(resource)
|
|
174
|
-
|
|
175
|
-
# Put the file into the row id map so we can use the file id provided by download_progress_updated
|
|
176
|
-
# and such to update the download progress in the tree
|
|
177
|
-
if resource.id not in self.row_id_map:
|
|
178
|
-
self.row_id_map[resource.id] = [row]
|
|
179
|
-
else:
|
|
180
|
-
self.row_id_map[resource.id].append(row)
|
|
181
|
-
|
|
182
|
-
# Add the row to the group
|
|
183
|
-
group_node.addChild(row)
|
|
184
|
-
|
|
185
|
-
# Add the module/page to the list
|
|
186
|
-
groups.append(group_node)
|
|
187
|
-
|
|
188
|
-
# Add all the items and expand them
|
|
189
|
-
self.insertTopLevelItems(0, groups)
|
|
190
|
-
self.expandAll()
|
|
191
|
-
|
|
192
|
-
def clear(self):
|
|
193
|
-
super().clear()
|
|
194
|
-
self._setup_header()
|
|
195
|
-
self.row_id_map.clear()
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from typing import Sequence
|
|
2
|
-
|
|
3
|
-
from PySide6.QtWidgets import * # QWidget, QTreeView, QGroupBox, QBoxLayout, QHeaderView, QHBoxLayout, QComboBox, QLabel
|
|
4
|
-
|
|
5
|
-
import qcanvas.db as db
|
|
6
|
-
from qcanvas.ui.viewer.file_list import FileList
|
|
7
|
-
from qcanvas.util.constants import default_assignments_module_names
|
|
8
|
-
from qcanvas.util.download_pool import DownloadPool
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class FileColumn(QGroupBox):
|
|
12
|
-
def __init__(self, column_name, download_pool: DownloadPool):
|
|
13
|
-
super().__init__(title=column_name)
|
|
14
|
-
|
|
15
|
-
self.tree = FileList(download_pool)
|
|
16
|
-
self.setLayout(QBoxLayout(QBoxLayout.Direction.TopToBottom))
|
|
17
|
-
self.layout().addWidget(self.tree)
|
|
18
|
-
|
|
19
|
-
def load_items(self, items: Sequence[db.ModuleItem | db.Module]):
|
|
20
|
-
self.tree.load_items(items)
|
|
21
|
-
|
|
22
|
-
def clear(self):
|
|
23
|
-
self.tree.clear()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class FileViewTab(QWidget):
|
|
27
|
-
|
|
28
|
-
def __init__(self, download_pool: DownloadPool):
|
|
29
|
-
super().__init__()
|
|
30
|
-
|
|
31
|
-
self.files_column = FileColumn("Files", download_pool)
|
|
32
|
-
self.assignment_files_column = FileColumn("Assignment files", download_pool)
|
|
33
|
-
|
|
34
|
-
layout = QHBoxLayout()
|
|
35
|
-
layout.addWidget(self.files_column)
|
|
36
|
-
layout.addWidget(self.assignment_files_column)
|
|
37
|
-
|
|
38
|
-
self.setLayout(layout)
|
|
39
|
-
|
|
40
|
-
def load_course_files(self, course: db.Course):
|
|
41
|
-
module_items: list[db.ModuleItem] = []
|
|
42
|
-
assignment_items: list[db.ModuleItem] = []
|
|
43
|
-
|
|
44
|
-
for module_item in course.module_items:
|
|
45
|
-
if module_item.module.name.lower() in default_assignments_module_names:
|
|
46
|
-
assignment_items.append(module_item)
|
|
47
|
-
else:
|
|
48
|
-
module_items.append(module_item)
|
|
49
|
-
|
|
50
|
-
if course.preferences.files_group_by_preference == db.GroupByPreference.GROUP_BY_MODULES:
|
|
51
|
-
exclude_assignments_module = list(
|
|
52
|
-
filter(lambda x: x.name.lower() not in default_assignments_module_names, course.modules))
|
|
53
|
-
|
|
54
|
-
self.files_column.load_items(exclude_assignments_module)
|
|
55
|
-
else:
|
|
56
|
-
self.files_column.load_items(module_items)
|
|
57
|
-
|
|
58
|
-
self.assignment_files_column.load_items(assignment_items + course.assignments)
|
|
59
|
-
|
|
60
|
-
def clear(self):
|
|
61
|
-
self.files_column.clear()
|
|
62
|
-
self.assignment_files_column.clear()
|
|
@@ -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))
|
qcanvas/util/app_settings.py
DELETED
|
@@ -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 +0,0 @@
|
|
|
1
|
-
from .data_manager import DataManager
|