|
@@ -2,10 +2,123 @@
|
2
|
2
|
Piket server, handles events generated by the client.
|
3
|
3
|
"""
|
4
|
4
|
|
|
5
|
+import datetime
|
|
6
|
+import os
|
|
7
|
+
|
|
8
|
+from sqlalchemy.exc import SQLAlchemyError
|
5
|
9
|
from flask import Flask, jsonify, abort, request
|
|
10
|
+from flask_sqlalchemy import SQLAlchemy
|
|
11
|
+
|
6
|
12
|
|
|
13
|
+DATA_HOME = os.environ.get("XDG_DATA_HOME", "~/.local/share")
|
|
14
|
+CONFIG_DIR = os.path.join(DATA_HOME, "piket_server")
|
|
15
|
+DB_PATH = os.path.expanduser(os.path.join(CONFIG_DIR, "database.sqlite3"))
|
|
16
|
+DB_URL = f"sqlite:///{DB_PATH}"
|
7
|
17
|
|
8
|
18
|
app = Flask("piket_server")
|
|
19
|
+app.config["SQLALCHEMY_DATABASE_URI"] = DB_URL
|
|
20
|
+app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
21
|
+db = SQLAlchemy(app)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+# ---------- Models ----------
|
|
25
|
+class Person(db.Model):
|
|
26
|
+ """ Represents a person to be shown on the lists. """
|
|
27
|
+
|
|
28
|
+ __tablename__ = "people"
|
|
29
|
+
|
|
30
|
+ person_id = db.Column(db.Integer, primary_key=True)
|
|
31
|
+ name = db.Column(db.String, nullable=False)
|
|
32
|
+
|
|
33
|
+ consumptions = db.relationship("Consumption", backref="person", lazy=True)
|
|
34
|
+
|
|
35
|
+ def __repr__(self) -> str:
|
|
36
|
+ return f"<Person {self.person_id}: {self.name}>"
|
|
37
|
+
|
|
38
|
+ @property
|
|
39
|
+ def as_dict(self) -> dict:
|
|
40
|
+ return {
|
|
41
|
+ "person_id": self.person_id,
|
|
42
|
+ "name": self.name,
|
|
43
|
+ "consumptions": {
|
|
44
|
+ ct.consumption_type_id: Consumption.query.filter_by(person=self)
|
|
45
|
+ .filter_by(consumption_type=ct)
|
|
46
|
+ .count()
|
|
47
|
+ for ct in ConsumptionType.query.all()
|
|
48
|
+ },
|
|
49
|
+ }
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+class Settlement(db.Model):
|
|
53
|
+ """ Represents a settlement of the list. """
|
|
54
|
+
|
|
55
|
+ __tablename__ = "settlements"
|
|
56
|
+
|
|
57
|
+ settlement_id = db.Column(db.Integer, primary_key=True)
|
|
58
|
+ name = db.Column(db.String, nullable=False)
|
|
59
|
+
|
|
60
|
+ consumptions = db.relationship("Consumption", backref="settlement", lazy=True)
|
|
61
|
+
|
|
62
|
+ def __repr__(self) -> str:
|
|
63
|
+ return f"<Settlement {self.settlement_id}: {self.name}>"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+class ConsumptionType(db.Model):
|
|
67
|
+ """ Represents a type of consumption to be counted. """
|
|
68
|
+
|
|
69
|
+ __tablename__ = "consumption_types"
|
|
70
|
+
|
|
71
|
+ consumption_type_id = db.Column(db.Integer, primary_key=True)
|
|
72
|
+ name = db.Column(db.String, nullable=False)
|
|
73
|
+ icon = db.Column(db.String)
|
|
74
|
+
|
|
75
|
+ consumptions = db.relationship("Consumption", backref="consumption_type", lazy=True)
|
|
76
|
+
|
|
77
|
+ def __repr__(self) -> str:
|
|
78
|
+ return f"<ConsumptionType: {self.name}>"
|
|
79
|
+
|
|
80
|
+ @property
|
|
81
|
+ def as_dict(self) -> dict:
|
|
82
|
+ return {
|
|
83
|
+ "consumption_type_id": self.consumption_type_id,
|
|
84
|
+ "name": self.name,
|
|
85
|
+ "icon": self.icon,
|
|
86
|
+ }
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+class Consumption(db.Model):
|
|
90
|
+ """ Represent one consumption to be counted. """
|
|
91
|
+
|
|
92
|
+ __tablename__ = "consumptions"
|
|
93
|
+
|
|
94
|
+ consumption_id = db.Column(db.Integer, primary_key=True)
|
|
95
|
+ person_id = db.Column(db.Integer, db.ForeignKey("people.person_id"), nullable=True)
|
|
96
|
+ consumption_type_id = db.Column(
|
|
97
|
+ db.Integer,
|
|
98
|
+ db.ForeignKey("consumption_types.consumption_type_id"),
|
|
99
|
+ nullable=False,
|
|
100
|
+ )
|
|
101
|
+ settlement_id = db.Column(
|
|
102
|
+ db.Integer, db.ForeignKey("settlements.settlement_id"), nullable=True
|
|
103
|
+ )
|
|
104
|
+ created_at = db.Column(
|
|
105
|
+ db.DateTime, default=datetime.datetime.utcnow, nullable=False
|
|
106
|
+ )
|
|
107
|
+
|
|
108
|
+ def __repr__(self) -> str:
|
|
109
|
+ return f"<Consumption: {self.consumption_type.name} for {self.person.name}>"
|
|
110
|
+
|
|
111
|
+ @property
|
|
112
|
+ def as_dict(self) -> dict:
|
|
113
|
+ return {
|
|
114
|
+ "person_id": self.person_id,
|
|
115
|
+ "consumption_type_id": self.consumption_type_id,
|
|
116
|
+ "settlement_id": self.settlement_id,
|
|
117
|
+ "created_at": self.created_at.isoformat(),
|
|
118
|
+ }
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+# ---------- Models ----------
|
9
|
122
|
|
10
|
123
|
|
11
|
124
|
@app.route("/ping")
|
|
@@ -33,19 +146,16 @@ NEXT_ID = len(PEOPLE)
|
33
|
146
|
@app.route("/people", methods=["GET"])
|
34
|
147
|
def get_people():
|
35
|
148
|
""" Return a list of currently known people. """
|
36
|
|
- people = [p for p in PEOPLE.values()]
|
37
|
|
- people.sort(key=lambda p: p["name"])
|
38
|
|
- return jsonify(people=people)
|
|
149
|
+ people = Person.query.order_by(Person.name).all()
|
|
150
|
+ result = [person.as_dict for person in people]
|
|
151
|
+ return jsonify(people=result)
|
39
|
152
|
|
40
|
153
|
|
41
|
154
|
@app.route("/people/<int:person_id>", methods=["GET"])
|
42
|
155
|
def get_person(person_id: int):
|
43
|
|
- person = PEOPLE.get(person_id)
|
44
|
|
-
|
45
|
|
- if not person:
|
46
|
|
- abort(404)
|
|
156
|
+ person = Person.query.get_or_404(person_id)
|
47
|
157
|
|
48
|
|
- return jsonify(person=person)
|
|
158
|
+ return jsonify(person=person.as_dict)
|
49
|
159
|
|
50
|
160
|
|
51
|
161
|
@app.route("/people", methods=["POST"])
|
|
@@ -56,34 +166,36 @@ def add_person():
|
56
|
166
|
Required parameters:
|
57
|
167
|
- name (str)
|
58
|
168
|
"""
|
59
|
|
- global NEXT_ID
|
60
|
|
-
|
61
|
|
- data = request.get_json()
|
62
|
|
-
|
63
|
|
- if not data:
|
64
|
|
- abort(400)
|
65
|
|
-
|
66
|
|
- name = data.get("name")
|
|
169
|
+ json = request.get_json()
|
67
|
170
|
|
68
|
|
- if not name:
|
69
|
|
- abort(400)
|
70
|
|
- person = {"id": NEXT_ID, "name": name, "count": 0}
|
|
171
|
+ if not json:
|
|
172
|
+ return jsonify({"error": "Could not parse JSON."}), 400
|
71
|
173
|
|
72
|
|
- PEOPLE[NEXT_ID] = person
|
|
174
|
+ data = json.get("person") or {}
|
|
175
|
+ person = Person(name=data.get("name"))
|
|
176
|
+ try:
|
|
177
|
+ db.session.add(person)
|
|
178
|
+ db.session.commit()
|
|
179
|
+ except SQLAlchemyError:
|
|
180
|
+ return jsonify({"error": "Invalid arguments for Person."})
|
73
|
181
|
|
74
|
|
- NEXT_ID += 1
|
75
|
|
-
|
76
|
|
- return jsonify(person=person)
|
|
182
|
+ return jsonify(person=person.as_dict)
|
77
|
183
|
|
78
|
184
|
|
79
|
185
|
@app.route("/people/<int:person_id>/add_consumption", methods=["POST"])
|
80
|
186
|
def add_consumption(person_id: int):
|
81
|
|
- person = PEOPLE.get(person_id)
|
82
|
|
-
|
83
|
|
- if not person:
|
84
|
|
- abort(404)
|
85
|
|
-
|
86
|
|
- increment = int(request.form.get("amount", 1))
|
87
|
|
- person["count"] += increment
|
88
|
|
-
|
89
|
|
- return jsonify(person=person)
|
|
187
|
+ person = Person.query.get_or_404(person_id)
|
|
188
|
+
|
|
189
|
+ consumption = Consumption(person=person, consumption_type_id=1)
|
|
190
|
+ try:
|
|
191
|
+ db.session.add(consumption)
|
|
192
|
+ db.session.commit()
|
|
193
|
+ except SQLAlchemyError:
|
|
194
|
+ return (
|
|
195
|
+ jsonify(
|
|
196
|
+ {"error": "Invalid Consumption parameters.", "person": person.as_dict}
|
|
197
|
+ ),
|
|
198
|
+ 400,
|
|
199
|
+ )
|
|
200
|
+
|
|
201
|
+ return jsonify(person=person.as_dict, consumption=consumption.as_dict)
|