Browse Source

Make aardbei_sync actually work

Maarten van den Berg 5 years ago
parent
commit
cc9edc28bb
1 changed files with 227 additions and 39 deletions
  1. 227 39
      piket_server/aardbei_sync.py

+ 227 - 39
piket_server/aardbei_sync.py

@@ -1,35 +1,202 @@
1 1
 from __future__ import annotations
2 2
 
3
-from typing import List, Dict, Any, Tuple, Optional
4
-from dataclasses import dataclass
3
+import datetime
4
+import json
5 5
 import logging
6
+from dataclasses import asdict, dataclass
7
+from enum import Enum
8
+from typing import Any, Dict, List, NewType, Optional, Tuple
6 9
 
7 10
 import requests
8 11
 
9
-from piket_server import Person, db
12
+from piket_server.models import Person
13
+from piket_server.flask import db
14
+
15
+# AARDBEI_ENDPOINT = "https://aardbei.app"
16
+AARDBEI_ENDPOINT = "http://localhost:3000"
17
+
18
+ActivityId = NewType("ActivityId", int)
19
+PersonId = NewType("PersonId", int)
20
+MemberId = NewType("MemberId", int)
21
+ParticipantId = NewType("ParticipantId", int)
10 22
 
11 23
 
12 24
 @dataclass(frozen=True)
13
-class SparseAardbeiPerson:
25
+class AardbeiPerson:
26
+    aardbei_id: PersonId
14 27
     full_name: str
15
-    display_name: str
16
-    aardbei_id: int
28
+
29
+    @classmethod
30
+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiPerson:
31
+        d = data["person"]
32
+        return cls(full_name=d["full_name"], aardbei_id=PersonId(d["id"]))
33
+
34
+
35
+@dataclass(frozen=True)
36
+class AardbeiMember:
37
+    person: AardbeiPerson
38
+    aardbei_id: MemberId
17 39
     is_leader: bool
40
+    display_name: str
41
+
42
+    @classmethod
43
+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiMember:
44
+        logging.debug("Init with data %s", json.dumps(data))
45
+        d = data["member"]
46
+        person = AardbeiPerson.from_aardbei_dict(d)
47
+        return cls(
48
+            person=person,
49
+            aardbei_id=MemberId(d["id"]),
50
+            is_leader=d["is_leader"],
51
+            display_name=d["display_name"],
52
+        )
53
+
54
+
55
+@dataclass(frozen=True)
56
+class AardbeiParticipant:
57
+    person: AardbeiPerson
58
+    member: Optional[AardbeiMember]
59
+    aardbei_id: ParticipantId
60
+    attending: bool
61
+    is_organizer: bool
62
+    notes: Optional[str]
63
+
64
+    @property
65
+    def name(self) -> str:
66
+        if self.member is not None:
67
+            return self.member.display_name
68
+
69
+        return self.person.full_name
70
+
71
+    @classmethod
72
+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiParticipant:
73
+        d = data["participant"]
74
+        person = AardbeiPerson.from_aardbei_dict(d)
75
+
76
+        member: Optional[AardbeiMember] = None
77
+        if d["member"] is not None:
78
+            member = AardbeiMember.from_aardbei_dict(d)
79
+
80
+        aardbei_id = ParticipantId(d["id"])
81
+
82
+        return cls(
83
+            person=person,
84
+            member=member,
85
+            aardbei_id=aardbei_id,
86
+            attending=d["attending"],
87
+            is_organizer=d["is_organizer"],
88
+            notes=d["notes"],
89
+        )
90
+
91
+
92
+class NoResponseAction(Enum):
93
+    Present = "present"
94
+    Absent = "absent"
95
+
96
+
97
+@dataclass(frozen=True)
98
+class ResponseCounts:
99
+    present: int
100
+    absent: int
101
+    unknown: int
18 102
 
19 103
     @classmethod
20
-    def from_aardbei_dict(cls, data: Dict[str, Any]) -> SparseAardbeiPerson:
104
+    def from_aardbei_dict(cls, data: Dict[str, int]) -> ResponseCounts:
21 105
         return cls(
22
-            full_name=data["member"]["person"]["full_name"],
23
-            display_name=data["member"]["display_name"],
24
-            aardbei_id=data["member"]["person"]["id"],
25
-            is_leader=data["member"]["is_leader"],
106
+            present=data["present"], absent=data["absent"], unknown=data["unknown"]
26 107
         )
27 108
 
28 109
 
29 110
 @dataclass(frozen=True)
