diff --git a/blogs/tests/test_parser.py b/blogs/tests/test_parser.py index dd144705f..1cacf9d11 100644 --- a/blogs/tests/test_parser.py +++ b/blogs/tests/test_parser.py @@ -11,7 +11,7 @@ class BlogParserTest(unittest.TestCase): def setUpClass(cls): super().setUpClass() cls.test_file_path = get_test_rss_path() - cls.entries = get_all_entries("file://{}".format(cls.test_file_path)) + cls.entries = get_all_entries("file:///{}".format(cls.test_file_path)) def test_entries(self): self.assertEqual(len(self.entries), 25) diff --git a/events/importer.py b/events/importer.py index 4987af8c2..2930b4043 100644 --- a/events/importer.py +++ b/events/importer.py @@ -4,7 +4,7 @@ import requests from .models import EventLocation, Event, OccurringRule -from .utils import convert_dt_to_aware +from .utils import extract_date_or_datetime DATE_RESOLUTION = timedelta(1) TIME_RESOLUTION = timedelta(0, 0, 1) @@ -29,8 +29,8 @@ def import_occurrence(self, event, event_data): # Django will already convert to datetime by setting the time to 0:00, # but won't add any timezone information. We will convert them to # aware datetime objects manually. - dt_start = convert_dt_to_aware(event_data['DTSTART'].dt) - dt_end = convert_dt_to_aware(event_data['DTEND'].dt) + dt_start = extract_date_or_datetime(event_data['DTSTART'].dt) + dt_end = extract_date_or_datetime(event_data['DTEND'].dt) # Let's mark those occurrences as 'all-day'. all_day = ( diff --git a/events/models.py b/events/models.py index 1f315654d..a10add51d 100644 --- a/events/models.py +++ b/events/models.py @@ -253,7 +253,10 @@ def duration(self): @property def single_day(self): - return self.dt_start.date() == self.dt_end.date() + return ( + self.dt_start.date() == self.dt_end.date() or + self.all_day is True and self.dt_start + datetime.timedelta(days=1) == self.dt_end + ) def duration_default(): @@ -319,7 +322,10 @@ def dt_end(self): @property def single_day(self): - return self.dt_start.date() == self.dt_end.date() + return ( + self.dt_start.date() == self.dt_end.date() or + self.all_day is True and self.dt_start + datetime.timedelta(days=1) == self.dt_end + ) def save(self, *args, **kwargs): self.duration_internal = timedelta_parse(self.duration) diff --git a/events/templatetags/events.py b/events/templatetags/events.py index 1c580c6ef..23af52f43 100644 --- a/events/templatetags/events.py +++ b/events/templatetags/events.py @@ -1,5 +1,6 @@ from django import template from django.utils import timezone +from datetime import date, timedelta from ..models import Event @@ -14,3 +15,9 @@ def get_events_upcoming(limit=5, only_featured=False): if only_featured: qs.filter(featured=True) return qs[:limit] + + +@register.filter_function +def exclude_ending_day(next_event_date): + return next_event_date - timedelta(days=1) + diff --git a/events/tests/test_importer.py b/events/tests/test_importer.py index 8df0db327..5243e9c7d 100644 --- a/events/tests/test_importer.py +++ b/events/tests/test_importer.py @@ -1,6 +1,7 @@ import os -import unittest + from django.test import TestCase +from django.utils.timezone import datetime, make_aware from django.conf import settings from events.importer import ICSImporter @@ -20,7 +21,7 @@ def setUpClass(cls): def test_injest(self): importer = ICSImporter(self.calendar) - with open(EVENTS_CALENDAR) as fh: + with open(EVENTS_CALENDAR, encoding='utf-8') as fh: ical = fh.read() importer.import_events_from_text(ical) @@ -61,6 +62,9 @@ def test_modified_event(self): e.description.rendered, 'PythonCamp Cologne 2016' ) + self.assertTrue(e.next_or_previous_time.all_day) + self.assertEqual(e.next_or_previous_time.dt_start, make_aware(datetime(year=2016, month=4, day=2))) + self.assertEqual(e.next_or_previous_time.dt_end, make_aware(datetime(year=2016, month=4, day=4))) ical = """\ BEGIN:VCALENDAR @@ -94,3 +98,49 @@ def test_modified_event(self): self.assertEqual(e.pk, e2.pk) self.assertEqual(e2.calendar.url, EVENTS_CALENDAR_URL) self.assertEqual(e2.description.rendered, 'Python Istanbul') + self.assertTrue(e2.next_or_previous_time.all_day) + self.assertEqual(e2.next_or_previous_time.dt_start, make_aware(datetime(year=2016, month=4, day=2))) + self.assertEqual(e2.next_or_previous_time.dt_end, make_aware(datetime(year=2016, month=4, day=4))) + + ical = """\ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//www.marudot.com//iCal Event Maker +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/London +TZURL:http://tzurl.org/zoneinfo-outlook/Europe/London +X-LIC-LOCATION:Europe/London +BEGIN:DAYLIGHT +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +TZNAME:BST +DTSTART:19700329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +TZNAME:GMT +DTSTART:19701025T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20171005T184245Z +UID:20171005T184245Z-752317732@marudot.com +DTSTART;TZID="Europe/London":20171031T190000 +DTEND;TZID="Europe/London":20171031T210000 +SUMMARY:Python Sheffield +DESCRIPTION:Monthly Python user group in Sheffield\, United Kingdom http://groups.google.com/group/python-sheffield Twitter: @pysheff +LOCATION:GIST Lab in Sheffield (http://thegisthub.net/groups/gistlab/) +END:VEVENT +END:VCALENDAR +""" + importer.import_events_from_text(ical) + + e3 = Event.objects.get(uid='20171005T184245Z-752317732@marudot.com') + self.assertEqual(e3.description.rendered, 'Monthly Python user group in Sheffield, United Kingdom http://groups.google.com/group/python-sheffield Twitter: @pysheff') + self.assertFalse(e3.next_or_previous_time.all_day) + self.assertEqual(e3.next_or_previous_time.dt_start, make_aware(datetime(year=2017, month=10, day=31, hour=19))) + self.assertEqual(e3.next_or_previous_time.dt_end, make_aware(datetime(year=2017, month=10, day=31, hour=21))) diff --git a/events/tests/test_models.py b/events/tests/test_models.py index 0f3bafe76..2416b0bca 100644 --- a/events/tests/test_models.py +++ b/events/tests/test_models.py @@ -47,6 +47,24 @@ def test_occurring_event(self): self.assertEqual(self.event.next_time, None) self.assertTrue(self.event.is_past) + def test_occurring_event_single_day(self): + now = seconds_resolution(timezone.now()) + + occurring_time_dtstart = now + datetime.timedelta(days=3) + occurring_time_dtend = occurring_time_dtstart + datetime.timedelta(days=1) + + ot = OccurringRule.objects.create( + event=self.event, + dt_start=occurring_time_dtstart, + dt_end=occurring_time_dtend, + all_day=True + ) + + self.assertEqual(self.event.next_time.dt_start, occurring_time_dtstart) + self.assertEqual(self.event.previous_time, None) + self.assertEqual(self.event.next_or_previous_time.dt_start, occurring_time_dtstart) + self.assertTrue(self.event.next_time.single_day) + def test_recurring_event(self): now = seconds_resolution(timezone.now()) @@ -62,7 +80,6 @@ def test_recurring_event(self): self.assertEqual(self.event.next_time.dt_start, recurring_time_dtstart) self.assertTrue(rt.valid_dt_end()) - rt.begin = now - datetime.timedelta(days=5) rt.finish = now - datetime.timedelta(days=3) rt.save() @@ -71,6 +88,25 @@ def test_recurring_event(self): self.assertEqual(event.next_time, None) self.assertEqual(Event.objects.for_datetime().count(), 0) + def test_recurring_event_single_day(self): + now = seconds_resolution(timezone.now()) + + recurring_time_dtstart = now + datetime.timedelta(days=3) + recurring_time_dtend = recurring_time_dtstart + datetime.timedelta(days=1) + + rt = RecurringRule.objects.create( + event=self.event, + begin=recurring_time_dtstart, + finish=recurring_time_dtend, + all_day=True, + duration="1 days" + ) + + self.assertEqual(self.event.next_time.dt_start, recurring_time_dtstart) + self.assertEqual(self.event.previous_time, None) + self.assertEqual(self.event.next_or_previous_time.dt_start, recurring_time_dtstart) + self.assertTrue(self.event.next_time.single_day) + def test_rrule(self): now = seconds_resolution(timezone.now()) diff --git a/events/tests/test_templatetags.py b/events/tests/test_templatetags.py new file mode 100644 index 000000000..bb41239ab --- /dev/null +++ b/events/tests/test_templatetags.py @@ -0,0 +1,27 @@ +from django.test import TestCase + +import datetime + +from ..templatetags.events import exclude_ending_day + + +class TemplateTagsTests(TestCase): + def test_exclude_ending_day(self): + ending_date = datetime.datetime(year=2017, month=10, day=9, hour=0, minute=0, second=0) + + ending_date_after_filter = exclude_ending_day(ending_date) + + self.assertEqual( + ending_date_after_filter, + datetime.datetime(year=2017, month=10, day=8) + ) + + def test_exclude_ending_date_1st_january(self): + ending_date = datetime.datetime(year=2017, month=1, day=1, hour=0, minute=0, second=0) + + ending_date_after_filter = exclude_ending_day(ending_date) + + self.assertEqual( + ending_date_after_filter, + datetime.datetime(year=2016, month=12, day=31) + ) \ No newline at end of file diff --git a/events/utils.py b/events/utils.py index 7eca18f95..e99281a6d 100644 --- a/events/utils.py +++ b/events/utils.py @@ -14,6 +14,10 @@ def minutes_resolution(dt): return dt - dt.second * datetime.timedelta(0, 1, 0) - dt.microsecond * datetime.timedelta(0, 0, 1) +def extract_date_or_datetime(dt): + return dt if not isinstance(dt, datetime.datetime) else convert_dt_to_aware(dt) + + def date_to_datetime(date, tzinfo=None): if tzinfo is None: tzinfo = pytz.UTC diff --git a/peps/converters.py b/peps/converters.py index 430ccaa6f..59d3d6b85 100644 --- a/peps/converters.py +++ b/peps/converters.py @@ -33,7 +33,7 @@ def convert_pep0(): for a Python.org Page returns the core body HTML necessary only """ pep0_path = os.path.join(settings.PEP_REPO_PATH, 'pep-0000.html') - pep0_content = open(pep0_path).read() + pep0_content = open(pep0_path, encoding="utf-8 ").read() soup = BeautifulSoup(pep0_content) diff --git a/templates/events/event_detail.html b/templates/events/event_detail.html index 80773bd6b..59ffc04df 100644 --- a/templates/events/event_detail.html +++ b/templates/events/event_detail.html @@ -1,5 +1,5 @@ {% extends "base.html" %} - +{% load events %} {% block page_title %}{{ object.title|striptags }} | {{ SITE_INFO.site_name }}{% endblock %} {% block og_title %}{{ object.title|striptags }}{% endblock %} @@ -25,13 +25,26 @@

