118118 UploadAgendaForm , UploadBlueSheetForm , UploadMinutesForm , UploadSlidesForm ,
119119 UploadNarrativeMinutesForm )
120120
121+ from icalendar import Calendar , Event
122+ from ietf .doc .templatetags .ietf_filters import absurl
123+
121124request_summary_exclude_group_types = ['team' ]
122125
123126
@@ -137,6 +140,10 @@ def send_interim_change_notice(request, meeting):
137140 message .related_groups .add (group )
138141 send_mail_message (request , message )
139142
143+ def parse_ical_line_endings (ical ):
144+ """Parse icalendar line endings to ensure they are RFC 5545 compliant"""
145+ return re .sub (r'\r(?!\n)|(?<!\r)\n' , '\r \n ' , ical )
146+
140147# -------------------------------------------------
141148# View Functions
142149# -------------------------------------------------
@@ -1982,8 +1989,10 @@ def agenda_by_type_ics(request,num=None,type=None):
19821989 ).order_by ('session__type__slug' ,'timeslot__time' )
19831990 if type :
19841991 assignments = assignments .filter (session__type__slug = type )
1985- updated = meeting .updated ()
1986- return render (request ,"meeting/agenda.ics" ,{"schedule" :schedule ,"updated" :updated ,"assignments" :assignments },content_type = "text/calendar" )
1992+
1993+ return render_icalendar (schedule , assignments )
1994+
1995+
19871996
19881997def session_draft_list (num , acronym ):
19891998 try :
@@ -2103,6 +2112,125 @@ def ical_session_status(assignment):
21032112 else :
21042113 return "CONFIRMED"
21052114
2115+ def render_icalendar (schedule , assignments ):
2116+ ical_content = generate_agenda_ical (schedule , assignments )
2117+ return HttpResponse (ical_content , content_type = "text/calendar" )
2118+
2119+ def generate_agenda_ical (schedule , assignments ):
2120+ """Generate iCalendar using the icalendar library"""
2121+
2122+ cal = Calendar ()
2123+ cal .add ("prodid" , "-//IETF//datatracker.ietf.org ical agenda//EN" )
2124+ cal .add ("version" , "2.0" )
2125+ cal .add ("method" , "PUBLISH" )
2126+
2127+ for item in assignments :
2128+ event = Event ()
2129+
2130+ uid = f"ietf-{ schedule .meeting .number } -{ item .timeslot .pk } -{ item .session .group .acronym } "
2131+ event .add ("uid" , uid )
2132+
2133+ # add custom field with meeting's local TZ
2134+ event .add ("x-meeting-tz" , schedule .meeting .time_zone )
2135+
2136+ if item .session .name :
2137+ summary = item .session .name
2138+ else :
2139+ group = item .session .group_at_the_time ()
2140+ summary = f"{ group .acronym } - { group .name } "
2141+
2142+ if item .session .agenda_note :
2143+ summary += f" ({ item .session .agenda_note } )"
2144+
2145+ event .add ("summary" , summary )
2146+
2147+ if item .timeslot .show_location and item .timeslot .get_location ():
2148+ event .add ("location" , item .timeslot .get_location ())
2149+
2150+ if item .session and hasattr (item .session , "current_status" ):
2151+ status = ical_session_status (item )
2152+ else :
2153+ status = ""
2154+ event .add ("status" , status )
2155+
2156+ event .add ("class" , "PUBLIC" )
2157+
2158+ event .add ("dtstart" , item .timeslot .utc_start_time ())
2159+ event .add ("dtend" , item .timeslot .utc_end_time ())
2160+
2161+ # DTSTAMP: when the event was created or last modified (in UTC)
2162+ dtstamp = item .timeslot .modified .astimezone (pytz .UTC )
2163+ event .add ("dtstamp" , dtstamp )
2164+
2165+ description_parts = [item .timeslot .name ]
2166+
2167+ if item .session .agenda_note :
2168+ description_parts .append (f"Note: { item .session .agenda_note } " )
2169+
2170+ if hasattr (item .session , "onsite_tool_url" ) and callable (
2171+ item .session .onsite_tool_url
2172+ ):
2173+ onsite_url = item .session .onsite_tool_url ()
2174+ if onsite_url :
2175+ description_parts .append (f"Onsite tool: { onsite_url } " )
2176+
2177+ if hasattr (item .session , "video_stream_url" ) and callable (
2178+ item .session .video_stream_url
2179+ ):
2180+ video_url = item .session .video_stream_url ()
2181+ if video_url :
2182+ description_parts .append (f"Meetecho: { video_url } " )
2183+
2184+ if (
2185+ item .timeslot .location
2186+ and hasattr (item .timeslot .location , "webex_url" )
2187+ and callable (item .timeslot .location .webex_url )
2188+ and item .timeslot .location .webex_url () is not None
2189+ ):
2190+ description_parts .append (f"Webex: { item .timeslot .location .webex_url ()} " )
2191+
2192+ if item .session .remote_instructions :
2193+ description_parts .append (
2194+ f"Remote instructions: { item .session .remote_instructions } "
2195+ )
2196+
2197+ try :
2198+ materials_url = absurl (
2199+ "ietf.meeting.views.session_details" ,
2200+ num = schedule .meeting .number ,
2201+ acronym = item .session .group .acronym ,
2202+ )
2203+ description_parts .append (f"Session materials: { materials_url } " )
2204+ event .add ("url" , materials_url )
2205+ except :
2206+ pass
2207+
2208+ if (
2209+ hasattr (schedule .meeting , "get_number" )
2210+ and schedule .meeting .get_number () is not None
2211+ ):
2212+ try :
2213+ agenda_url = absurl ("agenda" , num = schedule .meeting .number )
2214+ description_parts .append (
2215+ f"See in schedule: { agenda_url } #row-{ item .slug ()} "
2216+ )
2217+ except :
2218+ pass
2219+
2220+ agenda = item .session .agenda ()
2221+ if agenda and hasattr (agenda , "get_versionless_href" ):
2222+ agenda_url = agenda .get_versionless_href ()
2223+ description_parts .append (f"{ agenda .type } { agenda_url } " )
2224+
2225+ # Join all description parts with 2 newlines
2226+ description = "\n \n " .join (description_parts )
2227+ event .add ("description" , description )
2228+
2229+ # Add event to calendar
2230+ cal .add_component (event )
2231+
2232+ return cal .to_ical ().decode ("utf-8" )
2233+
21062234def parse_agenda_filter_params (querydict ):
21072235 """Parse agenda filter parameters from a request"""
21082236 if len (querydict ) == 0 :
@@ -2154,7 +2282,6 @@ def agenda_ical(request, num=None, acronym=None, session_id=None):
21542282 else :
21552283 meeting = get_meeting (num , type_in = None ) # get requested meeting, whatever its type
21562284 schedule = get_schedule (meeting )
2157- updated = meeting .updated ()
21582285
21592286 if schedule is None and acronym is None and session_id is None :
21602287 raise Http404
@@ -2180,15 +2307,7 @@ def agenda_ical(request, num=None, acronym=None, session_id=None):
21802307 elif session_id :
21812308 assignments = [ a for a in assignments if a .session_id == int (session_id ) ]
21822309
2183- for a in assignments :
2184- if a .session :
2185- a .session .ical_status = ical_session_status (a )
2186-
2187- return render (request , "meeting/agenda.ics" , {
2188- "schedule" : schedule ,
2189- "assignments" : assignments ,
2190- "updated" : updated
2191- }, content_type = "text/calendar" )
2310+ return render_icalendar (schedule , assignments )
21922311
21932312@cache_page (15 * 60 )
21942313def agenda_json (request , num = None ):
@@ -4138,7 +4257,7 @@ def upcoming_ical(request):
41384257 'assignments' : assignments ,
41394258 'ietfs' : ietfs ,
41404259 }, request = request )
4141- response = re . sub ( " \r (?! \n )|(?<! \r ) \n " , " \r \n " , response )
4260+ response = parse_ical_line_endings ( response )
41424261
41434262 response = HttpResponse (response , content_type = 'text/calendar' )
41444263 response ['Content-Disposition' ] = 'attachment; filename="upcoming.ics"'
@@ -4764,7 +4883,7 @@ def important_dates(request, num=None, output_format=None):
47644883 'meetings' : meetings ,
47654884 }, request = request )
47664885 # icalendar response file should have '\r\n' line endings per RFC5545
4767- response = HttpResponse (re . sub ( " \r (?! \n )|(?<! \r ) \n " , " \r \n " , ics ), content_type = 'text/calendar' )
4886+ response = HttpResponse (parse_ical_line_endings ( ics ), content_type = 'text/calendar' )
47684887 response ['Content-Disposition' ] = 'attachment; filename="important-dates.ics"'
47694888 return response
47704889
@@ -5198,3 +5317,4 @@ def import_session_minutes(request, session_id, num):
51985317 'contents_unchanged' : not contents_changed ,
51995318 },
52005319 )
5320+
0 commit comments