qcanvas 1.2.2__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of qcanvas might be problematic. Click here for more details.

Files changed (128) hide show
  1. qcanvas/__init__.py +54 -0
  2. qcanvas/app.py +93 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +13 -5
  4. qcanvas/backend_connectors/qcanvas_task_master.py +2 -2
  5. qcanvas/icons/__init__.py +5 -5
  6. qcanvas/icons/_icon_type.py +1 -1
  7. qcanvas/icons/icons.qrc +47 -43
  8. qcanvas/icons/rc_icons.py +1298 -1197
  9. qcanvas/settings/__init__.py +6 -0
  10. qcanvas/{util/settings → settings}/_client_settings.py +4 -4
  11. qcanvas/settings/_course_settings.py +54 -0
  12. qcanvas/{util/settings → settings}/_mapped_setting.py +2 -5
  13. qcanvas/{util/settings → settings}/_ui_settings.py +4 -4
  14. qcanvas/theme.py +101 -0
  15. qcanvas/ui/course_viewer/content_tree.py +9 -12
  16. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +3 -3
  17. qcanvas/ui/course_viewer/course_tree/course_tree.py +9 -8
  18. qcanvas/ui/course_viewer/course_viewer.py +35 -43
  19. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +4 -4
  21. qcanvas/ui/course_viewer/tabs/constants.py +1 -0
  22. qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
  23. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +4 -4
  24. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +7 -10
  25. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +6 -7
  26. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
  27. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -8
  28. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +5 -5
  30. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +15 -21
  31. qcanvas/ui/course_viewer/tree_widget_data_item.py +1 -1
  32. qcanvas/ui/memory_tree/_tree_memory.py +45 -42
  33. qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
  34. qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
  35. qcanvas/ui/qcanvas_window/__init__.py +1 -0
  36. qcanvas/ui/{main_ui → qcanvas_window}/course_viewer_container.py +10 -10
  37. qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +5 -5
  38. qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
  39. qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
  40. qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +10 -10
  41. qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +38 -33
  42. qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +5 -6
  43. qcanvas/ui/qml_components/AttachmentsList.ui.qml +15 -0
  44. qcanvas/ui/qml_components/AttachmentsListDelegate.qml +77 -0
  45. qcanvas/ui/qml_components/AttachmentsListModel.qml +19 -0
  46. qcanvas/ui/qml_components/AttachmentsPane.qml +11 -0
  47. qcanvas/ui/qml_components/CommentsList.ui.qml +15 -0
  48. qcanvas/ui/qml_components/CommentsListDelegate.ui.qml +118 -0
  49. qcanvas/ui/qml_components/CommentsListModel.qml +56 -0
  50. qcanvas/ui/qml_components/CommentsPane.qml +11 -0
  51. qcanvas/ui/qml_components/DecoratedText.ui.qml +44 -0
  52. qcanvas/ui/qml_components/Spacer.ui.qml +7 -0
  53. qcanvas/ui/qml_components/ThemedRectangle.qml +53 -0
  54. qcanvas/ui/qml_components/__init__.py +3 -0
  55. qcanvas/ui/qml_components/attachments_pane.py +72 -0
  56. qcanvas/ui/qml_components/comments_pane.py +85 -0
  57. qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
  58. qcanvas/ui/qml_components/qml_pane.py +22 -0
  59. qcanvas/ui/setup/setup_checker.py +1 -1
  60. qcanvas/ui/setup/setup_dialog.py +27 -10
  61. qcanvas/util/auto_downloader.py +9 -7
  62. qcanvas/util/basic_fonts.py +2 -2
  63. qcanvas/util/context_dict.py +12 -0
  64. qcanvas/util/file_icons.py +11 -19
  65. qcanvas/util/layouts.py +5 -7
  66. qcanvas/util/paths.py +17 -6
  67. qcanvas/util/qurl_util.py +1 -1
  68. qcanvas/util/ui_tools.py +118 -8
  69. qcanvas/util/url_checker.py +1 -1
  70. {qcanvas-1.2.2.dist-info → qcanvas-2.0.1.dist-info}/METADATA +17 -14
  71. qcanvas-2.0.1.dist-info/RECORD +89 -0
  72. {qcanvas-1.2.2.dist-info → qcanvas-2.0.1.dist-info}/WHEEL +1 -1
  73. qcanvas-2.0.1.dist-info/entry_points.txt +3 -0
  74. qcanvas/app_start/__init__.py +0 -59
  75. qcanvas/icons/_update_icons.py +0 -89
  76. qcanvas/icons/dark/actions/exit.svg +0 -3
  77. qcanvas/icons/dark/actions/mark_all_read.svg +0 -3
  78. qcanvas/icons/dark/actions/open_downloads.svg +0 -3
  79. qcanvas/icons/dark/actions/quick_login.svg +0 -3
  80. qcanvas/icons/dark/actions/sync.svg +0 -3
  81. qcanvas/icons/dark/branding/logo_transparent.svg +0 -303
  82. qcanvas/icons/dark/options/auto_download.svg +0 -3
  83. qcanvas/icons/dark/options/theme.svg +0 -3
  84. qcanvas/icons/dark/tabs/assignments.svg +0 -3
  85. qcanvas/icons/dark/tabs/mail.svg +0 -3
  86. qcanvas/icons/dark/tabs/pages.svg +0 -3
  87. qcanvas/icons/dark/tree_items/assignment.svg +0 -3
  88. qcanvas/icons/dark/tree_items/mail.svg +0 -3
  89. qcanvas/icons/dark/tree_items/module.svg +0 -3
  90. qcanvas/icons/dark/tree_items/page.svg +0 -3
  91. qcanvas/icons/light/actions/exit.svg +0 -3
  92. qcanvas/icons/light/actions/mark_all_read.svg +0 -3
  93. qcanvas/icons/light/actions/open_downloads.svg +0 -3
  94. qcanvas/icons/light/actions/quick_login.svg +0 -3
  95. qcanvas/icons/light/actions/sync.svg +0 -3
  96. qcanvas/icons/light/branding/logo_transparent.svg +0 -304
  97. qcanvas/icons/light/options/auto_download.svg +0 -3
  98. qcanvas/icons/light/options/ignore_old.svg +0 -3
  99. qcanvas/icons/light/options/include_videos.svg +0 -3
  100. qcanvas/icons/light/options/theme.svg +0 -3
  101. qcanvas/icons/light/tabs/assignments.svg +0 -3
  102. qcanvas/icons/light/tabs/mail.svg +0 -3
  103. qcanvas/icons/light/tabs/pages.svg +0 -3
  104. qcanvas/icons/light/tree_items/assignment.svg +0 -3
  105. qcanvas/icons/light/tree_items/mail.svg +0 -3
  106. qcanvas/icons/light/tree_items/module.svg +0 -3
  107. qcanvas/icons/light/tree_items/page.svg +0 -3
  108. qcanvas/icons/universal/branding/main_icon.svg +0 -325
  109. qcanvas/icons/universal/downloads/download_failed.svg +0 -23
  110. qcanvas/icons/universal/downloads/downloaded.svg +0 -23
  111. qcanvas/icons/universal/downloads/not_downloaded.svg +0 -23
  112. qcanvas/icons/universal/downloads/unknown.svg +0 -6
  113. qcanvas/icons/universal/tabs/assignments_new_content.svg +0 -3
  114. qcanvas/icons/universal/tabs/mail_new_content.svg +0 -3
  115. qcanvas/icons/universal/tabs/pages_new_content.svg +0 -3
  116. qcanvas/icons/universal/tree_items/semester.svg +0 -108
  117. qcanvas/run.py +0 -54
  118. qcanvas/ui/course_viewer/tabs/util.py +0 -11
  119. qcanvas/ui/main_ui/__init__.py +0 -0
  120. qcanvas/util/settings/__init__.py +0 -9
  121. qcanvas/util/themes/__init__.py +0 -2
  122. qcanvas/util/themes/_colour_scheme_helper.py +0 -38
  123. qcanvas/util/themes/_selected_theme.py +0 -10
  124. qcanvas/util/themes/_theme_changed_event.py +0 -17
  125. qcanvas/util/themes/_theme_changer.py +0 -86
  126. qcanvas-1.2.2.dist-info/RECORD +0 -118
  127. qcanvas-1.2.2.dist-info/entry_points.txt +0 -3
  128. /qcanvas/ui/{main_ui → qcanvas_window}/options/__init__.py +0 -0
