Commit dd16e9a0 authored by MOREAU Ulysse's avatar MOREAU Ulysse
Browse files

Merge branch '71-evenements-recurrents-crees-avec-1h-d-avance' into 'master'

Resolve "Evénements récurrents créés avec 1h d'avance"

Closes #71

See merge request !67
parents 6767bded 2007c22a
import datetime
from zoneinfo import ZoneInfo
from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from events.models import RecurrentEvent, Event
from django.utils import timezone from django.utils import timezone
import datetime
from events.models import RecurrentEvent
class Command(BaseCommand): class Command(BaseCommand):
help = 'Create events based on recurrent events.' help = "Create events based on recurrent events."
def handle(self, *args, **options): def handle(self, *args, **options) -> None:
for event in RecurrentEvent.objects.all(): """Create instances for recurrent events for at least the next 14 days"""
while True: for rec_event in RecurrentEvent.objects.all():
if event.last_created is None or event.last_created < timezone.now() + datetime.timedelta(days=14): while (
self.update_event(event) rec_event.last_created is None # Première création
else: or rec_event.last_created < timezone.now() + datetime.timedelta(days=14)
break ):
self.stdout.write('Successfully created events') # Tant que la dernière instance de l'évenement est
# dans moins de 14 jours
def update_event(self, event): # Créer une nouvelle instance
delta = datetime.timedelta(days=event.delay) self.update_rec_event(rec_event)
self.stdout.write("Successfully created events")
if event.last_created is None: # First creation
e = Event.objects.create(**{field: value for field, value in event.__dict__.items() if field in [field.column for field in Event._meta.fields if field.column not in ['id', 'model']]}) def update_rec_event(self, rec_event: RecurrentEvent) -> None:
delta = datetime.timedelta(days=rec_event.delay)
if rec_event.last_created is None: # First creation
e = rec_event.create_instance()
e.save() e.save()
event.last_created = e.start_time rec_event.last_created = e.start_time
event.save() rec_event.save()
self.stdout.write("First creation of %s, start = %s (delay=%d)" % (event, event.start_time, event.delay)) self.stdout.write(
f"First creation of {rec_event}, "
f"start = {rec_event.start_time} "
f"(delay={rec_event.delay})"
)
else: else:
delta_dates = event.last_created - event.start_time + delta delta_dates = rec_event.last_created - rec_event.start_time + delta
e = Event.objects.create(**{field: value for field, value in event.__dict__.items() if field in [field.column for field in Event._meta.fields if field.column not in ['id', 'model']]}) e = rec_event.create_instance()
e.start_time += delta_dates e.start_time += delta_dates
e.start_time = e.start_time.replace(hour=event.start_time.hour)
event.last_created = e.start_time
e.end_time += delta_dates e.end_time += delta_dates
e.end_time = e.end_time.replace(hour=event.end_time.hour)
e.end_inscriptions += delta_dates e.end_inscriptions += delta_dates
e.end_inscriptions = e.end_inscriptions.replace(hour=event.end_inscriptions.hour)
if e.invitations_start is not None: if e.invitations_start is not None:
e.invitations_start += delta_dates e.invitations_start += delta_dates
e.invitations_start = e.invitations_start.replace(hour=event.invitations_start.hour)
if settings.USE_TZ:
# If we use timezones, fix the hours in the local timezone
localtz = ZoneInfo(settings.TIME_ZONE)
e.start_time = e.start_time.astimezone(localtz).replace(
hour=rec_event.start_time.astimezone(localtz).hour
)
e.end_time = e.end_time.astimezone(localtz).replace(
hour=rec_event.end_time.astimezone(localtz).hour
)
e.end_inscriptions = e.end_inscriptions.astimezone(localtz).replace(
hour=rec_event.end_inscriptions.astimezone(localtz).hour
)
if e.invitations_start is not None and rec_event.invitations_start is not None:
e.invitations_start = e.invitations_start.astimezone(
localtz
).replace(hour=rec_event.invitations_start.astimezone(localtz).hour)
e.save() e.save()
event.save() rec_event.last_created = e.start_time
self.stdout.write("Creating event %s for date %s" % (event, event.last_created)) rec_event.save()
self.stdout.write(
f"Creating event {rec_event} for date {rec_event.last_created}"
)
...@@ -126,7 +126,7 @@ class Event(models.Model): ...@@ -126,7 +126,7 @@ class Event(models.Model):
class RecurrentEvent(Event): class RecurrentEvent(Event):
delay = models.IntegerField(default=1) delay = models.IntegerField(default=1) # In days
last_created = models.DateTimeField(null=True, blank=True, default=None) last_created = models.DateTimeField(null=True, blank=True, default=None)
class Meta: class Meta:
...@@ -134,6 +134,21 @@ class RecurrentEvent(Event): ...@@ -134,6 +134,21 @@ class RecurrentEvent(Event):
('manage_recurrent_event', 'Can manage recurrent event (add/del/edit'), ('manage_recurrent_event', 'Can manage recurrent event (add/del/edit'),
) )
def create_instance(self) -> Event:
"""Create an Event instance of the RecurrentEvent"""
return Event.objects.create(
**{
field: value
for field, value in self.__dict__.items()
if field
in { # Use sets for faster `in` checks
field.column
for field in Event._meta.fields
if field.column not in {"id", "model"}
}
}
)
class ExternLink(models.Model): class ExternLink(models.Model):
event = models.ForeignKey(Event, related_name="extern_links", on_delete=models.CASCADE) event = models.ForeignKey(Event, related_name="extern_links", on_delete=models.CASCADE)
......
import datetime import datetime
import uuid
from datetime import timezone from datetime import timezone
from freezegun import freeze_time
from io import StringIO from io import StringIO
import uuid from zoneinfo import ZoneInfo
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management import call_command from django.core.management import call_command
from django.templatetags.static import static from django.templatetags.static import static
from django.test import TestCase from django.test import TestCase
from freezegun import freeze_time
from .models import Event
from .models import ExternInscription
from .models import ExternLink
from .models import Inscription
from .models import Invitation
from .models import RecurrentEvent
from bde.models import Contributor from bde.models import Contributor
from .models import Event, Inscription, ExternInscription, ExternLink, Invitation, RecurrentEvent
class TestEvent(TestCase): class TestEvent(TestCase):
def setUp(self): def setUp(self):
self.event = Event.objects.create( self.event = Event.objects.create(
name="test", name="test",
end_inscriptions=datetime.datetime(2014, 7, 14, 12, 0, 0, tzinfo=timezone.utc), end_inscriptions=datetime.datetime(
2014, 7, 14, 12, 0, 0, tzinfo=timezone.utc
),
start_time=datetime.datetime(2014, 7, 14, 13, 0, 0, tzinfo=timezone.utc), start_time=datetime.datetime(2014, 7, 14, 13, 0, 0, tzinfo=timezone.utc),
end_time=datetime.datetime(2014, 7, 14, 14, 0, 0, tzinfo=timezone.utc), end_time=datetime.datetime(2014, 7, 14, 14, 0, 0, tzinfo=timezone.utc),
location="location", location="location",
...@@ -25,30 +35,29 @@ class TestEvent(TestCase): ...@@ -25,30 +35,29 @@ class TestEvent(TestCase):
private=False, private=False,
limited=False, limited=False,
max_inscriptions=0, max_inscriptions=0,
allow_extern=False allow_extern=False,
) )
def do_inscriptions(self, event, nb, extern=False): def do_inscriptions(self, event, nb, extern=False):
users = [] users = []
if extern: if extern:
ext = ExternLink.objects.create( ext = ExternLink.objects.create(
event=event, event=event, uuid=uuid.uuid4(), maximum=10, name=uuid.uuid4()
uuid=uuid.uuid4(),
maximum=10,
name=uuid.uuid4()
) )
for i in range(nb): for i in range(nb):
if extern: if extern:
ExternInscription.objects.create( ExternInscription.objects.create(
mail='{}@lol.com'.format(uuid.uuid4()), mail="{}@lol.com".format(uuid.uuid4()),
first_name="f", first_name="f",
last_name="l", last_name="l",
event=event, event=event,
via=ext via=ext,
) )
else: else:
u = User.objects.create_user(str(uuid.uuid4())[:30], 'AAA{}@exemple.com'.format(i), 'AAA') u = User.objects.create_user(
str(uuid.uuid4())[:30], "AAA{}@exemple.com".format(i), "AAA"
)
Inscription.objects.create(user=u, event=event) Inscription.objects.create(user=u, event=event)
return users return users
...@@ -62,10 +71,12 @@ class TestEvent(TestCase): ...@@ -62,10 +71,12 @@ class TestEvent(TestCase):
self.assertTrue(self.event.closed()) self.assertTrue(self.event.closed())
def test_photo_url(self): def test_photo_url(self):
self.assertEqual(self.event.photo_url(), static('images/default_event_icon.png')) self.assertEqual(
self.event.photo_url(), static("images/default_event_icon.png")
)
self.event.photo = "coucou.png" self.event.photo = "coucou.png"
self.event.save() self.event.save()
self.assertEqual(self.event.photo_url(), '/medias/coucou.png') self.assertEqual(self.event.photo_url(), "/medias/coucou.png")
def test_can_subscribe(self): def test_can_subscribe(self):
self.assertTrue(self.event.can_subscribe()) self.assertTrue(self.event.can_subscribe())
...@@ -87,68 +98,127 @@ class TestEvent(TestCase): ...@@ -87,68 +98,127 @@ class TestEvent(TestCase):
def test_external_link(self): def test_external_link(self):
ext = ExternLink.objects.create( ext = ExternLink.objects.create(
event=self.event, event=self.event, uuid=uuid.uuid4(), maximum=10, name=uuid.uuid4()
uuid=uuid.uuid4(),
maximum=10,
name=uuid.uuid4()
) )
for i in range(5): for i in range(5):
ExternInscription.objects.create( ExternInscription.objects.create(
mail='{}@lol.com'.format(uuid.uuid4()), mail="{}@lol.com".format(uuid.uuid4()),
first_name="f", first_name="f",
last_name="l", last_name="l",
event=self.event, event=self.event,
via=ext via=ext,
) )
self.assertTrue(ext.places_left()) self.assertTrue(ext.places_left())
for i in range(5): for i in range(5):
ExternInscription.objects.create( ExternInscription.objects.create(
mail='{}@lol.com'.format(uuid.uuid4()), mail="{}@lol.com".format(uuid.uuid4()),
first_name="f", first_name="f",
last_name="l", last_name="l",
event=self.event, event=self.event,
via=ext via=ext,
) )
self.assertFalse(ext.places_left()) self.assertFalse(ext.places_left())
@freeze_time("2014-07-14 11:00:00") @freeze_time("2014-07-14 11:00:00")
def test_to_come(self): def test_to_come(self):
# Event open # Event open
e1 = Event.objects.create(name="test", end_inscriptions=datetime.datetime(2014, 7, 14, 12, 0, 0, tzinfo=timezone.utc), start_time=datetime.datetime(2014, 7, 14, 13, 0, 0, tzinfo=timezone.utc), end_time=datetime.datetime(2014, 7, 14, 14, 0, 0, tzinfo=timezone.utc), location="location", description="description", price=0, photo="", private=False, limited=False, max_inscriptions=0, allow_extern=False) e1 = Event.objects.create(
name="test",
end_inscriptions=datetime.datetime(
2014, 7, 14, 12, 0, 0, tzinfo=timezone.utc
),
start_time=datetime.datetime(2014, 7, 14, 13, 0, 0, tzinfo=timezone.utc),
end_time=datetime.datetime(2014, 7, 14, 14, 0, 0, tzinfo=timezone.utc),
location="location",
description="description",
price=0,
photo="",
private=False,
limited=False,
max_inscriptions=0,
allow_extern=False,
)
# Private event # Private event
e2 = Event.objects.create(name="test2", end_inscriptions=datetime.datetime(2014, 7, 14, 12, 0, 0, tzinfo=timezone.utc), start_time=datetime.datetime(2014, 7, 14, 13, 0, 0, tzinfo=timezone.utc), end_time=datetime.datetime(2014, 7, 14, 14, 0, 0, tzinfo=timezone.utc), location="location", description="description", price=0, photo="", private=True, limited=False, max_inscriptions=0, allow_extern=False) e2 = Event.objects.create(
name="test2",
end_inscriptions=datetime.datetime(
2014, 7, 14, 12, 0, 0, tzinfo=timezone.utc
),
start_time=datetime.datetime(2014, 7, 14, 13, 0, 0, tzinfo=timezone.utc),
end_time=datetime.datetime(2014, 7, 14, 14, 0, 0, tzinfo=timezone.utc),
location="location",
description="description",
price=0,
photo="",
private=True,
limited=False,
max_inscriptions=0,
allow_extern=False,
)
# Closed private event # Closed private event
Event.objects.create(name="test3", end_inscriptions=datetime.datetime(2014, 7, 13, 12, 0, 0, tzinfo=timezone.utc), start_time=datetime.datetime(2014, 7, 13, 13, 0, 0, tzinfo=timezone.utc), end_time=datetime.datetime(2014, 7, 13, 14, 0, 0, tzinfo=timezone.utc), location="location", description="description", price=0, photo="", private=True, limited=False, max_inscriptions=0, allow_extern=False) Event.objects.create(
name="test3",
end_inscriptions=datetime.datetime(
2014, 7, 13, 12, 0, 0, tzinfo=timezone.utc
),
start_time=datetime.datetime(2014, 7, 13, 13, 0, 0, tzinfo=timezone.utc),
end_time=datetime.datetime(2014, 7, 13, 14, 0, 0, tzinfo=timezone.utc),
location="location",
description="description",
price=0,
photo="",
private=True,
limited=False,
max_inscriptions=0,
allow_extern=False,
)
# Closed event # Closed event
Event.objects.create(name="test4", end_inscriptions=datetime.datetime(2014, 7, 13, 12, 0, 0, tzinfo=timezone.utc), start_time=datetime.datetime(2014, 7, 13, 13, 0, 0, tzinfo=timezone.utc), end_time=datetime.datetime(2014, 7, 13, 14, 0, 0, tzinfo=timezone.utc), location="location", description="description", price=0, photo="", private=False, limited=False, max_inscriptions=0, allow_extern=False) Event.objects.create(
name="test4",
end_inscriptions=datetime.datetime(
2014, 7, 13, 12, 0, 0, tzinfo=timezone.utc
),
start_time=datetime.datetime(2014, 7, 13, 13, 0, 0, tzinfo=timezone.utc),
end_time=datetime.datetime(2014, 7, 13, 14, 0, 0, tzinfo=timezone.utc),
location="location",
description="description",
price=0,
photo="",
private=False,
limited=False,
max_inscriptions=0,
allow_extern=False,
)
u = User.objects.create_user(str(uuid.uuid4())[:30], 'BBB@exemple.com', 'AAA') u = User.objects.create_user(str(uuid.uuid4())[:30], "BBB@exemple.com", "AAA")
Inscription.objects.create(user=u, event=e2) Inscription.objects.create(user=u, event=e2)
u2 = User.objects.create_user(str(uuid.uuid4())[:30], 'CCC@exemple.com', 'AAA') u2 = User.objects.create_user(str(uuid.uuid4())[:30], "CCC@exemple.com", "AAA")
self.assertEqual(Event.to_come(u), [(0, self.event), (0, e1), (1, e2)]) self.assertEqual(Event.to_come(u), [(0, self.event), (0, e1), (1, e2)])
self.assertEqual(Event.to_come(u2), [(0, self.event), (0, e1)]) self.assertEqual(Event.to_come(u2), [(0, self.event), (0, e1)])
def test_invitations(self): def test_invitations(self):
u = User.objects.create_user(str(uuid.uuid4())[:30], 'BBB@exemple.com', 'AAA') u = User.objects.create_user(str(uuid.uuid4())[:30], "BBB@exemple.com", "AAA")
self.assertFalse(self.event.can_invite(u)) self.assertFalse(self.event.can_invite(u))
self.event.allow_invitations = True self.event.allow_invitations = True
self.event.save() self.event.save()
self.assertFalse(self.event.can_invite(u)) self.assertFalse(self.event.can_invite(u))
Contributor.take_full_contribution(u, 'cash') Contributor.take_full_contribution(u, "cash")
self.assertTrue(self.event.can_invite(u)) self.assertTrue(self.event.can_invite(u))
self.event.max_invitations = 1 self.event.max_invitations = 1
self.event.save() self.event.save()
self.assertTrue(self.event.can_invite(u)) self.assertTrue(self.event.can_invite(u))
Invitation.objects.create(mail="coucou@lol.com", Invitation.objects.create(
first_name="f", mail="coucou@lol.com",
last_name="l", first_name="f",
event=self.event, last_name="l",
user=u) event=self.event,
user=u,
)
self.assertFalse(self.event.can_invite(u)) self.assertFalse(self.event.can_invite(u))
self.event.max_invitations = 2 self.event.max_invitations = 2
...@@ -161,25 +231,54 @@ class TestEvent(TestCase): ...@@ -161,25 +231,54 @@ class TestEvent(TestCase):
self.event.save() self.event.save()
self.assertFalse(self.event.can_invite(u)) self.assertFalse(self.event.can_invite(u))
def test_recurrent_events_creation(self):
from pytz import timezone
localtz = timezone('Europe/Paris')
self.event.model = True class TestRecurrentEvent(TestCase):
self.event.save() @freeze_time("2021-11-13 18:00:00")
re = RecurrentEvent(**{key: value for key, value in self.event.__dict__.items() if key not in ('_state', )}) def test_recurrent_events_dst_consistency(self):
re.save() """Test that scheduled recurrent events are at the same local time, even with DST changes
First post-DST generated event is on 2021-11-05
"""
localtz = ZoneInfo("Europe/Paris")
re = RecurrentEvent.objects.create(
name="test",
start_time=datetime.datetime(2021, 10, 8, 11, 45, tzinfo=localtz),
end_time=datetime.datetime(2021, 10, 8, 14, 20, tzinfo=localtz),
end_inscriptions=datetime.datetime(2021, 10, 8, 11, 15, tzinfo=localtz),
location="location",
description="description",
delay=7,
model=True
)
re.refresh_from_db() # get UTC datetimes, as provided by default by the db
out = StringIO() out = StringIO()
call_command('create_recurrent_events', stdout=out) call_command("create_recurrent_events", stdout=out)
last = None local_start_time = re.start_time.astimezone(localtz)
for event in Event.objects.filter(model=False): local_end_time = re.end_time.astimezone(localtz)
t = localtz.localize(event.start_time.replace(tzinfo=None)) local_end_inscriptions = re.end_inscriptions.astimezone(localtz)
if last is not None:
try: events = list(Event.objects.filter(model=False))
self.assertEqual(t.time(), last.time()) for event in events:
except AssertionError: with self.subTest(event_start=event.start_time):
print(t, last) self.assertEqual(
raise local_start_time.hour,
last = t event.start_time.astimezone(localtz).hour,
"start_time",
)
self.assertEqual(
local_end_time.hour,
event.end_time.astimezone(localtz).hour,
"end_time",
)
self.assertEqual(
local_end_inscriptions.hour,
event.end_inscriptions.astimezone(localtz).hour,
"end_inscriptions",
)
for prev, curr in zip(events, events[1:]):
delta = curr.start_time - prev.start_time
self.assertEqual(delta.days, re.delay)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment