qcanvas 0.0.2a0__py3-none-any.whl → 0.0.3a0__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/__main__.py CHANGED
@@ -4,12 +4,17 @@ import sys
4
4
  from datetime import datetime
5
5
 
6
6
  import httpx
7
+ from PySide6.QtCore import Signal, Slot, QObject
8
+ from PySide6.QtWidgets import QProgressDialog, QMessageBox, QWidget, QMainWindow
9
+
7
10
  from qcanvas.QtVersionHelper.QtWidgets import QApplication
8
11
  from httpx import URL
9
12
  from sqlalchemy.ext.asyncio import create_async_engine
10
13
  from sqlalchemy.ext.asyncio.session import async_sessionmaker as AsyncSessionMaker
11
14
 
12
15
  from qcanvas.ui.main_ui import AppMainWindow
16
+ from qcanvas.ui.setup_dialog import SetupDialog
17
+ from qcanvas.util.constants import app_name
13
18
  from qcanvas.util.linkscanner import CanvasFileScanner
14
19
  from qcanvas.util.course_indexer import DataManager
15
20
  from qcanvas.net.canvas.canvas_client import CanvasClient
@@ -18,34 +23,75 @@ from qcanvas.util import AppSettings
18
23
  from qcanvas.util.linkscanner.canvas_media_object_scanner import CanvasMediaObjectScanner
19
24
  from qcanvas.util.linkscanner.dropbox_scanner import DropboxScanner
20
25
 
21
- from qasync import QEventLoop
26
+ from qasync import QEventLoop, asyncSlot
22
27
  import qcanvas.db as db
23
28
  import qdarktheme
24
29
 
25
- client = CanvasClient(canvas_url=URL(AppSettings.canvas_url), api_key=AppSettings.canvas_api_key)
26
-
27
30
  engine = create_async_engine("sqlite+aiosqlite:///meme", echo=False)
28
31
 
29
32
  logging.basicConfig()
30
33
  logging.getLogger("canvas_client").setLevel(logging.DEBUG)
31
- # logging.getLogger("aiosqlite").setLevel(logging.DEBUG)
32
-
33
- data_manager = DataManager(
34
- client=client,
35
- link_scanners=[CanvasFileScanner(client), DropboxScanner(httpx.AsyncClient()),
36
- CanvasMediaObjectScanner(client.client)],
37
- sessionmaker=AsyncSessionMaker(engine, expire_on_commit=False),
38
- last_update=datetime.min
39
- )
40
-
41
- data_manager.download_pool.download_progress_updated.connect(lambda x, y: print(f"Progress {x} {y}"))
42
34
 
43
35
  async def begin():
44
36
  # Create meta stuff
45
37
  async with engine.begin() as conn:
46
38
  await conn.run_sync(db.Base.metadata.create_all)
47
39
 
48
- await data_manager.init()
40
+
41
+ class LoaderWindow(QMainWindow):
42
+ init = Signal()
43
+ setup = Signal()
44
+ ready = Signal()
45
+
46
+ def __init__(self):
47
+ super().__init__()
48
+ self.init.connect(self.on_init)
49
+ self.setup.connect(self.on_setup)
50
+ self.ready.connect(self.on_ready)
51
+ self.setWindowTitle(app_name)
52
+ self.setCentralWidget(QProgressDialog("Verifying config", None, 0, 0))
53
+
54
+ self.init.emit()
55
+
56
+
57
+ @asyncSlot()
58
+ async def on_init(self):
59
+ try:
60
+ if not await CanvasClient.verify_config(AppSettings.canvas_url, AppSettings.canvas_api_key):
61
+ self.setup.emit()
62
+ else:
63
+ self.ready.emit()
64
+ except:
65
+ self.setup.emit()
66
+
67
+ @asyncSlot()
68
+ async def on_setup(self):
69
+ setup = SetupDialog(self)
70
+ setup.rejected.connect(lambda: sys.exit(0))
71
+ setup.accepted.connect(self.on_ready)
72
+ setup.show()
73
+
74
+ @asyncSlot()
75
+ async def on_ready(self):
76
+ client = CanvasClient(canvas_url=URL(AppSettings.canvas_url), api_key=AppSettings.canvas_api_key)
77
+ data_manager = DataManager(
78
+ client=client,
79
+ link_scanners=[CanvasFileScanner(client), DropboxScanner(httpx.AsyncClient()),
80
+ CanvasMediaObjectScanner(client.client)],
81
+ sessionmaker=AsyncSessionMaker(engine, expire_on_commit=False),
82
+ last_update=datetime.min
83
+ )
84
+
85
+ await data_manager.init()
86
+ self.close()
87
+ self.open_main_app(data_manager)
88
+
89
+ def open_main_app(self, data_manager: DataManager):
90
+ self.main_window = AppMainWindow(data_manager)
91
+ self.main_window.setWindowTitle(app_name)
92
+ # self.main_window.resize(1200, 600)
93
+ self.main_window.show()
94
+ self.setParent(self.main_window)
49
95
 
50
96
 
51
97
  if __name__ == '__main__':
@@ -61,9 +107,8 @@ if __name__ == '__main__':
61
107
  app_close_event = asyncio.Event()
62
108
  app.aboutToQuit.connect(app_close_event.set)
63
109
 
64
- main_window = AppMainWindow(data_manager)
65
- main_window.resize(800, 600)
66
- main_window.show()
110
+ loader_window = LoaderWindow()
111
+ loader_window.show()
67
112
 
