Digitale bierlijst

models.py 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. """
  2. Defines database models used by the server.
  3. """
  4. import datetime
  5. from typing import List, Dict, Any
  6. from collections import defaultdict
  7. from sqlalchemy import func
  8. from sqlalchemy.exc import SQLAlchemyError
  9. from piket_server.flask import db
  10. class Person(db.Model):
  11. """ Represents a person to be shown on the lists. """
  12. __tablename__ = "people"
  13. person_id = db.Column(db.Integer, primary_key=True)
  14. full_name = db.Column(db.String, nullable=False)
  15. display_name = db.Column(db.String, nullable=True)
  16. aardbei_id = db.Column(db.Integer, nullable=True)
  17. active = db.Column(db.Boolean, nullable=False, default=False)
  18. consumptions = db.relationship("Consumption", backref="person", lazy=True)
  19. def __repr__(self) -> str:
  20. return f"<Person {self.person_id}: {self.full_name}>"
  21. @property
  22. def as_dict(self) -> dict:
  23. return {
  24. "person_id": self.person_id,
  25. "active": self.active,
  26. "full_name": self.full_name,
  27. "display_name": self.display_name,
  28. "consumptions": {
  29. ct.consumption_type_id: Consumption.query.filter_by(person=self)
  30. .filter_by(settlement=None)
  31. .filter_by(consumption_type=ct)
  32. .filter_by(reversed=False)
  33. .count()
  34. for ct in ConsumptionType.query.all()
  35. },
  36. }
  37. class Export(db.Model):
  38. """ Represents a set of exported Settlements. """
  39. __tablename__ = "exports"
  40. export_id = db.Column(db.Integer, primary_key=True)
  41. created_at = db.Column(
  42. db.DateTime, default=datetime.datetime.utcnow, nullable=False
  43. )
  44. settlements = db.relationship("Settlement", backref="export", lazy=True)
  45. @property
  46. def as_dict(self) -> dict:
  47. return {
  48. "export_id": self.export_id,
  49. "created_at": self.created_at.isoformat(),
  50. "settlement_ids": [s.settlement_id for s in self.settlements],
  51. }
  52. class Settlement(db.Model):
  53. """ Represents a settlement of the list. """
  54. __tablename__ = "settlements"
  55. settlement_id = db.Column(db.Integer, primary_key=True)
  56. name = db.Column(db.String, nullable=False)
  57. export_id = db.Column(db.Integer, db.ForeignKey("exports.export_id"), nullable=True)
  58. consumptions = db.relationship("Consumption", backref="settlement", lazy=True)
  59. def __repr__(self) -> str:
  60. return f"<Settlement {self.settlement_id}: {self.name}>"
  61. @property
  62. def as_dict(self) -> dict:
  63. return {
  64. "settlement_id": self.settlement_id,
  65. "name": self.name,
  66. "consumption_summary": self.consumption_summary,
  67. "unique_people": self.unique_people,
  68. "per_person_counts": self.per_person_counts,
  69. "count_info": self.per_person,
  70. }
  71. @property
  72. def unique_people(self) -> int:
  73. q = (
  74. Consumption.query.filter_by(settlement=self)
  75. .filter_by(reversed=False)
  76. .group_by(Consumption.person_id)
  77. .count()
  78. )
  79. return q
  80. @property
  81. def consumption_summary(self) -> dict:
  82. q = (
  83. Consumption.query.filter_by(settlement=self)
  84. .filter_by(reversed=False)
  85. .group_by(Consumption.consumption_type_id)
  86. .order_by(ConsumptionType.name)
  87. .outerjoin(ConsumptionType)
  88. .with_entities(
  89. Consumption.consumption_type_id,
  90. ConsumptionType.name,
  91. func.count(Consumption.consumption_id),
  92. )
  93. .all()
  94. )
  95. return {r[0]: {"name": r[1], "count": r[2]} for r in q}
  96. @property
  97. def per_person(self) -> dict:
  98. # Get keys of seen consumption_types
  99. c_types = self.consumption_summary.keys()
  100. result = {}
  101. for type in c_types:
  102. c_type = ConsumptionType.query.get(type)
  103. result[type] = {"consumption_type": c_type.as_dict, "counts": {}}
  104. q = (
  105. Consumption.query.filter_by(settlement=self)
  106. .filter_by(reversed=False)
  107. .filter_by(consumption_type=c_type)
  108. .group_by(Consumption.person_id)
  109. .order_by(Person.full_name)
  110. .outerjoin(Person)
  111. .with_entities(
  112. Person.person_id,
  113. Person.full_name,
  114. func.count(Consumption.consumption_id),
  115. )
  116. .all()
  117. )
  118. for row in q:
  119. result[type]["counts"][row[0]] = {"name": row[1], "count": row[2]}
  120. return result
  121. @property
  122. def per_person_counts(self) -> Dict[int, Any]:
  123. """
  124. Output a more usable dict containing for each person in the settlement
  125. how many of each consumption type was counted.
  126. """
  127. q = (
  128. Consumption.query.filter_by(settlement=self)
  129. .filter_by(reversed=False)
  130. .group_by(Consumption.person_id)
  131. .group_by(Consumption.consumption_type_id)
  132. .group_by(Person.full_name)
  133. .outerjoin(Person)
  134. .with_entities(
  135. Consumption.person_id,
  136. Person.full_name,
  137. Consumption.consumption_type_id,
  138. func.count(Consumption.consumption_id),
  139. )
  140. .all()
  141. )
  142. res: Dict[int, Any] = defaultdict(dict)
  143. for row in q:
  144. item = res[row[0]]
  145. item["full_name"] = row[1]
  146. if not item.get("counts"):
  147. item["counts"] = {}
  148. item["counts"][row[2]] = row[3]
  149. return res
  150. class ConsumptionType(db.Model):
  151. """ Represents a type of consumption to be counted. """
  152. __tablename__ = "consumption_types"
  153. consumption_type_id = db.Column(db.Integer, primary_key=True)
  154. name = db.Column(db.String, nullable=False)
  155. icon = db.Column(db.String)
  156. active = db.Column(db.Boolean, default=True)
  157. consumptions = db.relationship("Consumption", backref="consumption_type", lazy=True)
  158. def __repr__(self) -> str:
  159. return f"<ConsumptionType: {self.name}>"
  160. @property
  161. def as_dict(self) -> dict:
  162. return {
  163. "consumption_type_id": self.consumption_type_id,
  164. "name": self.name,
  165. "icon": self.icon,
  166. "active": self.active,
  167. }
  168. class Consumption(db.Model):
  169. """ Represent one consumption to be counted. """
  170. __tablename__ = "consumptions"
  171. consumption_id = db.Column(db.Integer, primary_key=True)
  172. person_id = db.Column(db.Integer, db.ForeignKey("people.person_id"), nullable=True)
  173. consumption_type_id = db.Column(
  174. db.Integer,
  175. db.ForeignKey("consumption_types.consumption_type_id"),
  176. nullable=False,
  177. )
  178. settlement_id = db.Column(
  179. db.Integer, db.ForeignKey("settlements.settlement_id"), nullable=True
  180. )
  181. created_at = db.Column(
  182. db.DateTime, default=datetime.datetime.utcnow, nullable=False
  183. )
  184. reversed = db.Column(db.Boolean, default=False, nullable=False)
  185. def __repr__(self) -> str:
  186. return (
  187. f"<Consumption: {self.consumption_type.name} for {self.person.full_name}>"
  188. )
  189. @property
  190. def as_dict(self) -> dict:
  191. return {
  192. "consumption_id": self.consumption_id,
  193. "person_id": self.person_id,
  194. "consumption_type_id": self.consumption_type_id,
  195. "settlement_id": self.settlement_id,
  196. "created_at": self.created_at.isoformat(),
  197. "reversed": self.reversed,
  198. }