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,143 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
|
|
3
|
-
"""
|
|
4
|
-
from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
|
|
5
|
-
from datetime import datetime # noqa: F401 # pylint: disable=W0611
|
|
6
|
-
from enum import Enum # noqa: F401 # pylint: disable=W0611
|
|
7
|
-
from typing import ( # noqa: F401 # pylint: disable=W0611
|
|
8
|
-
Any,
|
|
9
|
-
Optional,
|
|
10
|
-
Union,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
from pydantic import ( # noqa: F401 # pylint: disable=W0611
|
|
14
|
-
BaseModel,
|
|
15
|
-
Extra,
|
|
16
|
-
Field,
|
|
17
|
-
Json,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
DEFINITION = """
|
|
21
|
-
fragment CanvasCourseData on Course {
|
|
22
|
-
_id
|
|
23
|
-
name
|
|
24
|
-
courseNickname
|
|
25
|
-
term {
|
|
26
|
-
endAt
|
|
27
|
-
startAt
|
|
28
|
-
name
|
|
29
|
-
id
|
|
30
|
-
}
|
|
31
|
-
assignmentsConnection @include(if: $detailed) {
|
|
32
|
-
nodes {
|
|
33
|
-
description
|
|
34
|
-
courseId
|
|
35
|
-
dueAt
|
|
36
|
-
createdAt
|
|
37
|
-
id
|
|
38
|
-
name
|
|
39
|
-
position
|
|
40
|
-
updatedAt
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
modulesConnection @include(if: $detailed) {
|
|
44
|
-
nodes {
|
|
45
|
-
name
|
|
46
|
-
id
|
|
47
|
-
moduleItems {
|
|
48
|
-
content {
|
|
49
|
-
... on File {
|
|
50
|
-
_id
|
|
51
|
-
displayName
|
|
52
|
-
createdAt
|
|
53
|
-
updatedAt
|
|
54
|
-
url
|
|
55
|
-
size
|
|
56
|
-
mimeClass
|
|
57
|
-
contentType
|
|
58
|
-
}
|
|
59
|
-
... on Page {
|
|
60
|
-
_id
|
|
61
|
-
title
|
|
62
|
-
updatedAt
|
|
63
|
-
createdAt
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class ConfiguredBaseModel(BaseModel):
|
|
74
|
-
class Config:
|
|
75
|
-
smart_union = True
|
|
76
|
-
extra = Extra.forbid
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class Term(ConfiguredBaseModel):
|
|
80
|
-
end_at: Optional[datetime] = Field(..., alias="endAt")
|
|
81
|
-
start_at: Optional[datetime] = Field(..., alias="startAt")
|
|
82
|
-
name: Optional[str] = Field(..., alias="name")
|
|
83
|
-
q_id: str = Field(..., alias="id")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class Assignment(ConfiguredBaseModel):
|
|
87
|
-
description: Optional[str] = Field(..., alias="description")
|
|
88
|
-
course_id: Optional[str] = Field(..., alias="courseId")
|
|
89
|
-
due_at: Optional[datetime] = Field(..., alias="dueAt")
|
|
90
|
-
created_at: Optional[datetime] = Field(..., alias="createdAt")
|
|
91
|
-
q_id: str = Field(..., alias="id")
|
|
92
|
-
name: Optional[str] = Field(..., alias="name")
|
|
93
|
-
position: Optional[int] = Field(..., alias="position")
|
|
94
|
-
updated_at: Optional[datetime] = Field(..., alias="updatedAt")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class AssignmentConnection(ConfiguredBaseModel):
|
|
98
|
-
nodes: Optional[list[Optional[Assignment]]] = Field(..., alias="nodes")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class ModuleItemInterface(ConfiguredBaseModel):
|
|
102
|
-
...
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class File(ModuleItemInterface):
|
|
106
|
-
m_id: str = Field(..., alias="_id")
|
|
107
|
-
display_name: Optional[str] = Field(..., alias="displayName")
|
|
108
|
-
created_at: Optional[datetime] = Field(..., alias="createdAt")
|
|
109
|
-
updated_at: Optional[datetime] = Field(..., alias="updatedAt")
|
|
110
|
-
url: Optional[str] = Field(..., alias="url")
|
|
111
|
-
size: Optional[str] = Field(..., alias="size")
|
|
112
|
-
mime_class: Optional[str] = Field(..., alias="mimeClass")
|
|
113
|
-
content_type: Optional[str] = Field(..., alias="contentType")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class Page(ModuleItemInterface):
|
|
117
|
-
m_id: str = Field(..., alias="_id")
|
|
118
|
-
title: Optional[str] = Field(..., alias="title")
|
|
119
|
-
updated_at: Optional[datetime] = Field(..., alias="updatedAt")
|
|
120
|
-
created_at: Optional[datetime] = Field(..., alias="createdAt")
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class ModuleItem(ConfiguredBaseModel):
|
|
124
|
-
content: Optional[Union[File, Page, ModuleItemInterface]] = Field(..., alias="content")
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class Module(ConfiguredBaseModel):
|
|
128
|
-
name: Optional[str] = Field(..., alias="name")
|
|
129
|
-
q_id: str = Field(..., alias="id")
|
|
130
|
-
module_items: Optional[list[ModuleItem]] = Field(..., alias="moduleItems")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class ModuleConnection(ConfiguredBaseModel):
|
|
134
|
-
nodes: Optional[list[Optional[Module]]] = Field(..., alias="nodes")
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class CanvasCourseData(ConfiguredBaseModel):
|
|
138
|
-
m_id: str = Field(..., alias="_id")
|
|
139
|
-
name: str = Field(..., alias="name")
|
|
140
|
-
course_nickname: Optional[str] = Field(..., alias="courseNickname")
|
|
141
|
-
term: Optional[Term] = Field(..., alias="term")
|
|
142
|
-
assignments_connection: Optional[AssignmentConnection] = Field(None, alias="assignmentsConnection")
|
|
143
|
-
modules_connection: Optional[ModuleConnection] = Field(None, alias="modulesConnection")
|
qcanvas/ui/container_item.py
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from PySide6.QtGui import QStandardItem
|
|
2
|
-
|
|
3
|
-
from qcanvas.util import tree_util as tree
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ContainerItem(QStandardItem):
|
|
7
|
-
def __init__(self, data: tree.HasText):
|
|
8
|
-
super().__init__()
|
|
9
|
-
self.content = data
|
|
10
|
-
self.setEditable(False)
|
|
11
|
-
self.setText(data.text)
|
qcanvas/ui/main_ui.py
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import sys
|
|
3
|
-
import traceback
|
|
4
|
-
from typing import Sequence, Optional
|
|
5
|
-
|
|
6
|
-
from PySide6.QtCore import Slot, Signal, Qt, QUrl, QObject
|
|
7
|
-
from PySide6.QtGui import QDesktopServices, QKeySequence
|
|
8
|
-
from PySide6.QtWidgets import *
|
|
9
|
-
from qasync import asyncSlot
|
|
10
|
-
|
|
11
|
-
import qcanvas.db.database as db
|
|
12
|
-
from qcanvas.ui.menu_bar.grouping_preferences_menu import GroupingPreferencesMenu
|
|
13
|
-
from qcanvas.ui.menu_bar.theme_selection_menu import ThemeSelectionMenu
|
|
14
|
-
from qcanvas.ui.status_bar_reporter import StatusBarReporter
|
|
15
|
-
from qcanvas.ui.viewer.course_list import CourseList
|
|
16
|
-
from qcanvas.ui.viewer.file_list import FileRow
|
|
17
|
-
from qcanvas.ui.viewer.file_view_tab import FileViewTab
|
|
18
|
-
from qcanvas.ui.viewer.page_list_viewer import AssignmentsViewer, PagesViewer, LinkTransformer
|
|
19
|
-
from qcanvas.util import self_updater
|
|
20
|
-
from qcanvas.util.app_settings import settings
|
|
21
|
-
from qcanvas.util.constants import app_name
|
|
22
|
-
from qcanvas.util.course_indexer import DataManager
|
|
23
|
-
from qcanvas.util.helpers.qaction_helper import create_qaction
|
|
24
|
-
|
|
25
|
-
_aux_settings = settings.auxiliary
|
|
26
|
-
_no_course_selected_text = "No course selected"
|
|
27
|
-
|
|
28
|
-
class AppMainWindow(QMainWindow):
|
|
29
|
-
logger = logging.getLogger()
|
|
30
|
-
loaded = Signal()
|
|
31
|
-
files_grouping_preference_changed = Signal(db.GroupByPreference)
|
|
32
|
-
|
|
33
|
-
def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
|
|
34
|
-
super().__init__(parent)
|
|
35
|
-
|
|
36
|
-
self.selected_course: db.Course | None = None
|
|
37
|
-
self.courses: Sequence[db.Course] = []
|
|
38
|
-
self.resources: dict[str, db.Resource] = {}
|
|
39
|
-
self.data_manager = data_manager
|
|
40
|
-
self.link_transformer = LinkTransformer(self.data_manager.link_scanners, self.resources)
|
|
41
|
-
|
|
42
|
-
right_splitter = QSplitter()
|
|
43
|
-
right_splitter.setOrientation(Qt.Orientation.Vertical)
|
|
44
|
-
|
|
45
|
-
self.sync_button = QPushButton("Synchronize")
|
|
46
|
-
self.sync_button.clicked.connect(self.sync_data)
|
|
47
|
-
|
|
48
|
-
self.course_list = CourseList(self.data_manager)
|
|
49
|
-
self.course_list.course_selected.connect(self.on_course_selected)
|
|
50
|
-
|
|
51
|
-
self.assignment_viewer = AssignmentsViewer(self.link_transformer)
|
|
52
|
-
self.assignment_viewer.viewer.anchorClicked.connect(self.viewer_link_clicked)
|
|
53
|
-
|
|
54
|
-
self.pages_viewer = PagesViewer(self.link_transformer)
|
|
55
|
-
self.pages_viewer.viewer.anchorClicked.connect(self.viewer_link_clicked)
|
|
56
|
-
|
|
57
|
-
self.file_viewer = FileViewTab(data_manager.download_pool)
|
|
58
|
-
self.file_viewer.files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
|
|
59
|
-
self.file_viewer.assignment_files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
|
|
60
|
-
|
|
61
|
-
self.tab_widget = QTabWidget()
|
|
62
|
-
self.tab_widget.insertTab(0, self.file_viewer, "Files")
|
|
63
|
-
self.tab_widget.insertTab(1, self.assignment_viewer, "Assignments")
|
|
64
|
-
self.tab_widget.insertTab(2, self.pages_viewer, "Pages")
|
|
65
|
-
|
|
66
|
-
self.course_name_label = QLabel(_no_course_selected_text)
|
|
67
|
-
self.course_name_label.setStyleSheet("font-weight: bold;")
|
|
68
|
-
course_stack_layout = self.create_layout_and_add_widgets(QVBoxLayout, self.course_name_label, self.tab_widget)
|
|
69
|
-
|
|
70
|
-
h_layout = self.create_layout_and_add_widgets(QHBoxLayout, self.course_list, course_stack_layout)
|
|
71
|
-
h_layout.setStretch(1, 1)
|
|
72
|
-
|
|
73
|
-
widget = QWidget()
|
|
74
|
-
widget.setLayout(self.create_layout_and_add_widgets(QVBoxLayout, h_layout, self.sync_button))
|
|
75
|
-
self.setCentralWidget(widget)
|
|
76
|
-
|
|
77
|
-
self.setup_menu_bar()
|
|
78
|
-
|
|
79
|
-
self.files_grouping_preference_changed.connect(self.on_grouping_preference_changed)
|
|
80
|
-
|
|
81
|
-
self.loaded.connect(self.load_course_list)
|
|
82
|
-
self.loaded.connect(self.check_for_update)
|
|
83
|
-
self.loaded.emit()
|
|
84
|
-
|
|
85
|
-
self.restore_window_position()
|
|
86
|
-
|
|
87
|
-
# Activate the statusbar so it doesn't just appear randomly later
|
|
88
|
-
bar: QStatusBar = self.statusBar()
|
|
89
|
-
# Set its height so it doesn't get bigger when there's a progress bar in it
|
|
90
|
-
bar.setFixedHeight(bar.height())
|
|
91
|
-
|
|
92
|
-
@staticmethod
|
|
93
|
-
def create_layout_and_add_widgets(layout_type: type, *widgets) -> QLayout:
|
|
94
|
-
layout = layout_type()
|
|
95
|
-
|
|
96
|
-
for widget in widgets:
|
|
97
|
-
if isinstance(widget, QLayout):
|
|
98
|
-
layout.addLayout(widget)
|
|
99
|
-
else:
|
|
100
|
-
layout.addWidget(widget)
|
|
101
|
-
|
|
102
|
-
return layout
|
|
103
|
-
|
|
104
|
-
def setup_menu_bar(self):
|
|
105
|
-
menu_bar = self.menuBar()
|
|
106
|
-
|
|
107
|
-
app_menu: QMenu = menu_bar.addMenu("App")
|
|
108
|
-
view_menu: QMenu = menu_bar.addMenu("View")
|
|
109
|
-
|
|
110
|
-
app_menu.addAction(self.setup_quick_authentication_action(app_menu))
|
|
111
|
-
app_menu.addMenu(ThemeSelectionMenu())
|
|
112
|
-
view_menu.addMenu(self.setup_group_by_menu())
|
|
113
|
-
|
|
114
|
-
def setup_group_by_menu(self) -> QMenu:
|
|
115
|
-
file_grouping_menu = GroupingPreferencesMenu(self.data_manager)
|
|
116
|
-
self.course_list.course_selected.connect(file_grouping_menu.course_changed)
|
|
117
|
-
file_grouping_menu.preference_changed.connect(self.on_grouping_preference_changed)
|
|
118
|
-
|
|
119
|
-
return file_grouping_menu
|
|
120
|
-
|
|
121
|
-
def setup_quick_authentication_action(self, parent: QObject):
|
|
122
|
-
return create_qaction(
|
|
123
|
-
name="Quick canvas login",
|
|
124
|
-
shortcut=QKeySequence("Ctrl+O"),
|
|
125
|
-
triggered=self.open_quick_auth_in_browser,
|
|
126
|
-
parent=parent
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
@asyncSlot()
|
|
130
|
-
async def open_quick_auth_in_browser(self):
|
|
131
|
-
opening_progress_dialog = QProgressDialog("Opening canvas", None, 0, 0, self)
|
|
132
|
-
opening_progress_dialog.setWindowTitle("Please wait")
|
|
133
|
-
opening_progress_dialog.show()
|
|
134
|
-
QDesktopServices.openUrl(await self.data_manager.client.get_temp_session_link())
|
|
135
|
-
opening_progress_dialog.close()
|
|
136
|
-
|
|
137
|
-
def closeEvent(self, event):
|
|
138
|
-
settings.geometry = self.saveGeometry()
|
|
139
|
-
settings.window_state = self.saveState()
|
|
140
|
-
|
|
141
|
-
def restore_window_position(self):
|
|
142
|
-
self.restoreGeometry(settings.geometry)
|
|
143
|
-
self.restoreState(settings.window_state)
|
|
144
|
-
|
|
145
|
-
@asyncSlot(QUrl)
|
|
146
|
-
async def viewer_link_clicked(self, url: QUrl):
|
|
147
|
-
# The url of a transformed link will start with a specific prefix
|
|
148
|
-
if url.toString().startswith(LinkTransformer.transformed_url_prefix):
|
|
149
|
-
# The rest of the 'url' is just the file id
|
|
150
|
-
resource = self.resources[url.toString().removeprefix(LinkTransformer.transformed_url_prefix)]
|
|
151
|
-
|
|
152
|
-
await self.data_manager.download_resource(resource)
|
|
153
|
-
QDesktopServices.openUrl(QUrl.fromLocalFile(resource.download_location.absolute()))
|
|
154
|
-
else:
|
|
155
|
-
QDesktopServices.openUrl(url)
|
|
156
|
-
|
|
157
|
-
@asyncSlot(QTreeWidgetItem, int)
|
|
158
|
-
async def download_file_from_file_pane(self, item: QTreeWidgetItem, _: int):
|
|
159
|
-
if isinstance(item, FileRow):
|
|
160
|
-
await self.data_manager.download_resource(item.resource)
|
|
161
|
-
QDesktopServices.openUrl(QUrl.fromLocalFile(item.resource.download_location.absolute()))
|
|
162
|
-
|
|
163
|
-
@asyncSlot()
|
|
164
|
-
async def sync_data(self):
|
|
165
|
-
self.sync_button.setEnabled(False)
|
|
166
|
-
self.sync_button.setText("Synchronizing")
|
|
167
|
-
try:
|
|
168
|
-
await self.data_manager.synchronize_with_canvas(StatusBarReporter(self.statusBar()))
|
|
169
|
-
await self.load_course_list()
|
|
170
|
-
|
|
171
|
-
finally:
|
|
172
|
-
self.sync_button.setEnabled(True)
|
|
173
|
-
self.sync_button.setText("Synchronize")
|
|
174
|
-
|
|
175
|
-
@asyncSlot()
|
|
176
|
-
async def load_course_list(self):
|
|
177
|
-
self.courses = (await self.data_manager.get_data())
|
|
178
|
-
self.selected_course = None
|
|
179
|
-
self.resources.clear()
|
|
180
|
-
|
|
181
|
-
for course in self.courses:
|
|
182
|
-
self.resources.update({resource.id: resource for resource in course.resources})
|
|
183
|
-
|
|
184
|
-
self.course_list.load_course_list(self.courses)
|
|
185
|
-
|
|
186
|
-
@asyncSlot()
|
|
187
|
-
async def check_for_update(self):
|
|
188
|
-
try:
|
|
189
|
-
newer_version, installed_version = await self_updater.get_newer_version()
|
|
190
|
-
|
|
191
|
-
if newer_version is not None and newer_version != settings.ignored_update:
|
|
192
|
-
msg_box = QMessageBox(
|
|
193
|
-
QMessageBox.Icon.Question,
|
|
194
|
-
"Update available",
|
|
195
|
-
f"There is an update available ({installed_version} -> {newer_version})\nDo you want to update?\nThe program will close after the update is finished.",
|
|
196
|
-
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
197
|
-
self
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
def ignore_update():
|
|
201
|
-
settings.ignored_update = newer_version
|
|
202
|
-
|
|
203
|
-
msg_box.accepted.connect(self.do_self_update)
|
|
204
|
-
msg_box.rejected.connect(ignore_update)
|
|
205
|
-
msg_box.show()
|
|
206
|
-
else:
|
|
207
|
-
print("No update available (or skipping this update)")
|
|
208
|
-
except BaseException as e:
|
|
209
|
-
sys.stderr.write(f"Could not check for updates: {e}\n")
|
|
210
|
-
traceback.print_exc()
|
|
211
|
-
sys.stderr.write("This can be ignored if in a dev environment\n")
|
|
212
|
-
|
|
213
|
-
@asyncSlot()
|
|
214
|
-
async def do_self_update(self):
|
|
215
|
-
try:
|
|
216
|
-
progress_diag = QProgressDialog("Updating", None, 0, 0, self)
|
|
217
|
-
progress_diag.setWindowTitle(app_name)
|
|
218
|
-
progress_diag.show()
|
|
219
|
-
await self_updater.do_update()
|
|
220
|
-
self.close()
|
|
221
|
-
except BaseException as e:
|
|
222
|
-
traceback.print_exc()
|
|
223
|
-
|
|
224
|
-
QMessageBox(
|
|
225
|
-
QMessageBox.Icon.Critical,
|
|
226
|
-
"Error",
|
|
227
|
-
"An error occurred during the update",
|
|
228
|
-
parent=self
|
|
229
|
-
).show()
|
|
230
|
-
|
|
231
|
-
@Slot(db.Course)
|
|
232
|
-
def on_course_selected(self, course: Optional[db.Course]):
|
|
233
|
-
if course is not None:
|
|
234
|
-
self.selected_course = course
|
|
235
|
-
# todo these should really be slots connected to this signal...
|
|
236
|
-
self.pages_viewer.fill_tree(course)
|
|
237
|
-
self.assignment_viewer.fill_tree(course)
|
|
238
|
-
self.file_viewer.load_course_files(course)
|
|
239
|
-
self.course_name_label.setText(course.name)
|
|
240
|
-
else:
|
|
241
|
-
self.selected_course = None
|
|
242
|
-
self.file_viewer.clear()
|
|
243
|
-
self.pages_viewer.clear()
|
|
244
|
-
self.assignment_viewer.clear()
|
|
245
|
-
self.course_name_label.setText(_no_course_selected_text)
|
|
246
|
-
|
|
247
|
-
@asyncSlot(db.CoursePreferences)
|
|
248
|
-
async def on_grouping_preference_changed(self):
|
|
249
|
-
self.file_viewer.load_course_files(self.selected_course)
|
qcanvas/ui/menu_bar/__init__.py
DELETED
|
File without changes
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
from PySide6.QtCore import Slot, Signal
|
|
2
|
-
from PySide6.QtWidgets import QMenu, QWidget
|
|
3
|
-
from qasync import asyncSlot
|
|
4
|
-
|
|
5
|
-
import qcanvas.db as db
|
|
6
|
-
from qcanvas.util.course_indexer import DataManager
|
|
7
|
-
from qcanvas.util.helpers.qaction_helper import create_qaction
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class GroupingPreferencesMenu(QMenu):
|
|
11
|
-
_preference_changed_private = Signal(db.GroupByPreference)
|
|
12
|
-
preference_changed = Signal()
|
|
13
|
-
|
|
14
|
-
def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
|
|
15
|
-
super().__init__("Group files by", parent)
|
|
16
|
-
self.setEnabled(False)
|
|
17
|
-
|
|
18
|
-
self.data_manager = data_manager
|
|
19
|
-
self._selected_course: db.Course | None = None
|
|
20
|
-
|
|
21
|
-
self.group_by_modules_action = self._make_action("Modules", db.GroupByPreference.GROUP_BY_MODULES)
|
|
22
|
-
self.group_by_pages_action = self._make_action("Pages", db.GroupByPreference.GROUP_BY_PAGES)
|
|
23
|
-
|
|
24
|
-
self.addActions([self.group_by_pages_action, self.group_by_modules_action])
|
|
25
|
-
|
|
26
|
-
self._preference_changed_private.connect(self._on_preference_changed)
|
|
27
|
-
|
|
28
|
-
def _make_action(self, text: str, preference_value: db.GroupByPreference):
|
|
29
|
-
return create_qaction(
|
|
30
|
-
name=text,
|
|
31
|
-
parent=self,
|
|
32
|
-
triggered=lambda: self._preference_changed_private.emit(preference_value),
|
|
33
|
-
checkable=True,
|
|
34
|
-
checked=False
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
@Slot()
|
|
38
|
-
def course_changed(self, course: db.Course | None):
|
|
39
|
-
if course is not None:
|
|
40
|
-
self._selected_course = course
|
|
41
|
-
self._update_actions()
|
|
42
|
-
self.setEnabled(True)
|
|
43
|
-
else:
|
|
44
|
-
self._selected_course = None
|
|
45
|
-
self.setEnabled(False)
|
|
46
|
-
|
|
47
|
-
@asyncSlot()
|
|
48
|
-
async def _on_preference_changed(self, preference: db.GroupByPreference):
|
|
49
|
-
if self._selected_course is not None:
|
|
50
|
-
self._selected_course.preferences.files_group_by_preference = preference
|
|
51
|
-
self._update_actions()
|
|
52
|
-
# todo not sure if this should go in main_ui or here...
|
|
53
|
-
await self.data_manager.update_item(self._selected_course.preferences)
|
|
54
|
-
|
|
55
|
-
self.preference_changed.emit()
|
|
56
|
-
|
|
57
|
-
def _update_actions(self):
|
|
58
|
-
preference = self._selected_course.preferences.files_group_by_preference
|
|
59
|
-
|
|
60
|
-
self.group_by_pages_action.setChecked(preference == db.GroupByPreference.GROUP_BY_PAGES)
|
|
61
|
-
self.group_by_modules_action.setChecked(preference == db.GroupByPreference.GROUP_BY_MODULES)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
from PySide6.QtGui import QActionGroup
|
|
2
|
-
from PySide6.QtWidgets import QMenu, QWidget
|
|
3
|
-
|
|
4
|
-
from qcanvas.util.app_settings import settings
|
|
5
|
-
from qcanvas.util.helpers import theme_helper
|
|
6
|
-
from qcanvas.util.helpers.qaction_helper import create_qaction
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def change_theme(theme_name: str):
|
|
10
|
-
settings.theme = theme_name
|
|
11
|
-
theme_helper.apply_selected_theme()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class ThemeSelectionMenu(QMenu):
|
|
15
|
-
def __init__(self, parent: QWidget | None = None):
|
|
16
|
-
super().__init__("Theme", parent)
|
|
17
|
-
|
|
18
|
-
action_group = QActionGroup(self)
|
|
19
|
-
|
|
20
|
-
light_theme = self._create_action("Light", "light")
|
|
21
|
-
dark_theme = self._create_action("Dark", "dark")
|
|
22
|
-
auto_theme = self._create_action("Auto (YMMV)", "auto")
|
|
23
|
-
native_theme = self._create_action("Native (requires restart)", "native")
|
|
24
|
-
|
|
25
|
-
actions = [light_theme, dark_theme, auto_theme, native_theme]
|
|
26
|
-
|
|
27
|
-
self.addActions(actions)
|
|
28
|
-
|
|
29
|
-
for theme in actions:
|
|
30
|
-
action_group.addAction(theme)
|
|
31
|
-
|
|
32
|
-
def _create_action(self, text: str, theme_name: str):
|
|
33
|
-
return create_qaction(
|
|
34
|
-
name=text,
|
|
35
|
-
parent=self,
|
|
36
|
-
triggered=lambda: change_theme(theme_name),
|
|
37
|
-
checkable=True,
|
|
38
|
-
checked=settings.theme == theme_name
|
|
39
|
-
)
|
qcanvas/ui/setup_dialog.py
DELETED
|
@@ -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
|