@@ -0,0 +1,6 @@
1
+ from ._client_settings import _ClientSettings
2
+ from ._ui_settings import _UISettings
3
+ from ._course_settings import course_configs
4
+
5
+ client = _ClientSettings()
6
+ ui = _UISettings()
@@ -1,11 +1,11 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import Optional
3
3
 
4
- from qcanvas_api_clients.canvas import CanvasClientConfig
5
- from qcanvas_api_clients.panopto import PanoptoClientConfig
4
+ from libqcanvas_clients.canvas import CanvasClientConfig
5
+ from libqcanvas_clients.panopto import PanoptoClientConfig
6
6
 
7
7
  from qcanvas.util import paths
8
- from qcanvas.util.settings._mapped_setting import BoolSetting, MappedSetting
8
+ from ._mapped_setting import BoolSetting, MappedSetting
9
9
 
10
10
  _logger = logging.getLogger(__name__)
11
11
 
@@ -0,0 +1,54 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from aiofile import async_open
5
+ from pydantic import BaseModel, RootModel, Field, ValidationError
6
+
7
+ from qcanvas.util import paths
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+
12
+ class CourseConfigData(BaseModel):
13
+ nickname: str | None = Field(default=None)
14
+
15
+ async def save(self) -> None:
16
+ await course_configs.save()
17
+
18
+
19
+ _CourseConfigurations = RootModel[dict[str, CourseConfigData]]
20
+
21
+
22
+ class _CourseConfig:
23
+ def __init__(self):
24
+ self._root_model = self._load_root_model()
25
+
26
+ def _load_root_model(self) -> _CourseConfigurations:
27
+ if self._storage_path.exists():
28
+ try:
29
+ return _CourseConfigurations.model_validate_json(
30
+ self._storage_path.read_text()
31
+ )
32
+ except ValidationError as e:
33
+ _logger.error("Failed to load course configs", exc_info=e)
34
+
35
+ return _CourseConfigurations({})
36
+
37
+ async def save(self) -> None:
38
+ async with async_open(self._storage_path, "wt") as file:
39
+ await file.write(self._root_model.model_dump_json(indent=4))
40
+
41
+ @property
42
+ def _storage_path(self) -> Path:
43
+ return paths.config_storage() / "course_settings.json"
44
+
45
+ def __getitem__(self, item: str) -> CourseConfigData:
46
+ if item in self._root_model.root:
47
+ return self._root_model.root[item]
48
+ else:
49
+ new_config = CourseConfigData()
50
+ self._root_model.root[item] = new_config
51
+ return new_config
52
+
53
+
54
+ course_configs = _CourseConfig()
@@ -1,14 +1,11 @@
1
1
  import logging
