4 Commits 786fa37b68 ... 7b3e643142

Autore SHA1 Messaggio Data
  Maarten van den Berg 7b3e643142 Fix Mypy errors 5 anni fa
  Maarten van den Berg 86a3331acd Export piket-cli entry point 5 anni fa
  Maarten van den Berg ec8352343f Enable unique constraints for unique fields 5 anni fa
  Maarten van den Berg a88793eba0 Add "people create" 5 anni fa

+ 14 - 0
piket_client/cli.py

70
     print(table)
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
 @cli.group()
87
 @cli.group()
74
 def settlements():
88
 def settlements():
75
     pass
89
     pass

+ 20 - 10
piket_client/gui.py

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

+ 23 - 16
piket_client/model.py

130
             )
130
             )
131
             return None
131
             return None
132
 
132
 
133
-    def create(self) -> Optional[Person]:
133
+    def create(self) -> Union[Person, NetworkError]:
134
         """ Create a new Person from the current attributes. As tuples are
134
         """ Create a new Person from the current attributes. As tuples are
135
         immutable, a new Person with the correct id is returned. """
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
         try:
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
     def set_active(self, new_state=True) -> Optional[Person]:
164
     def set_active(self, new_state=True) -> Optional[Person]:
158
         req = requests.patch(
165
         req = requests.patch(

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

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
     __tablename__ = "people"
18
     __tablename__ = "people"
19
 
19
 
20
     person_id = db.Column(db.Integer, primary_key=True)
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
     active = db.Column(db.Boolean, nullable=False, default=False)
24
     active = db.Column(db.Boolean, nullable=False, default=False)
25
 
25
 
26
     consumptions = db.relationship("Consumption", backref="person", lazy=True)
26
     consumptions = db.relationship("Consumption", backref="person", lazy=True)
194
     __tablename__ = "consumption_types"
194
     __tablename__ = "consumption_types"
195
 
195
 
196
     consumption_type_id = db.Column(db.Integer, primary_key=True)
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
     icon = db.Column(db.String)
198
     icon = db.Column(db.String)
199
     active = db.Column(db.Boolean, default=True)
199
     active = db.Column(db.Boolean, default=True)
200
 
200
 

+ 5 - 1
piket_server/routes/people.py

43
         return jsonify({"error": "Could not parse JSON."}), 400
43
         return jsonify({"error": "Could not parse JSON."}), 400
44
 
44
 
45
     data = json.get("person") or {}
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
     try:
52
     try:
49
         db.session.add(person)
53
         db.session.add(person)

+ 1 - 0
setup.py

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