qcanvas 0.0.5.7a0__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 +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.post0.dist-info/METADATA +61 -0
  59. qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
  60. {qcanvas-0.0.5.7a0.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 -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
@@ -0,0 +1,212 @@
1
+ import logging
2
+ from threading import Semaphore
3
+
4
+ from qasync import asyncSlot
5
+ from qcanvas_api_clients.canvas import CanvasClient, CanvasClientConfig
6
+ from qcanvas_api_clients.panopto import PanoptoClient, PanoptoClientConfig
7
+ from qcanvas_api_clients.util.request_exceptions import ConfigInvalidError
8
+ from qtpy.QtCore import QUrl, Signal, Slot
9
+ from qtpy.QtGui import QDesktopServices, QPixmap
10
+ from qtpy.QtWidgets import *
11
+
12
+ import qcanvas.util.settings as settings
13
+ from qcanvas import icons
14
+ from qcanvas.util import is_url
15
+ from qcanvas.util.layouts import grid_layout_widget, layout
16
+
17
+ _logger = logging.getLogger(__name__)
18
+
19
+ _tutorial_url = (
20
+ "https://www.iorad.com/player/2053777/Canvas---How-to-generate-an-access-token-"
21
+ )
22
+
23
+
24
+ class SetupDialog(QDialog):
25
+ closed = Signal()
26
+
27
+ def __init__(self):
28
+ super().__init__()
29
+
30
+ self.setWindowTitle("Configure QCanvas")
31
+ self.setMinimumSize(550, 200)
32
+ self.resize(550, 200)
33
+ self.setWindowIcon(QPixmap(icons.main_icon))
34
+
35
+ self._semaphore = Semaphore()
36
+ self._canvas_url_box = QLineEdit(settings.client.canvas_url)
37
+ self._canvas_api_key_box = QLineEdit(settings.client.canvas_api_key)
38
+ self._canvas_api_key_box.setEchoMode(QLineEdit.EchoMode.Password)
39
+ self._panopto_url_box = QLineEdit(settings.client.panopto_url)
40
+ self._button_box = self._setup_button_box()
41
+ self._button_box.accepted.connect(self._accepted)
42
+ self._button_box.helpRequested.connect(self._help_requested)
43
+ self._waiting_indicator = self._setup_progress_bar()
44
+
45
+ self.setLayout(
46
+ layout(
47
+ QVBoxLayout,
48
+ grid_layout_widget(
49
+ [
50
+ [QLabel("Canvas URL"), self._canvas_url_box],
51
+ [QLabel("Canvas API Key"), self._canvas_api_key_box],
52
+ [QLabel("Panopto URL"), self._panopto_url_box],
53
+ ]
54
+ ),
55
+ self._waiting_indicator,
56
+ self._button_box,
57
+ )
58
+ )
59
+
60
+ def _setup_button_box(self) -> QDialogButtonBox:
61
+ box = QDialogButtonBox()
62
+ box.addButton(QDialogButtonBox.StandardButton.Ok)
63
+ box.addButton("Get a canvas API key", QDialogButtonBox.ButtonRole.HelpRole)
64
+ return box
65
+
66
+ def _setup_progress_bar(self) -> QProgressBar:
67
+ progress = QProgressBar()
68
+ progress.setMaximum(0)
69
+ progress.setMinimum(0)
70
+ self._set_retain_size_when_hidden(progress)
71
+ progress.hide()
72
+ return progress
73
+
74
+ def _set_retain_size_when_hidden(self, widget: QWidget) -> None:
75
+ size_policy = widget.sizePolicy()
76
+ size_policy.setRetainSizeWhenHidden(True)
77
+ widget.setSizePolicy(size_policy)
78
+
79
+ # @Slot()
80
+ @asyncSlot()
81
+ async def _accepted(self) -> None:
82
+ if self._semaphore.acquire(False):
83
+ try:
84
+ self._clear_errors()
85
+ if self._check_inputs():
86
+ return
87
+
88
+ self._waiting_indicator.setVisible(True)
89
+
90
+ canvas_config = CanvasClientConfig(
91
+ api_token=self._canvas_api_key_box.text().strip(),
92
+ canvas_url=self._get_url(self._canvas_url_box),
93
+ )
94
+
95
+ if not await self._check_canvas_config(canvas_config):
96
+ return
97
+
98
+ if not await self._check_panopto_config(canvas_config):
99
+ self._show_panopto_help()
100
+ return
101
+
102
+ finally:
103
+ self._waiting_indicator.setVisible(False)
104
+ self._semaphore.release()
105
+
106
+ _logger.debug("Credentials are a-ok!")
107
+ self._save_and_close()
108
+ else:
109
+ _logger.debug("Validation already in progress")
110
+
111
+ def _clear_errors(self) -> None:
112
+ for line_edit in [
113
+ self._canvas_url_box,
114
+ self._panopto_url_box,
115
+ self._canvas_api_key_box,
116
+ ]:
117
+ line_edit.setStyleSheet(None)
118
+ line_edit.setToolTip(None)
119
+
120
+ def _check_inputs(self) -> bool:
121
+ had_error = False
122
+
123
+ if not is_url(self._get_url(self._canvas_url_box)):
124
+ had_error = True
125
+ self._show_error(self._canvas_url_box, "Canvas URL is invalid")
126
+ if len(self._canvas_api_key_box.text().strip()) == 0:
127
+ had_error = True
128
+ self._show_error(self._canvas_api_key_box, "Canvas API key is empty")
129
+ if not is_url(self._get_url(self._panopto_url_box)):
130
+ had_error = True
131
+ self._show_error(self._panopto_url_box, "Panopto URL is invalid")
132
+
133
+ return had_error
134
+
135
+ def _get_url(self, line_edit: QLineEdit) -> str:
136
+ url = line_edit.text().strip()
137
+
138
+ if not url.startswith("http"):
139
+ return "https://" + url
140
+ else:
141
+ return url
142
+
143
+ async def _check_canvas_config(self, canvas_config: CanvasClientConfig) -> bool:
144
+ try:
145
+ await CanvasClient.verify_config(canvas_config)
146
+ return True
147
+ except ConfigInvalidError:
148
+ self._show_error(self._canvas_api_key_box, "Canvas API key is invalid")
149
+ return False
150
+
151
+ def _show_error(self, line_edit: QLineEdit, text: str) -> None:
152
+ line_edit.setToolTip(text)
153
+ self._waiting_indicator.hide()
154
+ self._highlight_line_edit(line_edit)
155
+
156
+ def _highlight_line_edit(self, line_edit: QLineEdit) -> None:
157
+ line_edit.setStyleSheet("QLineEdit { border: 1px solid red }")
158
+
159
+ async def _check_panopto_config(self, canvas_config: CanvasClientConfig) -> bool:
160
+ client = CanvasClient(canvas_config)
161
+ try:
162
+ await PanoptoClient.verify_config(
163
+ PanoptoClientConfig(panopto_url=self._get_url(self._panopto_url_box)),
164
+ client,
165
+ )
166
+ return True
167
+ except ConfigInvalidError:
168
+ return False
169
+ finally:
170
+ await client.aclose()
171
+
172
+ def _show_panopto_help(self):
173
+ msg = QMessageBox(
174
+ QMessageBox.Icon.Information,
175
+ "Panopto Authentication",
176
+ "In order for QCanvas to use panopto, you need to link your panopto account to your canvas account. "
177
+ "A page will open in your web browser to do this when you click OK. It may ask you to sign into canvas.\n\n"
178
+ 'Please tick "Remember my authorisation for this service" or QCanvas may not function correctly.\n\n'
179
+ "QCanvas can't access anything entered in your browser.",
180
+ QMessageBox.StandardButton.Ok,
181
+ self,
182
+ )
183
+ msg.accepted.connect(self._open_panopto_login)
184
+ msg.show()
185
+
186
+ @Slot()
187
+ def _open_panopto_login(self) -> None:
188
+ url = QUrl(self._get_url(self._panopto_url_box))
189
+ url.setPath("/Panopto/Pages/Auth/Login.aspx")
190
+ url.setQuery("instance=Canvas&AllowBounce=true")
191
+ QDesktopServices.openUrl(url)
192
+
193
+ def _save_and_close(self) -> None:
194
+ settings.client.canvas_url = self._get_url(self._canvas_url_box)
195
+ settings.client.panopto_url = self._get_url(self._panopto_url_box)
196
+ settings.client.canvas_api_key = self._canvas_api_key_box.text().strip()
197
+ self.closed.emit()
198
+ self.close()
199
+
200
+ @Slot()
201
+ def _help_requested(self) -> None:
202
+ msg = QMessageBox(
203
+ QMessageBox.Icon.Information,
204
+ "Help",
205
+ "An interactive tutorial will open in your browser when you click OK.\n\n"
206
+ 'Note that the "purpose" text doesn\'t matter and you can enter anything you want.\n\n'
207
+ 'You should also leave the "expires" item blank if you want the key to last forever.\n\n'
208
+ "Don't share this key. You can revoke it at any time.",
209
+ parent=self,
210
+ )
211
+ msg.accepted.connect(lambda: QDesktopServices.openUrl(_tutorial_url))
212
+ msg.show()
qcanvas/util/__init__.py CHANGED
@@ -0,0 +1,2 @@
1
+ # todo remove this
2
+ from .url_checker import is_url
@@ -0,0 +1,12 @@
1
+ from qtpy.QtGui import QFont
2
+ from qtpy.QtWidgets import QLabel
3
+
4
+ normal_font = QFont()
5
+ bold_font = QFont()
6
+ bold_font.setBold(True)
7
+
8
+
9
+ def bold_label(text: str) -> QLabel:
10
+ result = QLabel(text)
11
+ result.setFont(bold_font)
12
+ return result
@@ -0,0 +1,23 @@
1
+ import logging
2
+
3
+ import qcanvas_backend.database.types as db
4
+ from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+
9
+ # todo stub for now
10
+ class _RM(ResourceManager):
11
+ def on_download_progress(self, resource: db.Resource, current: int, total: int):
12
+ if total == 0 and current == 0:
13
+ _logger.info(f"download of {resource.file_name}: ?%")
14
+ else:
15
+ _logger.info(
16
+ f"download of {resource.file_name}: {(current / total) * 100}%"
17
+ )
18
+
19
+ def on_download_failed(self, resource: db.Resource):
20
+ _logger.info(f"download of {resource.file_name} failed")
21
+
22
+ def on_download_finished(self, resource: db.Resource):
23
+ _logger.info(f"download of {resource.file_name} finished")
@@ -0,0 +1,25 @@
1
+ import logging
2
+ import re
3
+
4
+ from bs4 import BeautifulSoup, ResultSet
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+
9
+ def clean_up_html(html: str) -> str:
10
+ html = re.sub(r"background-color:\s?initial;?", "", html)
11
+ html = html.replace(" ", " ")
12
+
13
+ doc = BeautifulSoup(html, "html.parser")
14
+
15
+ # Remove all scripts and css
16
+ _remove_tags(doc.find_all(["link", "script"]))
17
+ # Remove font awesome icons (which don't load anyway)
18
+ _remove_tags(doc.find_all(["span"], class_=["dp-icon-content"]))
19
+
20
+ return str(doc)
21
+
22
+
23
+ def _remove_tags(tags: ResultSet) -> None:
24
+ for tag in tags:
25
+ tag.decompose()
@@ -0,0 +1,52 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ from qtpy.QtCore import Qt
5
+ from qtpy.QtWidgets import *
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class GridItem(NamedTuple):
13
+ widget: QWidget
14
+ col_span: int = 1
15
+ row_span: int = 1
16
+ alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignLeft
17
+
18
+
19
+ def layout_widget(layout_type: Type[T], *items: QWidget, **kwargs) -> QWidget:
20
+ widget = QWidget()
21
+ widget.setLayout(layout(layout_type, *items, **kwargs))
22
+ return widget
23
+
24
+
25
+ def layout(layout_type: Type[T], *items: QWidget, **kwargs) -> T:
26
+ result_layout: QLayout = layout_type(**kwargs)
27
+
28
+ for item in items:
29
+ result_layout.addWidget(item)
30
+
31
+ return result_layout
32
+
33
+
34
+ def grid_layout_widget(grid: Iterable[Iterable[QWidget | GridItem]]) -> QWidget:
35
+ widget = QWidget()
36
+ widget.setLayout(grid_layout(grid))
37
+ return widget
38
+
39
+
40
+ def grid_layout(grid: Iterable[Iterable[QWidget | GridItem]]) -> QGridLayout:
41
+ result_layout = QGridLayout()
42
+
43
+ for row, row_list in enumerate(grid):
44
+ for col, item in enumerate(row_list):
45
+ if isinstance(item, GridItem):
46
+ result_layout.addWidget(
47
+ item.widget, row, col, item.row_span, item.col_span, item.alignment
48
+ )
49
+ else:
50
+ result_layout.addWidget(item, row, col)
51
+
52
+ return result_layout
qcanvas/util/logs.py ADDED
@@ -0,0 +1,6 @@
1
+ import logging
2
+
3
+
4
+ def set_levels(levels: dict[str, int]) -> None:
5
+ for k, v in levels.items():
6
+ logging.getLogger(k).setLevel(v)
qcanvas/util/paths.py ADDED
@@ -0,0 +1,41 @@
1
+ import logging
2
+ import platform
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import platformdirs
7
+ from qtpy.QtCore import QDir, QSettings
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+ _is_running_on_windows = platform.system() == "Windows"
12
+ _is_running_on_linux = platform.system() == "Linux"
13
+ _is_running_as_pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
14
+
15
+
16
+ def client_settings() -> QSettings:
17
+ if _is_running_as_pyinstaller and _is_running_on_windows:
18
+ return QSettings(
19
+ str(Path(QDir.homePath()) / ".config" / "QCanvasTeam" / "canvas.ini"),
20
+ QSettings.Format.IniFormat,
21
+ )
22
+ else:
23
+ return QSettings("QCanvasTeam", "QCanvas")
24
+
25
+
26
+ def root() -> Path:
27
+ root_path = Path()
28
+
29
+ if _is_running_as_pyinstaller:
30
+ root_path = platformdirs.user_data_path("QCanvasReborn", "QCanvasTeam")
31
+
32
+ _logger.debug("Root path %s", root_path.absolute())
33
+ return root_path
34
+
35
+
36
+ def ui_storage() -> Path:
37
+ return root() / ".UI"
38
+
39
+
40
+ def data_storage() -> Path:
41
+ return root() / ".DATA"
@@ -0,0 +1,9 @@
1
+ import logging
2
+
3
+ from qcanvas.util.settings._client_settings import _ClientSettings
4
+ from qcanvas.util.settings._ui_settings import _UISettings
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+ client = _ClientSettings()
9
+ ui = _UISettings()
@@ -0,0 +1,29 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ from qcanvas_api_clients.canvas import CanvasClientConfig
5
+ from qcanvas_api_clients.panopto import PanoptoClientConfig
6
+
7
+ from qcanvas.util import paths
8
+ from qcanvas.util.settings._mapped_setting import BoolSetting, MappedSetting
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ class _ClientSettings:
14
+ settings = paths.client_settings()
15
+ canvas_url: MappedSetting[Optional[str]] = MappedSetting(default=None)
16
+ canvas_api_key: MappedSetting[Optional[str]] = MappedSetting(default=None)
17
+ panopto_url: MappedSetting[Optional[str]] = MappedSetting(default=None)
18
+ quick_sync_enabled: BoolSetting = BoolSetting(default=False)
19
+ sync_on_start: BoolSetting = BoolSetting(default=False)
20
+
21
+ @property
22
+ def canvas_config(self) -> CanvasClientConfig:
23
+ return CanvasClientConfig(
24
+ api_token=self.canvas_api_key, canvas_url=self.canvas_url
25
+ )
26
+
27
+ @property
28
+ def panopto_config(self) -> PanoptoClientConfig:
29
+ return PanoptoClientConfig(panopto_url=self.panopto_url)
@@ -0,0 +1,63 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ from qtpy.QtCore import QSettings
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+
9
+ class MappedSetting[T]:
10
+ """
11
+ Acts as a proxy for a named value in a QSettings object.
12
+ Stores the value in memory when initialised and updates it accordingly, to protect it from changes on disk.
13
+ """
14
+
15
+ def __init__(self, default: T | None = None):
16
+ self.settings_object: QSettings
17
+ self.default = default
18
+ self.value = None
19
+
20
+ def __get__(self, instance, owner) -> T:
21
+ return self.value
22
+
23
+ def __set__(self, instance, value) -> None:
24
+ self.value = value
25
+ self._write(value)
26
+
27
+ def __set_name__(self, owner, name) -> None:
28
+ if hasattr(owner, "settings") and isinstance(owner.settings, QSettings):
29
+ self.settings_object = owner.settings
30
+ else:
31
+ raise AttributeError(
32
+ "Expected owner object to have a 'settings' attribute of type QSettings"
33
+ )
34
+
35
+ self.setting_name = name
36
+ self.value = self._read()
37
+
38
+ def _read(self) -> object:
39
+ return self.settings_object.value(self.setting_name, self.default)
40
+
41
+ def _write(self, value: object) -> None:
42
+ self.settings_object.setValue(self.setting_name, value)
43
+
44
+
45
+ class BoolSetting(MappedSetting[bool]):
46
+ def __init__(self, default: bool = False):
47
+ super().__init__(default)
48
+
49
+ @override
50
+ def _read(self) -> bool:
51
+ try:
52
+ # noinspection PyTypeChecker
53
+ value: str = super()._read()
54
+ return value.lower() == "true"
55
+ except:
56
+ return self.default
57
+
58
+ @override
59
+ def _write(self, value: object) -> None:
60
+ if not isinstance(value, bool):
61
+ raise TypeError()
62
+
63
+ super()._write(str(value).lower())
@@ -0,0 +1,34 @@
1
+ import logging
2
+
3
+ from qtpy.QtCore import QByteArray, QSettings
4
+
5
+ from qcanvas.util.settings._mapped_setting import MappedSetting
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+ _default_theme = "auto"
10
+
11
+
12
+ def ensure_theme_is_valid(theme: str) -> str:
13
+ if theme not in ["auto", "light", "dark", "native"]:
14
+ return _default_theme
15
+ else:
16
+ return theme
17
+
18
+
19
+ class ThemeSetting(MappedSetting):
20
+ def __init__(self):
21
+ super().__init__(default=_default_theme)
22
+
23
+ def __get__(self, instance, owner):
24
+ return ensure_theme_is_valid(super().__get__(instance, owner))
25
+
26
+ def __set__(self, instance, value):
27
+ super().__set__(instance, ensure_theme_is_valid(value))
28
+
29
+
30
+ class _UISettings:
31
+ settings = QSettings("QCanvasTeam", "QCanvas")
32
+ theme: ThemeSetting = ThemeSetting()
33
+ last_geometry: MappedSetting[QByteArray] = MappedSetting()
34
+ last_window_state: MappedSetting[QByteArray] = MappedSetting()
@@ -0,0 +1,41 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from qtpy.QtGui import QKeySequence
5
+ from qtpy.QtWidgets import *
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+
10
+ def make_truncatable(widget: QWidget) -> None:
11
+ widget.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
12
+
13
+
14
+ def create_qaction(
15
+ *,
16
+ name: str,
17
+ shortcut: QKeySequence | None = None,
18
+ parent: QMenu = None,
19
+ triggered: Any = None,
20
+ checkable: bool | None = None,
21
+ checked: bool | None = None,
22
+ ) -> QAction:
23
+ action = QAction(name)
24
+
25
+ if shortcut is not None:
26
+ action.setShortcut(shortcut)
27
+
28
+ if parent is not None:
29
+ action.setParent(parent)
30
+ parent.addAction(action)
31
+
32
+ if triggered is not None:
33
+ action.triggered.connect(triggered)
34
+
35
+ if checkable is not None:
36
+ action.setCheckable(checkable)
37
+
38
+ if checked is not None:
39
+ action.setChecked(checked)
40
+
41
+ return action
@@ -0,0 +1,13 @@
1
+ import logging
2
+ from urllib.parse import urlparse
3
+
4
+ _logger = logging.getLogger(__name__)
5
+
6
+
7
+ def is_url(url: str) -> bool:
8
+ # https://overflow.perennialte.ch/questions/7160737/how-to-validate-a-url-in-python-malformed-or-not#
9
+ try:
10
+ result = urlparse(url)
11
+ return all([result.scheme, result.netloc])
12
+ except ValueError:
13
+ return False
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.1
2
+ Name: qcanvas
3
+ Version: 1.0.3.post0
4
+ Summary:
5
+ Author: QCanvas
6
+ Author-email: QCanvas@noreply.codeberg.org
7
+ Requires-Python: >=3.12,<3.13
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
11
+ Requires-Dist: asynctaskpool (>=0.2.1,<0.3.0)
12
+ Requires-Dist: lightdb (>=2.0,<3.0)
13
+ Requires-Dist: platformdirs (>=4.2.2,<5.0.0)
14
+ Requires-Dist: pyinstaller (>=6.9.0,<7.0.0)
15
+ Requires-Dist: pyqtdarktheme-fork (>=2.3.2,<3.0.0)
16
+ Requires-Dist: qasync (>=0.27.1,<0.28.0)
17
+ Requires-Dist: qcanvas-api-clients (>=0.2.2,<0.3.0)
18
+ Requires-Dist: qcanvas-backend (==0.1.10a1)
19
+ Requires-Dist: qtpy (>=2.4.1,<3.0.0)
20
+ Requires-Dist: setuptools (==70.3.0)
21
+ Requires-Dist: sqlalchemy (>=2.0.31,<3.0.0)
22
+ Description-Content-Type: text/markdown
23
+
24
+ # QCanvas
25
+
26
+ QCanvas is a desktop client for Canvas LMS.
27
+
28
+ # Downloads
29
+
30
+ Download it from [releases](https://github.com/QCanvas/QCanvasApp/releases)
31
+
32
+ # Development/Run from source
33
+
34
+ ## Prerequisites
35
+
36
+ - Python 3.12+ (use [pyenv](https://github.com/pyenv/pyenv) if your distro does not have that version)
37
+ - [Pipx](https://pipx.pypa.io/stable/) (optional)
38
+ - Poetry (recommended to install using `pipx install poetry`)
39
+ - [Appimagetool](https://github.com/AppImage/appimagetool) (Only for building the appimage)
40
+
41
+ ## Get started
42
+
43
+ ```bash
44
+ git clone https://github.com/QCanvas/QCanvasApp.git
45
+ cd QCanvasApp
46
+
47
+ # Enter shell and run it
48
+ poetry shell
49
+ poetry install
50
+ python qcanvas/run.py
51
+
52
+ # Alternatively you can run it like this:
53
+ poetry install
54
+ poetry run python qcanvas/run.py
55
+ ```
56
+
57
+ ## Build custom AppImage
58
+
59
+ ```bash
60
+ bash build_appimage.sh
61
+ ```