4 Commitit 786fa37b68 ... 7b3e643142

Tekijä SHA1 Viesti Päivämäärä
  Maarten van den Berg 7b3e643142 Fix Mypy errors 5 vuotta sitten
  Maarten van den Berg 86a3331acd Export piket-cli entry point 5 vuotta sitten
  Maarten van den Berg ec8352343f Enable unique constraints for unique fields 5 vuotta sitten
  Maarten van den Berg a88793eba0 Add "people create" 5 vuotta sitten

+ 14 - 0
piket_client/cli.py

@@ -70,6 +70,20 @@ def list_people(active: bool) -> None:
70 70
     print(table)
71 71
 
72 72
 
73
+@people.command("create")
74
+@click.option("--display-name", type=click.STRING)
75
+@click.argument("name", type=click.STRING)
76
+def create_person(name: str, display_name: str) -> None:
77
+    """Create a person."""
78
+    person = Person(full_name=name, display_name=display_name).create()
79
+
80
+    if isinstance(person, NetworkError):
81
+        print_error(f"Could not create Person: {person.value}")
82
+        return
83
+
84
+    print_ok(f'Created person "{name}" with ID {person.person_id}.')
85
+
86
+
73 87
 @cli.group()
74 88
 def settlements():
75 89
     pass

+ 20 - 10
piket_client/gui.py

@@ -3,8 +3,10 @@ Provides the graphical front-end for Piket.
3 3
 """
4 4
 import collections
5 5
 import logging
6
+import math
6 7
 import os
7 8
 import sys
9
+from typing import Deque
8 10
 
9 11
 import qdarkstyle
10 12
 
@@ -78,7 +80,7 @@ class NameButton(QPushButton):
78 80
     @Slot()
79 81
     def rebuild(self) -> None:
80 82
         """ Refresh the Person object and the label. """
81
-        self.person = self.person.reload()
83
+        self.person = self.person.reload()  # type: ignore
82 84
         self.setText(self.current_label)
83 85
 
84 86
     @property
@@ -149,7 +151,8 @@ class NameButtons(QWidget):
149 151
         LOG.debug("Initializing NameButtons.")
150 152
 
151 153
         ps = Person.get_all(True)
152
-        num_columns = round(len(ps) / 10) + 1
154
+        assert not isinstance(ps, NetworkError)
155
+        num_columns = math.ceil(math.sqrt(len(ps)))
153 156
 
154 157
         if self.layout:
155 158
             LOG.debug("Removing %s widgets for rebuild", self.layout.count())
@@ -183,7 +186,7 @@ class PiketMainWindow(QMainWindow):
183 186
         self.toolbar = None
184 187
         self.osk = None
185 188
         self.undo_action = None
186
-        self.undo_queue = collections.deque([], 15)
189
+        self.undo_queue: Deque[Consumption] = collections.deque([], 15)
187 190
         self.init_ui()
188 191
 
189 192
     def init_ui(self) -> None:
@@ -212,6 +215,7 @@ class PiketMainWindow(QMainWindow):
212 215
 
213 216
         # Initialize toolbar
214 217
         self.toolbar = QToolBar()
218
+        assert self.toolbar is not None
215 219
         self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
216 220
         self.toolbar.setIconSize(QSize(icon_size, icon_size))
217 221
 
@@ -239,7 +243,7 @@ class PiketMainWindow(QMainWindow):
239 243
         self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
240 244
         self.toolbar.setFloatable(False)
241 245
         self.toolbar.setMovable(False)
242
-        self.ct_ag = QActionGroup(self.toolbar)
246
+        self.ct_ag: QActionGroup = QActionGroup(self.toolbar)
243 247
         self.ct_ag.setExclusive(True)
244 248
 
245 249
         cts = ConsumptionType.get_all()
@@ -311,6 +315,8 @@ class PiketMainWindow(QMainWindow):
311 315
         """ Ask for a new Person and register it, then rebuild the central