{% endif %} {% with object.next_or_previous_time as next_time %} + {% if next_time.single_day %}

- - + + + {% if next_time.valid_dt_end %} - {% endif %}{% endif %}, + + {% endif %}

@@ -39,11 +52,33 @@

- - , - + + + + {% if next_time.all_day %} + + {% else %} + + {% endif %}

{% endif %} + {% endwith %} diff --git a/templates/events/includes/time_tag.html b/templates/events/includes/time_tag.html index 34bcaa60f..da0658285 100644 --- a/templates/events/includes/time_tag.html +++ b/templates/events/includes/time_tag.html @@ -1,5 +1,52 @@ +{% load events %} + +{% with dt_start=next_time.dt_start %} + {% if next_time.single_day %} - + +{% with dt_end=next_time.dt_end %} + +{% endwith %} + +{% elif next_time.all_day %} + +{% with dt_end=next_time.dt_end|exclude_ending_day %} + +{% endwith %} + {% else %} - -{% endif %} \ No newline at end of file + +{% with dt_end=next_time.dt_end %} + +{% endwith %} + +{% endif %} +{% endwith %} diff --git a/templates/search/includes/events.event.html b/templates/search/includes/events.event.html index f015b5513..99ae28828 100644 --- a/templates/search/includes/events.event.html +++ b/templates/search/includes/events.event.html @@ -1,24 +1,66 @@ +{% load events %} +

Event – {{ result.name }}

{% with result.object.next_or_previous_time as next_time %} +{% with next_time.dt_start as dt_start %} + {% if next_time.single_day %} + +{% with next_time.dt_end as dt_end %}

- - + + {% if next_time.valid_dt_end %} - {% endif %}, - + + {% endif %} +

+{% endwith %} + +{% elif next_time.all_day %} + +{% with next_time.dt_end|exclude_ending_day as dt_end %} + +

+ + + +

+{% endwith %} {% else %} +{% with next_time.dt_end as dt_end %}

- - , - + + +

+{% endwith %} + {% endif %} + +{% endwith %} {% endwith %} {% if result.venue %}