111
+class SparseAardbeiActivity:
112
+    aardbei_id: ActivityId
113
+    name: str
114
+    description: str
115
+    location: str
116
+    start: datetime.datetime
117
+    end: Optional[datetime.datetime]
118
+    deadline: Optional[datetime.datetime]
119
+    reminder_at: Optional[datetime.datetime]
120
+    no_response_action: NoResponseAction
121
+    response_counts: ResponseCounts
122
+
123
+    def distance(self, reference: datetime.datetime) -> datetime.timedelta:
124
+        """Calculate how long ago this Activity ended / how much time until it starts."""
125
+        if self.end is not None:
126
+            if reference > self.start and reference < self.end:
127
+                return datetime.timedelta(seconds=0)
128
+
129
+            elif reference < self.start:
130
+                return self.start - reference
131
+
132
+            elif reference > self.end:
133
+                return reference - self.end
134
+
135
+        if reference > self.start:
136
+            return reference - self.start
137
+
138
+        return self.start - reference
139
+
140
+    @classmethod
141
+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> SparseAardbeiActivity:
142
+        start: datetime.datetime = datetime.datetime.fromisoformat(
143
+            data["activity"]["start"]
144
+        )
145
+        end: Optional[datetime.datetime] = None
146
+
147
+        if data["activity"]["end"] is not None:
148
+            end = datetime.datetime.fromisoformat(data["activity"]["end"])
149
+
150
+        deadline: Optional[datetime.datetime] = None
151
+        if data["activity"]["deadline"] is not None:
152
+            deadline = datetime.datetime.fromisoformat(data["activity"]["deadline"])
153
+
154
+        reminder_at: Optional[datetime.datetime] = None
155
+        if data["activity"]["reminder_at"] is not None:
156
+            reminder_at = datetime.datetime.fromisoformat(
157
+                data["activity"]["reminder_at"]
158
+            )
159
+
160
+        no_response_action = NoResponseAction(data["activity"]["no_response_action"])
161
+
162
+        response_counts = ResponseCounts.from_aardbei_dict(
163
+            data["activity"]["response_counts"]
164
+        )
165
+
166
+        return cls(
167
+            aardbei_id=ActivityId(data["activity"]["id"]),
168
+            name=data["activity"]["name"],
169
+            description=data["activity"]["description"],
170
+            location=data["activity"]["location"],
171
+            start=start,
172
+            end=end,
173
+            deadline=deadline,
174
+            reminder_at=reminder_at,
175
+            no_response_action=no_response_action,
176
+            response_counts=response_counts,
177
+        )
178
+
179
+
180
+@dataclass(frozen=True)
181
+class AardbeiActivity(SparseAardbeiActivity):
182
+    participants: List[AardbeiParticipant]
183
+
184
+    @classmethod
185
+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiActivity:
186
+        participants: List[AardbeiParticipant] = [
187
+            AardbeiParticipant.from_aardbei_dict(x)
188
+            for x in data["activity"]["participants"]
189
+        ]
190
+
191
+        sparse = super().from_aardbei_dict(data)
192
+
193
+        return cls(participants=participants, **asdict(sparse))
194
+
195
+
196
+@dataclass(frozen=True)
30 197
 class AardbeiMatch:
31 198
     local: Person
32
-    remote: SparseAardbeiPerson
199
+    remote: AardbeiMember
33 200
 
34 201
 
35 202
 @dataclass(frozen=True)
@@ -38,37 +205,37 @@ class AardbeiLink:
38 205
     """People that exist on both sides, but aren't linked in the people table."""
39 206
     altered_name: List[AardbeiMatch]
40 207
     """People that are already linked but changed one of their names."""
41
-    remote_only: List[SparseAardbeiPerson]
208
+    remote_only: List[AardbeiMember]
42 209
     """People that only exist on the remote."""
43 210
 
44 211
 
45
-def get_aardbei_people(token: str) -> List[SparseAardbeiPerson]:
212
+def get_aardbei_people(token: str) -> List[AardbeiMember]:
46 213
     resp = requests.get(
47
-        "https://aardbei.app/api/groups/0/", headers={"Authorization": f"Group {token}"}
214
+        f"{AARDBEI_ENDPOINT}/api/groups/0/", headers={"Authorization": f"Group {token}"}
48 215
     )
49 216
     resp.raise_for_status()
50 217
 
51 218
     members = resp.json()["group"]["members"]
52 219
 
53
-    return [SparseAardbeiPerson.from_aardbei_dict(x) for x in members]
220
+    return [AardbeiMember.from_aardbei_dict(x) for x in members]
54 221
 
55 222
 
56
-def match_local_aardbei(aardbei_people: List[SparseAardbeiPerson]) -> AardbeiLink:
223
+def match_local_aardbei(aardbei_members: List[AardbeiMember]) -> AardbeiLink:
57 224
     matches: List[AardbeiMatch] = []
58 225
     altered_name: List[AardbeiMatch] = []
59
-    remote_only: List[SparseAardbeiPerson] = []
226
+    remote_only: List[AardbeiMember] = []
60 227
 
61
-    for aardbei_person in aardbei_people:
228
+    for member in aardbei_members:
62 229
         p: Optional[Person] = Person.query.filter_by(
63
-            aardbei_id=aardbei_person.aardbei_id
230
+            aardbei_id=member.aardbei_id
64 231
         ).one_or_none()
