From 4b418315e44201d6e4dad39b6f71cdd7be8c9b9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 8 Oct 2021 15:48:14 +0200
Subject: [PATCH] agendas: event publication_datetime (#56634)

---
 .../migrations/0101_publication_datetime.py   | 16 +++++
 .../migrations/0102_publication_datetime.py   | 31 ++++++++++
 .../migrations/0103_publication_datetime.py   | 15 +++++
 chrono/agendas/models.py                      | 24 ++++----
 chrono/api/serializers.py                     |  2 +-
 chrono/api/views.py                           |  2 +-
 chrono/manager/forms.py                       | 22 +++++--
 .../chrono/manager_agenda_event_fragment.html |  4 +-
 .../chrono/manager_event_detail_fragment.html |  2 +-
 .../chrono/manager_sample_events.txt          |  4 +-
 chrono/manager/views.py                       |  7 ++-
 tests/api/test_all.py                         |  4 +-
 tests/api/test_datetimes.py                   | 12 ++--
 tests/api/test_event.py                       | 12 ++--
 tests/manager/test_all.py                     |  8 +--
 tests/manager/test_event.py                   | 59 +++++++++++++------
 tests/test_agendas.py                         |  4 +-
 tests/test_import_export.py                   |  5 +-
 18 files changed, 169 insertions(+), 64 deletions(-)
 create mode 100644 chrono/agendas/migrations/0101_publication_datetime.py
 create mode 100644 chrono/agendas/migrations/0102_publication_datetime.py
 create mode 100644 chrono/agendas/migrations/0103_publication_datetime.py

diff --git a/chrono/agendas/migrations/0101_publication_datetime.py b/chrono/agendas/migrations/0101_publication_datetime.py
new file mode 100644
index 0000000..5c23e46
--- /dev/null
+++ b/chrono/agendas/migrations/0101_publication_datetime.py
@@ -0,0 +1,16 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0100_event_dml'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='event',
+            name='publication_datetime',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='Publication date/time'),
+        ),
+    ]
diff --git a/chrono/agendas/migrations/0102_publication_datetime.py b/chrono/agendas/migrations/0102_publication_datetime.py
new file mode 100644
index 0000000..db03d45
--- /dev/null
+++ b/chrono/agendas/migrations/0102_publication_datetime.py
@@ -0,0 +1,31 @@
+import datetime
+
+from django.db import migrations
+from django.utils.timezone import localtime, make_aware
+
+
+def forwards(apps, schema_editor):
+    Event = apps.get_model('agendas', 'Event')
+    for event in Event.objects.filter(publication_date__isnull=False):
+        event.publication_datetime = make_aware(
+            datetime.datetime.combine(event.publication_date, datetime.time(0, 0))
+        )
+        event.save()
+
+
+def backwards(apps, schema_editor):
+    Event = apps.get_model('agendas', 'Event')
+    for event in Event.objects.filter(publication_datetime__isnull=False):
+        event.publication_date = localtime(event.publication_datetime).date()
+        event.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0101_publication_datetime'),
+    ]
+
+    operations = [
+        migrations.RunPython(forwards, reverse_code=backwards),
+    ]
diff --git a/chrono/agendas/migrations/0103_publication_datetime.py b/chrono/agendas/migrations/0103_publication_datetime.py
new file mode 100644
index 0000000..d5a3915
--- /dev/null
+++ b/chrono/agendas/migrations/0103_publication_datetime.py
@@ -0,0 +1,15 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0102_publication_datetime'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='event',
+            name='publication_date',
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index f0e97f3..df54127 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -133,7 +133,7 @@ def django_template_validator(value):
 def event_template_validator(value):
     example_event = Event(
         start_datetime=now(),
-        publication_date=now().date(),
+        publication_datetime=now(),
         recurrence_end_date=now().date(),
         places=1,
         duration=1,
@@ -666,7 +666,7 @@ class Agenda(models.Model):
             entries = entries.filter(start_datetime__gte=localtime(now()))
             # exclude non published events
             entries = entries.filter(
-                Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date())
+                Q(publication_datetime__isnull=True) | Q(publication_datetime__lte=now())
             )
             if not include_full:
                 entries = entries.filter(Q(full=False) | Q(primary_event__isnull=False))
