diff --git a/client/agenda/AgendaScheduleList.vue b/client/agenda/AgendaScheduleList.vue index 3897e1d62b4..12003b86668 100644 --- a/client/agenda/AgendaScheduleList.vue +++ b/client/agenda/AgendaScheduleList.vue @@ -263,7 +263,7 @@ const meetingEvents = computed(() => { key: `sesshd-${item.id}`, displayType: 'session-head', timeslot: itemTimeSlot, - name: `${item.adjustedStart.toFormat('cccc')} ${item.slotName}`, + name: `${item.adjustedStart.setZone(agendaStore.meeting.timezone).toFormat('cccc')} ${item.slotName}`, cssClasses: 'agenda-table-display-session-head' + (isLive ? ' agenda-table-live' : '') }) } diff --git a/ietf/doc/utils_search.py b/ietf/doc/utils_search.py index 0353d529656..00046ed3047 100644 --- a/ietf/doc/utils_search.py +++ b/ietf/doc/utils_search.py @@ -251,7 +251,6 @@ def num(i): if query and hasattr(query, "urlencode"): # fed a Django QueryDict d = query.copy() for h in meta['headers']: - h["sort_url"] = "?" + d.urlencode() if h['key'] == sort_key: h['sorted'] = True if sort_reversed: @@ -262,5 +261,6 @@ def num(i): d["sort"] = "-" + h["key"] else: d["sort"] = h["key"] + h["sort_url"] = "?" + d.urlencode() return (docs, meta) diff --git a/ietf/ietfauth/forms.py b/ietf/ietfauth/forms.py index 2c2c57047e7..ce9f58f490b 100644 --- a/ietf/ietfauth/forms.py +++ b/ietf/ietfauth/forms.py @@ -10,8 +10,6 @@ from django.core.exceptions import ValidationError from django.db import models from django.contrib.auth.models import User -from django.utils.html import mark_safe # type:ignore -from django.urls import reverse as urlreverse from django_password_strength.widgets import PasswordStrengthInput, PasswordConfirmationInput @@ -31,8 +29,6 @@ def clean_email(self): return email if email.lower() != email: raise forms.ValidationError('The supplied address contained uppercase letters. Please use a lowercase email address.') - if User.objects.filter(username__iexact=email).exists(): - raise forms.ValidationError('An account with the email address you provided already exists.') return email @@ -164,11 +160,6 @@ class NewEmailForm(forms.Form): def clean_new_email(self): email = self.cleaned_data.get("new_email", "") - if email: - existing = Email.objects.filter(address=email).first() - if existing: - raise forms.ValidationError("Email address '%s' is already assigned to account '%s' (%s)" % (existing, existing.person and existing.person.user, existing.person)) - for pat in settings.EXCLUDED_PERSONAL_EMAIL_REGEX_PATTERNS: if re.search(pat, email): raise ValidationError("This email address is not valid in a datatracker account") @@ -193,21 +184,6 @@ def __init__(self, role, *args, **kwargs): class ResetPasswordForm(forms.Form): username = forms.EmailField(label="Your email (lowercase)") - def clean_username(self): - """Verify that the username is valid - - In addition to EmailField's checks, verifies that a User matching the username exists. - """ - username = self.cleaned_data["username"] - if not User.objects.filter(username__iexact=username).exists(): - raise forms.ValidationError(mark_safe( - "Didn't find a matching account. " - "If you don't have an account yet, you can create one.".format( - urlreverse('ietf.ietfauth.views.create_account') - ) - )) - return username - class TestEmailForm(forms.Form): email = forms.EmailField(required=False) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 37aea4f8c64..a652d5b37df 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -165,7 +165,7 @@ def test_create_account_failure_template(self): r = render_to_string('registration/manual.html', { 'account_request_email': settings.ACCOUNT_REQUEST_EMAIL }) self.assertTrue("Additional Assistance Required" in r) - def register_and_verify(self, email): + def register(self, email): url = urlreverse(ietf.ietfauth.views.create_account) # register email @@ -175,6 +175,9 @@ def register_and_verify(self, email): self.assertContains(r, "Account request received") self.assertEqual(len(outbox), 1) + def register_and_verify(self, email): + self.register(email) + # go to confirm page confirm_url = self.extract_confirm_url(outbox[-1]) r = self.client.get(confirm_url) @@ -229,6 +232,20 @@ def test_create_subscribed_account(self): self.register_and_verify(email) settings.LIST_ACCOUNT_DELAY = saved_delay + def test_create_existing_account(self): + # create account once + email = "new-account@example.com" + self.register_and_verify(email) + + # create account again + self.register(email) + + # check notification + note = get_payload_text(outbox[-1]) + self.assertIn(email, note) + self.assertIn("A datatracker account for that email already exists", note) + self.assertIn(urlreverse(ietf.ietfauth.views.password_reset), note) + def test_ietfauth_profile(self): EmailFactory(person__user__username='plain') GroupFactory(acronym='mars') @@ -317,11 +334,14 @@ def test_ietfauth_profile(self): self.assertEqual(r.status_code, 200) self.assertEqual(Email.objects.filter(address=new_email_address, person__user__username=username, active=1).count(), 1) - # check that we can't re-add it - that would give a duplicate - r = self.client.get(confirm_url) + # try and add it again + empty_outbox() + r = self.client.post(url, with_new_email_address) self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - self.assertEqual(len(q('[name="action"][value="confirm"]')), 0) + self.assertEqual(len(outbox), 1) + note = get_payload_text(outbox[-1]) + self.assertIn(new_email_address, note) + self.assertIn("already associated with your account", note) pronoundish = base_data.copy() pronoundish["pronouns_freetext"] = "baz/boom" @@ -437,11 +457,11 @@ def test_reset_password(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) - # ask for reset, wrong username + # ask for reset, wrong username (form should not fail) r = self.client.post(url, { 'username': "nobody@example.com" }) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertTrue(len(q("form .is-invalid")) > 0) + self.assertTrue(len(q("form .is-invalid")) == 0) # ask for reset empty_outbox() @@ -518,9 +538,9 @@ def test_reset_password_without_person(self): user.save() empty_outbox() r = self.client.post(url, { 'username': user.username}) - self.assertContains(r, 'No known active email addresses', status_code=200) + self.assertContains(r, 'We have sent you an email with instructions', status_code=200) q = PyQuery(r.content) - self.assertTrue(len(q("form .is-invalid")) > 0) + self.assertTrue(len(q("form .is-invalid")) == 0) self.assertEqual(len(outbox), 0) def test_reset_password_address_handling(self): @@ -530,14 +550,14 @@ def test_reset_password_address_handling(self): person.email_set.update(active=False) empty_outbox() r = self.client.post(url, { 'username': person.user.username}) - self.assertContains(r, 'No known active email addresses', status_code=200) + self.assertContains(r, 'We have sent you an email with instructions', status_code=200) q = PyQuery(r.content) - self.assertTrue(len(q("form .is-invalid")) > 0) + self.assertTrue(len(q("form .is-invalid")) == 0) self.assertEqual(len(outbox), 0) active_address = EmailFactory(person=person).address r = self.client.post(url, {'username': person.user.username}) - self.assertNotContains(r, 'No known active email addresses', status_code=200) + self.assertContains(r, 'We have sent you an email with instructions', status_code=200) self.assertEqual(len(outbox), 1) to = outbox[0].get('To') self.assertIn(active_address, to) diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index fb6aec43336..b0ebccbe035 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -112,33 +112,47 @@ def index(request): # redirect_to = settings.LOGIN_REDIRECT_URL # return HttpResponseRedirect(redirect_to) + def create_account(request): - to_email = None + new_account_email = None - if request.method == 'POST': + if request.method == "POST": form = RegistrationForm(request.POST) if form.is_valid(): - to_email = form.cleaned_data['email'] # This will be lowercase if form.is_valid() - - # For the IETF 113 Registration period (at least) we are lowering the barriers for account creation - # to the simple email round-trip check - send_account_creation_email(request, to_email) - - # The following is what to revert to should that lowered barrier prove problematic - # existing = Subscribed.objects.filter(email__iexact=to_email).first() - # ok_to_create = ( Allowlisted.objects.filter(email__iexact=to_email).exists() - # or existing and (existing.time + TimeDelta(seconds=settings.LIST_ACCOUNT_DELAY)) < DateTime.now() ) - # if ok_to_create: - # send_account_creation_email(request, to_email) - # else: - # return render(request, 'registration/manual.html', { 'account_request_email': settings.ACCOUNT_REQUEST_EMAIL }) + new_account_email = form.cleaned_data[ + "email" + ] # This will be lowercase if form.is_valid() + + user = User.objects.filter(username__iexact=new_account_email) + email = Email.objects.filter(address__iexact=new_account_email) + if user.exists() or email.exists(): + email = user.person.email_address() if user else new_account_email + send_account_creation_exists_email(request, new_account_email, email) + else: + # For the IETF 113 Registration period (at least) we are lowering the + # barriers for account creation to the simple email round-trip check + send_account_creation_email(request, new_account_email) + + # The following is what to revert to should that lowered barrier prove problematic + # existing = Subscribed.objects.filter(email__iexact=new_account_email).first() + # ok_to_create = ( Allowlisted.objects.filter(email__iexact=new_account_email).exists() + # or existing and (existing.time + TimeDelta(seconds=settings.LIST_ACCOUNT_DELAY)) < DateTime.now() ) + # if ok_to_create: + # send_account_creation_email(request, new_account_email) + # else: + # return render(request, 'registration/manual.html', { 'account_request_email': settings.ACCOUNT_REQUEST_EMAIL }) else: form = RegistrationForm() - return render(request, 'registration/create.html', { - 'form': form, - 'to_email': to_email, - }) + return render( + request, + "registration/create.html", + { + "form": form, + "to_email": new_account_email, + }, + ) + def send_account_creation_email(request, to_email): auth = django.core.signing.dumps(to_email, salt="create_account") @@ -153,6 +167,23 @@ def send_account_creation_email(request, to_email): }) +def send_account_creation_exists_email(request, new_account_email, to_email): + domain = Site.objects.get_current().domain + subject = "Attempted account creation at %s" % domain + from_email = settings.DEFAULT_FROM_EMAIL + send_mail( + request, + to_email, + from_email, + subject, + "registration/creation_exists_email.txt", + { + "domain": domain, + "username": new_account_email, + }, + ) + + def confirm_account(request, auth): try: email = django.core.signing.loads(auth, salt="create_account", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60) @@ -255,17 +286,25 @@ def profile(request): auth = django.core.signing.dumps([person.user.username, to_email], salt="add_email") domain = Site.objects.get_current().domain - subject = 'Confirm email address for %s' % person.name from_email = settings.DEFAULT_FROM_EMAIL - send_mail(request, to_email, from_email, subject, 'registration/add_email_email.txt', { - 'domain': domain, - 'auth': auth, - 'email': to_email, - 'person': person, - 'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK, - }) - + existing = Email.objects.filter(address=to_email).first() + if existing: + subject = 'Attempt to add your email address by %s' % person.name + send_mail(request, to_email, from_email, subject, 'registration/add_email_exists_email.txt', { + 'domain': domain, + 'email': to_email, + 'person': person, + }) + else: + subject = 'Confirm email address for %s' % person.name + send_mail(request, to_email, from_email, subject, 'registration/add_email_email.txt', { + 'domain': domain, + 'auth': auth, + 'email': to_email, + 'person': person, + 'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK, + }) for r in roles: e = r.email_form.cleaned_data["email"] @@ -417,14 +456,10 @@ def password_reset(request): # The form validation checks that a matching User exists. Add the person__isnull check # because the OneToOne field does not gracefully handle checks for user.person is Null. # If we don't get a User here, we know it's because there's no related Person. + # We still report that the action succeeded, so we're not leaking the existence of user + # email addresses. user = User.objects.filter(username__iexact=submitted_username, person__isnull=False).first() - if not (user and user.person.email_set.filter(active=True).exists()): - form.add_error( - 'username', - 'No known active email addresses are associated with this account. ' - 'Please contact the secretariat for assistance.', - ) - else: + if user and user.person.email_set.filter(active=True).exists(): data = { 'username': user.username, 'password': user.password and user.password[-4:], @@ -445,7 +480,7 @@ def password_reset(request): 'username': submitted_username, 'expire': settings.MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK, }) - success = True + success = True else: form = ResetPasswordForm() return render(request, 'registration/password_reset.html', { @@ -777,7 +812,7 @@ class Meta: @person_required def apikey_disable(request): person = request.user.person - choices = [ (k.hash(), str(k)) for k in person.apikeys.all() ] + choices = [ (k.hash(), str(k)) for k in person.apikeys.exclude(valid=False) ] # class KeyDeleteForm(forms.Form): hash = forms.ChoiceField(label='Key', choices=choices) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 3afb47bbfdf..fd07821af3c 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -591,7 +591,7 @@ def test_notify(self): get_payload_text(outbox[len_before + 1]).replace('\n', ' ') ) self.assertIn(f'{settings.IDTRACKER_BASE_URL}{urlreverse("ietf.ipr.views.showlist")}', get_payload_text(outbox[len_before]).replace('\n',' ')) - self.assertIn(f'{settings.IDTRACKER_BASE_URL}{urlreverse("ietf.ipr.views.history",kwargs=dict(id=ipr.pk))}', get_payload_text(outbox[len_before+1]).replace('\n',' ')) + self.assertIn(f'{settings.IDTRACKER_BASE_URL}{urlreverse("ietf.ipr.views.show",kwargs=dict(id=ipr.pk))}', get_payload_text(outbox[len_before+1]).replace('\n',' ')) def test_notify_generic(self): RoleFactory(name_id='ad',group__acronym='gen') diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index a266efafa97..e3f4874f4be 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -826,7 +826,7 @@ def get_announcement_initial(meeting, is_change=False): desc=desc, date=meeting.date, change=change) - body = render_to_string('meeting/interim_announcement.txt', locals()) + body = render_to_string('meeting/interim_announcement.txt', locals() | {"settings": settings}) initial['body'] = body return initial diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index c945e8bb97b..5785e24a1a1 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -4510,10 +4510,11 @@ def do_interim_send_announcement_test(self, base_session=False, extra_session=Fa if sess: timeslot = sess.official_timeslotassignment().timeslot self.assertIn(timeslot.time.strftime('%Y-%m-%d'), announcement_text) - self.assertIn( - '(%s to %s UTC)' % ( + self.assertRegex( + announcement_text, + r'(%s\s+to\s+%s\s+UTC)' % ( timeslot.utc_start_time().strftime('%H:%M'),timeslot.utc_end_time().strftime('%H:%M') - ), announcement_text) + )) # Count number of sessions listed if base_session and extra_session: expected_session_matches = 3 diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index 72256b63298..b38225dd4f4 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -13,7 +13,7 @@ title="Document changes" href="/feed/document-changes/{{ name }}/"> + content="{{ doc.title }} {% if doc.get_state_slug == 'rfc' and not snapshot %}(RFC {{ rfc_number }}{% if published %}, {{ doc.pub_date|date:'F Y' }}{% endif %}{% if obsoleted_by %}; obsoleted by {% for rel in obsoleted_by %}{{ rel.source.canonical_name|prettystdname}}{% if not forloop.last%}, {% endif %}{% endfor %}{% endif %}){% endif %}"> {% endblock %} {% block morecss %}.inline { display: inline; }{% endblock %} {% block title %} diff --git a/ietf/templates/doc/document_html.html b/ietf/templates/doc/document_html.html index 9823a0d376c..7ee8f81248c 100644 --- a/ietf/templates/doc/document_html.html +++ b/ietf/templates/doc/document_html.html @@ -34,7 +34,7 @@ href="/feed/document-changes/{{ doc.name }}/"> diff --git a/ietf/templates/doc/drafts_in_iesg_process.html b/ietf/templates/doc/drafts_in_iesg_process.html index 528678118db..def5e98cbad 100644 --- a/ietf/templates/doc/drafts_in_iesg_process.html +++ b/ietf/templates/doc/drafts_in_iesg_process.html @@ -55,7 +55,7 @@

