Browse Source

Make a mess but also working consumption type switching

Maarten van den Berg 6 years ago
parent
commit
7d63ce5701
3 changed files with 211 additions and 17 deletions
  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
 
7
 # pylint: disable=E0611
7
 # pylint: disable=E0611
8
 from PySide2.QtWidgets import (
8
 from PySide2.QtWidgets import (
9
+    QAction,
10
+    QActionGroup,
9
     QApplication,
11
     QApplication,
10
     QGridLayout,
12
     QGridLayout,
11
     QInputDialog,
13
     QInputDialog,
16
     QWidget,
18
     QWidget,
17
 )
19
 )
18
 from PySide2.QtGui import QIcon
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
 # pylint: enable=E0611
23
 # pylint: enable=E0611
22
 
24
 
26
     dbus = None
28
     dbus = None
27
 
29
 
28
 from piket_client.sound import PLOP_WAVE
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
 def plop() -> None:
34
 def plop() -> None:
37
 class NameButton(QPushButton):
39
 class NameButton(QPushButton):
38
     """ Wraps a QPushButton to provide a counter. """
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
         self.person = person
43
         self.person = person
42
-        self.count = person.consumptions["1"]
44
+        self.active_id = active_id
43
 
45
 
44
         super().__init__(self.current_label, *args, **kwargs)
46
         super().__init__(self.current_label, *args, **kwargs)
45
         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
47
         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
46
 
48
 
47
         self.clicked.connect(self.process_click)
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
     @property
64
     @property
50
     def current_label(self) -> str:
65
     def current_label(self) -> str:
51
         """ Return the label to show on the button. """
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
     def process_click(self) -> None:
69
     def process_click(self) -> None:
55
         """ Process a click on this button. """
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
             self.setText(self.current_label)
72
             self.setText(self.current_label)
59
             plop()
73
             plop()
60
         else:
74
         else:
65
     """ Main widget responsible for capturing presses and registering them.
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
         super().__init__()
85
         super().__init__()
70
 
86
 
71
         self.layout = None
87
         self.layout = None
88
+        self.active_consumption_type_id = consumption_type_id
72
         self.init_ui()
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
     def init_ui(self) -> None:
96
     def init_ui(self) -> None:
75
         """ Initialize UI: build GridLayout, retrieve People and build a button
97
         """ Initialize UI: build GridLayout, retrieve People and build a button
76
         for each. """
98
         for each. """
77
         self.layout = QGridLayout()
99
         self.layout = QGridLayout()
78
 
100
 
79
         for index, person in enumerate(Person.get_all()):
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
             self.layout.addWidget(button, index // 2, index % 2)
104
             self.layout.addWidget(button, index // 2, index % 2)
82
 
105
 
83
         self.setLayout(self.layout)
106
         self.setLayout(self.layout)
87
     """ QMainWindow subclass responsible for showing the main application
110
     """ QMainWindow subclass responsible for showing the main application
88
     window. """
111
     window. """
89
 
112
 
113
+    consumption_type_changed = Signal(str)
114
+
90
     def __init__(self) -> None:
115
     def __init__(self) -> None:
91
         super().__init__()
116
         super().__init__()
92
 
117
 
111
         # Go full screen
136
         # Go full screen
112
         self.setWindowState(Qt.WindowActive | Qt.WindowFullScreen)
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
         font_metrics = self.fontMetrics()
139
         font_metrics = self.fontMetrics()
119
         icon_size = font_metrics.height() * 2
140
         icon_size = font_metrics.height() * 2
120
 
141
 
132
         self.toolbar.addWidget(self.create_spacer())
153
         self.toolbar.addWidget(self.create_spacer())
133
 
154
 
134
         # Right
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
         self.addToolBar(self.toolbar)
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
     def show_keyboard(self) -> None:
184
     def show_keyboard(self) -> None:
139
         ''' Show the virtual keyboard, if possible. '''
185
         ''' Show the virtual keyboard, if possible. '''
140
         if self.osk:
186
         if self.osk:

+ 94 - 2
piket_client/model.py

18
     person_id: int = None
18
     person_id: int = None
19
     consumptions: dict = {}
19
     consumptions: dict = {}
20
 
20
 
21
-    def add_consumption(self) -> bool:
21
+    def add_consumption(self, type_id: str) -> bool:
22
         ''' Register a consumption for this Person. '''
22
         ''' Register a consumption for this Person. '''
23
         req = requests.post(
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
         try:
27
         try:
27
             data = req.json()
28
             data = req.json()
123
             person_id = data['person_id'],
124
             person_id = data['person_id'],
124
             consumptions = data['consumptions']
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
 NEXT_ID = len(PEOPLE)
143
 NEXT_ID = len(PEOPLE)
144
 
144
 
145
 
145
 
146
+# Person
146
 @app.route("/people", methods=["GET"])
147
 @app.route("/people", methods=["GET"])
147
 def get_people():
148
 def get_people():
148
     """ Return a list of currently known people. """
149
     """ Return a list of currently known people. """
177
         db.session.add(person)
178
         db.session.add(person)
178
         db.session.commit()
179
         db.session.commit()
179
     except SQLAlchemyError:
180
     except SQLAlchemyError:
180
-        return jsonify({"error": "Invalid arguments for Person."})
181
+        return jsonify({"error": "Invalid arguments for Person."}), 400
181
 
182
 
182
     return jsonify(person=person.as_dict), 201
183
     return jsonify(person=person.as_dict), 201
183
 
184
 
199
         )
200
         )
200
 
201
 
201
     return jsonify(person=person.as_dict, consumption=consumption.as_dict), 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