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.
- 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 +6 -6
- 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.7a0.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 -338
- 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 -251
- 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 -48
- 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.7a0.dist-info/METADATA +0 -21
- qcanvas-0.0.5.7a0.dist-info/RECORD +0 -62
- /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,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()
|
qcanvas/util/layouts.py
ADDED
|
@@ -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
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,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()
|
qcanvas/util/ui_tools.py
ADDED
|
@@ -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
|
+
```
|