Преглед на файлове

Make a mess but also working consumption type switching

Maarten van den Berg преди 6 години
родител
ревизия
7d63ce5701
променени са 3 файла, в които са добавени 211 реда и са изтрити 17 реда
  1. 60 14
      piket_client/gui.py
  2. 94 2
      piket_client/model.py
  3. 57 1
      piket_server/__init__.py

+ 60 - 14
piket_client/gui.py

@@ -6,6 +6,8 @@ import sys
6 6
 
7 7
 # pylint: disable=E0611
8 8
 from PySide2.QtWidgets import (
9
+    QAction,
10
+    QActionGroup,
9 11
     QApplication,
10 12
     QGridLayout,
11 13
     QInputDialog,
@@ -16,7 +18,7 @@ from PySide2.QtWidgets import (
16 18
     QWidget,
17 19
 )
18 20
 from PySide2.QtGui import QIcon
19
-from PySide2.QtCore import QSize, Qt
21
+from PySide2.QtCore import QSize, Qt, QObject, Signal, Slot
20 22
 
21 23
 # pylint: enable=E0611
22 24
 
@@ -26,7 +28,7 @@ except ImportError:
26 28
     dbus = None
27 29
 
28 30
 from piket_client.sound import PLOP_WAVE
29
-from piket_client.model import Person
31
+from piket_client.model import Person, ConsumptionType
30 32
 
31 33
 
32 34
 def plop() -> None:
@@ -37,24 +39,36 @@ def plop() -> None:
37 39
 class NameButton(QPushButton):
38 40
     """ Wraps a QPushButton to provide a counter. """
39 41
 
40
-    def __init__(self, person: Person, *args, **kwargs) -> None:
42
+    def __init__(self, person: Person, active_id: str, *args, **kwargs) -> None:
41 43
         self.person = person
42
-        self.count = person.consumptions["1"]
44
+        self.active_id = active_id
43 45
 
44 46
         super().__init__(self.current_label, *args, **kwargs)
45 47
         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
46 48
 
47 49
         self.clicked.connect(self.process_click)
48 50
 
51
+    @Slot(str)
52
+    def new_active_id(self, new_id: str):
53
+        self.active_id = new_id
54
+        self.setText(self.current_label)
55
+
56
+    @property
57
+    def active_consumption_type_id(self) -> str:
58
+        return self.parent().active_consumption_type_id
59
+
60
+    @property
61
+    def current_count(self) -> int:
62
+        return self.person.consumptions.get(self.active_id, 0)
63
+
49 64
     @property
50 65
     def current_label(self) -> str:
51 66
         """ Return the label to show on the button. """
52
-        return f"{self.person.name} ({self.count})"
67
+        return f"{self.person.name} ({self.current_count})"
53 68
 
54 69
     def process_click(self) -> None:
55 70
         """ Process a click on this button. """
56
-        if self.person.add_consumption():
57
-            self.count = self.person.consumptions["1"]
71
+        if self.person.add_consumption(self.active_id):
58 72
             self.setText(self.current_label)
59 73
             plop()
60 74
         else:
@@ -65,19 +79,28 @@ class NameButtons(QWidget):
65 79
     """ Main widget responsible for capturing presses and registering them.
66 80
     """
67 81
 
68
-    def __init__(self) -> None:
82
+    new_id_set = Signal(str)
83
+
84
+    def __init__(self, consumption_type_id) -> None:
69 85
         super().__init__()
70 86
 
71 87
         self.layout = None
88
+        self.active_consumption_type_id = consumption_type_id
72 89
         self.init_ui()
73 90
 
91
+    @Slot(str)
92
+    def consumption_type_changed(self, new_id: str):
93
+        self.active_consumption_type_id = new_id
94
+        self.new_id_set.emit(new_id)
95
+
74 96
     def init_ui(self) -> None:
75 97
         """ Initialize UI: build GridLayout, retrieve People and build a button
76 98
         for each. """
77 99
         self.layout = QGridLayout()
78 100
 
79 101
         for index, person in enumerate(Person.get_all()):
80
-            button = NameButton(person)
102
+            button = NameButton(person, self.active_consumption_type_id, self)
103
+            self.new_id_set.connect(button.new_active_id)
81 104
             self.layout.addWidget(button, index // 2, index % 2)
82 105
 
83 106
         self.setLayout(self.layout)
@@ -87,6 +110,8 @@ class PiketMainWindow(QMainWindow):
87 110
     """ QMainWindow subclass responsible for showing the main application
88 111
     window. """
89 112
 
113
+    consumption_type_changed = Signal(str)
114
+
90 115
     def __init__(self) -> None:
91 116
         super().__init__()
92 117
 
@@ -111,10 +136,6 @@ class PiketMainWindow(QMainWindow):
111 136
         # Go full screen
112 137
         self.setWindowState(Qt.WindowActive | Qt.WindowFullScreen)
113 138
 
114
-        # Initialize main widget
115
-        self.main_widget = NameButtons()
116
-        self.setCentralWidget(self.main_widget)
117
-
118 139
         font_metrics = self.fontMetrics()
119 140
         icon_size = font_metrics.height() * 2
120 141
 
@@ -132,9 +153,34 @@ class PiketMainWindow(QMainWindow):
132 153
         self.toolbar.addWidget(self.create_spacer())
133 154
 
134 155
         # Right
135
-        self.toolbar.addAction(self.load_icon("beer_bottle.svg"), "Bierrr")
156
+        ag = QActionGroup(self.toolbar)
157
+        ag.setExclusive(True)
158
+
159
+        for ct in ConsumptionType.get_all():
160
+            action = QAction(
161
+                self.load_icon(ct.icon or 'beer_bottle.svg'),
162
+                ct.name,
163
+                ag
164
+            )
165
+            action.setCheckable(True)
166
+            action.setData(str(ct.consumption_type_id))
167
+
168
+        ag.actions()[0].setChecked(True)
169
+        [self.toolbar.addAction(a) for a in ag.actions()]
170
+        ag.triggered.connect(self.consumption_type_change)
171
+
136 172
         self.addToolBar(self.toolbar)
137 173
 
174
+        # Initialize main widget
175
+        self.main_widget = NameButtons(ag.actions()[0].data())
176
+        self.consumption_type_changed.connect(self.main_widget.consumption_type_changed)
177
+        self.setCentralWidget(self.main_widget)
178
+
179
+
180
+    @Slot(QAction)
181
+    def consumption_type_change(self, action: QAction):
182
+        self.consumption_type_changed.emit(action.data())
183
+
138 184
     def show_keyboard(self) -> None:
139 185
         ''' Show the virtual keyboard, if possible. '''
140 186
         if self.osk:

+ 94 - 2
piket_client/model.py

@@ -18,10 +18,11 @@ class Person(NamedTuple):
18 18
     person_id: int = None
19 19
     consumptions: dict = {}
20 20
 
21
-    def add_consumption(self) -> bool:
21
+    def add_consumption(self, type_id: str) -> bool:
22 22
         ''' Register a consumption for this Person. '''
23 23
         req = requests.post(
24
-            urljoin(SERVER_URL, f'people/{self.person_id}/add_consumption')
24
+            urljoin(SERVER_URL,
25
+                f'people/{self.person_id}/add_consumption/{type_id}')
25 26
         )
26 27
         try:
27 28
             data = req.json()
@@ -123,3 +124,94 @@ class Person(NamedTuple):
123 124
             person_id = data['person_id'],
124 125
             consumptions = data['consumptions']
125 126
         )
127
+
128
+class ConsumptionType(NamedTuple):
129
+    ''' Represents a stored ConsumptionType. '''
130
+
131
+    name: str
132
+    consumption_type_id: int = None
133
+    icon: str = None
134
+
135
+    def create(self) -> 'Person':
136
+        ''' Create a new ConsumptionType from the current attributes. As tuples
137
+        are immutable, a new ConsumptionType with the correct id is returned.
138
+        '''
139
+        req = requests.post(
140
+            urljoin(SERVER_URL, "consumption_types"), json={"consumption_type":
141
+                {"name": self.name, "icon": self.icon}}
142
+        )
143
+
144
+        try:
145
+            data = req.json()
146
+        except ValueError:
147
+            LOG.error(
148
+                'Did not get JSON on adding ConsumptionType (%s): %s',
149
+                req.status_code, req.content
150
+            )
151
+            return None
152
+
153
+        if 'error' in data or req.status_code != 201:
154
+            LOG.error(
155
+                'Could not create ConsumptionType (%s): %s',
156
+                req.status_code, data
157
+            )
158
+            return None
159
+
160
+        return Person.from_dict(data['consumption_type'])
161
+
162
+    @classmethod
163
+    def get(cls, consumption_type_id: int) -> 'ConsumptionType':
164
+        ''' Retrieve a ConsumptionType by id. '''
165
+        req = requests.get(urljoin(SERVER_URL,
166
+            f'/consumption_types/{consumption_type_id}'))
167
+
168
+        try:
169
+            data = req.json()
170
+
171
+            if 'error' in data:
172
+                LOG.warning(
173
+                    'Could not get consumption type %s (%s): %s',
174
+                    consumption_type_id, req.status_code, data
175
+                )
176
+                return None
177
+
178
+            return cls.from_dict(data['consumption_type'])
179
+
180
+        except ValueError:
181
+            LOG.error(
182
+                'Did not get JSON from server on getting consumption type (%s): %s',
183
+                req.status_code, req.content
184
+            )
185
+            return None
186
+
187
+    @classmethod
188
+    def get_all(cls) -> ['ConsumptionType']:
189
+        ''' Get all active ConsumptionTypes. '''
190
+        req = requests.get(urljoin(SERVER_URL, '/consumption_types'))
191
+
192
+        try:
193
+            data = req.json()
194
+
195
+            if 'error' in data:
196
+                LOG.warning(
197
+                    'Could not get consumption types (%s): %s',
198
+                    req.status_code, data
199
+                )
200
+
201
+            return [cls.from_dict(item) for item in data['consumption_types']]
202
+
203
+        except ValueError:
204
+            LOG.error(
205
+                'Did not get JSON from server on getting ConsumptionTypes (%s): %s',
206
+                req.status_code, req.content
207
+            )
208
+            return None
209
+
210
+    @classmethod
211
+    def from_dict(cls, data: dict) -> 'ConsumptionType':
212
+        ''' Reconstruct a ConsumptionType from a dict. '''
213
+        return ConsumptionType(
214
+            name = data['name'],
215
+            consumption_type_id = data['consumption_type_id'],
216
+            icon = data.get('icon')
217
+        )

+ 57 - 1
piket_server/__init__.py

@@ -143,6 +143,7 @@ PEOPLE = {
143 143
 NEXT_ID = len(PEOPLE)
144 144
 
145 145
 
146
+# Person
146 147
 @app.route("/people", methods=["GET"])
147 148
 def get_people():
148 149
     """ Return a list of currently known people. """
@@ -177,7 +178,7 @@ def add_person():
177 178
         db.session.add(person)
178 179
         db.session.commit()
179 180
     except SQLAlchemyError:
180
-        return jsonify({"error": "Invalid arguments for Person."})
181
+        return jsonify({"error": "Invalid arguments for Person."}), 400
181 182
 
182 183
     return jsonify(person=person.as_dict), 201
183 184
 
@@ -199,3 +200,58 @@ def add_consumption(person_id: int):
199 200
         )
200 201
 
201 202
     return jsonify(person=person.as_dict, consumption=consumption.as_dict), 201
203
+
204
+@app.route("/people/<int:person_id>/add_consumption/<int:ct_id>", methods=["POST"])
205
+def add_consumption2(person_id: int, ct_id: int):
206
+    person = Person.query.get_or_404(person_id)
207
+
208
+    consumption = Consumption(person=person, consumption_type_id=ct_id)
209
+    try:
210
+        db.session.add(consumption)
211
+        db.session.commit()
212
+    except SQLAlchemyError:
213
+        return (
214
+            jsonify(
215
+                {"error": "Invalid Consumption parameters.", "person": person.as_dict}
216
+            ),
217
+            400,
218
+        )
219
+
220
+    return jsonify(person=person.as_dict, consumption=consumption.as_dict), 201
221
+
222
+
223
+# ConsumptionType
224
+@app.route("/consumption_types", methods=["GET"])
225
+def get_consumption_types():
226
+    ''' Return a list of currently active consumption types. '''
227
+    ctypes = ConsumptionType.query.all()
228
+    result = [ct.as_dict for ct in ctypes]
229
+    return jsonify(consumption_types=result)
230
+
231
+@app.route("/consumption_types/<int:consumption_type_id>", methods=["GET"])
232
+def get_consumption_type(consumption_type_id: int):
233
+    ct = ConsumptionType.query.get_or_404(consumption_type_id)
234
+
235
+    return jsonify(consumption_type=ct.as_dict)
236
+
237
+@app.route("/consumption_types", methods=["POST"])
238
+def add_consumption_type():
239
+    """ Add a new ConsumptionType.  """
240
+    json = request.get_json()
241
+
242
+    if not json:
243
+        return jsonify({"error": "Could not parse JSON."}), 400
244
+
245
+    data = json.get('consumption_type') or {}
246
+    ct = ConsumptionType(
247
+        name=data.get('name'),
248
+        icon=data.get('icon')
249
+    )
250
+
251
+    try:
252
+        db.session.add(ct)
253
+        db.session.commit()
254
+    except SQLAlchemyError:
255
+        return jsonify({'error': 'Invalid arguments for ConsumptionType.'}), 400
256
+
257
+    return jsonify(consumption_type=ct.as_dict), 201