312 316
         widget. """
313 317
         inactive_persons = Person.get_all(False)
318
+        assert not isinstance(inactive_persons, NetworkError)
319
+
314 320
         inactive_persons.sort(key=lambda p: p.name)
315 321
         inactive_names = [p.name for p in inactive_persons]
316 322
 
@@ -331,9 +337,10 @@ class PiketMainWindow(QMainWindow):
331 337
                 person.set_active(True)
332 338
 
333 339
             else:
334
-                person = Person(name=name)
335
-                person = person.create()
340
+                person = Person(full_name=name, display_name=None, )
341
+                person.create()
336 342
 
343
+            assert self.main_widget is not None
337 344
             self.main_widget.init_ui()
338 345
 
339 346
     def add_consumption_type(self) -> None:
@@ -344,8 +351,8 @@ class PiketMainWindow(QMainWindow):
344 351
         self.hide_keyboard()
345 352
 
346 353
         if ok and name:
347
-            ct = ConsumptionType(name=name)
348
-            ct = ct.create()
354
+            ct = ConsumptionType(name=name).create()
355
+            assert not isinstance(ct, NetworkError)
349 356
 
350 357
             action = QAction(
351 358
                 self.load_icon(ct.icon or "beer_bottle.svg"), ct.name, self.ct_ag
@@ -353,6 +360,7 @@ class PiketMainWindow(QMainWindow):
353 360
             action.setCheckable(True)
354 361
             action.setData(str(ct.consumption_type_id))
355 362
 
363
+            assert self.toolbar is not None
356 364
             self.toolbar.addAction(action)
357 365
 
358 366
     def confirm_quit(self) -> None:
@@ -383,8 +391,10 @@ class PiketMainWindow(QMainWindow):
383 391
             self.undo_queue.append(to_undo)
384 392
 
385 393
         elif not self.undo_queue:
394
+            assert self.undo_action is not None
386 395
             self.undo_action.setDisabled(True)
387 396
 
397
+        assert self.main_widget is not None
388 398
         self.main_widget.init_ui()
389 399
 
390 400
     @Slot(Consumption)
@@ -480,9 +490,9 @@ def main() -> None:
480 490
                     f'{item["count"]} {item["name"]}'
481 491
                     for item in settlement.consumption_summary.values()
482 492
                 ]
483
-                info = ", ".join(info)
493
+                info2 = ", ".join(info)
484 494
                 QMessageBox.information(
485
-                    None, "Lijst afgesloten", f"VO! Op deze lijst stonden: {info}"
495
+                    None, "Lijst afgesloten", f"VO! Op deze lijst stonden: {info2}"
486 496
                 )
487 497
 
488 498
                 main_window = PiketMainWindow()

+ 23 - 16
piket_client/model.py

@@ -130,29 +130,36 @@ class Person(NamedTuple):
130 130
             )
131 131
             return None
132 132
 
133
-    def create(self) -> Optional[Person]:
133
+    def create(self) -> Union[Person, NetworkError]:
134 134
         """ Create a new Person from the current attributes. As tuples are
