Maarten van den Berg 6 years ago
parent
commit
90045e24d8
3 changed files with 178 additions and 3 deletions
  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
     QApplication,
15
     QApplication,
16
     QGridLayout,
16
     QGridLayout,
17
     QInputDialog,
17
     QInputDialog,
18
+    QLineEdit,
18
     QMainWindow,
19
     QMainWindow,
19
     QMessageBox,
20
     QMessageBox,
20
     QPushButton,
21
     QPushButton,
33
     dbus = None
34
     dbus = None
34
 
35
 
35
 from piket_client.sound import PLOP_WAVE, UNDO_WAVE
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
 import piket_client.logger
38
 import piket_client.logger
38
 
39
 
39
 LOG = logging.getLogger(__name__)
40
 LOG = logging.getLogger(__name__)
86
 
87
 
87
     def process_click(self) -> None:
88
     def process_click(self) -> None:
88
         """ Process a click on this button. """
89
         """ Process a click on this button. """
90
+        LOG.debug('Button clicked.')
89
         result = self.person.add_consumption(self.active_id)
91
         result = self.person.add_consumption(self.active_id)
90
         if result:
92
         if result:
91
             plop()
93
             plop()
92
             self.setText(self.current_label)
94
             self.setText(self.current_label)
93
             self.consumption_created.emit(result)
95
             self.consumption_created.emit(result)
94
         else:
96
         else:
95
-            print("Jantoeternuitje, kapot")
97
+            LOG.error("Failed to add consumption", extra={'person':
98
+                self.person})
96
 
99
 
97
     def confirm_hide(self) -> None:
100
     def confirm_hide(self) -> None:
101
+        LOG.debug('Button right-clicked.')
98
         ok = QMessageBox.warning(
102
         ok = QMessageBox.warning(
99
             self.window(),
103
             self.window(),
100
             "Persoon verbergen?",
104
             "Persoon verbergen?",
128
     def consumption_type_changed(self, new_id: str):
132
     def consumption_type_changed(self, new_id: str):
129
         """ Process a change of the consumption type and propagate to the
133
         """ Process a change of the consumption type and propagate to the
130
         contained buttons. """
134
         contained buttons. """
135
+        LOG.debug('Consumption type updated in NameButtons.',
136
+                extra={'new_id': new_id})
131
         self.active_consumption_type_id = new_id
137
         self.active_consumption_type_id = new_id
132
         self.new_id_set.emit(new_id)
138
         self.new_id_set.emit(new_id)
133
 
139
 
135
         """ Initialize UI: build GridLayout, retrieve People and build a button
141
         """ Initialize UI: build GridLayout, retrieve People and build a button
136
         for each. """
142
         for each. """
137
 
143
 
144
+        LOG.debug('Initializing NameButtons.')
145
+
138
         ps = Person.get_all(True)
146
         ps = Person.get_all(True)
139
         num_columns = round(len(ps) / 10) + 1
147
         num_columns = round(len(ps) / 10) + 1
140
 
148
 
162
     consumption_type_changed = Signal(str)
170
     consumption_type_changed = Signal(str)
163
 
171
 
164
     def __init__(self) -> None:
172
     def __init__(self) -> None:
173
+        LOG.debug('Initializing PiketMainWindow.')
165
         super().__init__()
174
         super().__init__()
166
 
175
 
167
         self.main_widget = None
176
         self.main_widget = None
169
         self.toolbar = None
178
         self.toolbar = None
170
         self.osk = None
179
         self.osk = None
171
         self.undo_action = None
180
         self.undo_action = None
172
-        self.undo_queue = collections.deque([], 20)
181
+        self.undo_queue = collections.deque([], 15)
173
         self.init_ui()
182
         self.init_ui()
174
 
183
 
175
     def init_ui(self) -> None:
184
     def init_ui(self) -> None:
429
 
438
 
430
     # Load main window
439
     # Load main window
431
     main_window = PiketMainWindow()
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
     main_window.show()
483
     main_window.show()
433
 
484
 
434
     # Let's go
485
     # Let's go

+ 41 - 0
piket_client/model.py

29
         except requests.ConnectionError as ex:
29
         except requests.ConnectionError as ex:
30
             return False, ex
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
 class Person(NamedTuple):
51
 class Person(NamedTuple):
34
     """ Represents a Person, as retrieved from the database. """
52
     """ Represents a Person, as retrieved from the database. """
313
                 req.content,
331
                 req.content,
314
             )
332
             )
315
             return False
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
 import os
6
 import os
7
 
7
 
8
 from sqlalchemy.exc import SQLAlchemyError
8
 from sqlalchemy.exc import SQLAlchemyError
9
+from sqlalchemy import func
9
 from flask import Flask, jsonify, abort, request
10
 from flask import Flask, jsonify, abort, request
10
 from flask_sqlalchemy import SQLAlchemy
11
 from flask_sqlalchemy import SQLAlchemy
11
 
12
 
43
             "name": self.name,
44
             "name": self.name,
44
             "consumptions": {
45
             "consumptions": {
45
                 ct.consumption_type_id: Consumption.query.filter_by(person=self)
46
                 ct.consumption_type_id: Consumption.query.filter_by(person=self)
47
+                .filter_by(settlement=None)
46
                 .filter_by(consumption_type=ct)
48
                 .filter_by(consumption_type=ct)
47
                 .filter_by(reversed=False)
49
                 .filter_by(reversed=False)
48
                 .count()
50
                 .count()
64
     def __repr__(self) -> str:
66
     def __repr__(self) -> str:
65
         return f"<Settlement {self.settlement_id}: {self.name}>"
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
 class ConsumptionType(db.Model):
94
 class ConsumptionType(db.Model):
69
     """ Represents a type of consumption to be counted. """
95
     """ Represents a type of consumption to be counted. """
131
     """ Return a status ping. """
157
     """ Return a status ping. """
132
     return "Pong"
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
 # Person
190
 # Person
136
 @app.route("/people", methods=["GET"])
191
 @app.route("/people", methods=["GET"])
291
         return jsonify({"error": "Invalid arguments for ConsumptionType."}), 400
346
         return jsonify({"error": "Invalid arguments for ConsumptionType."}), 400
292
 
347
 
293
     return jsonify(consumption_type=ct.as_dict), 201
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)