118
118
UploadAgendaForm , UploadBlueSheetForm , UploadMinutesForm , UploadSlidesForm ,
119
119
UploadNarrativeMinutesForm )
120
120
121
+ from icalendar import Calendar , Event
122
+ from ietf .doc .templatetags .ietf_filters import absurl
123
+
121
124
request_summary_exclude_group_types = ['team' ]
122
125
123
126
@@ -137,6 +140,10 @@ def send_interim_change_notice(request, meeting):
137
140
message .related_groups .add (group )
138
141
send_mail_message (request , message )
139
142
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
+
140
147
# -------------------------------------------------
141
148
# View Functions
142
149
# -------------------------------------------------
@@ -1982,8 +1989,10 @@ def agenda_by_type_ics(request,num=None,type=None):
1982
1989
).order_by ('session__type__slug' ,'timeslot__time' )
1983
1990
if type :
1984
1991
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
+
1987
1996
1988
1997
def session_draft_list (num , acronym ):
1989
1998
try :
@@ -2103,6 +2112,125 @@ def ical_session_status(assignment):
2103
2112
else :
2104
2113
return "CONFIRMED"
2105
2114
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
+
2106
2234
def parse_agenda_filter_params (querydict ):
2107
2235
"""Parse agenda filter parameters from a request"""
2108
2236
if len (querydict ) == 0 :
@@ -2154,7 +2282,6 @@ def agenda_ical(request, num=None, acronym=None, session_id=None):
2154
2282
else :
2155
2283
meeting = get_meeting (num , type_in = None ) # get requested meeting, whatever its type
2156
2284
schedule = get_schedule (meeting )
2157
- updated = meeting .updated ()
2158
2285
2159
2286
if schedule is None and acronym is None and session_id is None :
2160
2287
raise Http404
@@ -2180,15 +2307,7 @@ def agenda_ical(request, num=None, acronym=None, session_id=None):
2180
2307
elif session_id :
2181
2308
assignments = [ a for a in assignments if a .session_id == int (session_id ) ]
2182
2309
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 )
2192
2311
2193
2312
@cache_page (15 * 60 )
2194
2313
def agenda_json (request , num = None ):
@@ -4138,7 +4257,7 @@ def upcoming_ical(request):
4138
4257
'assignments' : assignments ,
4139
4258
'ietfs' : ietfs ,
4140
4259
}, request = request )
4141
- response = re . sub ( " \r (?! \n )|(?<! \r ) \n " , " \r \n " , response )
4260
+ response = parse_ical_line_endings ( response )
4142
4261
4143
4262
response = HttpResponse (response , content_type = 'text/calendar' )
4144
4263
response ['Content-Disposition' ] = 'attachment; filename="upcoming.ics"'
@@ -4764,7 +4883,7 @@ def important_dates(request, num=None, output_format=None):
4764
4883
'meetings' : meetings ,
4765
4884
}, request = request )
4766
4885
# 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' )
4768
4887
response ['Content-Disposition' ] = 'attachment; filename="important-dates.ics"'
4769
4888
return response
4770
4889
@@ -5198,3 +5317,4 @@ def import_session_minutes(request, session_id, num):
5198
5317
'contents_unchanged' : not contents_changed ,
5199
5318
},
5200
5319
)
5320
+
0 commit comments