diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index b1bbc62907..1746ed17de 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -5220,7 +5220,9 @@ def test_upcoming_ical(self): assert_ical_response_is_valid(self, r, expected_event_summaries=expected_event_summaries, expected_event_count=len(expected_event_summaries)) - self.assertContains(r, 'Remote instructions: https://someurl.example.com') + # Unfold long lines that might have been folded by iCal + content_unfolded = r.content.decode('utf-8').replace('\r\n ', '') + self.assertIn('Remote instructions: https://someurl.example.com', content_unfolded) Session.objects.filter(meeting__type_id='interim').update(remote_instructions='') r = self.client.get(url) @@ -5228,7 +5230,8 @@ def test_upcoming_ical(self): assert_ical_response_is_valid(self, r, expected_event_summaries=expected_event_summaries, expected_event_count=len(expected_event_summaries)) - self.assertNotContains(r, 'Remote instructions:') + content_unfolded = r.content.decode('utf-8').replace('\r\n ', '') + self.assertNotIn('Remote instructions:', content_unfolded) updated = meeting.updated() self.assertIsNotNone(updated) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 69635d6219..0c49558c39 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -4417,22 +4417,71 @@ def upcoming_ical(request): else: ietfs = [] - meeting_vtz = {meeting.vtimezone() for meeting in meetings} - meeting_vtz.discard(None) - - # icalendar response file should have '\r\n' line endings per RFC5545 - response = render_to_string('meeting/upcoming.ics', { - 'vtimezones': ''.join(sorted(meeting_vtz)), - 'assignments': assignments, - 'ietfs': ietfs, - }, request=request) - response = parse_ical_line_endings(response) + response = render_upcoming_ical(assignments, ietfs, request) response = HttpResponse(response, content_type='text/calendar') response['Content-Disposition'] = 'attachment; filename="upcoming.ics"' return response +def render_upcoming_ical(assignments, meetings, request): + """Generate upcoming using the icalendar library""" + + cal = Calendar() + cal.add("prodid", "-//IETF//datatracker.ietf.org ical upcoming//EN") + cal.add("version", "2.0") + cal.add("method", "PUBLISH") + + for item in assignments: + event = Event() + + event.add("uid", f"ietf-{item.session.meeting.number}-{item.timeslot.pk}") + event.add("summary", f"{item.session.group.acronym.lower()} - {item.session.name if item.session.name else item.session.group.name}") + + if item.schedule.meeting.city: + event.add("location", f"{item.schedule.meeting.city},{item.schedule.meeting.country}") + + event.add("status", item.session.ical_status) + event.add("class", "PUBLIC") + + event.add("dtstart", item.timeslot.utc_start_time()) + event.add("dtend", item.timeslot.utc_end_time()) + event.add("dtstamp", item.timeslot.modified) + if item.session.agenda(): + event.add("url", item.session.agenda().get_href()) + + description_lines = [] + if item.timeslot.name: + description_lines.append(f"{item.timeslot.name}") + if item.session.agenda_note: + description_lines.append(f"Note: {item.session.agenda_note}") + + for material in item.session.materials.all(): + title_part = f" ({material.title})" if material.type.name != "Agenda" else "" + description_lines.append(f"{material.type}{title_part}: {material.get_href()}") + + if item.session.remote_instructions: + description_lines.append(f"Remote instructions: {item.session.remote_instructions}") + + event.add("description", "\n".join(description_lines)) + cal.add_component(event) + + for meeting in meetings: + event = Event() + event.add("uid", f"ietf-{meeting.number}") + event.add("summary", f"IETF {meeting.number}") + if meeting.city: + event.add("location", f"{meeting.city},{meeting.country}") + event.add("class", "PUBLIC") + event.add("dtstart", meeting.date) + event.add("dtend", meeting.end_date() + datetime.timedelta(days=1)) + event.add("dtstamp", meeting.cached_updated) + event.add("url", f"{request.scheme}://{request.get_host()}{reverse('agenda', kwargs={'num': meeting.number})}") + + cal.add_component(event) + + return cal.to_ical().decode("utf-8") + def upcoming_json(request): '''Return Upcoming meetings in json format''' today = date_today() diff --git a/ietf/templates/meeting/upcoming.ics b/ietf/templates/meeting/upcoming.ics deleted file mode 100644 index 5eca7ec81d..0000000000 --- a/ietf/templates/meeting/upcoming.ics +++ /dev/null @@ -1,32 +0,0 @@ -{% load humanize tz %}{% autoescape off %}{% load ietf_filters textfilters %}BEGIN:VCALENDAR -VERSION:2.0 -METHOD:PUBLISH -PRODID:-//IETF//datatracker.ietf.org ical upcoming//EN -{{vtimezones}}{% for item in assignments %}BEGIN:VEVENT -UID:ietf-{{item.session.meeting.number}}-{{item.timeslot.pk}} -SUMMARY:{% if item.session.name %}{{item.session.group.acronym|lower}} - {{item.session.name|ics_esc}}{% else %}{{item.session.group.acronym|lower}} - {{item.session.group.name}}{%endif%} -{% if item.schedule.meeting.city %}LOCATION:{{item.schedule.meeting.city}},{{item.schedule.meeting.country}} -{% endif %}STATUS:{{item.session.ical_status}} -CLASS:PUBLIC -DTSTART{% ics_date_time item.timeslot.local_start_time item.schedule.meeting.time_zone %} -DTEND{% ics_date_time item.timeslot.local_end_time item.schedule.meeting.time_zone %} -DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %}{% if item.session.agenda %} -URL:{{item.session.agenda.get_href}}{% endif %} -DESCRIPTION:{% if item.timeslot.name %}{{item.timeslot.name|ics_esc}}\n{% endif %}{% if item.session.agenda_note %} - Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% for material in item.session.materials.all %} - \n{{material.type}}{% if material.type.name != "Agenda" %} - ({{material.title|ics_esc}}){% endif %}: - {{material.get_href}}\n{% endfor %}{% if item.session.remote_instructions %} - Remote instructions: {{ item.session.remote_instructions }}\n{% endif %} -END:VEVENT -{% endfor %}{% for meeting in ietfs %}BEGIN:VEVENT -UID:ietf-{{ meeting.number }} -SUMMARY:IETF {{ meeting.number }}{% if meeting.city %} -LOCATION:{{ meeting.city }},{{ meeting.country }}{% endif %} -CLASS:PUBLIC -DTSTART;VALUE=DATE{% if meeting.time_zone %};TZID={{ meeting.time_zone|ics_esc }}{% endif %}:{{ meeting.date|date:"Ymd" }} -DTEND;VALUE=DATE{% if meeting.time_zone %};TZID={{ meeting.time_zone|ics_esc }}{% endif %}:{{ meeting.end_date|next_day|date:"Ymd" }} -DTSTAMP{% ics_date_time meeting.cached_updated|utc 'utc' %} -URL:{{ request.scheme }}://{{ request.get_host }}{% url 'agenda' num=meeting.number %} -END:VEVENT -{% endfor %}END:VCALENDAR{% endautoescape %}