Digitale bierlijst

models.py 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. }
  70. @property
  71. def unique_people(self) -> int:
  72. q = (
  73. Consumption.query.filter_by(settlement=self)
  74. .filter_by(reversed=False)
  75. .group_by(Consumption.person_id)
  76. .count()
  77. )
  78. return q
  79. @property
  80. def consumption_summary(self) -> dict:
  81. q = (
  82. Consumption.query.filter_by(settlement=self)
  83. .filter_by(reversed=False)
  84. .group_by(Consumption.consumption_type_id)
  85. .order_by(ConsumptionType.name)
  86. .outerjoin(ConsumptionType)
  87. .with_entities(
  88. Consumption.consumption_type_id,
  89. ConsumptionType.name,
  90. func.count(Consumption.consumption_id),
  91. )
  92. .all()
  93. )
  94. return {r[0]: {"name": r[1], "count": r[2]} for r in q}
  95. @property
  96. def per_person(self) -> dict:
  97. # Get keys of seen consumption_types
  98. c_types = self.consumption_summary.keys()
  99. result = {}
  100. for type in c_types:
  101. c_type = ConsumptionType.query.get(type)
  102. result[type] = {"consumption_type": c_type.as_dict, "counts": {}}
  103. q = (
  104. Consumption.query.filter_by(settlement=self)
  105. .filter_by(reversed=False)
  106. .filter_by(consumption_type=c_type)
  107. .group_by(Consumption.person_id)
  108. .order_by(Person.full_name)
  109. .outerjoin(Person)
  110. .with_entities(
  111. Person.person_id,
  112. Person.full_name,
  113. func.count(Consumption.consumption_id),
  114. )
  115. .all()
  116. )
  117. for row in q:
  118. result[type]["counts"][row[0]] = {"name": row[1], "count": row[2]}
  119. return result
  120. @property
  121. def per_person_counts(self) -> Dict[int, Any]:
  122. """
  123. Output a more usable dict containing for each person in the settlement
  124. how many of each consumption type was counted.
  125. """
  126. q = (
  127. Consumption.query.filter_by(settlement=self)
  128. .filter_by(reversed=False)
  129. .group_by(Consumption.person_id)
  130. .group_by(Consumption.consumption_type_id)
  131. .group_by(Person.full_name)
  132. .outerjoin(Person)
  133. .with_entities(
  134. Consumption.person_id,
  135. Person.full_name,
  136. Consumption.consumption_type_id,
  137. func.count(Consumption.consumption_id),
  138. )
  139. .all()
  140. )
  141. res: Dict[int, Any] = defaultdict(dict)
  142. for row in q:
  143. item = res[row[0]]
  144. item["full_name"] = row[1]
  145. if not item.get("counts"):
  146. item["counts"] = {}
  147. item["counts"][row[2]] = row[3]
  148. return res
  149. class ConsumptionType(db.Model):
  150. """ Represents a type of consumption to be counted. """
  151. __tablename__ = "consumption_types"
  152. consumption_type_id = db.Column(db.Integer, primary_key=True)
  153. name = db.Column(db.String, nullable=False)
  154. icon = db.Column(db.String)
  155. active = db.Column(db.Boolean, default=True)
  156. consumptions = db.relationship("Consumption", backref="consumption_type", lazy=True)
  157. def __repr__(self) -> str:
  158. return f"<ConsumptionType: {self.name}>"
  159. @property
  160. def as_dict(self) -> dict:
  161. return {
  162. "consumption_type_id": self.consumption_type_id,
  163. "name": self.name,
  164. "icon": self.icon,
  165. }
  166. class Consumption(db.Model):
  167. """ Represent one consumption to be counted. """
  168. __tablename__ = "consumptions"
  169. consumption_id = db.Column(db.Integer, primary_key=True)
  170. person_id = db.Column(db.Integer, db.ForeignKey("people.person_id"), nullable=True)
  171. consumption_type_id = db.Column(
  172. db.Integer,
  173. db.ForeignKey("consumption_types.consumption_type_id"),
  174. nullable=False,
  175. )
  176. settlement_id = db.Column(
  177. db.Integer, db.ForeignKey("settlements.settlement_id"), nullable=True
  178. )
  179. created_at = db.Column(
  180. db.DateTime, default=datetime.datetime.utcnow, nullable=False
  181. )
  182. reversed = db.Column(db.Boolean, default=False, nullable=False)
  183. def __repr__(self) -> str:
  184. return (
  185. f"<Consumption: {self.consumption_type.name} for {self.person.full_name}>"
  186. )
  187. @property
  188. def as_dict(self) -> dict:
  189. return {
  190. "consumption_id": self.consumption_id,
  191. "person_id": self.person_id,
  192. "consumption_type_id": self.consumption_type_id,
  193. "settlement_id": self.settlement_id,
  194. "created_at": self.created_at.isoformat(),
  195. "reversed": self.reversed,
  196. }