68
113
  with event_loop:
69
114
  event_loop.run_until_complete(app_close_event.wait())
qcanvas/db/database.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import pathlib
2
+ import platform
2
3
  from dataclasses import dataclass
3
4
  from datetime import datetime
4
5
  from enum import Enum
@@ -68,7 +69,7 @@ class CoursePreferences(Base):
68
69
  course: Mapped["Course"] = relationship(back_populates="preferences")
69
70
 
70
71
 
71
- class Course(MappedAsDataclass, Base, tree.HasText, init=False):
72
+ class Course(MappedAsDataclass, Base, init=False):
72
73
  __tablename__ = "courses"
73
74
 
74
75
  id: Mapped[str] = mapped_column(primary_key=True)
@@ -85,11 +86,6 @@ class Course(MappedAsDataclass, Base, tree.HasText, init=False):
85
86
  assignments: Mapped[List["Assignment"]] = relationship(back_populates="course")
86
87
  resources: Mapped[List["Resource"]] = relationship(back_populates="course")
87
88
 
88
- @property
89
- def text(self) -> str:
90
- return self.name
91
-
92
-
93
89
  class Term(MappedAsDataclass, Base):
94
90
  """
95
91
  A term object.
@@ -216,7 +212,13 @@ class Resource(MappedAsDataclass, Base, tree.HasText):
216
212
 
217
213
  @property
218
214
  def download_location(self) -> pathlib.Path:
219
- return pathlib.Path("download", f"{self.id}@{self.file_name}")
215
+ file_id: str = self.id
216
+
217
+ # Colon is illegal in microsoft windows file names
218
+ if platform.system() == "Windows":
219
+ file_id.replace(':', '$')
220
+
221
+ return pathlib.Path("download", f"{file_id}@{self.file_name}")
220
222
 
221
223
 
222
224
  class ModuleItem(MappedAsDataclass, Base, tree.HasText):
@@ -60,7 +60,7 @@ class CanvasClient(SelfAuthenticating):
60
60
  _net_op_sem = asyncio.Semaphore(20)
61
61
 
62
62
  @staticmethod
63
- async def verify_config(canvas_url: URL, api_key: str) -> bool:
63
+ async def verify_config(canvas_url: str, api_key: str) -> bool:
64
64
  """
65
65
  Makes a request to canvas to verify that the url and key are correct.
66
66
  :param canvas_url: The canvas url to verify
@@ -70,7 +70,7 @@ class CanvasClient(SelfAuthenticating):
70
70
  client = httpx.AsyncClient()
71
71
  # Make a request to an endpoint that returns very little/no data (for students at least) to check if everything
72
72
  # is working