{{ title }}


Action holder{{ doc.documentactionholder_set.all|pluralize }}: {% for action_holder in doc.documentactionholder_set.all %} - {% person_link action_holder.person title=action_holder.role_for_doc %} {{ action_holder|action_holder_badge }}{% if not forloop.last %},{% endif %} + {% person_link action_holder.person title=action_holder.role_for_doc %}{% if action_holder|action_holder_badge %} {{ action_holder|action_holder_badge }}{% endif %}{% if not forloop.last %},{% endif %} {% endfor %} {% endif %} {% if doc.note %} diff --git a/ietf/templates/ietfauth/apikeys.html b/ietf/templates/ietfauth/apikeys.html index ab6194151c8..5ac7f6f453a 100644 --- a/ietf/templates/ietfauth/apikeys.html +++ b/ietf/templates/ietfauth/apikeys.html @@ -37,9 +37,9 @@

{{ key.created }} {{ key.latest }} {{ key.count }} - {{ key.valid }} + {{ key.valid }} - {{ key.hash }} + {{ key.hash }} {% if key.valid %} @@ -63,4 +63,4 @@

{% endblock %} {% block js %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/ipr/posted_document_email.txt b/ietf/templates/ipr/posted_document_email.txt index 397b686b295..1c56e8deac1 100644 --- a/ietf/templates/ipr/posted_document_email.txt +++ b/ietf/templates/ipr/posted_document_email.txt @@ -6,7 +6,7 @@ Cc: {{ cc_email }} Dear {{ to_name }}: {% filter wordwrap:78 %} -An IPR disclosure that pertains to your {{ doc_info }} was submitted to the IETF Secretariat on {{ ipr.get_latest_event_submitted.time|date:"Y-m-d" }} and has been posted on the "IETF Page of Intellectual Property Rights Disclosures" ({{ settings.IDTRACKER_BASE_URL }}{% url "ietf.ipr.views.history" id=ipr.pk %}). The title of the IPR disclosure is "{{ ipr.title }}" +An IPR disclosure that pertains to your {{ doc_info }} was submitted to the IETF Secretariat on {{ ipr.get_latest_event_submitted.time|date:"Y-m-d" }} and has been posted on the "IETF Page of Intellectual Property Rights Disclosures" ({{ settings.IDTRACKER_BASE_URL }}{% url "ietf.ipr.views.show" id=ipr.pk %}). The title of the IPR disclosure is "{{ ipr.title }}" {% endfilter %} Thank you diff --git a/ietf/templates/meeting/agenda.txt b/ietf/templates/meeting/agenda.txt index 7f612cfd91e..d00be80d145 100644 --- a/ietf/templates/meeting/agenda.txt +++ b/ietf/templates/meeting/agenda.txt @@ -23,7 +23,7 @@ {% endif %}{% if item.slot_type.slug == 'regular' %}{% ifchanged %} {{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.timeslot.name }} -{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.group_parent_at_the_time.acronym|upper|ljust:4 }} {{ item.session.group_at_the_time.acronym|ljust:10 }} {{ item.session.group_at_the_time.name }} {% if item.session.group_at_the_time.state_id == "bof" %}BOF{% elif item.session.group_at_the_time.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %} +{% endifchanged %}{{ item.timeslot.location.name|truncatechars:18|ljust:18 }} {% if item.session.group_parent_at_the_time %}{{ item.session.group_parent_at_the_time.acronym|upper|truncatechars:6|ljust:6 }}{% else %} {% endif %} {{ item.session.group_at_the_time.acronym|truncatechars:12|ljust:12 }} {{ item.session.group_at_the_time.name }} {% if item.session.group_at_the_time.state_id == "bof" %}BOF{% elif item.session.group_at_the_time.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %} {% endif %}{% if item.slot_type.slug == "break" %} {{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.slot_type.slug == "other" %} {{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %} diff --git a/ietf/templates/meeting/interim_announcement.txt b/ietf/templates/meeting/interim_announcement.txt index f9d5394c3d8..6ea2e4c9dc5 100644 --- a/ietf/templates/meeting/interim_announcement.txt +++ b/ietf/templates/meeting/interim_announcement.txt @@ -1,8 +1,6 @@ {% load ietf_filters tz %}{% timezone meeting.tz %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW. -{% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold -{% if assignments.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.time | utc | date:"H:i" }} to {{ assignments.first.timeslot.end_time | utc | date:"H:i" }} UTC){% endif %}. -{% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting. +{% endif %}{% filter wordwrap:78 %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold {% if assignments.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.time | utc | date:"H:i" }} to {{ assignments.first.timeslot.end_time | utc | date:"H:i" }} UTC){% endif %}.{% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting. {% for assignment in assignments %}Session {{ forloop.counter }}: {{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.time | utc | date:"H:i" }} to {{ assignment.timeslot.end_time | utc | date:"H:i" }} UTC){% endif %} @@ -10,11 +8,15 @@ {% if meeting.city %}Meeting Location: {{ meeting.city }}, {{ meeting.country }} -{% endif %}Agenda: +{% endif %}{% endfilter %} +Agenda: {{ meeting.session_set.first.agenda | document_content | default_if_none:"(No agenda submitted)" }} Information about remote participation: -{{ meeting.session_set.first.remote_instructions }} +{{ meeting.session_set.first.remote_instructions.rstrip }} -{{ meeting.session_set.first.agenda_note }} -{% endtimezone %} \ No newline at end of file +{{ meeting.session_set.first.agenda_note.rstrip|wordwrap:78 }}{% endtimezone %} + +-- +A calendar subscription for all {{ group.acronym }} meetings is available at +{{ settings.IDTRACKER_BASE_URL }}{% url 'ietf.meeting.views.upcoming_ical' %}?show={{ group.acronym }} diff --git a/ietf/templates/registration/add_email_exists_email.txt b/ietf/templates/registration/add_email_exists_email.txt new file mode 100644 index 00000000000..ed23a746e1f --- /dev/null +++ b/ietf/templates/registration/add_email_exists_email.txt @@ -0,0 +1,14 @@ +{% autoescape off %}{% load ietf_filters %} +Hello, + +{% filter wordwrap:78 %}We have received a request to add the email address {{ email }} to the user account '{{ person.user }}' at {{ domain }}. +This email address {{ email }} is already associated with your account at {{ domain }} and cannot be associated with two accounts.{% endfilter %} + +If you did not request this change, you may safely ignore this email, +as no actions have been taken. + +Best regards, + + The datatracker login manager service + (for the IETF Secretariat) +{% endautoescape %} diff --git a/ietf/templates/registration/creation_exists_email.txt b/ietf/templates/registration/creation_exists_email.txt new file mode 100644 index 00000000000..a7dc363ac5d --- /dev/null +++ b/ietf/templates/registration/creation_exists_email.txt @@ -0,0 +1,17 @@ +{% autoescape off %}{% load ietf_filters %} +Hello, + +{% filter wordwrap:78 %}We have received an account creation request for {{ username }} at {{ domain }}.{% endfilter %} + +{% filter wordwrap:78 %}A datatracker account for that email already exists. If you have forgotten the password for the {{ username }} account, please go to the following link and follow the instructions there:{% endfilter %} + + https://{{ domain }}{% url "ietf.ietfauth.views.password_reset" %} + +If you have not requested the account creation you can ignore this email, your +credentials have been left untouched. + +Best regards, + + The datatracker login manager service + (for the IETF Secretariat) +{% endautoescape %} diff --git a/ietf/templates/registration/password_reset.html b/ietf/templates/registration/password_reset.html index e3540344ab4..42aa74495fe 100644 --- a/ietf/templates/registration/password_reset.html +++ b/ietf/templates/registration/password_reset.html @@ -10,6 +10,8 @@

Password reset successful

Your password reset request has been successfully received. We have sent you an email with instructions on how to set a new password. + If you do not receive this email, please contact + {{ settings.SECRETARIAT_SUPPORT_EMAIL }}.

{% else %}

Password reset

diff --git a/playwright/helpers/meeting.js b/playwright/helpers/meeting.js index c627bf1c9ea..29d748d640a 100644 --- a/playwright/helpers/meeting.js +++ b/playwright/helpers/meeting.js @@ -121,6 +121,7 @@ let lastSessionId = 25000 let lastRecordingId = 150000 function createEvent ({ name = '', + slotName = '', startDateTime, duration = '1h', area, @@ -152,6 +153,7 @@ function createEvent ({ acronym: group.keyword, duration: typeof duration === 'string' ? ms(duration) / 1000 : duration, name: eventName, + slotName: slotName, startDateTime: startDateTime.toISO({ includeOffset: false, suppressMilliseconds: true }), status, type, @@ -514,7 +516,7 @@ module.exports = { _.times(8, () => { // 8 lanes per session time const { area, ...group } = daySessions.pop() schedule.push(createEvent({ - name: 'Session I', + slotName: 'Session I', startDateTime: curDay.set({ hour: 10 }), duration: '2h', type: 'regular', @@ -543,7 +545,7 @@ module.exports = { _.times(8, () => { // 8 lanes per session time const { area, ...group } = daySessions.pop() schedule.push(createEvent({ - name: 'Session II', + slotName: 'Session II', startDateTime: curDay.set({ hour: 13, minute: 30 }), duration: '1h', type: 'regular', @@ -574,7 +576,7 @@ module.exports = { _.times(8, () => { // 8 lanes per session time const { area, ...group } = daySessions.pop() schedule.push(createEvent({ - name: 'Session III', + slotName: 'Session III', startDateTime: curDay.set({ hour: 15 }), duration: '2h', type: 'regular', diff --git a/playwright/tests/meeting/agenda.spec.js b/playwright/tests/meeting/agenda.spec.js index 1c5a82f1861..0bc24799611 100644 --- a/playwright/tests/meeting/agenda.spec.js +++ b/playwright/tests/meeting/agenda.spec.js @@ -138,6 +138,7 @@ test.describe('past - desktop', () => { .setLocale(BROWSER_LOCALE) .toFormat('DD \'at\' T ZZZZ') await expect(page.locator('.agenda h6').first()).toContainText(localDateTime) + await expect(page.locator('.agenda .agenda-table-display-session-head .agenda-table-cell-name').first()).toContainText('Monday Session I') // Switch to UTC await tzUtcBtnLocator.click() await expect(tzUtcBtnLocator).toHaveClass(/n-button--primary-type/) @@ -148,10 +149,12 @@ test.describe('past - desktop', () => { .toFormat('DD \'at\' T ZZZZ') await expect(page.locator('.agenda h6').first()).toContainText(utcDateTime) await expect(page.locator('.agenda .agenda-timezone-ddn')).toContainText('UTC') + await expect(page.locator('.agenda .agenda-table-display-session-head .agenda-table-cell-name').first()).toContainText('Monday Session I') // Switch back to meeting timezone await tzMeetingBtnLocator.click() await expect(tzMeetingBtnLocator).toHaveClass(/n-button--primary-type/) await expect(page.locator('.agenda .agenda-timezone-ddn')).toContainText('Tokyo') + await expect(page.locator('.agenda .agenda-table-display-session-head .agenda-table-cell-name').first()).toContainText('Monday Session I') }) })