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
 from __future__ import annotations
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
 import logging
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
 import requests
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
 @dataclass(frozen=True)
24
 @dataclass(frozen=True)
13
-class SparseAardbeiPerson:
25
+class AardbeiPerson:
26
+    aardbei_id: PersonId
14
     full_name: str
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
     is_leader: bool
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
     @classmethod
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
         return cls(
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
 @dataclass(frozen=True)
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
 class AardbeiMatch:
197
 class AardbeiMatch:
31
     local: Person
198
     local: Person
32
-    remote: SparseAardbeiPerson
199
+    remote: AardbeiMember
33
 
200
 
34
 
201
 
35
 @dataclass(frozen=True)
202
 @dataclass(frozen=True)
38
     """People that exist on both sides, but aren't linked in the people table."""
205
     """People that exist on both sides, but aren't linked in the people table."""
39
     altered_name: List[AardbeiMatch]
206
     altered_name: List[AardbeiMatch]
40
     """People that are already linked but changed one of their names."""
207
     """People that are already linked but changed one of their names."""
41
-    remote_only: List[SparseAardbeiPerson]
208
+    remote_only: List[AardbeiMember]
42
     """People that only exist on the remote."""
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
     resp = requests.get(
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
     resp.raise_for_status()
216
     resp.raise_for_status()
50
 
217
 
51
     members = resp.json()["group"]["members"]
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
     matches: List[AardbeiMatch] = []
224
     matches: List[AardbeiMatch] = []
58
     altered_name: List[AardbeiMatch] = []
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
         p: Optional[Person] = Person.query.filter_by(
229
         p: Optional[Person] = Person.query.filter_by(
63
-            aardbei_id=aardbei_person.aardbei_id
230
+            aardbei_id=member.aardbei_id
64
         ).one_or_none()
231
         ).one_or_none()
65
 
232
 
66
         if p is not None:
233
         if p is not None:
67
             if (
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
             else:
240
             else:
74
                 logging.info(
241
                 logging.info(
81
 
248
 
82
             continue
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
         if p is not None:
253
         if p is not None:
87
-            matches.append(AardbeiMatch(p, aardbei_person))
254
+            matches.append(AardbeiMatch(p, member))
88
         else:
255
         else:
89
-            remote_only.append(aardbei_person)
256
+            remote_only.append(member)
90
 
257
 
91
     return AardbeiLink(matches, altered_name, remote_only)
258
     return AardbeiLink(matches, altered_name, remote_only)
92
 
259
 
106
         db.session.add(match.local)
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
         pnew = Person(
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
             active=False,
282
             active=False,
116
         )
283
         )
117
         logging.info(
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
         db.session.add(pnew)
290
         db.session.add(pnew)
121
 
291
 
123
 def update_names(matches: List[AardbeiMatch]) -> None:
293
 def update_names(matches: List[AardbeiMatch]) -> None:
124
     for match in matches:
294
     for match in matches:
125
         p = match.local
295
         p = match.local
126
-        aardbei_person = match.remote
296
+        member = match.remote
297
+        aardbei_person = member.person
127
 
298
 
128
         changed = False
299
         changed = False
129
 
300
 
139
             p.full_name = aardbei_person.full_name
310
             p.full_name = aardbei_person.full_name
140
             changed = True
311
             changed = True
141
 
312
 
142
-        if p.display_name != aardbei_person.display_name:
313
+        if p.display_name != member.display_name:
143
             logging.info(
314
             logging.info(
144
                 "Updating %s (L%s/R%s) display name %s to %s",
315
                 "Updating %s (L%s/R%s) display name %s to %s",
145
-                aardbei_person.full_name,
316
+                p.full_name,
146
                 p.person_id,
317
                 p.person_id,
147
                 aardbei_person.aardbei_id,
318
                 aardbei_person.aardbei_id,
148
                 p.display_name,
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
             changed = True
323
             changed = True
154
 
324
 
155
         assert changed, "got match but didn't update anything"
325
         assert changed, "got match but didn't update anything"
157
         db.session.add(p)
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
 if __name__ == "__main__":
347
 if __name__ == "__main__":
161
-    logging.basicConfig(level=logging.INFO)
348
+    logging.basicConfig(level=logging.DEBUG)
162
 
349
 
163
     token = input("Token: ")
350
     token = input("Token: ")
164
     aardbei_people = get_aardbei_people(token)
351
     aardbei_people = get_aardbei_people(token)
352
+    activities = get_activities(token)
165
     link = match_local_aardbei(aardbei_people)
353
     link = match_local_aardbei(aardbei_people)
166
     link_matches(link.matches)
354
     link_matches(link.matches)
167
     create_missing(link.remote_only)
355
     create_missing(link.remote_only)