diff --git a/ietf/api/views.py b/ietf/api/views.py index b4dd7f05d6..22523b2f17 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -114,7 +114,11 @@ class ApiV2PersonExportView(DetailView, JsonExportMixin): model = Person def err(self, code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def post(self, request): querydict = request.POST.copy() @@ -152,7 +156,11 @@ def post(self, request): def api_new_meeting_registration_v2(request): '''REST API to notify the datatracker about a new meeting registration''' def _http_err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def _api_response(result): return JsonResponse(data={"result": result}) @@ -192,7 +200,11 @@ def _api_response(result): process_single_registration(reg_data, meeting) - return HttpResponse('Success', status=202, content_type='text/plain') + return HttpResponse( + 'Success', + status=202, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def version(request): @@ -511,7 +523,11 @@ def related_email_list(request, email): to Datatracker, via Person object """ def _http_err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method == "GET": try: @@ -637,7 +653,11 @@ def ingest_email_handler(request, test_mode=False): """ def _http_err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def _api_response(result): return JsonResponse(data={"result": result}) diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 9e2a417933..0ba340890d 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -257,7 +257,11 @@ def edit_position(request, name, ballot_id): @csrf_exempt def api_set_position(request): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method == 'POST': ad = request.user.person name = request.POST.get('doc') @@ -290,7 +294,11 @@ def err(code, text): addrs, frm, subject, body = build_position_email(ad, doc, pos) send_mail_text(request, addrs.to, frm, subject, body, cc=addrs.cc) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def build_position_email(balloter, doc, pos): diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index f1fe77f763..7b9f489b44 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -221,7 +221,7 @@ def agenda_txt(request, date=None): "date": data["date"], "sections": sorted(data["sections"].items(), key=lambda x:[int(p) for p in x[0].split('.')]), "domain": Site.objects.get_current().domain, - }, content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET) + }, content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}") @role_required('Area Director', 'Secretariat') def agenda_moderator_package(request, date=None): @@ -277,14 +277,23 @@ def leaf_section(num, section): @role_required('Area Director', 'Secretariat') def agenda_package(request, date=None): data = agenda_data(date) - return render(request, "iesg/agenda_package.txt", { + return render( + request, + "iesg/agenda_package.txt", + { "date": data["date"], "sections": sorted(data["sections"].items()), "roll_call": data["sections"]["1.1"]["text"], "minutes": data["sections"]["1.3"]["text"], - "management_items": [(num, section) for num, section in data["sections"].items() if "6" < num < "7"], + "management_items": [ + (num, section) + for num, section in data["sections"].items() + if "6" < num < "7" + ], "domain": Site.objects.get_current().domain, - }, content_type='text/plain') + }, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def agenda_documents_txt(request): @@ -315,7 +324,10 @@ def agenda_documents_txt(request): d.rev, ) rows.append("\t".join(row)) - return HttpResponse("\n".join(rows), content_type='text/plain') + return HttpResponse( + "\n".join(rows), + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) class RescheduleForm(forms.Form): telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False) @@ -610,4 +622,7 @@ def telechat_agenda_content_manage(request): @role_required("Secretariat", "IAB Chair", "Area Director") def telechat_agenda_content_view(request, section): content = get_object_or_404(TelechatAgendaContent, section__slug=section, section__used=True) - return HttpResponse(content=content.text, content_type="text/plain; charset=utf-8") + return HttpResponse( + content=content.text, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) diff --git a/ietf/meeting/admin.py b/ietf/meeting/admin.py index 1c5d5c67b5..d886a9a4b6 100644 --- a/ietf/meeting/admin.py +++ b/ietf/meeting/admin.py @@ -3,11 +3,13 @@ from django.contrib import admin +from django.db.models import Count from ietf.meeting.models import (Attended, Meeting, Room, Session, TimeSlot, Constraint, Schedule, SchedTimeSessAssignment, ResourceAssociation, FloorPlan, UrlResource, SessionPresentation, ImportantDate, SlideSubmission, SchedulingEvent, BusinessConstraint, - ProceedingsMaterial, MeetingHost, Registration, RegistrationTicket) + ProceedingsMaterial, MeetingHost, Registration, RegistrationTicket, + AttendanceTypeName) class UrlResourceAdmin(admin.ModelAdmin): @@ -219,8 +221,9 @@ class MeetingFilter(admin.SimpleListFilter): parameter_name = 'meeting_id' def lookups(self, request, model_admin): - # Your queryset to limit choices - choices = Meeting.objects.filter(type='ietf').values_list('id', 'number') + # only include meetings with registration records + meetings = Meeting.objects.filter(type='ietf').annotate(reg_count=Count('registration')).filter(reg_count__gt=0).order_by('-date') + choices = meetings.values_list('id', 'number') return choices def queryset(self, request, queryset): @@ -228,23 +231,60 @@ def queryset(self, request, queryset): return queryset.filter(meeting__id=self.value()) return queryset +class AttendanceFilter(admin.SimpleListFilter): + title = 'Attendance Type' + parameter_name = 'attendance_type' + + def lookups(self, request, model_admin): + choices = AttendanceTypeName.objects.all().values_list('slug', 'name') + return choices + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(tickets__attendance_type__slug=self.value()).distinct() + return queryset + class RegistrationTicketInline(admin.TabularInline): model = RegistrationTicket class RegistrationAdmin(admin.ModelAdmin): model = Registration - # list_filter = [('meeting', Meeting.objects.filter(type='ietf')), ] - list_filter = [MeetingFilter, ] - list_display = ['meeting', 'first_name', 'last_name', 'affiliation', 'country_code', 'person', 'email', ] - search_fields = ['meeting__number', 'first_name', 'last_name', 'affiliation', 'country_code', 'email', ] + list_filter = [AttendanceFilter, MeetingFilter] + list_display = ['meeting', 'first_name', 'last_name', 'display_attendance', 'affiliation', 'country_code', 'email', ] + search_fields = ['first_name', 'last_name', 'affiliation', 'country_code', 'email', ] raw_id_fields = ['person'] inlines = [RegistrationTicketInline, ] + ordering = ['-meeting__date', 'last_name'] + + def display_attendance(self, instance): + '''Only display the most significant ticket in the list. + To see all the tickets inspect the individual instance + ''' + if instance.tickets.filter(attendance_type__slug='onsite').exists(): + return 'onsite' + elif instance.tickets.filter(attendance_type__slug='remote').exists(): + return 'remote' + elif instance.tickets.filter(attendance_type__slug='hackathon_onsite').exists(): + return 'hackathon onsite' + elif instance.tickets.filter(attendance_type__slug='hackathon_remote').exists(): + return 'hackathon remote' + display_attendance.short_description = "Attendance" # type: ignore # https://github.com/python/mypy/issues/2087 + admin.site.register(Registration, RegistrationAdmin) class RegistrationTicketAdmin(admin.ModelAdmin): model = RegistrationTicket list_filter = ['attendance_type', ] - list_display = ['registration', 'attendance_type', 'ticket_type'] + # not available until Django 5.2, the name of a related field, using the __ notation + # list_display = ['registration__meeting', 'registration', 'attendance_type', 'ticket_type', 'registration__email'] + # list_select_related = ('registration',) + list_display = ['registration', 'attendance_type', 'ticket_type', 'display_meeting'] search_fields = ['registration__first_name', 'registration__last_name', 'registration__email'] raw_id_fields = ['registration'] + ordering = ['-registration__meeting__date', 'registration__last_name'] + + def display_meeting(self, instance): + return instance.registration.meeting.number + display_meeting.short_description = "Meeting" # type: ignore # https://github.com/python/mypy/issues/2087 + admin.site.register(RegistrationTicket, RegistrationTicketAdmin) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 8bd70a3733..85eda5a8f4 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2760,7 +2760,10 @@ def upload_session_bluesheets(request, session_id, num): ota = session.official_timeslotassignment() sess_time = ota and ota.timeslot.time if not sess_time: - return HttpResponseGone("Cannot receive uploads for an unscheduled session. Please check the session ID.", content_type="text/plain") + return HttpResponseGone( + "Cannot receive uploads for an unscheduled session. Please check the session ID.", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) save_error = save_bluesheet(request, session, file, encoding=form.file_encoding[file.name]) @@ -2821,7 +2824,7 @@ def upload_session_minutes(request, session_id, num): except SessionNotScheduledError: return HttpResponseGone( "Cannot receive uploads for an unscheduled session. Please check the session ID.", - content_type="text/plain", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", ) except SaveMaterialsError as err: form.add_error(None, str(err)) @@ -2880,7 +2883,7 @@ def upload_session_narrativeminutes(request, session_id, num): except SessionNotScheduledError: return HttpResponseGone( "Cannot receive uploads for an unscheduled session. Please check the session ID.", - content_type="text/plain", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", ) except SaveMaterialsError as err: form.add_error(None, str(err)) @@ -2978,7 +2981,10 @@ def upload_session_agenda(request, session_id, num): ota = session.official_timeslotassignment() sess_time = ota and ota.timeslot.time if not sess_time: - return HttpResponseGone("Cannot receive uploads for an unscheduled session. Please check the session ID.", content_type="text/plain") + return HttpResponseGone( + "Cannot receive uploads for an unscheduled session. Please check the session ID.", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if session.meeting.type_id=='ietf': name = 'agenda-%s-%s' % (session.meeting.number, session.group.acronym) @@ -4282,11 +4288,17 @@ def api_set_meetecho_recording_name(request): name: the name to use for the recording at meetecho player """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != "POST": return HttpResponseNotAllowed( - content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + content="Method not allowed", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + permitted_methods=('POST',), ) session_id = request.POST.get('session_id', None) @@ -4306,7 +4318,11 @@ def err(code, text): session.meetecho_recording_name = name session.save() - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @role_required('Recording Manager') @@ -4320,11 +4336,17 @@ def api_set_session_video_url(request): url: The recording url (on YouTube, or whatever) """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return HttpResponseNotAllowed( - content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + content="Method not allowed", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + permitted_methods=('POST',), ) # Temporary: fall back to deprecated interface if we have old-style parameters. @@ -4363,7 +4385,11 @@ def err(code, text): time = session.official_timeslotassignment().timeslot.time title = 'Video recording for %s on %s at %s' % (session.group.acronym, time.date(), time.time()) create_recording(session, incoming_url, title=title, user=request.user.person) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def deprecated_api_set_session_video_url(request): @@ -4372,7 +4398,11 @@ def deprecated_api_set_session_video_url(request): Uses meeting/group/item to identify session. """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method == 'POST': # parameters: # apikey: the poster's personal API key @@ -4426,7 +4456,11 @@ def err(code, text): else: return err(405, "Method not allowed") - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @@ -4478,7 +4512,11 @@ def api_add_session_attendees(request): ) def err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != "POST": return err(405, "Method not allowed") @@ -4531,7 +4569,11 @@ def err(code, text): if save_error: return err(400, save_error) - return HttpResponse("Done", status=200, content_type="text/plain") + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @@ -4539,7 +4581,11 @@ def err(code, text): @csrf_exempt def api_upload_chatlog(request): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return err(405, "Method not allowed") apidata_post = request.POST.get('apidata') @@ -4572,14 +4618,22 @@ def err(code, text): write_doc_for_session(session, 'chatlog', filename, json.dumps(apidata['chatlog'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @role_required('Recording Manager') @csrf_exempt def api_upload_polls(request): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return err(405, "Method not allowed") apidata_post = request.POST.get('apidata') @@ -4612,7 +4666,11 @@ def err(code, text): write_doc_for_session(session, 'polls', filename, json.dumps(apidata['polls'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @role_required('Recording Manager', 'Secretariat') @@ -4627,11 +4685,17 @@ def api_upload_bluesheet(request): [{'name': 'Name', 'affiliation': 'Organization', }, ...] """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return HttpResponseNotAllowed( - content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + content="Method not allowed", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + permitted_methods=('POST',), ) session_id = request.POST.get('session_id', None) @@ -4666,7 +4730,11 @@ def err(code, text): save_err = save_bluesheet(request, session, file) if save_err: return err(400, save_err) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def important_dates(request, num=None, output_format=None): @@ -5090,7 +5158,7 @@ def import_session_minutes(request, session_id, num): except SessionNotScheduledError: return HttpResponseGone( "Cannot import minutes for an unscheduled session. Please check the session ID.", - content_type="text/plain", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", ) except SaveMaterialsError as err: form.add_error(None, str(err)) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index d34126b1e7..c04e13f92b 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -1017,7 +1017,10 @@ def view_feedback_nominee(request, year, nominee_id): 'positions': ','.join([str(p) for p in feedback.positions.all()]), }, request=request) - response = HttpResponse(response, content_type='text/plain') + response = HttpResponse( + response, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) response['Content-Disposition'] = f'attachment; filename="{fn}"' return response elif submit == 'reclassify': diff --git a/ietf/settings.py b/ietf/settings.py index 64679ca1d8..3af01d76e6 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -729,6 +729,8 @@ def skip_unreadable_post(record): "*~", # tilde temp-files "#*", # files beginning with a hashmark "500.html", # isn't loaded by regular loader, but checked by test_500_page() + "ietf/templates/admin/meeting/RegistrationTicket/change_list.html", + "ietf/templates/admin/meeting/Registration/change_list.html", ] TEST_COVERAGE_MAIN_FILE = os.path.join(BASE_DIR, "../release-coverage.json") diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 2b9b55c00e..043b613016 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -755,4 +755,7 @@ def get_submission_or_404(submission_id, access_token=None): def async_poke_test(request): result = poke.delay() - return HttpResponse(f'Poked {result}', content_type='text/plain') + return HttpResponse( + f'Poked {result}', + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) diff --git a/ietf/templates/admin/meeting/Registration/change_list.html b/ietf/templates/admin/meeting/Registration/change_list.html new file mode 100644 index 0000000000..62784b2cb6 --- /dev/null +++ b/ietf/templates/admin/meeting/Registration/change_list.html @@ -0,0 +1,10 @@ +{% extends "admin/change_list.html" %} + +{% block search %} + {{ block.super }} {# This includes the original search form #} + {% if cl.search_fields %} {# Only show if search is enabled for the model #} +

+ Hint: Search by: {{ cl.search_fields|join:", "|capfirst }}. +

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/admin/meeting/RegistrationTicket/change_list.html b/ietf/templates/admin/meeting/RegistrationTicket/change_list.html new file mode 100644 index 0000000000..62784b2cb6 --- /dev/null +++ b/ietf/templates/admin/meeting/RegistrationTicket/change_list.html @@ -0,0 +1,10 @@ +{% extends "admin/change_list.html" %} + +{% block search %} + {{ block.super }} {# This includes the original search form #} + {% if cl.search_fields %} {# Only show if search is enabled for the model #} +

+ Hint: Search by: {{ cl.search_fields|join:", "|capfirst }}. +

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/registration/edit_profile.html b/ietf/templates/registration/edit_profile.html index 1837016b15..1e4ab169e1 100644 --- a/ietf/templates/registration/edit_profile.html +++ b/ietf/templates/registration/edit_profile.html @@ -32,12 +32,19 @@

Your account

Change password - {% if person.photo %} + {% if person.photo or person.role_set.exists %}
-
{% include "person/photo.html" with person=person %}
+
+ {% if person.photo %} + {% include "person/photo.html" with person=person %} + {% endif %} + {% if person.role_set.exists %} +

Email support@ietf.org + to update your photo.

+ {% endif %}
{% endif %}
diff --git a/ietf/utils/decorators.py b/ietf/utils/decorators.py index 56c28c4b19..5e94dda91d 100644 --- a/ietf/utils/decorators.py +++ b/ietf/utils/decorators.py @@ -48,7 +48,7 @@ def require_api_key(f): @wraps(f) def _wrapper(request, *args, **kwargs): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse(text, status=code, content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}") # Check method and get hash if request.method == 'POST': hash = request.POST.get('apikey')