@@ -745,7 +745,7 @@ class Agenda(models.Model):
 
     def get_open_recurring_events(self):
         return self.event_set.filter(
-            Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
+            Q(publication_datetime__isnull=True) | Q(publication_datetime__lte=now()),
             recurrence_days__isnull=False,
             recurrence_end_date__gt=localtime(now()).date(),
         )
@@ -773,7 +773,7 @@ class Agenda(models.Model):
             exceptions = self.prefetched_exceptions
         else:
             recurring_events = self.event_set.filter(
-                Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
+                Q(publication_datetime__isnull=True) | Q(publication_datetime__lte=now()),
                 recurrence_days__isnull=False,
             )
             exceptions = self.get_recurrence_exceptions(min_start, max_start)
@@ -884,7 +884,7 @@ class Agenda(models.Model):
         qs, user_external_id=None, show_past_events=False, min_start=None, max_start=None
     ):
         event_queryset = Event.objects.filter(
-            Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
+            Q(publication_datetime__isnull=True) | Q(publication_datetime__lte=now()),
             recurrence_days__isnull=True,
             cancelled=False,
         ).order_by()
@@ -899,7 +899,7 @@ class Agenda(models.Model):
             event_queryset = event_queryset.filter(start_datetime__lt=max_start)
 
         recurring_event_queryset = Event.objects.filter(
-            Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
+            Q(publication_datetime__isnull=True) | Q(publication_datetime__lte=now()),
             recurrence_days__isnull=False,
         )
         exceptions_desk = Desk.objects.filter(slug='_exceptions_holder').prefetch_related(
@@ -1355,7 +1355,7 @@ class Event(models.Model):
     recurrence_end_date = models.DateField(_('Recurrence end date'), null=True, blank=True)
     primary_event = models.ForeignKey('self', null=True, on_delete=models.CASCADE, related_name='recurrences')
     duration = models.PositiveIntegerField(_('Duration (in minutes)'), default=None, null=True, blank=True)
-    publication_date = models.DateField(_('Publication date'), blank=True, null=True)
+    publication_datetime = models.DateTimeField(_('Publication date/time'), blank=True, null=True)
     places = models.PositiveIntegerField(_('Places'))
     waiting_list_places = models.PositiveIntegerField(_('Places in waiting list'), default=0)
     label = models.CharField(
@@ -1451,7 +1451,7 @@ class Event(models.Model):
         self.save(update_fields=['checked'])
 
     def in_bookable_period(self):
-        if self.publication_date and localtime(now()).date() < self.publication_date:
+        if self.publication_datetime and now() < self.publication_datetime:
             return False
         if self.agenda.maximal_booking_delay and self.start_datetime > self.agenda.max_booking_datetime:
             return False
@@ -1545,7 +1545,7 @@ class Event(models.Model):
                 for field in [
                     'label',
                     'duration',
-                    'publication_date',
+                    'publication_datetime',
                     'places',
                     'waiting_list_places',
                     'description',
@@ -1565,7 +1565,9 @@ class Event(models.Model):
         )
         return {
             'start_datetime': make_naive(self.start_datetime).strftime('%Y-%m-%d %H:%M:%S'),
-            'publication_date': self.publication_date.strftime('%Y-%m-%d') if self.publication_date else None,
+            'publication_datetime': make_naive(self.publication_datetime).strftime('%Y-%m-%d %H:%M:%S')
+            if self.publication_datetime
+            else None,
             'recurrence_days': self.recurrence_days,
             'recurrence_week_interval': self.recurrence_week_interval,
             'recurrence_end_date': recurrence_end_date,
@@ -1659,7 +1661,7 @@ class Event(models.Model):
             duration=self.duration,
             places=self.places,
             waiting_list_places=self.waiting_list_places,
-            publication_date=self.publication_date,
+            publication_datetime=self.publication_datetime,
             label=self.label,
             description=self.description,
             pricing=self.pricing,
diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py
index 85de836..5c8ff51 100644
--- a/chrono/api/serializers.py
+++ b/chrono/api/serializers.py
@@ -150,7 +150,7 @@ class EventSerializer(serializers.ModelSerializer):
             'recurrence_week_interval',
             'recurrence_end_date',
             'duration',
-            'publication_date',
+            'publication_datetime',
             'places',
             'waiting_list_places',
             'label',
diff --git a/chrono/api/views.py b/chrono/api/views.py
index e33a54a..02f8b5d 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -2259,7 +2259,7 @@ class Events(APIView):
             for field in changed_data:
                 if field in (
                     'recurrence_end_date',
-                    'publication_date',
+                    'publication_datetime',
                     'recurrence_days',
                     'recurrence_week_interval',
                 ):
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index abdbb98..ae7b04e 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -216,7 +216,6 @@ class EventForm(NewEventForm):
     class Meta:
         model = Event
         widgets = {
-            'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
             'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
         }
         fields = [
@@ -228,7 +227,7 @@ class EventForm(NewEventForm):
             'recurrence_week_interval',
             'recurrence_end_date',
             'duration',
-            'publication_date',
+            'publication_datetime',
             'places',
             'waiting_list_places',
             'description',
@@ -237,6 +236,7 @@ class EventForm(NewEventForm):
         ]
         field_classes = {
             'start_datetime': SplitDateTimeField,
+            'publication_datetime': SplitDateTimeField,
         }
 
     def __init__(self, *args, **kwargs):
@@ -252,7 +252,7 @@ class EventForm(NewEventForm):
             for field in (
                 'slug',
                 'recurrence_end_date',
-                'publication_date',
+                'publication_datetime',
                 'frequency',
                 'recurrence_days',
                 'recurrence_week_interval',
@@ -706,14 +706,24 @@ class ImportEventsForm(forms.Form):
                 column_index += 1
 
             if len(csvline) >= 10 and csvline[9]:  # publication date is optional
-                for date_fmt in ('%Y-%m-%d', '%d/%m/%Y'):
+                for datetime_fmt in (
+                    '%Y-%m-%d',
+                    '%d/%m/%Y',
+                    '%Y-%m-%d %H:%M',
+                    '%d/%m/%Y %H:%M',
+                    '%d/%m/%Y %Hh%M',
+                    '%Y-%m-%d %H:%M:%S',
+                    '%d/%m/%Y %H:%M:%S',
+                ):
                     try:
-                        event.publication_date = datetime.datetime.strptime(csvline[9], date_fmt).date()
+                        event.publication_datetime = make_aware(
+                            datetime.datetime.strptime(csvline[9], datetime_fmt)
+                        )
                         break
                     except ValueError:
                         continue
                 else:
-                    raise ValidationError(_('Invalid file format. (date format, line %d)') % (i + 1))
+                    raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i + 1))
 
             if len(csvline) >= 11 and csvline[10]:  # duration is optional
                 try:
diff --git a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
index f832427..5b65db9 100644
--- a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
+++ b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
@@ -43,8 +43,8 @@
       {% blocktrans with places=event.waiting_list_places count booked_places=event.booked_waiting_list_places %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
     {% endif %}
     {% endif %}
-    {% if view_mode == 'settings_view' and event.publication_date %}
-    ({% trans "publication date:" %} {{ event.publication_date }})
+    {% if view_mode == 'settings_view' and event.publication_datetime %}
+    ({% trans "publication date:" %} {{ event.publication_datetime }})
     {% endif %}
     {% if not event.in_bookable_period %}
     ({% trans "out of bookable period" %})
diff --git a/chrono/manager/templates/chrono/manager_event_detail_fragment.html b/chrono/manager/templates/chrono/manager_event_detail_fragment.html
index 82fc792..5803b2c 100644
--- a/chrono/manager/templates/chrono/manager_event_detail_fragment.html
+++ b/chrono/manager/templates/chrono/manager_event_detail_fragment.html
@@ -5,7 +5,7 @@
  {% if object.description %}{{ object.description|linebreaks }}{% endif %}
  {% if object.pricing %}<p>{% trans "Pricing:" %} {{ object.pricing }}</p>{% endif %}
  {% if object.url %}<p><a href="{{ object.url }}">{{ object.url|truncatechars:100 }}</a></p>{% endif %}
- {% if object.publication_date %}<p>{% trans "Publication date:" %} {{ object.publication_date }}</p>{% endif %}
+ {% if object.publication_datetime %}<p>{% trans "Publication date:" %} {{ object.publication_datetime }}</p>{% endif %}
 </div>
 </div>
 {% endif %}
diff --git a/chrono/manager/templates/chrono/manager_sample_events.txt b/chrono/manager/templates/chrono/manager_sample_events.txt
index 180fe26..373e594 100644
--- a/chrono/manager/templates/chrono/manager_sample_events.txt
+++ b/chrono/manager/templates/chrono/manager_sample_events.txt
@@ -1,2 +1,2 @@
-{% load i18n %}{% trans 'date' %},{% trans 'time' %},{% trans 'number of places' %},{% trans 'number of places in waiting list' %},{% trans 'label' %},{% trans 'identifier' %},{% trans 'description' %},{% trans 'pricing' %},{% trans 'URL' %},{% trans 'publication date' %},{% trans 'duration' %}
-{{ some_future_date|date:"Y-m-d" }},{{ some_future_date|date:"H:i" }},15,0,{% trans "example event" as label %}{{ label }},{{ label|slugify }},,,https://www.example.net,{{ some_future_date|date:"Y-m-d" }},30
+{% load i18n %}{% trans 'date' %},{% trans 'time' %},{% trans 'number of places' %},{% trans 'number of places in waiting list' %},{% trans 'label' %},{% trans 'identifier' %},{% trans 'description' %},{% trans 'pricing' %},{% trans 'URL' %},{% trans 'publication date/time' %},{% trans 'duration' %}
+{{ some_future_date|date:"Y-m-d" }},{{ some_future_date|date:"H:i" }},15,0,{% trans "example event" as label %}{{ label }},{{ label|slugify }},,,https://www.example.net,{{ some_future_date|date:"Y-m-d H:i" }},30
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index ad0eae4..defafe1 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -1804,12 +1804,15 @@ class AgendaExportEventsView(ManagedAgendaMixin, View):
                 _('description'),
                 _('pricing'),
                 _('URL'),
-                _('publication date'),
+                _('publication date/time'),
                 _('duration'),
             ]
         )
         for event in self.agenda.event_set.all():
             start_datetime = localtime(event.start_datetime)
+            publication_datetime = (
+                localtime(event.publication_datetime) if event.publication_datetime else None
+            )
             writer.writerow(
                 [
                     start_datetime.strftime('%Y-%m-%d'),
@@ -1821,7 +1824,7 @@ class AgendaExportEventsView(ManagedAgendaMixin, View):
                     event.description,
                     event.pricing,
                     event.url,
-                    event.publication_date.strftime('%Y-%m-%d') if event.publication_date else '',
+                    publication_datetime.strftime('%Y-%m-%d %H:%M') if publication_datetime else '',
                     event.duration,
                 ]
             )
diff --git a/tests/api/test_all.py b/tests/api/test_all.py
index dd24e7f..8965292 100644
--- a/tests/api/test_all.py
+++ b/tests/api/test_all.py
@@ -255,7 +255,7 @@ def test_agendas_api(app):
     event2.booking_set.all().delete()
     event2.refresh_from_db()
     assert event2.full is False
-    event_agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=20))
+    event_agenda.event_set.update(publication_datetime=now() + datetime.timedelta(days=20))
     assert list(event_agenda.get_open_events()) == []
     resp = app.get('/api/agenda/', params={'with_open_events': '1'})
     assert len(resp.json['data']) == 0
@@ -470,7 +470,7 @@ def test_agenda_detail_api(app):
     event2.booking_set.all().delete()
     event2.refresh_from_db()
     assert event2.full is False
-    agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=20))
+    agenda.event_set.update(publication_datetime=now() + datetime.timedelta(days=20))
     resp = app.get('/api/agenda/%s/' % agenda.slug)
     assert list(agenda.get_open_events()) == []
     assert resp.json['data']['opened_events_available'] is False
diff --git a/tests/api/test_datetimes.py b/tests/api/test_datetimes.py
index 6d38386..9397d72 100644
--- a/tests/api/test_datetimes.py
+++ b/tests/api/test_datetimes.py
@@ -50,12 +50,12 @@ def test_datetimes_api(app, some_data):
     check_bookability(resp.json['data'])
     assert resp.json['data'][0]['description'] is None
 
-    agenda.event_set.update(publication_date=localtime(now()).date() + datetime.timedelta(days=1))
+    agenda.event_set.update(publication_datetime=now() + datetime.timedelta(minutes=1))
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
     assert len(resp.json['data']) == 0
     check_bookability(resp.json['data'])
 
-    agenda.event_set.update(publication_date=localtime(now()).date())
+    agenda.event_set.update(publication_datetime=now())
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
     assert len(resp.json['data']) == 2
     check_bookability(resp.json['data'])