135 135
         immutable, a new Person with the correct id is returned. """
136
-        req = requests.post(
137
-            urljoin(SERVER_URL, "people"),
138
-            json={"person": {"name": self.name, "active": True}},
139
-        )
140 136
 
141 137
         try:
142
-            data = req.json()
143
-        except ValueError:
144
-            LOG.error(
145
-                "Did not get JSON on adding Person (%s): %s",
146
-                req.status_code,
147
-                req.content,
138
+            req = requests.post(
139
+                urljoin(SERVER_URL, "people"),
140
+                json={
141
+                    "person": {
142
+                        "full_name": self.full_name,
143
+                        "display_name": self.display_name,
144
+                        "active": True,
145
+                    }
146
+                },
148 147
             )
149
-            return None
148
+            req.raise_for_status()
149
+            data = req.json()
150
+            return Person.from_dict(data["person"])
150 151
 
151
-        if "error" in data or req.status_code != 201:
152
-            LOG.error("Could not create Person (%s): %s", req.status_code, data)
153
-            return None
152
+        except requests.ConnectionError as e:
153
+            LOG.exception(e)
154
+            return NetworkError.ConnectionFailure
154 155
 
155
-        return Person.from_dict(data["person"])
156
+        except requests.HTTPError as e:
157
+            LOG.exception(e)
158
+            return NetworkError.HttpFailure
159
+
160
+        except ValueError as e:
161
+            LOG.exception(e)
162
+            return NetworkError.InvalidData
156 163
 
157 164
     def set_active(self, new_state=True) -> Optional[Person]:
158 165
         req = requests.patch(

+ 36 - 0
piket_server/alembic/versions/6a5989118ee3_enable_unique_constraints.py

@@ -0,0 +1,36 @@
1
+"""Enable unique constraints
2
+
3
+Revision ID: 6a5989118ee3
4
+Revises: cca57457a0a6
5
+Create Date: 2019-09-22 17:04:01.945713
6
+
7
+"""
8
+from alembic import op
9
+import sqlalchemy as sa
10
+
11
+
12
+# revision identifiers, used by Alembic.
13
+revision = "6a5989118ee3"
14
+down_revision = "cca57457a0a6"
15
+branch_labels = None
16
+depends_on = None
17
+
18
+
19
+def upgrade():
20
+    with op.batch_alter_table("consumption_types") as batch_op:
21
+        batch_op.create_unique_constraint("uc_consumption_types_name", ["name"])
22
+
23
+    with op.batch_alter_table("people") as batch_op2:
24
+        batch_op2.create_unique_constraint("uc_people_aardbei_id", ["aardbei_id"])
25
+        batch_op2.create_unique_constraint("uc_people_full_name", ["full_name"])
26
+        batch_op2.create_unique_constraint("uc_people_display_name", ["display_name"])
27
+
28
+
29
+def downgrade():
30
+    with op.batch_alter_table("people") as batch_op2:
31
+        batch_op2.drop_constraint("uc_people_display_name", type_="unique")
32
+        batch_op2.drop_constraint("uc_people_full_name", type_="unique")
33
+        batch_op2.drop_constraint("uc_people_aardbei_id", type_="unique")
34
+
35
+    with op.batch_alter_table("consumption_types") as batch_op:
36
+        batch_op.drop_constraint("uc_consumption_types_name", type_="unique")

+ 4 - 4
piket_server/models.py

@@ -18,9 +18,9 @@ class Person(db.Model):
18 18
     __tablename__ = "people"
19 19
 
20 20
     person_id = db.Column(db.Integer, primary_key=True)
21
-    full_name = db.Column(db.String, nullable=False)
22
-    display_name = db.Column(db.String, nullable=True)
23
-    aardbei_id = db.Column(db.Integer, nullable=True)
21
+    full_name = db.Column(db.String, nullable=False, unique=True)
22
+    display_name = db.Column(db.String, nullable=True, unique=True)
23
+    aardbei_id = db.Column(db.Integer, nullable=True, unique=True)
24 24
     active = db.Column(db.Boolean, nullable=False, default=False)
25 25
 
26 26
     consumptions = db.relationship("Consumption", backref="person", lazy=True)
@@ -194,7 +194,7 @@ class ConsumptionType(db.Model):
194 194
     __tablename__ = "consumption_types"
195 195
 
196 196
     consumption_type_id = db.Column(db.Integer, primary_key=True)
197
-    name = db.Column(db.String, nullable=False)
197
+    name = db.Column(db.String, nullable=False, unique=True)
198 198
     icon = db.Column(db.String)
199 199
     active = db.Column(db.Boolean, default=True)
200 200
 

+ 5 - 1
piket_server/routes/people.py

@@ -43,7 +43,11 @@ def add_person():
43 43
         return jsonify({"error": "Could not parse JSON."}), 400
44 44
 
45 45
     data = json.get("person") or {}
46
-    person = Person(name=data.get("name"), active=data.get("active", False))
46
+    person = Person(
47
+        full_name=data.get("full_name"),
48
+        active=data.get("active", False),
49
+        display_name=data.get("display_name", None),
50
+    )
47 51
 
48 52
     try:
49 53
         db.session.add(person)

+ 1 - 0
setup.py

@@ -18,6 +18,7 @@ setup(
18 18
     entry_points={
19 19
         "console_scripts": [
20 20
             "piket-client=piket_client.gui:main",
21
+            "piket-cli=piket_client.cli:cli",
21 22
             "piket-seed=piket_server.seed:main",
22 23
         ]
23 24
     },