73
- response = await client.get(url=canvas_url.join("api/v1/accounts"),
73
+ response = await client.get(url=URL(canvas_url).join("api/v1/accounts"),
74
74
  headers={"Authorization": f"Bearer {api_key}"})
75
75
 
76
76
  return response.is_success
@@ -5,12 +5,17 @@ from qcanvas.util import tree_util as tree
5
5
 
6
6
 
7
7
  class ContainerItem(QStandardItem):
8
- content: tree.HasText
9
-
10
8
  def __init__(self, data: tree.HasText):
11
9
  super().__init__()
12
10
  self.content = data
11
+ self.setEditable(False)
12
+
13
13
 
14
14
  def data(self, role=257):
15
15
  if role == Qt.ItemDataRole.DisplayRole:
16
16
  return self.content.text
17
+
18
+
19
+
20
+
21
+
qcanvas/ui/main_ui.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from asyncio import Event
3
3
  from datetime import datetime
4
- from typing import Sequence
4
+ from typing import Sequence, Union, Optional
5
5
 
6
6
  from PySide6.QtCore import QUrl
7
7
  from PySide6.QtGui import QDesktopServices
@@ -9,23 +9,27 @@ from qasync import asyncSlot
9
9
 
10
10
  from qcanvas.QtVersionHelper.QtGui import QStandardItemModel, QStandardItem
11
11
  from qcanvas.QtVersionHelper.QtWidgets import *
12
- from qcanvas.QtVersionHelper.QtCore import QItemSelection, Slot, Signal, Qt, QModelIndex
12
+ from qcanvas.QtVersionHelper.QtCore import QItemSelection, Slot, Signal, Qt, QObject, QModelIndex
13
13
 
14
14
  import qcanvas.db.database as db
15
15
  from qcanvas.ui.container_item import ContainerItem
16
+ from qcanvas.ui.viewer.course_list import CourseList
16
17
  from qcanvas.ui.viewer.file_list import FileRow
17
18
  from qcanvas.ui.viewer.file_view_tab import FileViewTab
18
19
  from qcanvas.ui.viewer.page_list_viewer import AssignmentsViewer, PagesViewer, LinkTransformer
20
+ from qcanvas.util import AppSettings
21
+ from qcanvas.util.constants import app_name
19
22
  from qcanvas.util.course_indexer import DataManager
20
23
 
24
+ _aux_settings = AppSettings.auxiliary
21
25
 
22
26
  class AppMainWindow(QMainWindow):
23
27
  logger = logging.getLogger()
24
28
  loaded = Signal()
25
29
  operation_lock = Event()
26
30
 
27
- def __init__(self, data_manager: DataManager):
28
- super().__init__()
31
+ def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
32
+ super().__init__(parent)
29
33
 
30
34
  self.selected_course: db.Course | None = None
31
35
  self.courses: Sequence[db.Course] = []
@@ -33,19 +37,14 @@ class AppMainWindow(QMainWindow):
33
37
  self.data_manager = data_manager
34
38
  self.link_transformer = LinkTransformer(self.data_manager.link_scanners, self.resources)
35
39
 
36
- self.setWindowTitle("QCanvas (Under construction)")
37
-
38
40
  right_splitter = QSplitter()
39
41
  right_splitter.setOrientation(Qt.Orientation.Vertical)
40
42
 
41
43
  self.sync_button = QPushButton("Synchronize")
42
44
  self.sync_button.clicked.connect(self.sync_data)
43
45
 
44
- #todo just use QTreeWidget instead
45
- self.course_selector = QTreeView()
46
- self.course_selector_model = QStandardItemModel()
47
- self.course_selector.setModel(self.course_selector_model)
48
- self.course_selector.selectionModel().selectionChanged.connect(self.on_item_clicked)
46
+ self.course_list = CourseList(self.data_manager)
47
+ self.course_list.course_selected.connect(self.on_course_selected)
49
48
 
50
49
  self.assignment_viewer = AssignmentsViewer(self.link_transformer)
51
50
  self.assignment_viewer.viewer.anchorClicked.connect(self.viewer_link_clicked)
@@ -65,7 +64,7 @@ class AppMainWindow(QMainWindow):
65
64
  self.tab_widget.insertTab(2, self.pages_viewer, "Pages")
66
65
 
67
66
  h_layout = QHBoxLayout()
68
- h_layout.addWidget(self.course_selector)
67
+ h_layout.addWidget(self.course_list)
69
68
  h_layout.addWidget(self.tab_widget)
70
69
  h_layout.setStretch(1, 1)
71
70
 
@@ -80,6 +79,15 @@ class AppMainWindow(QMainWindow):
80
79
  self.loaded.connect(self.load_course_list)
81
80
  self.loaded.emit()
82
81
 
82
+ self.read_settings()
83
+
84
+ def closeEvent(self, event):
85
+ _aux_settings.setValue("geometry", self.saveGeometry())
86
+ _aux_settings.setValue("windowState", self.saveState())
87
+
88
+ def read_settings(self):
89
+ self.restoreGeometry(_aux_settings.value("geometry"))
90
+ self.restoreState(_aux_settings.value("windowState"))
83
91
 
84
92
  @asyncSlot(QUrl)
85
93
  async def viewer_link_clicked(self, url: QUrl):
@@ -112,71 +120,35 @@ class AppMainWindow(QMainWindow):
112
120
  self.sync_button.setEnabled(True)
113
121
  self.sync_button.setText("Synchronize")
114
122
 
115
- @staticmethod
116
- def group_courses_by_term(courses: Sequence[db.Course]):
117
- courses_grouped_by_term: dict[db.Term, list[db.Course]] = {}
118
-
119
- # Put courses into groups in the above dict
120
- for course in courses:
121
- if course.term in courses_grouped_by_term:
122
- courses_grouped_by_term[course.term].append(course)
123
- else:
124
- courses_grouped_by_term[course.term] = [course]
125
123
 
126
- # Convert the dict item list into a mutable list
127
- pairs = list(courses_grouped_by_term.items())
128
- # Sort them by start date, with most recent terms at the start
129
- pairs.sort(key=lambda x: x[0].start_at or datetime.min, reverse=True)
130
-
131
- return pairs
132
124
 
133
125
  @asyncSlot()
134
126
  async def load_course_list(self):
135
127
  self.courses = (await self.data_manager.get_data())
136
-
137
- self.resources.clear()
138
128
  self.selected_course = None
139
- self.course_selector_model.clear()
140
- self.course_selector_model.setHorizontalHeaderLabels(["Course"])
141
-
142
- courses_root = self.course_selector_model.invisibleRootItem()
143
-
144
- for term, courses in self.group_courses_by_term(self.courses):
145
- term_node = QStandardItem(term.name)
129
+ self.resources.clear()
146
130
 
147
- for course in courses:
148
- term_node.appendRow(ContainerItem(course))
149
- self.resources.update({resource.id: resource for resource in course.resources})
131
+ for course in self.courses:
132
+ self.resources.update({resource.id: resource for resource in course.resources})
150
133
 
151
- courses_root.appendRow(term_node)
134
+ self.course_list.load_course_list(self.courses)
152
135
 
153
- self.course_selector.expandAll()
154
- # self.link_transformer.files = self.resources
155
136
 
156
- @Slot(QItemSelection, QItemSelection)
157
- def on_item_clicked(self, selected: QItemSelection, deselected: QItemSelection):
158
- if len(selected.indexes()) == 0:
137
+ @Slot(db.Course)
138
+ def on_course_selected(self, course: Optional[db.Course]):
139
+ if course is not None:
140
+ self.selected_course = course
141
+ self.pages_viewer.fill_tree(course)
142
+ self.assignment_viewer.fill_tree(course)
143
+ self.file_viewer.load_course_files(course)
144
+ else:
159
145
  self.selected_course = None
160
- return
161
-
162
- node = self.course_selector_model.itemFromIndex(selected.indexes()[0])
146
+ self.file_viewer.clear()
163
147
 
164
- if isinstance(node, ContainerItem):
165
- item = node.content
166
-
167
- if isinstance(item, db.Course):
168
- self.selected_course = item
169
- self.pages_viewer.fill_tree(item)
170
- self.assignment_viewer.fill_tree(item)
171
- self.file_viewer.load_course_files(item)
172
- return
173
-
174
- self.selected_course = None
175
- self.file_viewer.clear()
176
148
 
177
149
  @asyncSlot(db.CoursePreferences)
178
150
  async def course_file_group_by_preference_changed(self, preference: db.GroupByPreference):
179
151
  self.selected_course.preferences.files_group_by_preference = preference
180
- await self.data_manager.update_course_preferences(self.selected_course.preferences)
152
+ await self.data_manager.update_item(self.selected_course.preferences)
181
153
  self.file_viewer.load_course_files(self.selected_course)
182
154
 
@@ -0,0 +1,151 @@
1
+ import traceback
2
+ from threading import Semaphore
3
+ from typing import Optional
4
+
5
+ from PySide6.QtWidgets import QProgressBar
6
+ from qasync import asyncSlot
7
+
8
+ from qcanvas.QtVersionHelper.QtGui import QWindow, QDesktopServices
9
+ from qcanvas.QtVersionHelper.QtWidgets import QDialog, QWidget, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QPushButton, QDialogButtonBox, QCheckBox, QGridLayout, QMessageBox
10
+ from qcanvas.QtVersionHelper.QtCore import Qt, Slot, QUrl
11
+ from qcanvas.net.canvas import CanvasClient
12
+ from qcanvas.util import AppSettings
13
+
14
+ tutorial_url = "https://www.iorad.com/player/2053777/Canvas---How-to-generate-an-access-token-"
15
+
16
+ def row(name: str) -> QWidget:
17
+ widget = QWidget()
18
+ layout = QHBoxLayout()
19
+
20
+ layout.addWidget(QLabel(name))
21
+ layout.addWidget(QLineEdit())
22
+
23
+ widget.setLayout(layout)
24
+
25
+ return widget
26
+
27
+
28
+ class SetupDialog(QDialog):
29
+ def __init__(self, parent: Optional[QWidget] = None, allow_cancel: bool = True):
30
+ super().__init__(parent)
31
+ self.setWindowTitle("Setup")
32
+ self._row_counter = 0
33
+ self._operation_sem = Semaphore()
34
+
35
+ self.progress_bar = QProgressBar()
36
+ self.progress_bar.setMaximum(0)
37
+ self.progress_bar.setMinimum(0)
38
+ self.progress_bar.setValue(0)
39
+ self.progress_bar.hide()
40
+
41
+ self.grid = QGridLayout()
42
+ grid_widget = QWidget()
43
+ grid_widget.setLayout(self.grid)
44
+
45
+ stack = QVBoxLayout()
46
+
47
+ self.canvas_url = QLineEdit(AppSettings.canvas_url or "")
48
+ # self.panopto_url = QLineEdit()
49
+ self.canvas_api_key = QLineEdit(AppSettings.canvas_api_key or "")
50
+
51
+ self._row("Canvas URL", self.canvas_url)
52
+ # self._row("Painopto URL", self.panopto_url)
53
+ self._row("Canvas API key", self.canvas_api_key)
54
+
55
+ self.grid.addWidget(self.progress_bar, self._row_counter, 0, 1, 2)
56
+
57
+ button_box = QDialogButtonBox(
58
+ QDialogButtonBox.Save | QDialogButtonBox.Cancel if allow_cancel else QDialogButtonBox.Ok)
59
+ button_box.addButton("How to get a canvas API key?", QDialogButtonBox.ButtonRole.HelpRole)
60
+
61
+ button_box.helpRequested.connect(self._show_help)
62
+ button_box.accepted.connect(self._verify)
63
+ button_box.rejected.connect(lambda: self.reject())
64
+
65
+ stack.addWidget(grid_widget)
66
+ stack.addWidget(button_box)
67
+
68
+ self.setLayout(stack)
69
+ self.resize(500, 200)
70
+
71
+ @asyncSlot()
72
+ async def _verify(self):
73
+ if self._operation_sem.acquire(False):
74
+ try:
75
+ def invalid(name: str):
76
+ msg = QMessageBox(QMessageBox.Icon.Warning,
77
+ "Error",
78
+ f"{name} is invalid",
79
+ parent=self
80
+ )
81
+ msg.show()
82
+
83
+ def ensure_protocol(url: str):
84
+ if len(url) > 0 and not (url.startswith("http://") or url.startswith("https://")):
85
+ return "https://" + url
86
+ else:
87
+ return url
88
+
89
+ canvas_url_text = ensure_protocol(self.canvas_url.text().strip())
90
+ # panopto_url_text = ensure_protocol(self.panopto_url.text().strip())
91
+ canvas_api_key_text = self.canvas_api_key.text().strip()
92
+
93
+ if not (len(canvas_url_text) > 0 and QUrl(canvas_url_text).isValid()):
94
+ invalid("Canvas URL")
95
+ return
96
+ # if not (len(panopto_url_text) > 0 and QUrl(panopto_url_text).isValid()):
97
+ # invalid("Panopto URL")
98
+ # return
99
+ elif not len(canvas_api_key_text) > 0:
100
+ invalid("API key")
101
+ elif not (await self._verify_canvas_config(canvas_url_text, canvas_api_key_text)):
102
+ msg = QMessageBox(QMessageBox.Icon.Warning,
103
+ "Error",
104
+ f"The canvas URL or API key is invalid.\nPlease check you entered them correctly.",
105
+ parent=self
106
+ )
107
+ msg.show()
108
+ else:
109
+ AppSettings.canvas_url = canvas_url_text
110
+ AppSettings.canvas_api_key = canvas_api_key_text
111
+
112
+ self.accept()
113
+ finally:
114
+ self._operation_sem.release()
115
+ else:
116
+ QMessageBox(QMessageBox.Icon.Critical, "Error", "An operation is already in progress", parent=self).show()
117
+
118
+ async def _verify_canvas_config(self, canvas_url: str, api_key: str) -> bool:
119
+ self.progress_bar.show()
120
+
121
+ try:
122
+ return await CanvasClient.verify_config(canvas_url, api_key)
123
+ except:
124
+ traceback.print_exc()
125
+ return False
126
+ finally:
127
+ self.progress_bar.hide()
128
+
129
+ @Slot()
130
+ def _show_help(self):
131
+ msg = QMessageBox(
132
+ QMessageBox.Icon.Information,
133
+ "Help",
134
+ """An interactive tutorial will open in your browser when you click OK.
135
+
136
+ Note that the "purpose" text doesn't matter and you can enter anything you want.
137
+
138
+ You should also leave the "expires" item blank if you want the key to last forever.
139
+
140
+ Don't share this key. You can revoke it at any time.""",
141
+ parent=self
142
+ )
143
+ msg.accepted.connect(lambda: QDesktopServices.openUrl(tutorial_url))
144
+ msg.show()
145
+
146
+
147
+ def _row(self, name: str, widget: QWidget):
148
+ self.grid.addWidget(QLabel(name), self._row_counter, 0)
149
+ self.grid.addWidget(widget, self._row_counter, 1)
150
+
151
+ self._row_counter += 1
@@ -0,0 +1,98 @@
1
+ from datetime import datetime
2
+ from typing import Sequence, Union, Optional
3
+
4
+ from qasync import asyncSlot
5
+
6
+ from qcanvas.QtVersionHelper.QtWidgets import QTreeView
7
+ from qcanvas.QtVersionHelper.QtGui import QStandardItemModel, QStandardItem
8
+ from qcanvas.QtVersionHelper.QtCore import QItemSelection, Slot, Signal, Qt, QObject, QModelIndex
9
+
10
+ import qcanvas.db as db
11
+ from qcanvas.util.course_indexer import DataManager
12
+
13
+
14
+ class CourseNode(QStandardItem, QObject):
15
+ name_changed = Signal(db.Course, str)
16
+
17
+ def __init__(self, course: db.Course):
18
+ QObject.__init__(self)
19
+ QStandardItem.__init__(self, course.preferences.local_name or course.name)
20
+ self.course = course
21
+
22
+ def setData(self, value, role = ...):
23
+ if isinstance(value, str):
24
+ value = value.strip()
25
+
26
+ if len(value) == 0:
27
+ super().setData(self.course.name, role)
28
+ self.name_changed.emit(self.course, None)
29
+ else:
30
+ super().setData(value, role)
31
+ self.name_changed.emit(self.course, value)
32
+ else:
33
+ super().setData(value, role)
34
+
35
+
36
+ class CourseList(QTreeView):
37
+ course_selected = Signal(db.Course)
38
+
39
+ def __init__(self, data_manager: DataManager):
40
+ super().__init__()
41
+ self.data_manager = data_manager
42
+ self.model = QStandardItemModel()
43
+ self.setModel(self.model)
44
+ self.selectionModel().selectionChanged.connect(self._on_selection_changed)
45
+
46
+ @asyncSlot(db.Course, str)
47
+ async def course_name_changed(self, course: db.Course, name: str):
48
+ course.preferences.local_name = name
49
+ await self.data_manager.update_item(course.preferences)
50
+
51
+ def load_course_list(self, courses: Sequence[db.Course]):
52
+ self.model.clear()
53
+ self.model.setHorizontalHeaderLabels(["Course"])
54
+
55
+ courses_root = self.model.invisibleRootItem()
56
+
57
+ for term, courses in self.group_courses_by_term(courses):
58
+ term_node = QStandardItem(term.name)
59
+ term_node.setEditable(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)
97
+
98
+
@@ -6,30 +6,31 @@ from bs4 import BeautifulSoup
6
6
  import qcanvas.db as db
7
7
  from qcanvas.QtVersionHelper.QtWidgets import QWidget, QTextBrowser, QTreeView, QHBoxLayout
8
8
  from qcanvas.QtVersionHelper.QtGui import QStandardItemModel
9
- from qcanvas.QtVersionHelper.QtCore import QItemSelection, Slot, QUrl, QModelIndex
9
+ from qcanvas.QtVersionHelper.QtCore import QItemSelection, Slot
10
10
  from qcanvas.ui.container_item import ContainerItem
11
11
  from qcanvas.util import canvas_garbage_remover
12
12
  from qcanvas.util.constants import default_assignments_module_names
13
13
  from qcanvas.util.course_indexer import resource_helpers
14
14
  from qcanvas.util.linkscanner import ResourceScanner
15
15
 
16
+
16
17
  class LinkTransformer:
17
18
  # This is used to indicate that a "link" is actually a resource. The resource id is concatenated to this string.
18
19
  # It just has to be a valid url or qt does not send it to anchorClicked properly
19
- transformed_url_prefix = "http://use-your-imagination.sexy/"
20
+ transformed_url_prefix = "data:,"
20
21
 
21
22
  def __init__(self, link_scanners: Sequence[ResourceScanner], files: dict[str, db.Resource]):
22
23
  self.link_scanners = link_scanners
23
24
  self.files = files
24
25
 
25
- def transform_links(self, html : str):
26
+ def transform_links(self, html: str):
26
27
  doc = BeautifulSoup(html, 'html.parser')
27
28
 
28
29
  for element in doc.find_all(resource_helpers.resource_elements):
29
30
  for scanner in self.link_scanners:
30
31
  if scanner.accepts_link(element):
31
32
  resource_id = f"{scanner.name}:{scanner.extract_id(element)}"
32
- #todo make images actually show on the viewer page if they're downloaded
33
+ # todo make images actually show on the viewer page if they're downloaded
33
34
  if resource_id in self.files:
34
35
  file = self.files[resource_id]
35
36
 
@@ -48,13 +49,14 @@ class LinkTransformer:
48
49
 
49
50
  return str(doc)
50
51
 
52
+
51
53
  class PageLikeViewer(QWidget):
52
- def __init__(self, header_name : str, link_transformer : LinkTransformer):
54
+ def __init__(self, header_name: str, link_transformer: LinkTransformer):
53
55
  super().__init__()
54
56
  self.viewer = QTextBrowser()
55
57
  self.viewer.setOpenLinks(False)
56
58
 
57
- #todo just use QTreeWidget instead
59
+ # todo just use QTreeWidget instead
58
60
  self.tree = QTreeView()
59
61
  self.model = QStandardItemModel()
60
62
  self.header_name = header_name
@@ -70,7 +72,6 @@ class PageLikeViewer(QWidget):
70
72
  layout.setStretch(1, 1)
71
73
  self.setLayout(layout)
72
74
 
73
-
74
75
  def fill_tree(self, data: db.Course):
75
76
  self.model.clear()
76
77
  self.viewer.clear()
@@ -78,7 +79,6 @@ class PageLikeViewer(QWidget):
78
79
  self.model.setHorizontalHeaderLabels([self.header_name])
79
80
  self.tree.expandAll()
80
81
 
81
-
82
82
  @abstractmethod
83
83
  def _internal_fill_tree(self, data: db.Course):
84
84
  ...
@@ -126,7 +126,7 @@ class AssignmentsViewer(PageLikeViewer):
126
126
  def __init__(self, link_transformer: LinkTransformer):
127
127
  super().__init__("Putting the ASS in assignments", link_transformer)
128
128
 
129
- def _internal_fill_tree(self, course: db.Course):
129
+ def _internal_fill_tree(self, course: db.Course):
130
130
  root = self.model.invisibleRootItem()
131
131
 
132
132
  default_assessments_module = None
@@ -1,15 +1,33 @@
1
- from datetime import datetime
2
-
3
- from qcanvas.QtVersionHelper.QtCore import QSettings
1
+ from qcanvas.QtVersionHelper.QtCore import QSettings, QUrl
4
2
 
5
3
 
6
4
  class _AppSettings:
7
- settings = QSettings("RetardSoft", "QCanvasViewer")
5
+ def __init__(self):
6
+ self.settings = QSettings("QCanvas", "client")
7
+ self.auxiliary = QSettings("QCanvas", "ui")
8
+ self._canvas_url = self.settings.value("canvas_url", None)
9
+ self._api_key = self.settings.value("api_key", defaultValue=None)
8
10
 
9
11
  @property
10
- def canvas_url(self) -> str:
11
- return str(self.settings.value("canvas_url"))
12
+ def canvas_url(self) -> str | None:
13
+ return self._canvas_url
14
+
15
+ @canvas_url.setter
16
+ def canvas_url(self, value: str):
17
+ self._canvas_url = value
18
+ self.settings.setValue("canvas_url", value)
12
19
 
13
20
  @property
14
- def canvas_api_key(self) -> str:
15
- return str(self.settings.value("api_key"))
21
+ def canvas_api_key(self) -> str | None:
22
+ return self._api_key
23
+
24
+ @canvas_api_key.setter
25
+ def canvas_api_key(self, value: str):
26
+ self._api_key = value
27
+ self.settings.setValue("api_key", value)
28
+
29
+ @property
30
+ def is_set(self):
31
+ return self.canvas_url is not None and QUrl(self.canvas_url).isValid() and self.canvas_url is not None
32
+
33
+
qcanvas/util/constants.py CHANGED
@@ -1 +1,2 @@
1
1
  default_assignments_module_names = ["assessments", "assessment"]
2
+ app_name = "QCanvas (Under construction)"
@@ -140,9 +140,9 @@ class DataManager:
140
140
 
141
141
  await self.download_pool.submit(resource.id, lambda: self._download_resource_helper(scanner, resource))
142
142
 
143
- async def update_course_preferences(self, preferences: db.CoursePreferences):
143
+ async def update_item(self, item: db.Base):
144
144
  async with self._session_maker.begin() as session:
145
- await session.merge(preferences)
145
+ await session.merge(item)
146
146
 
147
147
  async def get_data(self):
148
148
  """
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qcanvas
3
- Version: 0.0.2a0
3
+ Version: 0.0.3a0
4
4
  Summary: A canvas client
5
5
  Author: QCanvas
6
6
  Classifier: Operating System :: OS Independent
7
7
  Classifier: Programming Language :: Python :: 3
8
- Requires-Python: >=3.10
8
+ Requires-Python: ==3.11
9
9
  Requires-Dist: aiosqlite-custom~=0.19.0
10
10
  Requires-Dist: beautifulsoup4~=4.12.3
11
11
  Requires-Dist: bs4~=0.0.1
@@ -17,6 +17,5 @@ Requires-Dist: pyside6~=6.6.1
17
17
  Requires-Dist: qasync~=0.27.1
18
18
  Requires-Dist: qenerate-custom~=0.6.3
19
19
  Requires-Dist: requests~=2.31.0
20
- Requires-Dist: setuptools~=69.0.3
21
20
  Requires-Dist: sqlalchemy~=2.0.25
22
21
  Requires-Dist: tenacity~=8.2.3
@@ -1,17 +1,17 @@
1
1
  qcanvas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- qcanvas/__main__.py,sha256=I-oTK8nEaVo5OxJIRgM4AGoraYzXqkhJu2mwYp-yAww,2149
2
+ qcanvas/__main__.py,sha256=xrVcfihwbMdEeO3QwCaIU4C6pJjBBdkdtp8pjDpJrxs,3552
3
3
  qcanvas/QtVersionHelper/__init__.py,sha256=AHg0tv2hhTpapl6pZQWDWqOSes0ceHx0wMLSsHJEG1s,405
4
4
  qcanvas/QtVersionHelper/QtCore/__init__.py,sha256=LYVV4s0gsUn1yi8NLB_H0RPFR4IaLH0WzVcBJ4TA-3g,240
5
5
  qcanvas/QtVersionHelper/QtGui/__init__.py,sha256=Ml3Evje04C7hr2WSBALEJy79Yz7C92ZkE2hmBHbULzQ,1026
6
6
  qcanvas/QtVersionHelper/QtWidgets/__init__.py,sha256=Kq-bgC2LJzwuTmH_A8i7Y6WrhsQYT4RO3xEVDU8szEc,246
7
7
  qcanvas/db/__init__.py,sha256=wK5HUSGOplql2RBTpw6dSCwRjHChtTWX2zkhbe6OaQw,414
8
- qcanvas/db/database.py,sha256=UDBKFmce7Ed9fdgCFjAjxwgtcSdr3RrwXxdbMXpeJBE,10144
8
+ qcanvas/db/database.py,sha256=2eWREp5DhHfP5JBWGyQIKnc5iApvKMbUwjWUtXFT5Z4,10251
9
9
  qcanvas/db/db_converter_helper.py,sha256=LxmGxv50gM7Ir6TRksIqufnVk0WIL5BOAVTYl9MR6GE,2167
10
10
  qcanvas/net/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  qcanvas/net/custom_httpx_async_transport.py,sha256=kSkBD1v476CVPY2qWAarcyG8mbKyNDNuR32ByIhG94c,1212
12
12
  qcanvas/net/self_authenticating.py,sha256=Qyx8vYtSLDClx-WMcrVUeeqUI5jCftiuAqN2RU8goUE,2514
13
13
  qcanvas/net/canvas/__init__.py,sha256=NgLiTbeKoYKDVoEd1AoP8RB_tEFi6Jzwj9gGhSu5mw4,95
14
- qcanvas/net/canvas/canvas_client.py,sha256=uV75qHy91RhvSMqGkkypiJ5EGp7bZnra6D54G_ifRFQ,8865
14
+ qcanvas/net/canvas/canvas_client.py,sha256=Dnb3PC_9eNf2ynAH-HivYOAX2zTaDC5Al1a_FcJWrrA,8870
15
15
  qcanvas/net/canvas/legacy_canvas_types.py,sha256=QN1BFaDF-W1ep0X2Ax2n92-I66IzxqLdRwHYrAom5Ck,3926
16
16
  qcanvas/queries/__init__.py,sha256=tRUwMS1OvYIWRd4WpmAHrDrcelj_dLEaesxh9NngC6s,227
17
17
  qcanvas/queries/all_courses.gql,sha256=LoGcuyYtA0tVC2L8BVOmfC10DfvhFAxxhqhovvBYOuU,116
@@ -19,23 +19,24 @@ qcanvas/queries/all_courses.py,sha256=kt-vEclAYUGoscQc2W4wwYGwJ93CIElzF215I1wo7_
19
19
  qcanvas/queries/canvas_course_data.gql,sha256=KJHjdu5sMKld4L6mPGfUrhqRLx_wdgGH8fwTnmRFGrI,1115
20
20
  qcanvas/queries/canvas_course_data.py,sha256=JDWDGv-aZ1n9y5uNtKTG5v33BZqA7dceLzL3-_zoU94,4311
21
21
  qcanvas/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- qcanvas/ui/container_item.py,sha256=yfAPzkdxAYAHXkq8OCaAOgD9VPJCnwsDSscw-aYJ55Y,426
23
- qcanvas/ui/main_ui.py,sha256=v6TkxUvmZW7MCf0rxLWFQKRAI1mLk9ia6nFPFfpCgVU,7145
22
+ qcanvas/ui/container_item.py,sha256=rcoCp7xS4WGdbfH4pDOirTn3ioBQqCEsfgQzLKHiF7M,437
23
+ qcanvas/ui/main_ui.py,sha256=5g8ekck2vFsUjXMSkBEE6lkrgbOwex9x81uu2-41nVs,5975
24
+ qcanvas/ui/setup_dialog.py,sha256=EJjlsWlBo3bRncuDcdqctSVkEBm7VYv2veJlKhuHvd0,5666
24
25
  qcanvas/ui/viewer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- qcanvas/ui/viewer/course_viewer.py,sha256=vq2umErB_6cTsBtfcOAhJWKuB6UFscBMbterDdHH0ik,198
26
+ qcanvas/ui/viewer/course_list.py,sha256=qa7hqDi2gDToit0bN_TUXGuZSc7yQhPOFOho8yj_3Rc,3457
26
27
  qcanvas/ui/viewer/file_list.py,sha256=jL0BKls-90LSUzKA4-V_8ViznrzONNEUjLcRLPzLNCU,6441
27
28
  qcanvas/ui/viewer/file_view_tab.py,sha256=QAClZVnJQ0PsVD4AMR8DyfKwGwBdcY2M08pRCmuTH3g,4684
28
- qcanvas/ui/viewer/page_list_viewer.py,sha256=A4IKd70uTNgNq_r4XLiZanX3lTIFyhUZyvmyhvTS6Fc,5590
29
+ qcanvas/ui/viewer/page_list_viewer.py,sha256=4diKsosp5lZ0FnDzGIVabF8mevUIHS55ncrFGckk3yc,5542
29
30
  qcanvas/util/__init__.py,sha256=4nrMAMxWMpItgg2iaE1Z6ysoRVG3ljUTHrVNxOazGGQ,68
30
- qcanvas/util/app_settings.py,sha256=AnvmC0oWB0YrqYtJiT3gCoG2yfxc8dUEZg2ltnVNP8k,366
31
+ qcanvas/util/app_settings.py,sha256=3wFq_FMwPwPqr4v8tYMUM1O-ATdeA4ww2djaEt_nuzk,974
31
32
  qcanvas/util/canvas_garbage_remover.py,sha256=c6r6cyJomQ1zn5wFK-LX-ZsZXQZ-A2awBFtCB_JqUtE,1184
32
- qcanvas/util/constants.py,sha256=Gx8g7DPocfBw4dppog_uJgfZthKpptUY-H0PvVAlLMg,65
33
+ qcanvas/util/constants.py,sha256=e8xUAI3F-XogKQu_GMXETVaNbIiHfv4oc5XyFUpBzM0,106
33
34
  qcanvas/util/download_pool.py,sha256=ezQH3Dp8aPCYUudLfqgDmGILY_H_0KgrJR1BBg5GTN4,2375
34
35
  qcanvas/util/file_icon_helper.py,sha256=3xcvclI0QoKhE0_vPLWAqLgVpO-G_ra524DNYgg2YRM,630
35
36
  qcanvas/util/task_pool.py,sha256=tOkcjQ6MqjztpA8L94nqfFemrsWRYXuCx04oIJmxT48,8964
36
37
  qcanvas/util/course_indexer/__init__.py,sha256=qDPevtjuQv72m23QLg6uXN3MDugJuKYF_12xlsWehNI,37
37
38
  qcanvas/util/course_indexer/conversion_helpers.py,sha256=WggJmf4to7hpBpDTOpdBbaDQQv6OdEuLiFnhazcsjcM,2660
38
- qcanvas/util/course_indexer/data_manager.py,sha256=7HvXT1tBBiZ_20WOE6B6xb5pQsvGYyX-9EVfZGkYPn0,15530
39
+ qcanvas/util/course_indexer/data_manager.py,sha256=HROkN9BVISKSpndBKmsbPpgdJYs9bHeU7GgUcVsJSkI,15489
39
40
  qcanvas/util/course_indexer/resource_helpers.py,sha256=M1iT3ShQ2xe3BZ8wNnJU3nm6LiqDqLR2MwkDHaIc32Q,6465
40
41
  qcanvas/util/linkscanner/__init__.py,sha256=gIg-zNU0VtmJgEWNS__MNRrBToyKMXLo15q2UoVeI5I,96
41
42
  qcanvas/util/linkscanner/canvas_link_scanner.py,sha256=Bl2Rzyb9wdkYx8yN0_r1zdm3wiC8Y1Y-Csd0RIBuAfs,1466
@@ -46,6 +47,6 @@ qcanvas/util/tree_util/__init__.py,sha256=iW-k9zMIPL3xrDby8N38UsMNpri6t9qPceXznm
46
47
  qcanvas/util/tree_util/expanding_tree.py,sha256=Mmi0U8qe_CbwQo_areF_TMcqHlccKXIB1b29CwBagco,6748
47
48
  qcanvas/util/tree_util/model_helpers.py,sha256=gtkZAUIxRbFUSHeiI65GAD_y9q2-HrJPH1ijYSstS5c,794
48
49
  qcanvas/util/tree_util/tree_model.py,sha256=SDrm8XVZFllP1NFEnrTeSsCBct_Dx23pyJ5DGwoOhB0,3015
49
- qcanvas-0.0.2a0.dist-info/METADATA,sha256=LqRUI6zG1lUnnQcXc5kd0XEPybiS0d2Aj94QxgeFLLo,670
50
- qcanvas-0.0.2a0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
51
- qcanvas-0.0.2a0.dist-info/RECORD,,
50
+ qcanvas-0.0.3a0.dist-info/METADATA,sha256=e_xVT_Ai46iMlCEx1eYxWg8-EdXMoq7KTjt3Svpyrrs,636
51
+ qcanvas-0.0.3a0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
52
+ qcanvas-0.0.3a0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- import qcanvas.db as db
2
-
3
- class CourseViewer:
4
- course: db.Course | None = None
5
-
6
- def __init__(self):
7
- pass
8
-
9
- def load_course(self, course : db.Course):
10
- self.course = course
11
-
12
-