@@ -662,13 +662,13 @@ def test_recurring_events_api(app, user, freezer):
 
     # publication date is accounted for
     Event.objects.filter(primary_event=base_event).delete()
-    base_event.publication_date = now().replace(day=27)
+    base_event.publication_datetime = now().replace(day=27)
     base_event.save()
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
     assert len(resp.json['data']) == 0
 
     # events follow agenda display template
-    Event.objects.all().update(publication_date=None)
+    Event.objects.all().update(publication_datetime=None)
     agenda.event_display_template = '{{ event.label }} - {{ event.start_datetime }}'
     agenda.save()
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
@@ -677,7 +677,7 @@ def test_recurring_events_api(app, user, freezer):
     # check querysets
     agenda.event_display_template = ''
     agenda.save()
-    base_event.publication_date = None
+    base_event.publication_datetime = None
     base_event.recurrence_end_date = datetime.date.today() + datetime.timedelta(days=365)
     base_event.save()
     base_event.create_all_recurrences()
@@ -1356,7 +1356,7 @@ def test_recurring_events_api_list(app, freezer):
     assert resp.json['data'][3]['id'] == 'other:1'
     assert resp.json['data'][3]['text'] == 'Tuesday: Other'
 
-    event.publication_date = now() + datetime.timedelta(days=2)
+    event.publication_datetime = now() + datetime.timedelta(days=2)
     event.save()
     resp = app.get('/api/agenda/%s/recurring-events/' % agenda.slug)
     assert len(resp.json['data']) == 1
