Skip to content

Commit 363067e

Browse files
committed
feat: migrate chore UI to load events from local computed objects
1 parent 846825a commit 363067e

File tree

5 files changed

+214
-28
lines changed

5 files changed

+214
-28
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"description": "",
4+
"type": "object",
5+
"properties": {
6+
"title": {
7+
"type": "string",
8+
"minLength": 1
9+
},
10+
"version": {
11+
"type": "string",
12+
"minLength": 1
13+
},
14+
"chores": {
15+
"type": "array",
16+
"uniqueItems": true,
17+
"minItems": 1,
18+
"items": {
19+
"required": ["ts_str", "timestamp"],
20+
"properties": {
21+
"ts_str": {
22+
"type": "string",
23+
"minLength": 1
24+
},
25+
"timestamp": {
26+
"type": "number"
27+
},
28+
"events": {
29+
"type": "array",
30+
"uniqueItems": true,
31+
"minItems": 1,
32+
"items": {
33+
"required": ["time_str", "wiki_url"],
34+
"properties": {
35+
"chore": {
36+
"type": "object",
37+
"properties": {
38+
"chore_id": {
39+
"type": "number"
40+
},
41+
"description": {
42+
"type": "string",
43+
"minLength": 1
44+
},
45+
"min_required_people": {
46+
"type": "number"
47+
},
48+
"name": {
49+
"type": "string",
50+
"minLength": 1
51+
}
52+
},
53+
"required": [
54+
"chore_id",
55+
"description",
56+
"min_required_people",
57+
"name"
58+
]
59+
},
60+
"when": {
61+
"type": "object",
62+
"properties": {
63+
"human_str": {
64+
"type": "string",
65+
"minLength": 1
66+
},
67+
"timestamp": {
68+
"type": "number"
69+
}
70+
},
71+
"required": ["human_str", "timestamp"]
72+
},
73+
"time_str": {
74+
"type": "string",
75+
"minLength": 1
76+
},
77+
"volunteers": {
78+
"type": "array",
79+
"items": {
80+
"required": [],
81+
"properties": {}
82+
}
83+
},
84+
"wiki_url": {
85+
"type": "string",
86+
"minLength": 1
87+
}
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
94+
},
95+
"required": ["title", "version", "chores"]
96+
}

chores/tests/test_api.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import json
2+
3+
import time_machine
4+
from django.contrib.auth import get_user_model
5+
from django.test import TestCase
6+
from django.urls import reverse
7+
from faker import Faker
8+
from rest_framework import status
9+
10+
from ..models import Chore
11+
from .factories import UserFactory
12+
13+
fake = Faker()
14+
15+
User = get_user_model()
16+
17+
18+
# Load the schema from a file
19+
def load_schema(schema_path):
20+
with open(schema_path, "r") as schema_file:
21+
return json.load(schema_file)
22+
23+
24+
class ChoresAPITest(TestCase):
25+
def setUp(self):
26+
self.user = UserFactory()
27+
28+
def test_empty_chores_api_list_endpoint_returns_404(self):
29+
"""Test that the chores API list endpoint returns a 404 response"""
30+
url = reverse("chores_api")
31+
response = self.client.get(url)
32+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
33+
34+
@time_machine.travel("2025-07-13 14:56")
35+
def test_chores_api_list_endpoint_returns_200(self):
36+
"""Test that the chores API list endpoint returns a 200 response"""
37+
url = reverse("chores_api")
38+
Chore.objects.create(
39+
name=fake.name(),
40+
description="A test chore that needs volunteers.",
41+
class_type="BasicChore",
42+
wiki_url=fake.url(),
43+
configuration={
44+
"min_required_people": 1,
45+
"events_generation": {
46+
"event_type": "recurrent",
47+
"starting_time": "21/7/2021 8:00",
48+
"crontab": "0 22 * * sun",
49+
"take_one_every": 2,
50+
},
51+
"reminders": [
52+
{
53+
"reminder_type": "missing_volunteers",
54+
"when": {"days_before": 10, "time": "17:00"},
55+
"nudges": [
56+
{
57+
"nudge_type": "email",
58+
"nudge_key": "gentle_email_reminder",
59+
"destination": "[email protected]",
60+
"subject_template": "Volunteers needed next week",
61+
"body_template": "Hallo, we hebben {num_volunteers_needed} vrijwilliger nodig volgende week.\n\nOm stof in onze makerspace tegen te gaan willen we je vragen om te stofzuigen in de voorste ruimte en de houtwerkplaats, en eventueel de grote hal. \n\nAls je daar zin in hebt kun je ook de voorste ruimte dweilen met de hippe turbodweil die we hebben, maar als je weinig tijd hebt: Met alleen stofzuigen is al veel te winnen.\n\nDeze taak kan op elk moment gedurende de week worden gedaan. Je helpt wanneer het jou uitkomt, alle hulp is welkom!\n\nInformatie over schoonmaken op de Wiki: https://wiki.makerspaceleiden.nl/mediawiki/index.php/Chore_-_Dedustify \n\n\nClick here to sign up: {signup_url}",
62+
}
63+
],
64+
},
65+
{
66+
"reminder_type": "missing_volunteers",
67+
"when": {"days_before": 6, "time": "17:00"},
68+
"nudges": [
69+
{
70+
"nudge_type": "email",
71+
"nudge_key": "hard_email_reminder",
72+
"destination": "[email protected]",
73+
"subject_template": "Volunteers REALLY needed for this week",
74+
"body_template": "Hallo, we hebben nog steeds {num_volunteers_needed} vrijwilliger nodig voor deze week.\n\nOm stof in onze makerspace tegen te gaan willen we je vragen om te stofzuigen in de voorste ruimte en de houtwerkplaats, en eventueel de grote hal. \n\nAls je daar zin in hebt kun je ook de voorste ruimte dweilen met de hippe turbodweil die we hebben, maar als je weinig tijd hebt: Met alleen stofzuigen is al veel te winnen.\n\nDeze taak kan op elk moment gedurende de week worden gedaan. Je helpt wanneer het jou uitkomt, alle hulp is welkom!\n\nInformatie over schoonmaken op de Wiki: https://wiki.makerspaceleiden.nl/mediawiki/index.php/Chore_-_Dedustify \n\nClick here to sign up: {signup_url}",
75+
}
76+
],
77+
},
78+
{
79+
"reminder_type": "volunteers_who_signed_up",
80+
"when": {"days_before": 7, "time": "19:00"},
81+
},
82+
],
83+
},
84+
creator=self.user,
85+
)
86+
87+
response = self.client.get(url)
88+
self.assertEqual(response.status_code, status.HTTP_200_OK)
89+
90+
jsonBody = json.loads(response.content)
91+
print("------------------------------------------------")
92+
self.assertEqual(len(jsonBody["chores"]), 6)
93+
94+
# LADEBUG: Fixme
95+
# schema = load_schema(
96+
# os.path.join(os.path.dirname(__file__), "./fixtures/api_schema.json")
97+
# )
98+
# jsonschema.validate(jsonBody, schema)

chores/views.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,38 @@
1212
from django.shortcuts import redirect, render
1313
from django.template.loader import render_to_string
1414

15-
from selfservice.aggregator_adapter import get_aggregator_adapter
16-
15+
from .core import ChoreEventsLogic, Time
1716
from .models import Chore, ChoreVolunteer
1817

1918
logger = logging.getLogger(__name__)
2019

20+
NUMBER_OF_DAYS_AHEAD = 90
2121

22-
def getall(current_user_id=None, subset=None):
23-
aggregator_adapter = get_aggregator_adapter()
24-
if not aggregator_adapter:
25-
return HttpResponse(
26-
"No aggregator configuration found", status=500, content_type="text/plain"
27-
)
2822

23+
def chores_get_all_from(now):
24+
then = now + (NUMBER_OF_DAYS_AHEAD * 24 * 60 * 60) # Add 14 days
25+
chores = Chore.objects.all()
26+
data = ChoreEventsLogic(chores).get_events_from_to(
27+
Time.from_timestamp(now), Time.from_timestamp(then)
28+
)
29+
return [e.for_json() for e in data]
30+
31+
32+
def getall(current_user_id=None, subset=None):
2933
now = time.time()
3034
volunteers_turns = ChoreVolunteer.objects.filter(timestamp__gte=now)
3135
volunteers_by_key = defaultdict(list)
3236
for turn in volunteers_turns:
3337
key = f"{turn.chore.id}-{turn.timestamp}"
3438
volunteers_by_key[key].append(turn.user)
3539

36-
data = aggregator_adapter.get_chores()
40+
# FIXME: replace use of aggregator_adapter.get_chores()
41+
data = chores_get_all_from(now)
3742

3843
event_groups = []
3944
ts = None
4045
if data is not None:
41-
for event in data["events"]:
46+
for event in data:
4247
event_ts = datetime.fromtimestamp(event["when"]["timestamp"])
4348
event_ts_str = event_ts.strftime("%d%m%Y")
4449
event["time_str"] = event_ts.strftime("%H:%M")
@@ -87,6 +92,7 @@ def index_api(request, name=None):
8792

8893
if not chores:
8994
return HttpResponse("No chores found", status=404, content_type="text/plain")
95+
9096
payload = {
9197
"title": "Chores of this week",
9298
"version": "1.00",
@@ -115,23 +121,21 @@ def index(request):
115121

116122

117123
def get_chores_overview(current_user_id=None, subset=None):
118-
aggregator_adapter = get_aggregator_adapter()
119-
if not aggregator_adapter:
120-
return None, "No aggregator configuration found"
124+
now = time.time()
121125

122126
volunteers_turns = ChoreVolunteer.objects.all()
123127
volunteers_by_key = defaultdict(list)
124128
for turn in volunteers_turns:
125129
key = f"{turn.chore.id}-{turn.timestamp}"
126130
volunteers_by_key[key].append(turn.user)
127131

128-
data = aggregator_adapter.get_chores()
129-
if data is None:
132+
chore_events = chores_get_all_from(now)
133+
if chore_events is None:
130134
return None, "No data available"
131135

132136
event_groups = {}
133137

134-
for event in data["events"]:
138+
for event in chore_events:
135139
event_ts = datetime.fromtimestamp(event["when"]["timestamp"])
136140

137141
event["time_str"] = event_ts.strftime("%H:%M")

selfservice/aggregator_adapter.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,6 @@ def notification_test(self, user_id):
5959
def checkout(self, user_id):
6060
self._request_with_user_id("/space/checkout", user_id)
6161

62-
def get_chores(self):
63-
payload = self._request_with_user_id("/chores/overview")
64-
if payload:
65-
try:
66-
return json.loads(payload)
67-
except Exception:
68-
logger.error("Failed to parse the json chore: '{}'.".format(payload))
69-
return None
70-
7162

7263
def initialize_aggregator_adapter(base_url, username, password):
7364
return AggregatorAdapter(base_url, username, password)

selfservice/test_helpers/mocks.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,3 @@ def notification_test(self, user_id):
1616

1717
def checkout(self, user_id):
1818
pass
19-
20-
def get_chores(self):
21-
return {"chores": ["mocked-chore-1", "mocked-chore-2"]}

0 commit comments

Comments
 (0)