Maarten van den Berg 6 年之前
父節點
當前提交
90045e24d8
共有 3 個文件被更改,包括 178 次插入3 次删除
  1. 54 3
      piket_client/gui.py
  2. 41 0
      piket_client/model.py
  3. 83 0
      piket_server/__init__.py

+ 54 - 3
piket_client/gui.py

@@ -15,6 +15,7 @@ from PySide2.QtWidgets import (
15 15
     QApplication,
16 16
     QGridLayout,
17 17
     QInputDialog,
18
+    QLineEdit,
18 19
     QMainWindow,
19 20
     QMessageBox,
20 21
     QPushButton,
@@ -33,7 +34,7 @@ except ImportError:
33 34
     dbus = None
34 35
 
35 36
 from piket_client.sound import PLOP_WAVE, UNDO_WAVE
36
-from piket_client.model import Person, ConsumptionType, Consumption, ServerStatus
37
+from piket_client.model import Person, ConsumptionType, Consumption, ServerStatus, Settlement
37 38
 import piket_client.logger
38 39
 
39 40
 LOG = logging.getLogger(__name__)
@@ -86,15 +87,18 @@ class NameButton(QPushButton):
86 87
 
87 88
     def process_click(self) -> None:
88 89
         """ Process a click on this button. """
90
+        LOG.debug('Button clicked.')
89 91
         result = self.person.add_consumption(self.active_id)
90 92
         if result:
91 93
             plop()
92 94
             self.setText(self.current_label)
93 95
             self.consumption_created.emit(result)
94 96
         else:
95
-            print("Jantoeternuitje, kapot")
97
+            LOG.error("Failed to add consumption", extra={'person':
98
+                self.person})
96 99
 
97 100
     def confirm_hide(self) -> None:
101
+        LOG.debug('Button right-clicked.')
98 102
         ok = QMessageBox.warning(
99 103
             self.window(),
100 104
             "Persoon verbergen?",
@@ -128,6 +132,8 @@ class NameButtons(QWidget):
128 132
     def consumption_type_changed(self, new_id: str):
129 133
         """ Process a change of the consumption type and propagate to the
130 134
         contained buttons. """
135
+        LOG.debug('Consumption type updated in NameButtons.',
136
+                extra={'new_id': new_id})
131 137
         self.active_consumption_type_id = new_id
132 138
         self.new_id_set.emit(new_id)
133 139
 
@@ -135,6 +141,8 @@ class NameButtons(QWidget):
135 141
         """ Initialize UI: build GridLayout, retrieve People and build a button
136 142
         for each. """
137 143
 
144
+        LOG.debug('Initializing NameButtons.')
145
+
138 146
         ps = Person.get_all(True)
139 147
         num_columns = round(len(ps) / 10) + 1
140 148
 
@@ -162,6 +170,7 @@ class PiketMainWindow(QMainWindow):
162 170
     consumption_type_changed = Signal(str)
163 171
 
164 172
     def __init__(self) -> None:
173
+        LOG.debug('Initializing PiketMainWindow.')
165 174
         super().__init__()
166 175
 
167 176
         self.main_widget = None
@@ -169,7 +178,7 @@ class PiketMainWindow(QMainWindow):
169 178
         self.toolbar = None
170 179
         self.osk = None
171 180
         self.undo_action = None
172
-        self.undo_queue = collections.deque([], 20)
181
+        self.undo_queue = collections.deque([], 15)
173 182
         self.init_ui()
174 183
 
175 184
     def init_ui(self) -> None:
@@ -429,6 +438,48 @@ def main() -> None:
429 438
 
430 439
     # Load main window
431 440
     main_window = PiketMainWindow()
441
+
442
+    # Test unsettled consumptions
443
+    status = ServerStatus.unsettled_consumptions()
444
+
445
+    unsettled = status['unsettled']['amount']
446
+
447
+    if unsettled > 0:
448
+        first = status['unsettled']['first']
449
+        first_date = first.strftime('%c')
450
+        ok = QMessageBox.information(
451
+            None,
452
+            "Onafgesloten lijst",
453
+            f"Wil je verdergaan met een lijst met {unsettled} onafgesloten "
454
+            f"consumpties sinds {first_date}?",
455
+            QMessageBox.Yes,
456
+            QMessageBox.No
457
+        )
458
+        if ok == QMessageBox.No:
459
+            main_window.show_keyboard()
460
+            name, ok = QInputDialog.getText(
461
+                None,
462
+                "Lijst afsluiten",
463
+                "Voer een naam in voor de lijst of druk op OK. Laat de datum "
464
+                "staan.",
465
+                QLineEdit.Normal,
466
+                f"{first.strftime('%Y-%m-%d')}"
467
+            )
468
+            main_window.hide_keyboard()
469
+
470
+            if name and ok:
471
+                settlement = Settlement.create(name)
472
+                info = [f'{item["count"]} {item["name"]}' for item in
473
+                settlement.consumption_summary.values()]
474
+                info = ', '.join(info)
475
+                QMessageBox.information(
476
+                    None,
477
+                    "Lijst afgesloten",
478
+                    f"VO! Op deze lijst stonden: {info}"
479
+                )
480
+
481
+                main_window = PiketMainWindow()
482
+
432 483
     main_window.show()
433 484
 
434 485
     # Let's go

+ 41 - 0
piket_client/model.py

@@ -29,6 +29,24 @@ class ServerStatus:
29 29
         except requests.ConnectionError as ex:
30 30
             return False, ex
31 31
 
32
+    datetime_format = "%Y-%m-%dT%H:%M:%S.%f"
33
+    @classmethod
34
+    def unsettled_consumptions(cls) -> dict:
35
+        req = requests.get(urljoin(SERVER_URL, 'status'))
36
+
37
+        data = req.json()
38
+
39
+        if data['unsettled']['amount']:
40
+            data['unsettled']['first'] = datetime.datetime\
41
+                    .strptime(data['unsettled']['first'],
42
+                            cls.datetime_format)
43
+            data['unsettled']['last'] = datetime.datetime\
44
+                    .strptime(data['unsettled']['last'],
45
+                            cls.datetime_format)
46
+
47
+        return data
48
+
49
+
32 50
 
33 51
 class Person(NamedTuple):
34 52
     """ Represents a Person, as retrieved from the database. """
@@ -313,3 +331,26 @@ class Consumption(NamedTuple):
313 331
                 req.content,
314 332
             )
315 333
             return False
334
+
335
+class Settlement(NamedTuple):
336
+    """ Represents a stored Settlement. """
337
+    settlement_id: int
338
+    name: str
339
+    consumption_summary: dict
340
+
341
+    @classmethod
342
+    def from_dict(cls, data: dict) -> "Settlement":
343
+        return Settlement(
344
+            settlement_id=data['settlement_id'],
345
+            name=data['name'],
346
+            consumption_summary=data['consumption_summary']
347
+        )
348
+
349
+    @classmethod
350
+    def create(cls, name: str) -> "Settlement":
351
+        req = requests.post(
352
+            urljoin(SERVER_URL, '/settlements'),
353
+            json={'settlement': {'name': name}}
354
+        )
355
+
356
+        return cls.from_dict(req.json()['settlement'])

+ 83 - 0
piket_server/__init__.py

@@ -6,6 +6,7 @@ import datetime
6 6
 import os
7 7
 
8 8
 from sqlalchemy.exc import SQLAlchemyError
9
+from sqlalchemy import func
9 10
 from flask import Flask, jsonify, abort, request
10 11
 from flask_sqlalchemy import SQLAlchemy
11 12
 
@@ -43,6 +44,7 @@ class Person(db.Model):
43 44
             "name": self.name,
44 45
             "consumptions": {
45 46
                 ct.consumption_type_id: Consumption.query.filter_by(person=self)
47
+                .filter_by(settlement=None)
46 48
                 .filter_by(consumption_type=ct)
47 49
                 .filter_by(reversed=False)
48 50
                 .count()
@@ -64,6 +66,30 @@ class Settlement(db.Model):
64 66
     def __repr__(self) -> str:
65 67
         return f"<Settlement {self.settlement_id}: {self.name}>"
66 68
 
69
+    @property
70
+    def as_dict(self) -> dict:
71
+        return {
72
+            'settlement_id': self.settlement_id,
73
+            'name': self.name,
74
+            'consumption_summary': self.consumption_summary
75
+        }
76
+
77
+    @property
78
+    def consumption_summary(self) -> dict:
79
+        q = Consumption.\
80
+                query.\
81
+                filter_by(settlement=self).\
82
+                group_by(Consumption.consumption_type_id).\
83
+                outerjoin(ConsumptionType).\
84
+                with_entities(
85
+                    Consumption.consumption_type_id,
86
+                    ConsumptionType.name,
87
+                    func.count(Consumption.consumption_id),
88
+                ).\
89
+                all()
90
+
91
+        return {r[0]: {'name': r[1], 'count': r[2]} for r in q}
92
+
67 93
 
68 94
 class ConsumptionType(db.Model):
69 95
     """ Represents a type of consumption to be counted. """
@@ -131,6 +157,35 @@ def ping() -> None:
131 157
     """ Return a status ping. """
132 158
     return "Pong"
133 159
 
160
+@app.route("/status")
161
+def status() -> None:
162
+    """ Return a status dict with info about the database. """
163
+    unsettled_q = Consumption.query.\
164
+            filter_by(settlement=None).\
165
+            filter_by(reversed=False)
166
+
167
+    unsettled = unsettled_q.count()
168
+
169
+    first = None
170
+    last = None
171
+    if unsettled:
172
+        last = unsettled_q.\
173
+                order_by(Consumption.created_at.desc()).\
174
+                first().\
175
+                created_at.isoformat()
176
+        first = unsettled_q.\
177
+                order_by(Consumption.created_at.asc())\
178
+                .first()\
179
+                .created_at.isoformat()
180
+
181
+    return jsonify({
182
+        'unsettled': {
183
+            'amount': unsettled,
184
+            'first': first,
185
+            'last': last,
186
+        },
187
+    })
188
+
134 189
 
135 190
 # Person
136 191
 @app.route("/people", methods=["GET"])
@@ -291,3 +346,31 @@ def add_consumption_type():
291 346
         return jsonify({"error": "Invalid arguments for ConsumptionType."}), 400
292 347
 
293 348
     return jsonify(consumption_type=ct.as_dict), 201
349
+
350
+# Settlement
351
+@app.route("/settlements", methods=["GET"])
352
+def get_settlements():
353
+    """ Return a list of the active Settlements. """
354
+    result = Settlement.query.all()
355
+    return jsonify(settlements=[s.as_dict for s in result])
356
+
357
+@app.route("/settlements", methods=["POST"])
358
+def add_settlement():
359
+    """ Create a Settlement, and link all un-settled Consumptions to it. """
360
+    json = request.get_json()
361
+
362
+    if not json:
363
+        return jsonify({"error": "Could not parse JSON."}), 400
364
+
365
+    data = json.get('settlement') or {}
366
+    s = Settlement(name=data['name'])
367
+
368
+    db.session.add(s)
369
+    db.session.commit()
370
+
371
+    Consumption.query.filter_by(settlement=None).update({'settlement_id':
372
+        s.settlement_id})
373
+
374
+    db.session.commit()
375
+
376
+    return jsonify(settlement=s.as_dict)