diff --git a/tests/api/test_event.py b/tests/api/test_event.py
index e0456c0..c552481 100644
--- a/tests/api/test_event.py
+++ b/tests/api/test_event.py
@@ -224,13 +224,13 @@ def test_add_event(app, user):
     assert str(event.start_datetime) == '2021-11-15 14:38:00+00:00'
     assert str(event.start_datetime.tzinfo) == 'UTC'
     assert event.places == 10
-    assert event.publication_date is None
+    assert event.publication_datetime is None
 
     # add with almost all optional managed fields
     params = {
         'start_datetime': '2021-11-15 15:38',
         'duration': 42,
-        'publication_date': '2021-09-20',
+        'publication_datetime': '2021-09-20 10:00',
         'places': 11,
         'waiting_list_places': 3,
         'label': 'FOO camp',
@@ -252,6 +252,8 @@ def test_add_event(app, user):
     assert event.description == 'An event'
     assert event.pricing == 'free'
     assert event.url == 'http://example.org/foo/bar/?'
+    assert str(event.publication_datetime) == '2021-09-20 08:00:00+00:00'
+    assert str(event.publication_datetime.tzinfo) == 'UTC'
 
     # add with errors in recurrence_days list
     params = {
@@ -389,7 +391,7 @@ def test_update_event(app, user):
     params = {
         'start_datetime': '2021-11-15 15:38',
         'duration': 42,
-        'publication_date': '2021-09-20',
+        'publication_datetime': '2021-09-20 12:00',
         'places': 8,
         'waiting_list_places': 3,
         'label': 'FOO camp',
@@ -407,6 +409,8 @@ def test_update_event(app, user):
     assert event.description == 'An event'
     assert event.pricing == 'free'
     assert event.url == 'http://example.org/foo/bar/?'
+    assert str(event.publication_datetime) == '2021-09-20 10:00:00+00:00'
+    assert str(event.publication_datetime.tzinfo) == 'UTC'
 
     # update event as a recurring event
     params = {
@@ -448,7 +452,7 @@ def test_update_event(app, user):
 
     # try to update protected fields of one of the event recurrencies
     params = {
-        'publication_date': '2021-11-15',
+        'publication_datetime': '2021-11-15 12:00',
     }
     resp = app.patch(recurrence_url, params=params, status=400)
     assert resp.json['err']
diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py
index 3fd52cc..216acc9 100644
--- a/tests/manager/test_all.py
+++ b/tests/manager/test_all.py
@@ -1553,7 +1553,7 @@ def test_agenda_open_events_view(app, admin_user, manager_user):
         agenda=agenda,
         label='event E',
         start_datetime=now() + datetime.timedelta(days=3),
-        publication_date=today + datetime.timedelta(days=1),
+        publication_datetime=now() + datetime.timedelta(days=1),
         places=42,
     )
     # publication date in past
@@ -1561,7 +1561,7 @@ def test_agenda_open_events_view(app, admin_user, manager_user):
         agenda=agenda,
         label='event F',
         start_datetime=now() + datetime.timedelta(days=3),
-        publication_date=today - datetime.timedelta(days=1),
+        publication_datetime=now() - datetime.timedelta(days=1),
         places=42,
     )
     # weekly recurring event, first recurrence is in the past but second is in range
@@ -1591,7 +1591,7 @@ def test_agenda_open_events_view(app, admin_user, manager_user):
         agenda=agenda,
         label='event H',
         start_datetime=now().replace(year=today.year + 2, month=1, day=15),
-        publication_date=today - datetime.timedelta(days=1),
+        publication_datetime=now() - datetime.timedelta(days=1),
         places=42,
     )
     start_datetime = localtime(now().replace(year=today.year + 2, month=2, day=1)).replace(hour=0, minute=0)
@@ -1599,7 +1599,7 @@ def test_agenda_open_events_view(app, admin_user, manager_user):
         agenda=agenda,
         label='event H',
         start_datetime=start_datetime,
-        publication_date=today - datetime.timedelta(days=1),
+        publication_datetime=now() - datetime.timedelta(days=1),
         places=42,
     )
     resp = app.get('/manage/agendas/%s/events/open/' % agenda.pk)
diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index b2274c3..ffa8afc 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -33,7 +33,7 @@ def test_add_event(app, admin_user):
     resp = resp.form.submit()
     resp = resp.follow()
     event = Event.objects.get(places=10)
-    assert event.publication_date is None
+    assert event.publication_datetime is None
     assert "This agenda doesn't have any event yet." not in resp.text
     assert '/manage/agendas/%s/events/%s/' % (agenda.id, event.id) in resp.text
     assert ('Feb. 15, %s, 5 p.m.' % year) in resp.text
@@ -137,12 +137,14 @@ def test_edit_event(settings, app, admin_user):
     resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk))
     assert resp.form['start_datetime_0'].value == '2016-02-15'
     assert resp.form['start_datetime_1'].value == '17:00'
-    assert resp.form['publication_date'].value == ''
+    assert resp.form['publication_datetime_0'].value == ''
+    assert resp.form['publication_datetime_1'].value == ''
     assert resp.form['duration'].value == ''
     assert resp.form['description'].value == ''
     resp.form['start_datetime_0'] = '2016-02-16'
     resp.form['start_datetime_1'] = '17:00'
-    resp.form['publication_date'] = '2020-05-11'
+    resp.form['publication_datetime_0'] = '2020-05-11'
+    resp.form['publication_datetime_1'] = '12:00'
     resp.form['duration'].value = 45
     resp.form['places'] = 20
     resp.form['description'] = 'A description'
