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 events.models import RecurrentEvent, Event
from django.utils import timezone
import datetime
from events.models import RecurrentEvent
class Command(BaseCommand):
help = 'Create events based on recurrent events.'
def handle(self, *args, **options):
for event in RecurrentEvent.objects.all():
while True:
if event.last_created is None or event.last_created < timezone.now() + datetime.timedelta(days=14):
self.update_event(event)
else:
break
self.stdout.write('Successfully created events')
def update_event(self, event):
delta = datetime.timedelta(days=event.delay)
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']]})
help = "Create events based on recurrent events."
def handle(self, *args, **options) -> None:
"""Create instances for recurrent events for at least the next 14 days"""
for rec_event in RecurrentEvent.objects.all():
while (
rec_event.last_created is None # Première création
or rec_event.last_created < timezone.now() + datetime.timedelta(days=14)
):
# Tant que la dernière instance de l'évenement est
# dans moins de 14 jours
# Créer une nouvelle instance
self.update_rec_event(rec_event)
self.stdout.write("Successfully created events")
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()
event.last_created = e.start_time
event.save()
self.stdout.write("First creation of %s, start = %s (delay=%d)" % (event, event.start_time, event.delay))
rec_event.last_created = e.start_time
rec_event.save()
self.stdout.write(
f"First creation of {rec_event}, "
f"start = {rec_event.start_time} "
f"(delay={rec_event.delay})"
)
else:
delta_dates = event.last_created - 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']]})
delta_dates = rec_event.last_created - rec_event.start_time + delta
e = rec_event.create_instance()
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 = e.end_time.replace(hour=event.end_time.hour)
e.end_inscriptions += delta_dates
e.end_inscriptions = e.end_inscriptions.replace(hour=event.end_inscriptions.hour)
if e.invitations_start is not None:
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()
event.save()
self.stdout.write("Creating event %s for date %s" % (event, event.last_created))
rec_event.last_created = e.start_time
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):
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)
class Meta:
......@@ -134,6 +134,21 @@ class RecurrentEvent(Event):
('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):
event = models.ForeignKey(Event, related_name="extern_links", on_delete=models.CASCADE)
......
import datetime
import uuid
from datetime import timezone
from freezegun import freeze_time
from io import StringIO
import uuid
from zoneinfo import ZoneInfo
from django.contrib.auth.models import User
from django.core.management import call_command
from django.templatetags.static import static
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 .models import Event, Inscription, ExternInscription, ExternLink, Invitation, RecurrentEvent
class TestEvent(TestCase):
def setUp(self):
self.event = Event.objects.create(
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),
end_time=datetime.datetime(2014, 7, 14, 14, 0, 0, tzinfo=timezone.utc),
location="location",
......@@ -25,30 +35,29 @@ class TestEvent(TestCase):
private=False,
limited=False,
max_inscriptions=0,
allow_extern=False
allow_extern=False,
)
def do_inscriptions(self, event, nb, extern=False):
users = []
if extern:
ext = ExternLink.objects.create(
event=event,
uuid=uuid.uuid4(),
maximum=10,
name=uuid.uuid4()
event=event, uuid=uuid.uuid4(), maximum=10, name=uuid.uuid4()
)
for i in range(nb):
if extern:
ExternInscription.objects.create(
mail='{}@lol.com'.format(uuid.uuid4()),
mail="{}@lol.com".format(uuid.uuid4()),
first_name="f",
last_name="l",
event=event,
via=ext
via=ext,
)
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)
return users
......@@ -62,10 +71,12 @@ class TestEvent(TestCase):
self.assertTrue(self.event.closed())
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.save()
self.assertEqual(self.event.photo_url(), '/medias/coucou.png')
self.assertEqual(self.event.photo_url(), "/medias/coucou.png")
def test_can_subscribe(self):
self.assertTrue(self.event.can_subscribe())
......@@ -87,68 +98,127 @@ class TestEvent(TestCase):
def test_external_link(self):
ext = ExternLink.objects.create(
event=self.event,
uuid=uuid.uuid4(),
maximum=10,
name=uuid.uuid4()
event=self.event, uuid=uuid.uuid4(), maximum=10, name=uuid.uuid4()
)
for i in range(5):
ExternInscription.objects.create(
mail='{}@lol.com'.format(uuid.uuid4()),
mail="{}@lol.com".format(uuid.uuid4()),
first_name="f",
last_name="l",
event=self.event,
via=ext
via=ext,
)
self.assertTrue(ext.places_left())
for i in range(5):
ExternInscription.objects.create(
mail='{}@lol.com'.format(uuid.uuid4()),
mail="{}@lol.com".format(uuid.uuid4()),
first_name="f",
last_name="l",
event=self.event,
via=ext
via=ext,
)
self.assertFalse(ext.places_left())
@freeze_time("2014-07-14 11:00:00")
def test_to_come(self):
# 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
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
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
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)
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(u2), [(0, self.event), (0, e1)])
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.event.allow_invitations = True
self.event.save()
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.event.max_invitations = 1
self.event.save()
self.assertTrue(self.event.can_invite(u))
Invitation.objects.create(mail="coucou@lol.com",
first_name="f",
last_name="l",
event=self.event,
user=u)
Invitation.objects.create(
mail="coucou@lol.com",
first_name="f",
last_name="l",
event=self.event,
user=u,
)
self.assertFalse(self.event.can_invite(u))
self.event.max_invitations = 2
......@@ -161,25 +231,54 @@ class TestEvent(TestCase):
self.event.save()
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
self.event.save()
re = RecurrentEvent(**{key: value for key, value in self.event.__dict__.items() if key not in ('_state', )})
re.save()
class TestRecurrentEvent(TestCase):
@freeze_time("2021-11-13 18:00:00")
def test_recurrent_events_dst_consistency(self):
"""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()
call_command('create_recurrent_events', stdout=out)
last = None
for event in Event.objects.filter(model=False):
t = localtz.localize(event.start_time.replace(tzinfo=None))
if last is not None:
try:
self.assertEqual(t.time(), last.time())
except AssertionError:
print(t, last)
raise
last = t
call_command("create_recurrent_events", stdout=out)
local_start_time = re.start_time.astimezone(localtz)
local_end_time = re.end_time.astimezone(localtz)
local_end_inscriptions = re.end_inscriptions.astimezone(localtz)
events = list(Event.objects.filter(model=False))
for event in events:
with self.subTest(event_start=event.start_time):
self.assertEqual(
local_start_time.hour,
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