Skip to content

Commit 2d1d967

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

File tree

5 files changed

+215
-28
lines changed

5 files changed

+215
-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: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,39 @@
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+
# FIXME: replace use of aggregator_adapter.get_chores()
25+
then = now + (NUMBER_OF_DAYS_AHEAD * 24 * 60 * 60) # Add 14 days
26+
chores = Chore.objects.all()
27+
data = ChoreEventsLogic(chores).get_events_from_to(
28+
Time.from_timestamp(now), Time.from_timestamp(then)
29+
)
30+
return [e.for_json() for e in data]
31+
32+
33+
def getall(current_user_id=None, subset=None):
2934
now = time.time()
3035
volunteers_turns = ChoreVolunteer.objects.filter(timestamp__gte=now)
3136
volunteers_by_key = defaultdict(list)
3237
for turn in volunteers_turns:
3338
key = f"{turn.chore.id}-{turn.timestamp}"
3439
volunteers_by_key[key].append(turn.user)
3540

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

3844
event_groups = []
3945
ts = None
4046
if data is not None:
41-
for event in data["events"]:
47+
for event in data:
4248
event_ts = datetime.fromtimestamp(event["when"]["timestamp"])
4349
event_ts_str = event_ts.strftime("%d%m%Y")
4450
event["time_str"] = event_ts.strftime("%H:%M")
@@ -87,6 +93,7 @@ def index_api(request, name=None):
8793

8894
if not chores:
8995
return HttpResponse("No chores found", status=404, content_type="text/plain")
96+
9097
payload = {
9198
"title": "Chores of this week",
9299
"version": "1.00",
@@ -115,23 +122,21 @@ def index(request):
115122

116123

117124
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"
125+
now = time.time()
121126

122127
volunteers_turns = ChoreVolunteer.objects.all()
123128
volunteers_by_key = defaultdict(list)
124129
for turn in volunteers_turns:
125130
key = f"{turn.chore.id}-{turn.timestamp}"
126131
volunteers_by_key[key].append(turn.user)
127132

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

132137
event_groups = {}
133138

134-
for event in data["events"]:
139+
for event in chore_events:
135140
event_ts = datetime.fromtimestamp(event["when"]["timestamp"])
136141

137142
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)