@@ -153,7 +155,8 @@ def test_edit_event(settings, app, admin_user):
     assert 'Feb. 16, 2016, 5 p.m.' in resp.text
     event.refresh_from_db()
     assert event.places == 20
-    assert event.publication_date == datetime.date(2020, 5, 11)
+    assert str(event.publication_datetime) == '2020-05-11 10:00:00+00:00'
+    assert str(event.publication_datetime.tzinfo) == 'UTC'
     assert event.duration == 45
     assert event.end_datetime == event.start_datetime + datetime.timedelta(minutes=45)
     assert event.description == 'A description'
@@ -193,7 +196,7 @@ def test_edit_event_as_manager(app, manager_user):
         start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
         places=20,
         agenda=agenda,
-        publication_date=datetime.date(2020, 5, 11),
+        publication_datetime=make_aware(datetime.datetime(2020, 5, 11)),
     )
     app = login(app, username='manager', password='manager')
     resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id), status=403)
@@ -204,17 +207,19 @@ def test_edit_event_as_manager(app, manager_user):
     resp = resp.click('Feb. 15, 2016, 5 p.m.')
     assert resp.form['start_datetime_0'].value == '2016-02-15'
     assert resp.form['start_datetime_1'].value == '17:00'
-    assert resp.form['publication_date'].value == '2020-05-11'
+    assert resp.form['publication_datetime_0'].value == '2020-05-11'
+    assert resp.form['publication_datetime_1'].value == '00:00'
     resp.form['start_datetime_0'] = '2016-02-16'
     resp.form['start_datetime_1'] = '17:00'
-    resp.form['publication_date'] = ''
+    resp.form['publication_datetime_0'] = ''
+    resp.form['publication_datetime_1'] = ''
     resp.form['places'] = 20
     resp = resp.form.submit()
     resp = resp.follow()
     assert '/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id) in resp.text
     assert 'Feb. 16, 2016, 5 p.m.' in resp.text
     event.refresh_from_db()
-    assert event.publication_date is None
+    assert event.publication_datetime is None
 
 
 def test_edit_recurring_event(settings, app, admin_user, freezer):
@@ -301,7 +306,8 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
         'recurrence_days',
         'recurence_weekly_interval',
         'recurrence_end_date',
-        'publication_date',
+        'publication_datetime_0',
+        'publication_datetime_1',
     }.isdisjoint(resp.form.fields)
 
 
@@ -537,7 +543,7 @@ def test_export_events(app, admin_user):
     csv_export = resp.text
     assert (
         csv_export
-        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date,duration\r\n'
+        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n'
     )
 
     resp = app.get('/manage/agendas/%s/import-events' % agenda.id)
@@ -548,14 +554,14 @@ def test_export_events(app, admin_user):
     csv_export = resp.text
     assert (
         csv_export
-        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date,duration\r\n'
+        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n'
         '2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n'
     )
 
     resp = app.get('/manage/agendas/%s/import-events' % agenda.id)
     resp.form['events_csv_file'] = Upload(
         't.csv',
-        b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16,90',
+        b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90',
         'text/csv',
     )
     resp.form.submit(status=302)