65 232
 
66 233
         if p is not None:
67 234
             if (
68
-                p.full_name != aardbei_person.full_name
69
-                or p.display_name != aardbei_person.display_name
235
+                p.full_name != member.person.full_name
236
+                or p.display_name != member.display_name
70 237
             ):
71
-                altered_name.append(AardbeiMatch(p, aardbei_person))
238
+                altered_name.append(AardbeiMatch(p, member))
72 239
 
73 240
             else:
74 241
                 logging.info(
@@ -81,12 +248,12 @@ def match_local_aardbei(aardbei_people: List[SparseAardbeiPerson]) -> AardbeiLin
81 248
 
82 249
             continue
83 250
 
84
-        p = Person.query.filter_by(full_name=aardbei_person.full_name).one_or_none()
251
+        p = Person.query.filter_by(full_name=member.person.full_name).one_or_none()
85 252
 
86 253
         if p is not None:
87
-            matches.append(AardbeiMatch(p, aardbei_person))
254
+            matches.append(AardbeiMatch(p, member))
88 255
         else:
89
-            remote_only.append(aardbei_person)
256
+            remote_only.append(member)
90 257
 
91 258
     return AardbeiLink(matches, altered_name, remote_only)
92 259
 
@@ -106,16 +273,19 @@ def link_matches(matches: List[AardbeiMatch]) -> None:
106 273
         db.session.add(match.local)
107 274
 
108 275
 
109
-def create_missing(missing: List[SparseAardbeiPerson]) -> None:
110
-    for person in missing:
276
+def create_missing(missing: List[AardbeiMember]) -> None:
277
+    for member in missing:
111 278
         pnew = Person(
112
-            full_name=person.full_name,
113
-            display_name=person.display_name,
114
-            aardbei_id=person.aardbei_id,
279
+            full_name=member.person.full_name,
280
+            display_name=member.display_name,
281
+            aardbei_id=member.aardbei_id,
115 282
             active=False,
116 283
         )
117 284
         logging.info(
118
-            "Creating new person for %s (%s)", person.full_name, person.aardbei_id
285
+            "Creating new person for %s / %s (%s)",
286
+            member.person.full_name,
287
+            member.display_name,
288
+            member.aardbei_id,
119 289
         )
120 290
         db.session.add(pnew)
121 291
 
@@ -123,7 +293,8 @@ def create_missing(missing: List[SparseAardbeiPerson]) -> None:
123 293
 def update_names(matches: List[AardbeiMatch]) -> None:
124 294
     for match in matches:
125 295
         p = match.local
126
-        aardbei_person = match.remote
296
+        member = match.remote
297
+        aardbei_person = member.person
127 298
 
128 299
         changed = False
129 300
 
@@ -139,17 +310,16 @@ def update_names(matches: List[AardbeiMatch]) -> None:
139 310
             p.full_name = aardbei_person.full_name
140 311
             changed = True
141 312
 
142
-        if p.display_name != aardbei_person.display_name:
313
+        if p.display_name != member.display_name:
143 314
             logging.info(
144 315
                 "Updating %s (L%s/R%s) display name %s to %s",
145
-                aardbei_person.full_name,
316
+                p.full_name,
146 317
                 p.person_id,
147 318
                 aardbei_person.aardbei_id,
148 319
                 p.display_name,
149
-                aardbei_person.display_name,
320
+                member.display_name,
150 321
             )
151
-            p.display_name = aardbei_person.display_name
152
-
322
+            p.display_name = member.display_name
153 323
             changed = True
154 324
 
155 325
         assert changed, "got match but didn't update anything"
@@ -157,11 +327,29 @@ def update_names(matches: List[AardbeiMatch]) -> None:
157 327
         db.session.add(p)
158 328
 
159 329
 
330
+def get_activities(token: str) -> List[SparseAardbeiActivity]:
331
+    result: List[SparseAardbeiActivity] = []
332
+
333
+    for category in ("upcoming", "current", "previous"):
334
+        resp = requests.get(
335
+            f"{AARDBEI_ENDPOINT}/api/groups/0/{category}_activities",
336
+            headers={"Authorization": f"Group {token}"},
337
+        )
338
+
339
+        resp.raise_for_status()
340
+
341
+        for item in resp.json():
342
+            result.append(SparseAardbeiActivity.from_aardbei_dict(item))
343
+
344
+    return result
345
+
346
+
160 347
 if __name__ == "__main__":
161
-    logging.basicConfig(level=logging.INFO)
348
+    logging.basicConfig(level=logging.DEBUG)
162 349
 
163 350
     token = input("Token: ")
164 351
     aardbei_people = get_aardbei_people(token)
352
+    activities = get_activities(token)
165 353
     link = match_local_aardbei(aardbei_people)
166 354
     link_matches(link.matches)
167 355
     create_missing(link.remote_only)