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 +63 -18
- qcanvas/db/database.py +9 -7
- qcanvas/net/canvas/canvas_client.py +2 -2
- qcanvas/ui/container_item.py +7 -2
- qcanvas/ui/main_ui.py +34 -62
- qcanvas/ui/setup_dialog.py +151 -0
- qcanvas/ui/viewer/course_list.py +98 -0
- qcanvas/ui/viewer/page_list_viewer.py +9 -9
- qcanvas/util/app_settings.py +26 -8
- qcanvas/util/constants.py +1 -0
- qcanvas/util/course_indexer/data_manager.py +2 -2
- {qcanvas-0.0.2a0.dist-info → qcanvas-0.0.3a0.dist-info}/METADATA +2 -3
- {qcanvas-0.0.2a0.dist-info → qcanvas-0.0.3a0.dist-info}/RECORD +14 -13
- qcanvas/ui/viewer/course_viewer.py +0 -12
- {qcanvas-0.0.2a0.dist-info → qcanvas-0.0.3a0.dist-info}/WHEEL +0 -0
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
qcanvas/ui/container_item.py
CHANGED
|
@@ -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
|
-
|
|
45
|
-
self.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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(
|
|
157
|
-
def
|
|
158
|
-
if
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 = "
|
|
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
|
|
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
|
|
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,
|
|
129
|
+
def _internal_fill_tree(self, course: db.Course):
|
|
130
130
|
root = self.model.invisibleRootItem()
|
|
131
131
|
|
|
132
132
|
default_assessments_module = None
|
qcanvas/util/app_settings.py
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
from
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
143
|
+
async def update_item(self, item: db.Base):
|
|
144
144
|
async with self._session_maker.begin() as session:
|
|
145
|
-
await session.merge(
|
|
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.
|
|
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:
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
23
|
-
qcanvas/ui/main_ui.py,sha256=
|
|
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/
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
50
|
-
qcanvas-0.0.
|
|
51
|
-
qcanvas-0.0.
|
|
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,,
|
|
File without changes
|