2
- from typing import *
3
2
 
4
- from qtpy.QtCore import QSettings
3
+ from PySide6.QtCore import QSettings
5
4
 
6
5
  _logger = logging.getLogger(__name__)
7
6
 
8
- T = TypeVar("T")
9
7
 
10
-
11
- class MappedSetting(Generic[T]):
8
+ class MappedSetting[T]:
12
9
  """
13
10
  Acts as a proxy for a named value in a QSettings object.
14
11
  Stores the value in memory when initialised and updates it accordingly, to protect it from changes on disk.
@@ -1,16 +1,16 @@
1
1
  import logging
2
2
 
3
- from qtpy.QtCore import QByteArray, QSettings
3
+ from PySide6.QtCore import QByteArray, QSettings
4
4
 
5
- from qcanvas.util.settings._mapped_setting import MappedSetting
6
- from qcanvas.util.themes import default_theme, ensure_theme_is_valid
5
+ from ._mapped_setting import MappedSetting
6
+ from qcanvas.theme import ensure_theme_is_valid
7
7
 
8
8
  _logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
11
  class ThemeSetting(MappedSetting):
12
12
  def __init__(self):
13
- super().__init__(default=default_theme)
13
+ super().__init__(default="auto")
14
14
 
15
15
  def __get__(self, instance, owner):
16
16
  return ensure_theme_is_valid(super().__get__(instance, owner))
qcanvas/theme.py ADDED
@@ -0,0 +1,101 @@
1
+ import logging
2
+ from typing import Literal
3
+
4
+ import qdarktheme
5
+ from PySide6.QtCore import QObject, Signal, Property, Slot
6
+ from PySide6.QtGui import QGuiApplication, Qt, QIcon
7
+ from PySide6.QtWidgets import QStyleFactory, QApplication
8
+
9
+ type Theme = Literal["native", "auto", "dark", "light"]
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class _AppTheme(QObject):
15
+ themeChanged = Signal()
16
+ darkModeChanged = Signal()
17
+
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._last_system_theme = QGuiApplication.styleHints().colorScheme()
21
+ self._theme: Theme | None = None
22
+ self._dark_mode: bool | None = None
23
+
24
+ self.darkModeChanged.connect(self._set_icon_paths)
25
+ QGuiApplication.styleHints().colorSchemeChanged.connect(
26
+ self._on_system_theme_changed
27
+ )
28
+
29
+ @Property(bool, notify=darkModeChanged)
30
+ def dark_mode(self) -> bool:
31
+ assert self._theme is not None, "Theme has not been set"
32
+ return self._dark_mode
33
+
34
+ @Property(str, notify=themeChanged)
35
+ def theme(self) -> Theme:
36
+ assert self._theme is not None, "Theme has not been set"
37
+ return self._theme
38
+
39
+ @theme.setter
40
+ def theme(self, value: str):
41
+ value = ensure_theme_is_valid(value)
42
+
43
+ if value != self._theme:
44
+ self._update_theme(value)
45
+
46
+ def _update_theme(self, theme: str):
47
+ if theme is None or (theme == self._theme and theme not in ["native", "auto"]):
48
+ return
49
+
50
+ was_dark_mode = self._dark_mode
51
+
52
+ if theme != "native":
53
+ if theme == "auto":
54
+ self._dark_mode = _is_system_using_dark_mode()
55
+ selected_colour_scheme = "dark" if self._dark_mode else "light"
56
+ else:
57
+ self._dark_mode = theme == "dark"
58
+ selected_colour_scheme = theme
59
+
60
+ if was_dark_mode != self._dark_mode:
61
+ qdarktheme.setup_theme(
62
+ selected_colour_scheme,
63
+ custom_colors={"primary": "e02424"},
64
+ )
65
+
66
+ QApplication.setStyle(QStyleFactory.create("Fusion"))
67
+ else:
68
+ self._dark_mode = _is_system_using_dark_mode()
69
+
70
+ if theme != self._theme:
71
+ self._theme = theme
72
+ self.themeChanged.emit()
73
+
74
+ if was_dark_mode != self._dark_mode:
75
+ self.darkModeChanged.emit()
76
+
77
+ @Slot()
78
+ def _set_icon_paths(self):
79
+ QIcon.setFallbackSearchPaths(
80
+ [":icons/dark" if self._dark_mode else ":icons/light", ":icons/universal"]
81
+ )
82
+
83
+ @Slot()
84
+ def _on_system_theme_changed(self, scheme: Qt.ColorScheme):
85
+ if scheme != self._last_system_theme:
86
+ self._last_system_theme = scheme
87
+ self._update_theme(self._theme)
88
+
89
+
90
+ def _is_system_using_dark_mode() -> bool:
91
+ return QGuiApplication.styleHints().colorScheme() == Qt.ColorScheme.Dark
92
+
93
+
94
+ def ensure_theme_is_valid(theme_name: str) -> Theme:
95
+ if theme_name not in ["auto", "light", "dark", "native"]:
96
+ return "auto"
97
+ else:
98
+ return theme_name
99
+
100
+
101
+ app_theme = _AppTheme()
@@ -1,11 +1,11 @@
1
1
  import logging
2
2
  from abc import abstractmethod
3
- from typing import *
3
+ from typing import Optional, Self, Sequence
4
4
 
5
- import qcanvas_backend.database.types as db
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtCore import QItemSelection, Signal, Slot
8
- from qtpy.QtWidgets import *
5
+ from libqcanvas import db
6
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
7
+ from PySide6.QtCore import QItemSelection, Signal, Slot
8
+ from PySide6.QtWidgets import QHeaderView, QTreeWidgetItem
9
9
 
10
10
  from qcanvas.ui.course_viewer.tree_widget_data_item import AnyTreeDataItem
11
11
  from qcanvas.ui.memory_tree import MemoryTreeWidget
@@ -13,17 +13,14 @@ from qcanvas.util.basic_fonts import bold_font, normal_font
13
13
 
14
14
  _logger = logging.getLogger(__name__)
15
15
 
16
- T = TypeVar("T")
17
- U = TypeVar("U", bound=Type["ContentTree"])
18
16
 
19
-
20
- class ContentTree(MemoryTreeWidget, Generic[T]):
17
+ class ContentTree[T](MemoryTreeWidget):
21
18
  item_selected = Signal(object)
22
19
 
23
20
  @classmethod
24
- def create_from_receipt(
21
+ def create_from_receipt[U: Self](
25
22
  cls: U, course: db.Course, *, sync_receipt: SyncReceipt
26
- ) -> Type[U]:
23
+ ) -> type[U]:
27
24
  tree = cls(course.id)
28
25
  tree.reload(course, sync_receipt=sync_receipt)
29
26
  return tree
@@ -32,7 +29,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
32
29
  self,
33
30
  tree_name: str,
34
31
  *,
35
- emit_selection_signal_for_type: Type,
32
+ emit_selection_signal_for_type: type,
36
33
  ):
37
34
  super().__init__(tree_name)
38
35
  self._reloading = False
@@ -3,9 +3,9 @@ import logging
3
3
  import random
4
4
 
5
5
  from cachetools import cached
6
- from qtpy.QtCore import QByteArray
7
- from qtpy.QtGui import QColor, QPainter, QPixmap
8
- from qtpy.QtSvg import QSvgRenderer
6
+ from PySide6.QtCore import QByteArray
7
+ from PySide6.QtGui import QColor, QPainter, QPixmap
8
+ from PySide6.QtSvg import QSvgRenderer
9
9
 
10
10
  _logger = logging.getLogger(__name__)
11
11
  _transparent = QColor("#00000000")
@@ -1,11 +1,12 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import Sequence
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
- from qtpy.QtCore import Qt, Signal
4
+ from libqcanvas import db
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from PySide6.QtCore import Qt, Signal
7
7
 
8
8
  from qcanvas import icons
9
+ from qcanvas.settings import course_configs
9
10
  from qcanvas.ui.course_viewer.content_tree import ContentTree
10
11
  from qcanvas.ui.course_viewer.course_tree._course_icon_generator import (
11
12
  CourseIconGenerator,
@@ -22,7 +23,7 @@ class _CourseTreeItem(TreeWidgetDataItem):
22
23
  self,
23
24
  id=course.id,
24
25
  data=course,
25
- strings=[course.configuration.nickname or course.name],
26
+ strings=[course_configs[course.id].nickname or course.name],
26
27
  )
27
28
 
28
29
  self._owner = owner
@@ -34,7 +35,7 @@ class _CourseTreeItem(TreeWidgetDataItem):
34
35
  | Qt.ItemFlag.ItemIsEnabled
35
36
  )
36
37
 
37
- def setData(self, column: int, role: int, value: Any):
38
+ def setData(self, column: int, role: int, value: object):
38
39
  if column != 0 or not isinstance(value, str):
39
40
  return super().setData(column, role, value)
40
41
 
@@ -59,11 +60,11 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
59
60
  )
60
61
 
61
62
  def create_tree_items(
62
- self, terms: List[db.Term], sync_receipt: SyncReceipt
63
+ self, terms: list[db.Term], sync_receipt: SyncReceipt
63
64
  ) -> Sequence[MemoryTreeWidgetItem]:
64
65
  widgets = []
65
66
 
66
- for term in reversed(terms):
67
+ for term in terms:
67
68
  term_widget = self._create_term_widget(term)
68
69
  course_icon_generator = CourseIconGenerator(term.id)
69
70
 
@@ -1,20 +1,19 @@
1
1
  import logging
2
2
  from dataclasses import dataclass
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtCore import Slot
8
- from qtpy.QtGui import QIcon
9
- from qtpy.QtWidgets import *
4
+ from libqcanvas import db
5
+ from libqcanvas.net.resources.download.resource_manager import ResourceManager
6
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
7
+ from PySide6.QtCore import Slot
8
+ from PySide6.QtGui import QIcon
9
+ from PySide6.QtWidgets import QTabWidget, QVBoxLayout, QWidget
10
10
 
11
11
  from qcanvas import icons
12
12
  from qcanvas.ui.course_viewer.tabs.assignment_tab import AssignmentTab
13
13
  from qcanvas.ui.course_viewer.tabs.mail_tab import MailTab
14
14
  from qcanvas.ui.course_viewer.tabs.page_tab import PageTab
15
- from qcanvas.util.basic_fonts import bold_font
16
15
  from qcanvas.util.layouts import layout
17
- from qcanvas.util.ui_tools import make_truncatable
16
+ import qcanvas.util.ui_tools as ui
18
17
 
19
18
  _logger = logging.getLogger(__name__)
20
19
 
@@ -23,10 +22,10 @@ _logger = logging.getLogger(__name__)
23
22
  class _Tab:
24
23
  icon: QIcon
25
24
  highlighted_icon: QIcon
25
+ update_type: type
26
26
 
27
27
 
28
28
  class CourseViewer(QWidget):
29
-
30
29
  def __init__(
31
30
  self,
32
31
  course: db.Course,
@@ -39,9 +38,9 @@ class CourseViewer(QWidget):
39
38
  self._course_id = course.id
40
39
  self._previous_tab_index = 0
41
40
 
42
- self._course_label = QLabel(course.name)
43
- self._course_label.setFont(bold_font)
44
- make_truncatable(self._course_label)
41
+ self._course_label = ui.label(
42
+ course.name, font=ui.font(point_size=13, bold=True), allow_truncation=True
43
+ )
45
44
 
46
45
  self._pages_tab = PageTab.create_from_receipt(
47
46
  course=course,
@@ -73,23 +72,26 @@ class CourseViewer(QWidget):
73
72
  # icon=icons.tabs.pages,
74
73
  # highlighted_icon=icons.tabs.pages_new_content,
75
74
  # )
76
- self._PAGES_TAB = self._setup_tab(
75
+ self._PAGES_TAB = self._set_up_tab(
77
76
  name="Pages",
78
77
  widget=self._pages_tab,
79
78
  icon=icons.tabs.pages,
80
79
  highlighted_icon=icons.tabs.pages_new_content,
80
+ content_update_key=db.Page,
81
81
  )
82
- self._ASSIGNMENTS_TAB = self._setup_tab(
82
+ self._ASSIGNMENTS_TAB = self._set_up_tab(
83
83
  name="Assignments",
84
84
  widget=self._assignments_tab,
85
85
  icon=icons.tabs.assignments,
86
86
  highlighted_icon=icons.tabs.assignments_new_content,
87
+ content_update_key=db.Assignment,
87
88
  )
88
- self._MAIL_TAB = self._setup_tab(
89
+ self._MAIL_TAB = self._set_up_tab(
89
90
  name="Mail",
90
91
  widget=self._mail_tab,
91
92
  icon=icons.tabs.mail,
92
93
  highlighted_icon=icons.tabs.mail_new_content,
94
+ content_update_key=db.Message,
93
95
  )
94
96
  # self._tabs.addTab(QLabel("Not implemented"), "Panopto") # The meme lives on!
95
97
 
@@ -97,11 +99,17 @@ class CourseViewer(QWidget):
97
99
  self._tab_widget.currentChanged.connect(self._tab_changed)
98
100
  self._highlight_tabs(sync_receipt)
99
101
 
100
- def _setup_tab(
101
- self, widget: QWidget, icon: QIcon, highlighted_icon: QIcon, name: str
102
+ def _set_up_tab(
103
+ self,
104
+ *,
105
+ widget: QWidget,
106
+ icon: QIcon,
107
+ highlighted_icon: QIcon,
108
+ name: str,
109
+ content_update_key: type,
102
110
  ) -> int:
103
111
  index = self._tab_widget.addTab(widget, icon, name)
104
- self._tabs[index] = _Tab(icon, highlighted_icon)
112
+ self._tabs[index] = _Tab(icon, highlighted_icon, update_type=content_update_key)
105
113
  return index
106
114
 
107
115
  def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
@@ -116,35 +124,19 @@ class CourseViewer(QWidget):
116
124
  _logger.debug(f"Index = {index}")
117
125
  if index != -1:
118
126
  _logger.debug(f"Previous tab = {self._previous_tab_index}")
119
- self._unhighlight_tab(self._previous_tab_index)
127
+ self._set_tab_highlight(self._previous_tab_index, False)
120
128
  self._previous_tab_index = index
121
129
 
122
130
  def _highlight_tabs(self, sync_receipt: SyncReceipt) -> None:
123
131
  updates = sync_receipt.updates_by_course.get(self._course_id, None)
124
132
 
125
- if updates is not None:
126
- if len(updates.updated_pages) > 0:
127
- self._highlight_tab(self._PAGES_TAB)
128
- else:
129
- self._unhighlight_tab(self._PAGES_TAB)
130
-
131
- if len(updates.updated_assignments) > 0:
132
- self._highlight_tab(self._ASSIGNMENTS_TAB)
133
- else:
134
- self._unhighlight_tab(self._ASSIGNMENTS_TAB)
135
-
136
- if len(updates.updated_messages) > 0:
137
- self._highlight_tab(self._MAIL_TAB)
138
- else:
139
- self._unhighlight_tab(self._MAIL_TAB)
140
- else:
141
- for index in range(0, len(self._tabs)):
142
- self._unhighlight_tab(index)
143
-
144
- def _highlight_tab(self, tab_index: int) -> None:
145
- tab = self._tabs[tab_index]
146
- self._tab_widget.setTabIcon(tab_index, tab.highlighted_icon)
133
+ for tab_index, tab in enumerate(self._tabs.values()):
134
+ self._set_tab_highlight(
135
+ tab_index, updates is not None and updates[tab.update_type] is not None
136
+ )
147
137
 
148
- def _unhighlight_tab(self, tab_index: int) -> None:
138
+ def _set_tab_highlight(self, tab_index: int, highlighted: bool) -> None:
149
139
  tab = self._tabs[tab_index]
150
- self._tab_widget.setTabIcon(tab_index, tab.icon)
140
+ self._tab_widget.setTabIcon(
141
+ tab_index, tab.highlighted_icon if highlighted else tab.icon
142
+ )
@@ -1,28 +1,40 @@
1
1
  import logging
2
2
 
3
- import qcanvas_backend.database.types as db
4
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
- from qtpy.QtWidgets import *
3
+ from PySide6.QtCore import Qt
4
+ from libqcanvas import db
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from libqcanvas.util import as_local
7
+ from PySide6.QtWidgets import (
8
+ QLabel,
9
+ QLayout,
10
+ QDockWidget,
11
+ QMainWindow,
12
+ )
13
+ from typing_extensions import override
7
14
 
8
- from qcanvas.ui.course_viewer.tabs.assignment_tab.assignment_tree import AssignmentTree
15
+ import qcanvas.util.ui_tools as ui
16
+ from qcanvas.backend_connectors import FrontendResourceManager
17
+ from .assignment_tree import AssignmentTree
18
+ from qcanvas.ui.qml_components import CommentsPane, AttachmentsPane
9
19
  from qcanvas.ui.course_viewer.tabs.content_tab import ContentTab
10
- from qcanvas.ui.course_viewer.tabs.util import date_strftime_format
11
- from qcanvas.util.basic_fonts import bold_label
12
- from qcanvas.util.layouts import grid_layout
20
+ from qcanvas.ui.course_viewer.tabs.constants import (
21
+ date_strftime_format,
22
+ )
13
23
 
14
24
  _logger = logging.getLogger(__name__)
15
25
 
16
26
 
17
27
  class AssignmentTab(ContentTab):
18
-
19
28
  def __init__(
20
29
  self,
21
30
  *,
22
31
  course: db.Course,
23
32
  sync_receipt: SyncReceipt,
24
- downloader: ResourceManager,
33
+ downloader: FrontendResourceManager,
25
34
  ):
35
+ # must be before super init, otherwise _setup_layout will be called before it is initialised
36
+ self._main_container = QMainWindow()
37
+
26
38
  super().__init__(
27
39
  explorer=AssignmentTree.create_from_receipt(
28
40
  course, sync_receipt=sync_receipt
@@ -31,38 +43,104 @@ class AssignmentTab(ContentTab):
31
43
  downloader=downloader,
32
44
  )
33
45
 
46
+ # fixme: can't figure out how to get the panes to have the right size without showing them when nothing is selected
47
+ self._comments_pane = CommentsPane(downloader)
48
+ self._comments_dock = ui.dock_widget(
49
+ title="Comments",
50
+ name="comments_dock",
51
+ widget=self._comments_pane,
52
+ min_size=ui.size(150, 150),
53
+ features=QDockWidget.DockWidgetFeature.DockWidgetMovable,
54
+ hide=False,
55
+ )
56
+
57
+ self._submission_files_pane = AttachmentsPane(downloader)
58
+ self._submission_files_dock = ui.dock_widget(
59
+ title="Submission Files",
60
+ name="sub_files_dock",
61
+ widget=self._submission_files_pane,
62
+ min_size=ui.size(150, 150),
63
+ features=QDockWidget.DockWidgetFeature.DockWidgetMovable,
64
+ hide=False,
65
+ )
66
+
67
+ self._main_container.setCentralWidget(self._viewer)
68
+ self._main_container.addDockWidget(
69
+ Qt.DockWidgetArea.RightDockWidgetArea, self._submission_files_dock
70
+ )
71
+ self._main_container.addDockWidget(
72
+ Qt.DockWidgetArea.RightDockWidgetArea, self._comments_dock
73
+ )
74
+
75
+ self._main_container.resizeDocks(
76
+ [self._submission_files_dock, self._comments_dock],
77
+ [350, 350],
78
+ Qt.Orientation.Horizontal,
79
+ )
80
+ self._main_container.resizeDocks(
81
+ [self._submission_files_dock],
82
+ [200],
83
+ Qt.Orientation.Vertical,
84
+ )
85
+
34
86
  self._due_date_label = QLabel("")
35
87
  self._score_label = QLabel("")
36
-
37
88
  self.enable_info_grid()
38
89
 
39
- def setup_info_grid(self) -> QGridLayout:
40
- grid = grid_layout(
41
- [
42
- [
43
- bold_label("Due:"),
44
- self._due_date_label,
45
- ],
46
- [
47
- bold_label("Score:"),
48
- self._score_label,
49
- ],
50
- ]
90
+ @override
91
+ def _setup_layout(self) -> None:
92
+ super()._setup_layout()
93
+ self.content_grid.replaceWidget(
94
+ self._viewer,
95
+ self._main_container,
51
96
  )
52
97
 
53
- grid.setColumnStretch(0, 0)
54
- grid.setColumnStretch(1, 1)
98
+ self.content_grid.setColumnStretch(0, 1)
99
+ self.content_grid.setColumnStretch(1, 3)
55
100
 
56
- return grid
101
+ @override
102
+ def setup_info_grid(self) -> QLayout:
103
+ return ui.form_layout(
104
+ {"Due": self._due_date_label, "Score": self._score_label},
105
+ )
57
106
 
107
+ # fixme: kind of a misleading name? it's not just updating the info "grid" anymore
108
+ @override
58
109
  def update_info_grid(self, assignment: db.Assignment) -> None:
59
110
  if assignment.due_date is not None:
60
- due_text = assignment.due_date.strftime(date_strftime_format)
111
+ due_text = as_local(assignment.due_date).strftime(date_strftime_format)
61
112
  else:
62
- due_text = "?"
113
+ due_text = "No due date"
63
114
 
64
115
  self._due_date_label.setText(due_text)
65
116
 
117
+ last_submission = assignment.submissions[-1] if assignment.submissions else None
118
+ submission_score = "-"
119
+
120
+ if last_submission and last_submission.score:
121
+ submission_score = last_submission.score
122
+
66
123
  self._score_label.setText(
67
- f"{assignment.mark or '?'}/{assignment.max_mark_possible or '?'}"
124
+ f"<b>{submission_score}</b>/{assignment.max_score or '?'}"
68
125
  )
126
+
127
+ if last_submission and last_submission.attachments:
128
+ self._submission_files_pane.load_files(last_submission.attachments)
129
+ self._submission_files_dock.show()
130
+ else:
131
+ self._submission_files_pane.clear_files()
132
+ self._submission_files_dock.hide()
133
+
134
+ if last_submission and last_submission.comments:
135
+ self._comments_pane.load_comments(last_submission.comments)
136
+ self._comments_dock.show()
137
+ else:
138
+ self._comments_pane.clear_comments()
139
+ self._comments_dock.hide()
140
+
141
+ @override
142
+ def _show_blank(self) -> None:
143
+ super()._show_blank()
144
+
145
+ self._comments_dock.hide()
146
+ self._submission_files_dock.hide()
@@ -1,10 +1,10 @@
1
1
  import logging
2
2
  from typing import Sequence
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
- from qtpy.QtCore import Qt
7
- from qtpy.QtWidgets import QHeaderView
4
+ from libqcanvas import db
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from PySide6.QtCore import Qt
7
+ from PySide6.QtWidgets import QHeaderView
8
8
 
9
9
  from qcanvas import icons
10
10
  from qcanvas.ui.course_viewer.content_tree import ContentTree
@@ -0,0 +1 @@
1
+ date_strftime_format = "%A, %Y-%m-%d, %H:%M:%S"