@@ -563,9 +569,9 @@ def test_export_events(app, admin_user):
     csv_export = resp.text
     assert (
         csv_export
-        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date,duration\r\n'
+        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n'
         '2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n'
-        '2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16,90\r\n'
+        '2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90\r\n'
     )
 
 
@@ -738,7 +744,7 @@ def test_import_events(app, admin_user):
     assert event.description == ''
     assert event.pricing == ''
     assert event.url == ''
-    assert event.publication_date is None
+    assert event.publication_datetime is None
     assert event.duration is None
     Event.objects.all().delete()
     resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
@@ -753,16 +759,33 @@ def test_import_events(app, admin_user):
     assert event.description == 'description\nfoobar'
     assert event.pricing == 'pricing'
     assert event.url == 'url'
-    assert event.publication_date == datetime.date(2016, 10, 16)
+    assert str(event.publication_datetime) == '2016-10-15 22:00:00+00:00'
+    assert str(event.publication_datetime.tzinfo) == 'UTC'
+    assert event.duration == 90
+    Event.objects.all().delete()
+    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
+    resp.form['events_csv_file'] = Upload(
+        't.csv',
+        b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 10:00,90',
+        'text/csv',
+    )
+    resp = resp.form.submit(status=302)
+    assert Event.objects.count() == 1
+    event = Event.objects.get()
+    assert event.description == 'description\nfoobar'
+    assert event.pricing == 'pricing'
+    assert event.url == 'url'
+    assert str(event.publication_datetime) == '2016-10-16 08:00:00+00:00'
+    assert str(event.publication_datetime.tzinfo) == 'UTC'
     assert event.duration == 90
 
-    # publication date bad format
+    # publication date/time bad format
     resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
     resp.form['events_csv_file'] = Upload(
         't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,foobar', 'text/csv'
     )
     resp = resp.form.submit(status=200)
-    assert 'Invalid file format. (date format' in resp.text
+    assert 'Invalid file format. (date/time format' in resp.text
 
     # duration bad format
     resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index c33603c..0866641 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -446,7 +446,7 @@ def test_event_manager():
     [
         # no delay
         (10, 0, 0, 0, None, True),
-        # test publication_date
+        # test publication_datetime
         (10, 0, 0, 0, 1, False),
         (10, 0, 0, 0, 0, True),
         # test min and max delays
@@ -464,7 +464,7 @@ def test_event_bookable_period(start_days, start_minutes, min_delay, max_delay,
     )
     event = Event.objects.create(
         start_datetime=localtime() + datetime.timedelta(days=start_days, minutes=start_minutes),
-        publication_date=(localtime().date() + datetime.timedelta(days=pub_days)) if pub_days else None,
+        publication_datetime=(localtime() + datetime.timedelta(days=pub_days)) if pub_days else None,
         places=10,
         agenda=agenda,
     )
diff --git a/tests/test_import_export.py b/tests/test_import_export.py
index a6d3d5e..a07ea2d 100644
--- a/tests/test_import_export.py
+++ b/tests/test_import_export.py
@@ -183,7 +183,7 @@ def test_import_export_event_details(app):
         description='description',
         pricing='100',
         url='https://example.net/',
-        publication_date=datetime.date(2020, 5, 11),
+        publication_datetime=make_aware(datetime.datetime(2020, 5, 11)),
         places=42,
         start_datetime=now(),
         duration=30,
@@ -214,7 +214,8 @@ def test_import_export_event_details(app):
     assert first_imported_event.description == 'description'
     assert first_imported_event.pricing == '100'
     assert first_imported_event.url == 'https://example.net/'
-    assert first_imported_event.publication_date == datetime.date(2020, 5, 11)
+    assert str(first_imported_event.publication_datetime) == '2020-05-10 22:00:00+00:00'
+    assert str(first_imported_event.publication_datetime.tzinfo) == 'UTC'
     assert first_imported_event.duration == 30
     assert Agenda.objects.get(label='Foo Bar 2').event_set.first().slug == 'event'
 
-- 
2.33.0

