gini-toolkit 6.0.1.dev0__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.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/run_button.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""One morphing circular power button that replaces the separate Run / Stop
|
|
2
|
+
toolbar actions.
|
|
3
|
+
|
|
4
|
+
States (driven by MainWindow from the real run lifecycle):
|
|
5
|
+
|
|
6
|
+
* ready — green disc, white ▶ glyph. Click launches the topology.
|
|
7
|
+
* booting — a progress ring fills clockwise to (containers up / total) as the
|
|
8
|
+
fabric comes online; a soft core pulses. Click cancels (stops).
|
|
9
|
+
* running — accent disc, white ■ stop glyph, a gentle green "alive" breathing
|
|
10
|
+
ring. Click stops.
|
|
11
|
+
* stopping — muted disc with an indeterminate rotating arc while it winds down.
|
|
12
|
+
* error — red disc, white ▶ (retry), a pulsing red halo.
|
|
13
|
+
|
|
14
|
+
The widget only *paints* state; MainWindow decides what a click means and calls
|
|
15
|
+
`set_state` / `set_progress` at the right lifecycle points.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
|
|
21
|
+
from PySide6.QtCore import QRectF, QSize, Qt, QTimer, Signal
|
|
22
|
+
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF, QRadialGradient
|
|
23
|
+
from PySide6.QtCore import QPointF
|
|
24
|
+
from PySide6.QtWidgets import QWidget
|
|
25
|
+
|
|
26
|
+
_ANIM_STATES = {"booting", "running", "stopping", "error"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RunButton(QWidget):
|
|
30
|
+
clicked = Signal() # user pressed the button (MainWindow decides run vs stop)
|
|
31
|
+
|
|
32
|
+
def __init__(self, theme, parent=None) -> None:
|
|
33
|
+
super().__init__(parent)
|
|
34
|
+
self.theme = theme
|
|
35
|
+
self._state = "ready"
|
|
36
|
+
self._frac = 0.0 # eased boot-progress fraction actually drawn
|
|
37
|
+
self._target = 0.0 # up / total
|
|
38
|
+
self._up = self._total = 0
|
|
39
|
+
self._phase = 0.0 # animation phase (pulse / spin)
|
|
40
|
+
self._hover = False
|
|
41
|
+
self.setFixedSize(40, 34)
|
|
42
|
+
self.setCursor(Qt.PointingHandCursor)
|
|
43
|
+
self._refresh_tip()
|
|
44
|
+
self._timer = QTimer(self)
|
|
45
|
+
self._timer.setInterval(33)
|
|
46
|
+
self._timer.timeout.connect(self._tick)
|
|
47
|
+
|
|
48
|
+
# -- state in --------------------------------------------------------------
|
|
49
|
+
def set_state(self, state: str) -> None:
|
|
50
|
+
if state not in ("ready", "booting", "running", "stopping", "error"):
|
|
51
|
+
return
|
|
52
|
+
self._state = state
|
|
53
|
+
if state == "ready":
|
|
54
|
+
self._target = self._frac = 0.0
|
|
55
|
+
self._up = self._total = 0
|
|
56
|
+
elif state == "running":
|
|
57
|
+
self._target = 1.0
|
|
58
|
+
if state in _ANIM_STATES and not self._timer.isActive():
|
|
59
|
+
self._timer.start()
|
|
60
|
+
elif state not in _ANIM_STATES and self._timer.isActive():
|
|
61
|
+
self._timer.stop(); self._phase = 0.0
|
|
62
|
+
self._refresh_tip()
|
|
63
|
+
self.update()
|
|
64
|
+
|
|
65
|
+
def set_progress(self, up: int, total: int) -> None:
|
|
66
|
+
self._up, self._total = int(up), int(total)
|
|
67
|
+
self._target = (up / total) if total else 0.0
|
|
68
|
+
self._refresh_tip()
|
|
69
|
+
self.update()
|
|
70
|
+
|
|
71
|
+
def state(self) -> str:
|
|
72
|
+
return self._state
|
|
73
|
+
|
|
74
|
+
def _refresh_tip(self) -> None:
|
|
75
|
+
tip = {
|
|
76
|
+
"ready": "Run — launch this topology on Docker",
|
|
77
|
+
"booting": (f"Starting… {self._up}/{self._total} containers up"
|
|
78
|
+
if self._total else "Starting the topology…"),
|
|
79
|
+
"running": "Running — click to stop",
|
|
80
|
+
"stopping": "Stopping…",
|
|
81
|
+
"error": "Run failed — click to try again",
|
|
82
|
+
}[self._state]
|
|
83
|
+
self.setToolTip(tip)
|
|
84
|
+
|
|
85
|
+
# -- animation -------------------------------------------------------------
|
|
86
|
+
def _tick(self) -> None:
|
|
87
|
+
self._phase = (self._phase + 0.12) % (math.tau)
|
|
88
|
+
# ease the drawn fraction toward the real target so the ring glides
|
|
89
|
+
self._frac += (self._target - self._frac) * 0.25
|
|
90
|
+
self.update()
|
|
91
|
+
|
|
92
|
+
# -- theme hook (called by MainWindow on theme change) ---------------------
|
|
93
|
+
def refresh_theme(self) -> None:
|
|
94
|
+
self.update()
|
|
95
|
+
|
|
96
|
+
# -- input -----------------------------------------------------------------
|
|
97
|
+
def enterEvent(self, _e) -> None:
|
|
98
|
+
self._hover = True; self.update()
|
|
99
|
+
|
|
100
|
+
def leaveEvent(self, _e) -> None:
|
|
101
|
+
self._hover = False; self.update()
|
|
102
|
+
|
|
103
|
+
def mouseReleaseEvent(self, e) -> None:
|
|
104
|
+
if e.button() == Qt.LeftButton and self.rect().contains(e.position().toPoint()):
|
|
105
|
+
self.clicked.emit()
|
|
106
|
+
super().mouseReleaseEvent(e)
|
|
107
|
+
|
|
108
|
+
def sizeHint(self) -> QSize:
|
|
109
|
+
return QSize(40, 34)
|
|
110
|
+
|
|
111
|
+
# -- paint -----------------------------------------------------------------
|
|
112
|
+
def _disc_color(self) -> QColor:
|
|
113
|
+
t = self.theme.theme
|
|
114
|
+
return {
|
|
115
|
+
"ready": QColor(t.success),
|
|
116
|
+
"booting": QColor(t.success).darker(155), # dim so the progress ring pops
|
|
117
|
+
"running": QColor(t.accent),
|
|
118
|
+
"stopping": QColor(t.muted),
|
|
119
|
+
"error": QColor(t.danger),
|
|
120
|
+
}[self._state]
|
|
121
|
+
|
|
122
|
+
def paintEvent(self, _e) -> None:
|
|
123
|
+
t = self.theme.theme
|
|
124
|
+
p = QPainter(self)
|
|
125
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
126
|
+
w, h = self.width(), self.height()
|
|
127
|
+
cx, cy = w / 2.0, h / 2.0
|
|
128
|
+
r = min(w, h) / 2.0 - 4.0
|
|
129
|
+
disc = self._disc_color()
|
|
130
|
+
|
|
131
|
+
# ambient halo: hover on any state, plus the pulsing error / alive glow
|
|
132
|
+
halo = 0.0
|
|
133
|
+
if self._state == "error":
|
|
134
|
+
halo = 0.55 + 0.45 * math.sin(self._phase * 2)
|
|
135
|
+
elif self._state == "running":
|
|
136
|
+
halo = 0.25 + 0.20 * math.sin(self._phase)
|
|
137
|
+
elif self._hover:
|
|
138
|
+
halo = 0.5
|
|
139
|
+
if halo > 0.01:
|
|
140
|
+
glow_col = QColor(t.success) if self._state == "running" else disc
|
|
141
|
+
grad = QRadialGradient(cx, cy, r + 6)
|
|
142
|
+
gc = QColor(glow_col); gc.setAlpha(int(120 * halo))
|
|
143
|
+
grad.setColorAt(0.55, gc)
|
|
144
|
+
grad.setColorAt(1.0, QColor(gc.red(), gc.green(), gc.blue(), 0))
|
|
145
|
+
p.setPen(Qt.NoPen); p.setBrush(grad)
|
|
146
|
+
p.drawEllipse(QRectF(cx - r - 6, cy - r - 6, 2 * (r + 6), 2 * (r + 6)))
|
|
147
|
+
|
|
148
|
+
# the disc
|
|
149
|
+
core = QColor(disc)
|
|
150
|
+
if self._state == "booting": # subtle breathing core
|
|
151
|
+
core = core.lighter(int(108 + 8 * math.sin(self._phase * 2)))
|
|
152
|
+
p.setPen(QPen(disc.darker(135), 1.2))
|
|
153
|
+
p.setBrush(core)
|
|
154
|
+
p.drawEllipse(QRectF(cx - r, cy - r, 2 * r, 2 * r))
|
|
155
|
+
|
|
156
|
+
# rings
|
|
157
|
+
ring_rect = QRectF(cx - r + 3, cy - r + 3, 2 * (r - 3), 2 * (r - 3))
|
|
158
|
+
if self._state == "booting":
|
|
159
|
+
track = QPen(QColor(255, 255, 255, 85), 3.6); track.setCapStyle(Qt.RoundCap)
|
|
160
|
+
p.setPen(track); p.setBrush(Qt.NoBrush)
|
|
161
|
+
p.drawArc(ring_rect, 0, 360 * 16)
|
|
162
|
+
arc = QPen(QColor(255, 255, 255, 255), 3.6); arc.setCapStyle(Qt.RoundCap)
|
|
163
|
+
p.setPen(arc)
|
|
164
|
+
p.drawArc(ring_rect, 90 * 16, -int(self._frac * 360) * 16)
|
|
165
|
+
elif self._state == "stopping": # indeterminate spinner
|
|
166
|
+
arc = QPen(QColor(255, 255, 255, 210), 3); arc.setCapStyle(Qt.RoundCap)
|
|
167
|
+
p.setPen(arc); p.setBrush(Qt.NoBrush)
|
|
168
|
+
start = int(-math.degrees(self._phase * 2.4)) * 16
|
|
169
|
+
p.drawArc(ring_rect, start, 110 * 16)
|
|
170
|
+
|
|
171
|
+
# glyph
|
|
172
|
+
p.setPen(Qt.NoPen); p.setBrush(QColor("#ffffff"))
|
|
173
|
+
if self._state in ("running", "stopping"):
|
|
174
|
+
s = r * 0.62
|
|
175
|
+
p.drawRoundedRect(QRectF(cx - s / 2, cy - s / 2, s, s), 2, 2)
|
|
176
|
+
elif self._state in ("ready", "error"):
|
|
177
|
+
tri = QPolygonF([
|
|
178
|
+
QPointF(cx - r * 0.26, cy - r * 0.42),
|
|
179
|
+
QPointF(cx - r * 0.26, cy + r * 0.42),
|
|
180
|
+
QPointF(cx + r * 0.46, cy),
|
|
181
|
+
])
|
|
182
|
+
p.drawPolygon(tri)
|
|
183
|
+
p.end()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Settings dialog — organized into tabs (Appearance, Networking, GINI AI, Naming,
|
|
2
|
+
Pricing, Help). Edits the live Settings; the caller applies + persists the result."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtGui import QDoubleValidator
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, QLabel, QLineEdit,
|
|
8
|
+
QTabWidget, QVBoxLayout, QWidget,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
_THEMES = ("Dark", "Light", "GINI Brand", "High Contrast", "Sand", "Blue", "Green")
|
|
12
|
+
_TEXT_SIZES = ("Normal", "Large", "Extra Large")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _page(tabs: QTabWidget, title: str) -> QFormLayout:
|
|
16
|
+
"""Add a tab whose body is a QFormLayout, and return that form."""
|
|
17
|
+
w = QWidget()
|
|
18
|
+
lay = QVBoxLayout(w)
|
|
19
|
+
form = QFormLayout()
|
|
20
|
+
lay.addLayout(form)
|
|
21
|
+
lay.addStretch(1)
|
|
22
|
+
tabs.addTab(w, title)
|
|
23
|
+
return form
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _note(text: str) -> QLabel:
|
|
27
|
+
lbl = QLabel(text)
|
|
28
|
+
lbl.setObjectName("Faint")
|
|
29
|
+
lbl.setWordWrap(True)
|
|
30
|
+
return lbl
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SettingsDialog(QDialog):
|
|
34
|
+
"""Edits the live Settings; the caller applies + persists the returned values."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, parent, settings) -> None:
|
|
37
|
+
super().__init__(parent)
|
|
38
|
+
self.setWindowTitle("GINI Settings")
|
|
39
|
+
self.setMinimumWidth(480)
|
|
40
|
+
root = QVBoxLayout(self)
|
|
41
|
+
tabs = QTabWidget()
|
|
42
|
+
root.addWidget(tabs)
|
|
43
|
+
|
|
44
|
+
# --- Appearance --------------------------------------------------- #
|
|
45
|
+
appf = _page(tabs, "Appearance")
|
|
46
|
+
self.theme = QComboBox(); self.theme.addItems(_THEMES)
|
|
47
|
+
# Select the CURRENT theme by matching the list itself. This used to use a hand-written
|
|
48
|
+
# {name: index} map that only covered the first four themes — so Sand/Blue/Green fell through
|
|
49
|
+
# to index 0 and the dialog silently reported "Dark". Saving Settings for ANY reason (e.g.
|
|
50
|
+
# editing your Teaching Center credentials) then wrote Dark back and stole your theme.
|
|
51
|
+
# Deriving the index from _THEMES means the two can never drift apart again.
|
|
52
|
+
cur = (settings.theme or "Dark").strip().lower()
|
|
53
|
+
cur = {"brand": "gini brand"}.get(cur, cur) # tolerate the old short alias
|
|
54
|
+
names = [t.lower() for t in _THEMES]
|
|
55
|
+
self.theme.setCurrentIndex(names.index(cur) if cur in names else 0)
|
|
56
|
+
appf.addRow("Theme", self.theme)
|
|
57
|
+
self.text_size = QComboBox(); self.text_size.addItems(_TEXT_SIZES)
|
|
58
|
+
cur_sz = (getattr(settings, "text_size", "Normal") or "Normal").strip().lower()
|
|
59
|
+
sizes = [s.lower() for s in _TEXT_SIZES]
|
|
60
|
+
self.text_size.setCurrentIndex(sizes.index(cur_sz) if cur_sz in sizes else 0)
|
|
61
|
+
appf.addRow("Text size", self.text_size)
|
|
62
|
+
appf.addRow("", _note("Scales the whole interface — handy on large or high-resolution "
|
|
63
|
+
"displays. Applies instantly."))
|
|
64
|
+
self.reduced = QCheckBox("Reduce motion / animations")
|
|
65
|
+
self.reduced.setChecked(settings.reduced_motion)
|
|
66
|
+
appf.addRow("", self.reduced)
|
|
67
|
+
|
|
68
|
+
# --- Networking --------------------------------------------------- #
|
|
69
|
+
netf = _page(tabs, "Networking")
|
|
70
|
+
self.auto_internet = QCheckBox("Containers get internet automatically (default eth)")
|
|
71
|
+
self.auto_internet.setChecked(settings.auto_internet)
|
|
72
|
+
netf.addRow("", self.auto_internet)
|
|
73
|
+
netf.addRow("", _note("Off = faithful mode: a machine reaches the internet only via "
|
|
74
|
+
"a drawn Internet element, routed through your network."))
|
|
75
|
+
self.autobuild = QCheckBox("Build missing lab images automatically")
|
|
76
|
+
self.autobuild.setChecked(bool(getattr(settings, "autobuild_images", False)))
|
|
77
|
+
netf.addRow("", self.autobuild)
|
|
78
|
+
netf.addRow("", _note("The gRouter and SDN controller run from images built on this machine. "
|
|
79
|
+
"With this on, GINI builds one the first time you Run without it, "
|
|
80
|
+
"instead of printing a docker build command and stopping. The first "
|
|
81
|
+
"build takes a couple of minutes; after that it's instant."))
|
|
82
|
+
|
|
83
|
+
# --- GINI AI ------------------------------------------------------ #
|
|
84
|
+
aif = _page(tabs, "GINI AI")
|
|
85
|
+
self.llm_enabled = QCheckBox("Use a local LLM for open-ended questions")
|
|
86
|
+
self.llm_enabled.setChecked(settings.llm_enabled)
|
|
87
|
+
aif.addRow("", self.llm_enabled)
|
|
88
|
+
self.llm_url = QLineEdit(settings.llm_url)
|
|
89
|
+
self.llm_url.setPlaceholderText("http://localhost:11434")
|
|
90
|
+
aif.addRow("Server (Ollama)", self.llm_url)
|
|
91
|
+
self.llm_model = QLineEdit(settings.llm_model)
|
|
92
|
+
self.llm_model.setPlaceholderText("llama3.1 / gemma / qwen …")
|
|
93
|
+
aif.addRow("Model", self.llm_model)
|
|
94
|
+
# editing the server/model is a clear intent to use the LLM — auto-tick the box
|
|
95
|
+
self.llm_url.textEdited.connect(lambda *_: self.llm_enabled.setChecked(True))
|
|
96
|
+
self.llm_model.textEdited.connect(lambda *_: self.llm_enabled.setChecked(True))
|
|
97
|
+
self.llm_think = QCheckBox("Reasoning ('thinking') model")
|
|
98
|
+
self.llm_think.setChecked(settings.llm_think)
|
|
99
|
+
aif.addRow("", self.llm_think)
|
|
100
|
+
aif.addRow("", _note("Without an LLM, GINI still builds, explains, traces paths, "
|
|
101
|
+
"and runs Wizard recipes."))
|
|
102
|
+
|
|
103
|
+
# --- Naming ------------------------------------------------------- #
|
|
104
|
+
nf = _page(tabs, "Naming")
|
|
105
|
+
from ..domain.devices import default_prefix
|
|
106
|
+
self._prefix_edits = {}
|
|
107
|
+
for key, label in (("host", "Machine"), ("router", "Router"), ("switch", "Switch"),
|
|
108
|
+
("hub", "Hub"), ("instance", "Instance"), ("container", "Container")):
|
|
109
|
+
cur = settings.name_prefixes.get(key, default_prefix(key))
|
|
110
|
+
edit = QLineEdit(cur); edit.setPlaceholderText(default_prefix(key))
|
|
111
|
+
self._prefix_edits[key] = edit
|
|
112
|
+
nf.addRow(label, edit)
|
|
113
|
+
nf.addRow("", _note("e.g. set Machine to “Mach_” for Mach_1, Mach_2, …"))
|
|
114
|
+
|
|
115
|
+
# --- Pricing ------------------------------------------------------ #
|
|
116
|
+
from ..domain.pricing import rate_of
|
|
117
|
+
prf = _page(tabs, "Pricing")
|
|
118
|
+
self._price_edits = {}
|
|
119
|
+
for key, label in (("instance", "Instance (VM)"), ("container", "Container"),
|
|
120
|
+
("host", "Machine"), ("router", "Router"), ("switch", "Switch"),
|
|
121
|
+
("database", "Database"), ("object_store", "Object store"),
|
|
122
|
+
("dashboard", "Dashboards (Grafana)")):
|
|
123
|
+
edit = QLineEdit(f"{rate_of(key, settings.prices):g}")
|
|
124
|
+
edit.setValidator(QDoubleValidator(0.0, 100000.0, 2, edit))
|
|
125
|
+
edit.setPlaceholderText(f"{rate_of(key):g}")
|
|
126
|
+
self._price_edits[key] = edit
|
|
127
|
+
prf.addRow(label, edit)
|
|
128
|
+
prf.addRow("", _note("Toy “cloud bill” shown live in the dashboard while a lab runs. "
|
|
129
|
+
"Blank = use the default rate."))
|
|
130
|
+
|
|
131
|
+
# --- Teaching Center ---------------------------------------------- #
|
|
132
|
+
tcf = _page(tabs, "Teaching Center")
|
|
133
|
+
self.tc_url = QLineEdit(settings.tc_url)
|
|
134
|
+
self.tc_url.setPlaceholderText("http://localhost:8080")
|
|
135
|
+
tcf.addRow("Course server", self.tc_url)
|
|
136
|
+
self.tc_course = QLineEdit(settings.tc_course)
|
|
137
|
+
self.tc_course.setPlaceholderText("cs4480-fall26")
|
|
138
|
+
tcf.addRow("Course", self.tc_course)
|
|
139
|
+
self.tc_student = QLineEdit(settings.tc_student)
|
|
140
|
+
self.tc_student.setPlaceholderText("your username — e.g. ravi")
|
|
141
|
+
tcf.addRow("Username", self.tc_student)
|
|
142
|
+
self.tc_token = QLineEdit(settings.tc_token)
|
|
143
|
+
self.tc_token.setPlaceholderText("one-time token from your instructor")
|
|
144
|
+
self.tc_token.setEchoMode(QLineEdit.Password)
|
|
145
|
+
tcf.addRow("Enrolment token", self.tc_token)
|
|
146
|
+
self.tc_insecure = QCheckBox("Allow insecure (plain HTTP) connection")
|
|
147
|
+
self.tc_insecure.setChecked(bool(getattr(settings, "tc_allow_insecure", False)))
|
|
148
|
+
tcf.addRow("", self.tc_insecure)
|
|
149
|
+
tcf.addRow("", _note("Your password is never stored — signing in exchanges it for a session. "
|
|
150
|
+
"The enrolment token is used ONCE, to claim your account.\n\n"
|
|
151
|
+
"GINI refuses to send a password over plain HTTP to a remote server: on "
|
|
152
|
+
"shared wifi anyone could read it. Tick the box above only for a demo on "
|
|
153
|
+
"a network you trust (localhost is always allowed).\n\n"
|
|
154
|
+
"Leave the server blank to work offline — Missions then offers the local "
|
|
155
|
+
"practice catalog."))
|
|
156
|
+
|
|
157
|
+
# --- Hardware (GINI32 boards) ------------------------------------- #
|
|
158
|
+
# The one thing a board cannot discover for itself. It is the same network for
|
|
159
|
+
# every board in a class, so it is entered here once and then written to each
|
|
160
|
+
# board over USB by Hardware → Set Up a Board.
|
|
161
|
+
hwf = _page(tabs, "Hardware")
|
|
162
|
+
self.board_ssid = QLineEdit(getattr(settings, "board_wifi_ssid", "") or "")
|
|
163
|
+
self.board_ssid.setPlaceholderText("the Wi-Fi this laptop is on — 2.4 GHz")
|
|
164
|
+
hwf.addRow("Lab Wi-Fi name", self.board_ssid)
|
|
165
|
+
self.board_pw = QLineEdit(getattr(settings, "board_wifi_password", "") or "")
|
|
166
|
+
self.board_pw.setEchoMode(QLineEdit.Password)
|
|
167
|
+
hwf.addRow("Lab Wi-Fi password", self.board_pw)
|
|
168
|
+
hwf.addRow("", _note(
|
|
169
|
+
"A GINI32 board joins this network to reach gBuilder, so it must be the SAME "
|
|
170
|
+
"network this laptop is on. ESP32 radios are 2.4 GHz only — a 5 GHz-only "
|
|
171
|
+
"network will never be found.\n\n"
|
|
172
|
+
"Set boards up from Hardware → Set Up a Board with the board plugged in over "
|
|
173
|
+
"USB. That is the only step that needs a cable; everything afterwards is "
|
|
174
|
+
"wireless.\n\n"
|
|
175
|
+
"This password is stored in the config file so boards can be set up without "
|
|
176
|
+
"retyping it. Use the shared lab network here, not a personal account."))
|
|
177
|
+
|
|
178
|
+
# --- Help & Tour -------------------------------------------------- #
|
|
179
|
+
hf = _page(tabs, "Help")
|
|
180
|
+
self.show_help = QCheckBox("Show the feature tour (Cue Cards) at launch")
|
|
181
|
+
self.show_help.setChecked(settings.show_help_on_launch)
|
|
182
|
+
hf.addRow("", self.show_help)
|
|
183
|
+
hf.addRow("", _note("Reopen the tour any time from Help → Feature Tour."))
|
|
184
|
+
|
|
185
|
+
root.addWidget(_note("Saved to ~/.gini/config.json."))
|
|
186
|
+
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
|
187
|
+
bb.accepted.connect(self.accept)
|
|
188
|
+
bb.rejected.connect(self.reject)
|
|
189
|
+
root.addWidget(bb)
|
|
190
|
+
|
|
191
|
+
def values(self) -> dict:
|
|
192
|
+
from ..domain.devices import default_prefix
|
|
193
|
+
from ..domain.pricing import DEFAULT_RATES
|
|
194
|
+
prefixes = {}
|
|
195
|
+
for key, edit in self._prefix_edits.items():
|
|
196
|
+
p = edit.text().strip()
|
|
197
|
+
if p and p != default_prefix(key): # only store real overrides
|
|
198
|
+
prefixes[key] = p
|
|
199
|
+
prices = {}
|
|
200
|
+
for key, edit in self._price_edits.items():
|
|
201
|
+
txt = edit.text().strip()
|
|
202
|
+
if not txt:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
val = round(float(txt), 2)
|
|
206
|
+
except ValueError:
|
|
207
|
+
continue
|
|
208
|
+
if val != DEFAULT_RATES.get(key): # only store real overrides
|
|
209
|
+
prices[key] = val
|
|
210
|
+
return {
|
|
211
|
+
"theme": self.theme.currentText(),
|
|
212
|
+
"text_size": self.text_size.currentText(),
|
|
213
|
+
"reduced_motion": self.reduced.isChecked(),
|
|
214
|
+
"auto_internet": self.auto_internet.isChecked(),
|
|
215
|
+
"autobuild_images": self.autobuild.isChecked(),
|
|
216
|
+
"llm_enabled": self.llm_enabled.isChecked(),
|
|
217
|
+
"llm_url": self.llm_url.text().strip() or "http://localhost:11434",
|
|
218
|
+
"llm_model": self.llm_model.text().strip() or "llama3.1",
|
|
219
|
+
"llm_think": self.llm_think.isChecked(),
|
|
220
|
+
"name_prefixes": prefixes,
|
|
221
|
+
"prices": prices, # was computed but dropped -> KeyError
|
|
222
|
+
"show_help_on_launch": self.show_help.isChecked(),
|
|
223
|
+
"tc_url": self.tc_url.text().strip(),
|
|
224
|
+
"tc_course": self.tc_course.text().strip(),
|
|
225
|
+
"tc_student": self.tc_student.text().strip(),
|
|
226
|
+
"tc_token": self.tc_token.text().strip(),
|
|
227
|
+
"tc_allow_insecure": self.tc_insecure.isChecked(),
|
|
228
|
+
# GINI32: the lab Wi-Fi written to boards over USB. Not stripped of case —
|
|
229
|
+
# SSIDs are case-sensitive — but surrounding whitespace is, because a name
|
|
230
|
+
# pasted from a slide routinely carries a trailing space and the board would
|
|
231
|
+
# then hunt forever for a network that does not exist.
|
|
232
|
+
"board_wifi_ssid": self.board_ssid.text().strip(),
|
|
233
|
+
"board_wifi_password": self.board_pw.text(),
|
|
234
|
+
}
|
gini/ui/signin_dialog.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Sign in to the Teaching Center.
|
|
2
|
+
|
|
3
|
+
Two shapes, because a student's first sign-in is a different act from their tenth:
|
|
4
|
+
|
|
5
|
+
* **First time (claim).** The teacher enrolled you and handed you an enrolment token. You supply it
|
|
6
|
+
once, choose a password, and the token is spent. This is what stops a classmate from claiming
|
|
7
|
+
your identity: student ids are guessable, so "first password wins" would be an open door.
|
|
8
|
+
* **Every time after.** Student id + password.
|
|
9
|
+
|
|
10
|
+
The password is used once, to get a session token, and is never stored. If the course server isn't
|
|
11
|
+
HTTPS and isn't localhost, the client REFUSES to send it (see `teaching_center.refuse_plaintext_
|
|
12
|
+
password`) — and we surface that refusal as an explanation, not a stack trace.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import Qt
|
|
17
|
+
from PySide6.QtWidgets import (
|
|
18
|
+
QCheckBox, QDialog, QDialogButtonBox, QFormLayout, QLabel, QLineEdit, QVBoxLayout,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SignInDialog(QDialog):
|
|
23
|
+
def __init__(self, parent, settings, *, first_time: bool = False) -> None:
|
|
24
|
+
super().__init__(parent)
|
|
25
|
+
self.setWindowTitle("Sign in to your course")
|
|
26
|
+
self.setMinimumWidth(420)
|
|
27
|
+
self._settings = settings
|
|
28
|
+
root = QVBoxLayout(self)
|
|
29
|
+
|
|
30
|
+
head = QLabel(f"<b>{settings.tc_course or 'your course'}</b> · {settings.tc_url}")
|
|
31
|
+
head.setTextFormat(Qt.RichText)
|
|
32
|
+
root.addWidget(head)
|
|
33
|
+
|
|
34
|
+
form = QFormLayout()
|
|
35
|
+
self.student = QLineEdit(settings.tc_student)
|
|
36
|
+
self.student.setPlaceholderText("your username — e.g. ravi")
|
|
37
|
+
self.student.setToolTip("The username your instructor gave you (not your school ID).")
|
|
38
|
+
form.addRow("Username", self.student)
|
|
39
|
+
|
|
40
|
+
self.first = QCheckBox("First time — I have an enrolment token")
|
|
41
|
+
self.first.setChecked(bool(first_time))
|
|
42
|
+
form.addRow("", self.first)
|
|
43
|
+
|
|
44
|
+
self.enrol = QLineEdit(settings.tc_token)
|
|
45
|
+
self.enrol.setPlaceholderText("the token your instructor gave you")
|
|
46
|
+
form.addRow("Enrolment token", self.enrol)
|
|
47
|
+
|
|
48
|
+
self.password = QLineEdit()
|
|
49
|
+
self.password.setEchoMode(QLineEdit.Password)
|
|
50
|
+
self.password.setPlaceholderText("at least 8 characters")
|
|
51
|
+
form.addRow("Password", self.password)
|
|
52
|
+
|
|
53
|
+
self.confirm = QLineEdit()
|
|
54
|
+
self.confirm.setEchoMode(QLineEdit.Password)
|
|
55
|
+
form.addRow("Confirm password", self.confirm)
|
|
56
|
+
root.addLayout(form)
|
|
57
|
+
|
|
58
|
+
self.note = QLabel("")
|
|
59
|
+
self.note.setWordWrap(True)
|
|
60
|
+
self.note.setObjectName("Faint")
|
|
61
|
+
root.addWidget(self.note)
|
|
62
|
+
|
|
63
|
+
self.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
64
|
+
self.bb.button(QDialogButtonBox.Ok).setText("Sign in")
|
|
65
|
+
self.bb.accepted.connect(self._accept)
|
|
66
|
+
self.bb.rejected.connect(self.reject)
|
|
67
|
+
root.addWidget(self.bb)
|
|
68
|
+
|
|
69
|
+
self.first.toggled.connect(self._sync)
|
|
70
|
+
self._sync()
|
|
71
|
+
|
|
72
|
+
def _sync(self) -> None:
|
|
73
|
+
"""The claim fields only exist for a claim. A returning student shouldn't be asked to confirm
|
|
74
|
+
a password they already have."""
|
|
75
|
+
claiming = self.first.isChecked()
|
|
76
|
+
for w in (self.enrol, self.confirm):
|
|
77
|
+
w.setVisible(claiming)
|
|
78
|
+
lbl = self.layout().itemAt(1).layout().labelForField(w)
|
|
79
|
+
if lbl is not None:
|
|
80
|
+
lbl.setVisible(claiming)
|
|
81
|
+
self.bb.button(QDialogButtonBox.Ok).setText("Set password & sign in" if claiming
|
|
82
|
+
else "Sign in")
|
|
83
|
+
self.note.setText(
|
|
84
|
+
"Your instructor gave you a one-time enrolment token. You'll choose your own password "
|
|
85
|
+
"now; the token is then spent." if claiming else
|
|
86
|
+
"Signing in stores a session on this machine — never your password.")
|
|
87
|
+
self.adjustSize()
|
|
88
|
+
|
|
89
|
+
def _error(self, msg: str) -> None:
|
|
90
|
+
self.note.setText(msg)
|
|
91
|
+
self.note.setObjectName("")
|
|
92
|
+
self.note.setStyleSheet("color:#d9534f; font-weight:600;")
|
|
93
|
+
|
|
94
|
+
def _accept(self) -> None:
|
|
95
|
+
if not self.student.text().strip():
|
|
96
|
+
return self._error("Enter your student id.")
|
|
97
|
+
pw = self.password.text()
|
|
98
|
+
if len(pw) < 8:
|
|
99
|
+
return self._error("Your password must be at least 8 characters.")
|
|
100
|
+
if self.first.isChecked():
|
|
101
|
+
if not self.enrol.text().strip():
|
|
102
|
+
return self._error("Enter the enrolment token your instructor gave you.")
|
|
103
|
+
if pw != self.confirm.text():
|
|
104
|
+
return self._error("The two passwords don't match.")
|
|
105
|
+
self.accept()
|
|
106
|
+
|
|
107
|
+
def values(self) -> dict:
|
|
108
|
+
return {"student": self.student.text().strip(),
|
|
109
|
+
"password": self.password.text(),
|
|
110
|
+
"claim": self.first.isChecked(),
|
|
111
|
+
"enrolment_token": self.enrol.text().strip()}
|