Merge pull request #5740 from ietf-tools/feat/django4

feat!: django4
This commit is contained in:
Robert Sparks 2023-06-01 16:52:02 -05:00 committed by GitHub
commit f711c83a66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
123 changed files with 748 additions and 552 deletions

View file

@ -91,10 +91,10 @@ jobs:
echo "pkg_version=$NEXT_VERSION" >> $GITHUB_OUTPUT echo "pkg_version=$NEXT_VERSION" >> $GITHUB_OUTPUT
echo "::notice::Release $NEXT_VERSION created using branch $GITHUB_REF_NAME" echo "::notice::Release $NEXT_VERSION created using branch $GITHUB_REF_NAME"
else else
echo "Using TEST mode: 10.0.0-dev.$GITHUB_RUN_NUMBER" echo "Using TEST mode: 11.0.0-dev.$GITHUB_RUN_NUMBER"
echo "should_deploy=false" >> $GITHUB_OUTPUT echo "should_deploy=false" >> $GITHUB_OUTPUT
echo "pkg_version=10.0.0-dev.$GITHUB_RUN_NUMBER" >> $GITHUB_OUTPUT echo "pkg_version=11.0.0-dev.$GITHUB_RUN_NUMBER" >> $GITHUB_OUTPUT
echo "::notice::Non-production build 10.0.0-dev.$GITHUB_RUN_NUMBER created using branch $GITHUB_REF_NAME" echo "::notice::Non-production build 11.0.0-dev.$GITHUB_RUN_NUMBER created using branch $GITHUB_REF_NAME"
fi fi
# ----------------------------------------------------------------- # -----------------------------------------------------------------

View file

@ -4,6 +4,7 @@ on:
pull_request: pull_request:
branches: branches:
- 'main' - 'main'
- 'feat/django4'
paths: paths:
- 'client/**' - 'client/**'
- 'ietf/**' - 'ietf/**'

View file

@ -10,7 +10,7 @@ DATABASES = {
'HOST': '__DBHOST__', 'HOST': '__DBHOST__',
'PORT': 5432, 'PORT': 5432,
'NAME': 'datatracker', 'NAME': 'datatracker',
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql',
'USER': 'django', 'USER': 'django',
'PASSWORD': 'RkTkDPFnKpko', 'PASSWORD': 'RkTkDPFnKpko',
}, },

View file

@ -10,7 +10,7 @@ DATABASES = {
'HOST': '__DBHOST__', 'HOST': '__DBHOST__',
'PORT': 5432, 'PORT': 5432,
'NAME': 'datatracker', 'NAME': 'datatracker',
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql',
'USER': 'django', 'USER': 'django',
'PASSWORD': 'RkTkDPFnKpko', 'PASSWORD': 'RkTkDPFnKpko',
}, },

View file

@ -10,7 +10,7 @@ DATABASES = {
'HOST': 'db', 'HOST': 'db',
'PORT': 5432, 'PORT': 5432,
'NAME': 'datatracker', 'NAME': 'datatracker',
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql',
'USER': 'django', 'USER': 'django',
'PASSWORD': 'RkTkDPFnKpko', 'PASSWORD': 'RkTkDPFnKpko',
}, },

View file

@ -3,7 +3,7 @@ DATABASES = {
'HOST': 'db', 'HOST': 'db',
'PORT': 5432, 'PORT': 5432,
'NAME': 'datatracker', 'NAME': 'datatracker',
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql',
'USER': 'django', 'USER': 'django',
'PASSWORD': 'RkTkDPFnKpko', 'PASSWORD': 'RkTkDPFnKpko',
}, },

View file

@ -6,7 +6,7 @@ from . import checks # pyflakes:ignore
# Version must stay in single quotes for automatic CI replace # Version must stay in single quotes for automatic CI replace
# Don't add patch number here: # Don't add patch number here:
__version__ = '10.0.0-dev' __version__ = '11.0.0-dev'
# Release hash must stay in single quotes for automatic CI replace # Release hash must stay in single quotes for automatic CI replace
__release_hash__ = '' __release_hash__ = ''

View file

@ -9,11 +9,12 @@ from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, FieldError from django.core.exceptions import ObjectDoesNotExist, FieldError
from django.core.serializers.json import Serializer from django.core.serializers.json import Serializer
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.encoding import smart_text from django.utils.encoding import smart_str
from django.db.models import Field from django.db.models import Field
from django.db.models.query import QuerySet
from django.db.models.signals import post_save, post_delete, m2m_changed from django.db.models.signals import post_save, post_delete, m2m_changed
from django_stubs_ext import QuerySetAny
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -121,7 +122,7 @@ class AdminJsonSerializer(Serializer):
for name in expansions: for name in expansions:
try: try:
field = getattr(obj, name) field = getattr(obj, name)
#self._current["_"+name] = smart_text(field) #self._current["_"+name] = smart_str(field)
if not isinstance(field, Field): if not isinstance(field, Field):
options = self.options.copy() options = self.options.copy()
options["expand"] = [ v[len(name)+2:] for v in options["expand"] if v.startswith(name+"__") ] options["expand"] = [ v[len(name)+2:] for v in options["expand"] if v.startswith(name+"__") ]
@ -145,7 +146,7 @@ class AdminJsonSerializer(Serializer):
field_value = None field_value = None
else: else:
field_value = field field_value = field
if isinstance(field_value, QuerySet) or isinstance(field_value, list): if isinstance(field_value, QuerySetAny) or isinstance(field_value, list):
self._current[name] = dict([ (rel.pk, self.expand_related(rel, name)) for rel in field_value ]) self._current[name] = dict([ (rel.pk, self.expand_related(rel, name)) for rel in field_value ])
else: else:
if hasattr(field_value, "_meta"): if hasattr(field_value, "_meta"):
@ -188,10 +189,10 @@ class AdminJsonSerializer(Serializer):
related = related.natural_key() related = related.natural_key()
elif field.remote_field.field_name == related._meta.pk.name: elif field.remote_field.field_name == related._meta.pk.name:
# Related to remote object via primary key # Related to remote object via primary key
related = smart_text(related._get_pk_val(), strings_only=True) related = smart_str(related._get_pk_val(), strings_only=True)
else: else:
# Related to remote object via other field # Related to remote object via other field
related = smart_text(getattr(related, field.remote_field.field_name), strings_only=True) related = smart_str(getattr(related, field.remote_field.field_name), strings_only=True)
self._current[field.name] = related self._current[field.name] = related
def handle_m2m_field(self, obj, field): def handle_m2m_field(self, obj, field):
@ -201,7 +202,7 @@ class AdminJsonSerializer(Serializer):
elif self.use_natural_keys and hasattr(field.remote_field.to, 'natural_key'): elif self.use_natural_keys and hasattr(field.remote_field.to, 'natural_key'):
m2m_value = lambda value: value.natural_key() m2m_value = lambda value: value.natural_key()
else: else:
m2m_value = lambda value: smart_text(value._get_pk_val(), strings_only=True) m2m_value = lambda value: smart_str(value._get_pk_val(), strings_only=True)
self._current[field.name] = [m2m_value(related) self._current[field.name] = [m2m_value(related)
for related in getattr(obj, field.name).iterator()] for related in getattr(obj, field.name).iterator()]
@ -221,7 +222,7 @@ class JsonExportMixin(object):
# obj = None # obj = None
# #
# if obj is None: # if obj is None:
# raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(self.model._meta.verbose_name), 'key': escape(object_id)}) # raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_str(self.model._meta.verbose_name), 'key': escape(object_id)})
# #
# content_type = 'application/json' # content_type = 'application/json'
# return HttpResponse(serialize([ obj ], sort_keys=True, indent=3)[2:-2], content_type=content_type) # return HttpResponse(serialize([ obj ], sort_keys=True, indent=3)[2:-2], content_type=content_type)
@ -264,6 +265,6 @@ class JsonExportMixin(object):
qd = dict( ( k, json.loads(v)[0] ) for k,v in items ) qd = dict( ( k, json.loads(v)[0] ) for k,v in items )
except (FieldError, ValueError) as e: except (FieldError, ValueError) as e:
return HttpResponse(json.dumps({"error": str(e)}, sort_keys=True, indent=3), content_type=content_type) return HttpResponse(json.dumps({"error": str(e)}, sort_keys=True, indent=3), content_type=content_type)
text = json.dumps({smart_text(self.model._meta): qd}, sort_keys=True, indent=3) text = json.dumps({smart_str(self.model._meta): qd}, sort_keys=True, indent=3)
return HttpResponse(text, content_type=content_type) return HttpResponse(text, content_type=content_type)

View file

@ -691,7 +691,7 @@ class CustomApiTests(TestCase):
self.assertEqual(set(missing_fields), set(drop_fields)) self.assertEqual(set(missing_fields), set(drop_fields))
def test_api_version(self): def test_api_version(self):
DumpInfo.objects.create(date=timezone.datetime(2022,8,31,7,10,1,tzinfo=timezone.utc), host='testapi.example.com',tz='UTC') DumpInfo.objects.create(date=timezone.datetime(2022,8,31,7,10,1,tzinfo=datetime.timezone.utc), host='testapi.example.com',tz='UTC')
url = urlreverse('ietf.api.views.version') url = urlreverse('ietf.api.views.version')
r = self.client.get(url) r = self.client.get(url)
data = r.json() data = r.json()

View file

@ -1,7 +1,7 @@
# Copyright The IETF Trust 2017, All Rights Reserved # Copyright The IETF Trust 2017, All Rights Reserved
from django.conf import settings from django.conf import settings
from django.conf.urls import include from django.urls import include
from django.views.generic import TemplateView from django.views.generic import TemplateView
from ietf import api from ietf import api

View file

@ -22,6 +22,7 @@ from ietf.community.utils import docs_tracked_by_community_list, docs_matching_c
from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule
from ietf.doc.models import DocEvent, Document from ietf.doc.models import DocEvent, Document
from ietf.doc.utils_search import prepare_document_table from ietf.doc.utils_search import prepare_document_table
from ietf.utils.http import is_ajax
from ietf.utils.response import permission_denied from ietf.utils.response import permission_denied
def view_list(request, username=None): def view_list(request, username=None):
@ -142,7 +143,7 @@ def track_document(request, name, username=None, acronym=None):
if not doc in clist.added_docs.all(): if not doc in clist.added_docs.all():
clist.added_docs.add(doc) clist.added_docs.add(doc)
if request.is_ajax(): if is_ajax(request):
return HttpResponse(json.dumps({ 'success': True }), content_type='application/json') return HttpResponse(json.dumps({ 'success': True }), content_type='application/json')
else: else:
return HttpResponseRedirect(clist.get_absolute_url()) return HttpResponseRedirect(clist.get_absolute_url())
@ -162,7 +163,7 @@ def untrack_document(request, name, username=None, acronym=None):
if clist.pk is not None: if clist.pk is not None:
clist.added_docs.remove(doc) clist.added_docs.remove(doc)
if request.is_ajax(): if is_ajax(request):
return HttpResponse(json.dumps({ 'success': True }), content_type='application/json') return HttpResponse(json.dumps({ 'success': True }), content_type='application/json')
else: else:
return HttpResponseRedirect(clist.get_absolute_url()) return HttpResponseRedirect(clist.get_absolute_url())

View file

@ -11,7 +11,7 @@ from django.utils.html import strip_tags
from django.conf import settings from django.conf import settings
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.doc.templatetags.mail_filters import std_level_prompt from ietf.doc.templatetags.mail_filters import std_level_prompt
@ -175,7 +175,7 @@ def generate_ballot_writeup(request, doc):
e.doc = doc e.doc = doc
e.rev = doc.rev e.rev = doc.rev
e.desc = "Ballot writeup was generated" e.desc = "Ballot writeup was generated"
e.text = force_text(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana, 'doc': doc })) e.text = force_str(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana, 'doc': doc }))
# caller is responsible for saving, if necessary # caller is responsible for saving, if necessary
return e return e
@ -187,7 +187,7 @@ def generate_ballot_rfceditornote(request, doc):
e.doc = doc e.doc = doc
e.rev = doc.rev e.rev = doc.rev
e.desc = "RFC Editor Note for ballot was generated" e.desc = "RFC Editor Note for ballot was generated"
e.text = force_text(render_to_string("doc/mail/ballot_rfceditornote.txt")) e.text = force_str(render_to_string("doc/mail/ballot_rfceditornote.txt"))
e.save() e.save()
return e return e
@ -232,7 +232,7 @@ def generate_last_call_announcement(request, doc):
e.doc = doc e.doc = doc
e.rev = doc.rev e.rev = doc.rev
e.desc = "Last call announcement was generated" e.desc = "Last call announcement was generated"
e.text = force_text(mail) e.text = force_str(mail)
# caller is responsible for saving, if necessary # caller is responsible for saving, if necessary
return e return e
@ -252,7 +252,7 @@ def generate_approval_mail(request, doc):
e.doc = doc e.doc = doc
e.rev = doc.rev e.rev = doc.rev
e.desc = "Ballot approval text was generated" e.desc = "Ballot approval text was generated"
e.text = force_text(mail) e.text = force_str(mail)
# caller is responsible for saving, if necessary # caller is responsible for saving, if necessary
return e return e

View file

@ -0,0 +1,36 @@
# Generated by Django 4.0.10 on 2023-05-16 20:36
from django.db import migrations
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('person', '0001_initial'),
('doc', '0003_remove_document_info_order'),
]
operations = [
migrations.AlterField(
model_name='dochistory',
name='ad',
field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_%(class)s_set', to='person.person', verbose_name='area director'),
),
migrations.AlterField(
model_name='dochistory',
name='shepherd',
field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_%(class)s_set', to='person.email'),
),
migrations.AlterField(
model_name='document',
name='ad',
field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ad_%(class)s_set', to='person.person', verbose_name='area director'),
),
migrations.AlterField(
model_name='document',
name='shepherd',
field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shepherd_%(class)s_set', to='person.email'),
),
]

View file

@ -24,7 +24,7 @@ from django.urls import reverse as urlreverse
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.html import mark_safe # type:ignore from django.utils.html import mark_safe # type:ignore
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
@ -1134,7 +1134,7 @@ class DocHistory(DocumentInfo):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
def __str__(self): def __str__(self):
return force_text(self.doc.name) return force_str(self.doc.name)
def get_related_session(self): def get_related_session(self):
return self.doc.get_related_session() return self.doc.get_related_session()
@ -1196,7 +1196,7 @@ class DocAlias(models.Model):
return self.docs.first() return self.docs.first()
def __str__(self): def __str__(self):
return u"%s-->%s" % (self.name, ','.join([force_text(d.name) for d in self.docs.all() if isinstance(d, Document) ])) return u"%s-->%s" % (self.name, ','.join([force_str(d.name) for d in self.docs.all() if isinstance(d, Document) ]))
document_link = admin_link("document") document_link = admin_link("document")
class Meta: class Meta:
verbose_name = "document alias" verbose_name = "document alias"

View file

@ -13,8 +13,7 @@ from django.utils.html import escape
from django.template.defaultfilters import truncatewords_html, linebreaksbr, stringfilter, striptags from django.template.defaultfilters import truncatewords_html, linebreaksbr, stringfilter, striptags
from django.utils.safestring import mark_safe, SafeData from django.utils.safestring import mark_safe, SafeData
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.encoding import force_str # pyflakes:ignore force_str is used in the doctests
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -132,7 +131,7 @@ register.filter('fill', fill)
@register.filter @register.filter
def prettystdname(string, space=" "): def prettystdname(string, space=" "):
from ietf.doc.utils import prettify_std_name from ietf.doc.utils import prettify_std_name
return prettify_std_name(force_text(string or ""), space) return prettify_std_name(force_str(string or ""), space)
@register.filter @register.filter
def rfceditor_info_url(rfcnum : str): def rfceditor_info_url(rfcnum : str):

View file

@ -1779,7 +1779,7 @@ class DocTestCase(TestCase):
self.client.login(username='ad', password='ad+password') self.client.login(username='ad', password='ad+password')
r = self.client.post(urlreverse('ietf.doc.views_status_change.change_state',kwargs=dict(name=doc.name)),dict(new_state=iesgeval_pk)) r = self.client.post(urlreverse('ietf.doc.views_status_change.change_state',kwargs=dict(name=doc.name)),dict(new_state=iesgeval_pk))
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302)
r = self.client.get(r._headers["location"][1]) r = self.client.get(r.headers["location"])
self.assertContains(r, ">IESG Evaluation<") self.assertContains(r, ">IESG Evaluation<")
self.assertEqual(len(outbox), 2) self.assertEqual(len(outbox), 2)
self.assertIn('iesg-secretary',outbox[0]['To']) self.assertIn('iesg-secretary',outbox[0]['To'])

View file

@ -573,7 +573,7 @@ class ResurrectTests(DraftFileMixin, TestCase):
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(len(q('form [type=submit]')), 1) self.assertEqual(len(q('#content form [type=submit]')), 1)
# request resurrect # request resurrect
@ -609,7 +609,7 @@ class ResurrectTests(DraftFileMixin, TestCase):
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(len(q('form [type=submit]')), 1) self.assertEqual(len(q('#content form [type=submit]')), 1)
# complete resurrect # complete resurrect
events_before = draft.docevent_set.count() events_before = draft.docevent_set.count()

View file

@ -118,7 +118,7 @@ class EditAuthorsTests(IetfSeleniumTestCase):
# Must provide a "basis" (change reason) # Must provide a "basis" (change reason)
self.driver.find_element(By.ID, 'id_basis').send_keys('change testing') self.driver.find_element(By.ID, 'id_basis').send_keys('change testing')
# Now click the 'submit' button and check that the update was accepted. # Now click the 'submit' button and check that the update was accepted.
submit_button = self.driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]') submit_button = self.driver.find_element(By.CSS_SELECTOR, '#content button[type="submit"]')
self.driver.execute_script("arguments[0].click();", submit_button) # FIXME: no idea why this fails: self.driver.execute_script("arguments[0].click();", submit_button) # FIXME: no idea why this fails:
# self.scroll_to_element(submit_button) # self.scroll_to_element(submit_button)
# submit_button.click() # submit_button.click()
@ -132,4 +132,4 @@ class EditAuthorsTests(IetfSeleniumTestCase):
self.assertEqual( self.assertEqual(
list(draft.documentauthor_set.values_list('person', flat=True)), list(draft.documentauthor_set.values_list('person', flat=True)),
[first_auth.person.pk] + [auth.pk for auth in authors] [first_auth.person.pk] + [auth.pk for auth in authors]
) )

View file

@ -33,9 +33,9 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from django.conf.urls import include
from django.views.generic import RedirectView
from django.conf import settings from django.conf import settings
from django.urls import include
from django.views.generic import RedirectView
from ietf.doc import views_search, views_draft, views_ballot, views_status_change, views_doc, views_downref, views_stats, views_help, views_bofreq from ietf.doc import views_search, views_draft, views_ballot, views_status_change, views_doc, views_downref, views_stats, views_help, views_bofreq
from ietf.utils.urls import url from ietf.utils.urls import url

View file

@ -17,7 +17,6 @@ from zoneinfo import ZoneInfo
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.db.models import QuerySet
from django.forms import ValidationError from django.forms import ValidationError
from django.http import Http404 from django.http import Http404
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -25,6 +24,7 @@ from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django_stubs_ext import QuerySetAny
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.community.models import CommunityList from ietf.community.models import CommunityList
@ -345,7 +345,7 @@ def augment_events_with_revision(doc, events):
"""Take a set of events for doc and add a .rev attribute with the """Take a set of events for doc and add a .rev attribute with the
revision they refer to by checking NewRevisionDocEvents.""" revision they refer to by checking NewRevisionDocEvents."""
if isinstance(events, QuerySet): if isinstance(events, QuerySetAny):
qs = events.filter(newrevisiondocevent__isnull=False) qs = events.filter(newrevisiondocevent__isnull=False)
else: else:
qs = NewRevisionDocEvent.objects.filter(doc=doc) qs = NewRevisionDocEvent.objects.filter(doc=doc)
@ -353,7 +353,7 @@ def augment_events_with_revision(doc, events):
if doc.type_id == "draft" and doc.get_state_slug() == "rfc": if doc.type_id == "draft" and doc.get_state_slug() == "rfc":
# add fake "RFC" revision # add fake "RFC" revision
if isinstance(events, QuerySet): if isinstance(events, QuerySetAny):
e = events.filter(type="published_rfc").order_by('time').last() e = events.filter(type="published_rfc").order_by('time').last()
else: else:
e = doc.latest_event(type="published_rfc") e = doc.latest_event(type="published_rfc")

View file

@ -12,7 +12,7 @@ from django.conf import settings
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import smart_text, force_text from django.utils.encoding import smart_str, force_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -153,7 +153,7 @@ def generate_ballot_writeup(request, doc):
e.doc = doc e.doc = doc
e.rev = doc.rev, e.rev = doc.rev,
e.desc = "Ballot writeup was generated" e.desc = "Ballot writeup was generated"
e.text = force_text(render_to_string("doc/charter/ballot_writeup.txt")) e.text = force_str(render_to_string("doc/charter/ballot_writeup.txt"))
# caller is responsible for saving, if necessary # caller is responsible for saving, if necessary
return e return e
@ -197,7 +197,7 @@ def derive_new_work_text(review_text,group):
'Reply_to':'<iesg@ietf.org>'}) 'Reply_to':'<iesg@ietf.org>'})
if not addrs.cc: if not addrs.cc:
del m['Cc'] del m['Cc']
return smart_text(m.as_string()) return smart_str(m.as_string())
def default_review_text(group, charter, by): def default_review_text(group, charter, by):
now = timezone.now() now = timezone.now()

View file

@ -17,7 +17,7 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.html import escape from django.utils.html import escape
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -821,7 +821,7 @@ def charter_with_milestones_txt(request, name, rev):
try: try:
with io.open(os.path.join(settings.CHARTER_PATH, filename), 'r') as f: with io.open(os.path.join(settings.CHARTER_PATH, filename), 'r') as f:
charter_text = force_text(f.read(), errors='ignore') charter_text = force_str(f.read(), errors='ignore')
except IOError: except IOError:
charter_text = "Error reading charter text %s" % filename charter_text = "Error reading charter text %s" % filename

View file

@ -11,7 +11,7 @@ import requests
import email.utils import email.utils
from django.utils import timezone from django.utils import timezone
from django.utils.http import is_safe_url from django.utils.http import url_has_allowed_host_and_scheme
from simple_history.utils import update_change_reason from simple_history.utils import update_change_reason
@ -53,6 +53,7 @@ from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils.mail import send_mail_message from ietf.utils.mail import send_mail_message
from ietf.mailtrigger.utils import gather_address_lists from ietf.mailtrigger.utils import gather_address_lists
from ietf.utils.fields import MultiEmailField from ietf.utils.fields import MultiEmailField
from ietf.utils.http import is_ajax
from ietf.utils.response import permission_denied from ietf.utils.response import permission_denied
from ietf.utils.timezone import date_today, DEADLINE_TZINFO from ietf.utils.timezone import date_today, DEADLINE_TZINFO
@ -1087,11 +1088,16 @@ def review_wishes_remove(request, name):
def _generate_ajax_or_redirect_response(request, doc): def _generate_ajax_or_redirect_response(request, doc):
redirect_url = request.GET.get('next') redirect_url = request.GET.get("next")
url_is_safe = is_safe_url(url=redirect_url, allowed_hosts=request.get_host(), url_is_safe = url_has_allowed_host_and_scheme(
require_https=request.is_secure()) url=redirect_url,
if request.is_ajax(): allowed_hosts=request.get_host(),
return HttpResponse(json.dumps({'success': True}), content_type='application/json') require_https=request.is_secure(),
)
if is_ajax(request):
return HttpResponse(
json.dumps({"success": True}), content_type="application/json"
)
elif url_is_safe: elif url_is_safe:
return HttpResponseRedirect(redirect_url) return HttpResponseRedirect(redirect_url)
else: else:

View file

@ -765,7 +765,9 @@ def drafts_in_iesg_process(request):
if s.slug == "lc": if s.slug == "lc":
for d in docs: for d in docs:
e = d.latest_event(LastCallDocEvent, type="sent_last_call") e = d.latest_event(LastCallDocEvent, type="sent_last_call")
d.lc_expires = e.expires if e else datetime.datetime.min # If we don't have an event, use an arbitrary date in the past (but not datetime.datetime.min,
# which causes problems with timezone conversions)
d.lc_expires = e.expires if e else datetime.datetime(1950, 1, 1)
docs = list(docs) docs = list(docs)
docs.sort(key=lambda d: d.lc_expires) docs.sort(key=lambda d: d.lc_expires)

View file

@ -7,6 +7,7 @@ from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.db.models.aggregates import Count from django.db.models.aggregates import Count
from django.db.models.functions import TruncDate
from django.http import JsonResponse, HttpResponseBadRequest from django.http import JsonResponse, HttpResponseBadRequest
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
@ -24,7 +25,6 @@ from ietf.utils.timezone import date_today
epochday = datetime.datetime.utcfromtimestamp(0).date().toordinal() epochday = datetime.datetime.utcfromtimestamp(0).date().toordinal()
column_chart_conf = settings.CHART_TYPE_COLUMN_OPTIONS
def dt(s): def dt(s):
"Convert the date string returned by sqlite's date() to a datetime.date" "Convert the date string returned by sqlite's date() to a datetime.date"
@ -40,15 +40,12 @@ def model_to_timeline_data(model, field='time', **kwargs):
assert field in [ f.name for f in model._meta.get_fields() ] assert field in [ f.name for f in model._meta.get_fields() ]
objects = ( model.objects.filter(**kwargs) objects = ( model.objects.filter(**kwargs)
.annotate(date=TruncDate(field))
.order_by('date') .order_by('date')
.extra(select={'date': 'date(%s.%s)'% (model._meta.db_table, field) })
.values('date') .values('date')
.annotate(count=Count('id'))) .annotate(count=Count('id')))
if objects.exists(): if objects.exists():
obj_list = list(objects) obj_list = list(objects)
# This is needed for sqlite, when we're running tests:
if type(obj_list[0]['date']) != datetime.date:
obj_list = [ {'date': dt(e['date']), 'count': e['count']} for e in obj_list ]
today = date_today(datetime.timezone.utc) today = date_today(datetime.timezone.utc)
if not obj_list[-1]['date'] == today: if not obj_list[-1]['date'] == today:
obj_list += [ {'date': today, 'count': 0} ] obj_list += [ {'date': today, 'count': 0} ]

View file

@ -15,7 +15,7 @@ from django.http import Http404, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.html import escape from django.utils.html import escape
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -531,7 +531,7 @@ def rfc_status_changes(request):
) )
@role_required("Area Director","Secretariat") @role_required("Area Director","Secretariat")
def start_rfc_status_change(request,name): def start_rfc_status_change(request, name=None):
"""Start the RFC status change review process, setting the initial shepherding AD, and possibly putting the review on a telechat.""" """Start the RFC status change review process, setting the initial shepherding AD, and possibly putting the review on a telechat."""
if name: if name:
@ -665,7 +665,7 @@ def generate_last_call_text(request, doc):
e.doc = doc e.doc = doc
e.rev = doc.rev e.rev = doc.rev
e.desc = 'Last call announcement was generated' e.desc = 'Last call announcement was generated'
e.text = force_text(new_text) e.text = force_str(new_text)
e.save() e.save()
return e return e

View file

@ -14,9 +14,9 @@ from django.contrib.admin.utils import unquote
from django.core.management import load_command_class from django.core.management import load_command_class
from django.http import Http404 from django.http import Http404
from django.shortcuts import render from django.shortcuts import render
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone, from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent, GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
@ -152,7 +152,7 @@ class GroupAdmin(admin.ModelAdmin):
permission_denied(request, "You don't have edit permissions for this change.") permission_denied(request, "You don't have edit permissions for this change.")
if obj is None: if obj is None:
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)}) raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_str(opts.verbose_name), 'key': escape(object_id)})
return self.send_reminder(request, sdo=obj) return self.send_reminder(request, sdo=obj)

View file

@ -1,7 +1,7 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved # Copyright The IETF Trust 2013-2020, All Rights Reserved
from django.conf import settings from django.conf import settings
from django.conf.urls import include from django.urls import include
from django.views.generic import RedirectView from django.views.generic import RedirectView
from ietf.community import views as community_views from ietf.community import views as community_views

View file

@ -300,8 +300,27 @@ def active_groups(request, group_type=None):
raise Http404 raise Http404
def active_group_types(request): def active_group_types(request):
grouptypes = GroupTypeName.objects.filter(slug__in=['wg','rg','ag','rag','team','dir','review','area','program','iabasg','adm']).filter(group__state='active').annotate(group_count=Count('group')) grouptypes = (
return render(request, 'group/active_groups.html', {'grouptypes':grouptypes}) GroupTypeName.objects.filter(
slug__in=[
"wg",
"rg",
"ag",
"rag",
"team",
"dir",
"review",
"area",
"program",
"iabasg",
"adm",
]
)
.filter(group__state="active")
.order_by('order', 'name') # default ordering ignored for "GROUP BY" queries, make it explicit
.annotate(group_count=Count("group"))
)
return render(request, "group/active_groups.html", {"grouptypes": grouptypes})
def active_dirs(request): def active_dirs(request):
dirs = Group.objects.filter(type__in=['dir', 'review'], state="active").order_by("name") dirs = Group.objects.filter(type__in=['dir', 'review'], state="active").order_by("name")

View file

@ -11,14 +11,14 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django_password_strength.widgets import PasswordStrengthInput, PasswordConfirmationInput
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.person.models import Person, Email from ietf.person.models import Person, Email
from ietf.mailinglists.models import Allowlisted from ietf.mailinglists.models import Allowlisted
from ietf.utils.text import isascii from ietf.utils.text import isascii
from .widgets import PasswordStrengthInput, PasswordConfirmationInput
class RegistrationForm(forms.Form): class RegistrationForm(forms.Form):
email = forms.EmailField(label="Your email (lowercase)") email = forms.EmailField(label="Your email (lowercase)")

View file

@ -98,7 +98,7 @@ class IetfAuthTests(TestCase):
self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile)) self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile))
# try logging out # try logging out
r = self.client.get(urlreverse('django.contrib.auth.views.logout')) r = self.client.post(urlreverse('django.contrib.auth.views.logout'), {})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertNotContains(r, "accounts/logout") self.assertNotContains(r, "accounts/logout")
@ -215,7 +215,7 @@ class IetfAuthTests(TestCase):
self.assertContains(r, "Allowlist entry creation successful") self.assertContains(r, "Allowlist entry creation successful")
# log out # log out
r = self.client.get(urlreverse('django.contrib.auth.views.logout')) r = self.client.post(urlreverse('django.contrib.auth.views.logout'), {})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
# register and verify allowlisted email # register and verify allowlisted email
@ -664,7 +664,7 @@ class IetfAuthTests(TestCase):
"new_password_confirmation": "foobar", "new_password_confirmation": "foobar",
}) })
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'current_password', 'Invalid password') self.assertFormError(r.context["form"], 'current_password', 'Invalid password')
# mismatching new passwords # mismatching new passwords
r = self.client.post(chpw_url, {"current_password": "password", r = self.client.post(chpw_url, {"current_password": "password",
@ -672,7 +672,7 @@ class IetfAuthTests(TestCase):
"new_password_confirmation": "barfoo", "new_password_confirmation": "barfoo",
}) })
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', None, "The password confirmation is different than the new password") self.assertFormError(r.context["form"], None, "The password confirmation is different than the new password")
# correct password change # correct password change
r = self.client.post(chpw_url, {"current_password": "password", r = self.client.post(chpw_url, {"current_password": "password",
@ -711,7 +711,7 @@ class IetfAuthTests(TestCase):
"password": "password", "password": "password",
}) })
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'username', self.assertFormError(r.context["form"], 'username',
"Select a valid choice. fiddlesticks is not one of the available choices.") "Select a valid choice. fiddlesticks is not one of the available choices.")
# wrong password # wrong password
@ -719,7 +719,7 @@ class IetfAuthTests(TestCase):
"password": "foobar", "password": "foobar",
}) })
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'password', 'Invalid password') self.assertFormError(r.context["form"], 'password', 'Invalid password')
# correct username change # correct username change
r = self.client.post(chun_url, {"username": "othername@example.org", r = self.client.post(chun_url, {"username": "othername@example.org",

View file

@ -7,7 +7,8 @@
import oidc_provider.lib.claims import oidc_provider.lib.claims
from functools import wraps from functools import wraps, WRAPPER_ASSIGNMENTS
from urllib.parse import quote as urlquote
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
@ -15,8 +16,6 @@ from django.core.exceptions import PermissionDenied
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.decorators import available_attrs
from django.utils.http import urlquote
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -113,7 +112,7 @@ def passes_test_decorator(test_func, message):
error. The test function should be on the form fn(user) -> error. The test function should be on the form fn(user) ->
true/false.""" true/false."""
def decorate(view_func): def decorate(view_func):
@wraps(view_func, assigned=available_attrs(view_func)) @wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
def inner(request, *args, **kwargs): def inner(request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return HttpResponseRedirect('%s?%s=%s' % (settings.LOGIN_URL, REDIRECT_FIELD_NAME, urlquote(request.get_full_path()))) return HttpResponseRedirect('%s?%s=%s' % (settings.LOGIN_URL, REDIRECT_FIELD_NAME, urlquote(request.get_full_path())))

114
ietf/ietfauth/widgets.py Normal file
View file

@ -0,0 +1,114 @@
from django.forms import PasswordInput
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
# The PasswordStrengthInput and PasswordConfirmationInput widgets come from the
# django-password-strength project, https://pypi.org/project/django-password-strength/
#
# Original license:
#
# Copyright &copy; 2015 A.J. May and individual contributors. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
# following disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
# products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
class PasswordStrengthInput(PasswordInput):
"""
Form widget to show the user how strong his/her password is.
"""
def render(self, name, value, attrs=None, renderer=None):
strength_markup = """
<div style="margin-top: 10px;">
<div class="progress" style="margin-bottom: 10px;">
<div class="progress-bar progress-bar-warning password_strength_bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="5" style="width: 0%%"></div>
</div>
<p class="text-muted password_strength_info hidden">
<span class="label label-danger">
%s
</span>
<span style="margin-left:5px;">
%s
</span>
</p>
</div>
""" % (
_("Warning"),
_(
'This password would take <em class="password_strength_time"></em> to crack.'
),
)
try:
self.attrs["class"] = "%s password_strength".strip() % self.attrs["class"]
except KeyError:
self.attrs["class"] = "password_strength"
return mark_safe(
super(PasswordInput, self).render(name, value, attrs, renderer)
+ strength_markup
)
class Media:
js = (
"ietf/js/zxcvbn.js",
"ietf/js/password_strength.js",
)
class PasswordConfirmationInput(PasswordInput):
"""
Form widget to confirm the users password by letting him/her type it again.
"""
def __init__(self, confirm_with=None, attrs=None, render_value=False):
super(PasswordConfirmationInput, self).__init__(attrs, render_value)
self.confirm_with = confirm_with
def render(self, name, value, attrs=None, renderer=None):
if self.confirm_with:
self.attrs["data-confirm-with"] = "id_%s" % self.confirm_with
confirmation_markup = """
<div style="margin-top: 10px;" class="hidden password_strength_info">
<p class="text-muted">
<span class="label label-danger">
%s
</span>
<span style="margin-left:5px;">%s</span>
</p>
</div>
""" % (
_("Warning"),
_("Your passwords don't match."),
)
try:
self.attrs["class"] = (
"%s password_confirmation".strip() % self.attrs["class"]
)
except KeyError:
self.attrs["class"] = "password_confirmation"
return mark_safe(
super(PasswordInput, self).render(name, value, attrs, renderer)
+ confirmation_markup
)

View file

@ -6,7 +6,7 @@ from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed from django.utils.feedgenerator import Atom1Feed
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.encoding import force_text from django.utils.encoding import force_str
from ietf.ipr.models import IprDisclosureBase from ietf.ipr.models import IprDisclosureBase
@ -25,7 +25,7 @@ class LatestIprDisclosuresFeed(Feed):
return mark_safe(item.title) return mark_safe(item.title)
def item_description(self, item): def item_description(self, item):
return force_text(item.title) return force_str(item.title)
def item_pubdate(self, item): def item_pubdate(self, item):
return item.time return item.time

View file

@ -12,7 +12,7 @@ from email import message_from_bytes
from email.utils import parsedate_tz from email.utils import parsedate_tz
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.encoding import force_text, force_bytes from django.utils.encoding import force_str, force_bytes
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -102,7 +102,7 @@ def get_reply_to():
address with "plus addressing" using a random string. Guaranteed to be unique""" address with "plus addressing" using a random string. Guaranteed to be unique"""
local,domain = get_base_ipr_request_address().split('@') local,domain = get_base_ipr_request_address().split('@')
while True: while True:
rand = force_text(base64.urlsafe_b64encode(os.urandom(12))) rand = force_str(base64.urlsafe_b64encode(os.urandom(12)))
address = "{}+{}@{}".format(local,rand,domain) address = "{}+{}@{}".format(local,rand,domain)
q = Message.objects.filter(reply_to=address) q = Message.objects.filter(reply_to=address)
if not q: if not q:

View file

@ -9,16 +9,15 @@ import operator
from typing import Union # pyflakes:ignore from typing import Union # pyflakes:ignore
from email.utils import parseaddr from email.utils import parseaddr
from form_utils.forms import BetterModelForm
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models.query import QuerySet
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.db.models import Q from django.db.models import Q
#from django.forms.widgets import RadioFieldRenderer #from django.forms.widgets import RadioFieldRenderer
from django.core.validators import validate_email from django.core.validators import validate_email
from django_stubs_ext import QuerySetAny
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -132,7 +131,7 @@ class AddCommentForm(forms.Form):
# def render(self): # def render(self):
# output = [] # output = []
# for widget in self: # for widget in self:
# output.append(format_html(force_text(widget))) # output.append(format_html(force_str(widget)))
# return mark_safe('\n'.join(output)) # return mark_safe('\n'.join(output))
@ -204,7 +203,7 @@ class SearchLiaisonForm(forms.Form):
class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField): class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
'''If value is a QuerySet, return it as is (for use in widget.render)''' '''If value is a QuerySet, return it as is (for use in widget.render)'''
def prepare_value(self, value): def prepare_value(self, value):
if isinstance(value, QuerySet): if isinstance(value, QuerySetAny):
return value return value
if (hasattr(value, '__iter__') and if (hasattr(value, '__iter__') and
not isinstance(value, str) and not isinstance(value, str) and
@ -213,7 +212,7 @@ class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
return super(CustomModelMultipleChoiceField, self).prepare_value(value) return super(CustomModelMultipleChoiceField, self).prepare_value(value)
class LiaisonModelForm(BetterModelForm): class LiaisonModelForm(forms.ModelForm):
'''Specify fields which require a custom widget or that are not part of the model. '''Specify fields which require a custom widget or that are not part of the model.
''' '''
from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False) from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False)
@ -238,13 +237,6 @@ class LiaisonModelForm(BetterModelForm):
class Meta: class Meta:
model = LiaisonStatement model = LiaisonStatement
exclude = ('attachments','state','from_name','to_name') exclude = ('attachments','state','from_name','to_name')
fieldsets = [('From', {'fields': ['from_groups','from_contact', 'response_contacts'], 'legend': ''}),
('To', {'fields': ['to_groups','to_contacts'], 'legend': ''}),
('Other email addresses', {'fields': ['technical_contacts','action_holder_contacts','cc_contacts'], 'legend': ''}),
('Purpose', {'fields':['purpose', 'deadline'], 'legend': ''}),
('Reference', {'fields': ['other_identifiers','related_to'], 'legend': ''}),
('Liaison Statement', {'fields': ['title', 'submitted_date', 'body', 'attachments'], 'legend': ''}),
('Add attachment', {'fields': ['attach_title', 'attach_file', 'attach_button'], 'legend': ''})]
def __init__(self, user, *args, **kwargs): def __init__(self, user, *args, **kwargs):
super(LiaisonModelForm, self).__init__(*args, **kwargs) super(LiaisonModelForm, self).__init__(*args, **kwargs)
@ -476,14 +468,6 @@ class OutgoingLiaisonForm(LiaisonModelForm):
class Meta: class Meta:
model = LiaisonStatement model = LiaisonStatement
exclude = ('attachments','state','from_name','to_name','action_holder_contacts') exclude = ('attachments','state','from_name','to_name','action_holder_contacts')
# add approved field, no action_holder_contacts
fieldsets = [('From', {'fields': ['from_groups','from_contact','response_contacts','approved'], 'legend': ''}),
('To', {'fields': ['to_groups','to_contacts'], 'legend': ''}),
('Other email addresses', {'fields': ['technical_contacts','cc_contacts'], 'legend': ''}),
('Purpose', {'fields':['purpose', 'deadline'], 'legend': ''}),
('Reference', {'fields': ['other_identifiers','related_to'], 'legend': ''}),
('Liaison Statement', {'fields': ['title', 'submitted_date', 'body', 'attachments'], 'legend': ''}),
('Add attachment', {'fields': ['attach_title', 'attach_file', 'attach_button'], 'legend': ''})]
def is_approved(self): def is_approved(self):
return self.cleaned_data['approved'] return self.cleaned_data['approved']

View file

@ -3,11 +3,12 @@
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.db.models.query import QuerySet
from django.forms.widgets import Widget from django.forms.widgets import Widget
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from django_stubs_ext import QuerySetAny
class ButtonWidget(Widget): class ButtonWidget(Widget):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -34,7 +35,7 @@ class ShowAttachmentsWidget(Widget):
html = '<div id="id_%s">' % name html = '<div id="id_%s">' % name
html += '<span class="d-none showAttachmentsEmpty form-control widget">No files attached</span>' html += '<span class="d-none showAttachmentsEmpty form-control widget">No files attached</span>'
html += '<div class="attachedFiles form-control widget">' html += '<div class="attachedFiles form-control widget">'
if value and isinstance(value, QuerySet): if value and isinstance(value, QuerySetAny):
for attachment in value: for attachment in value:
html += '<a class="initialAttach" href="%s">%s</a>&nbsp' % (conditional_escape(attachment.document.get_href()), conditional_escape(attachment.document.title)) html += '<a class="initialAttach" href="%s">%s</a>&nbsp' % (conditional_escape(attachment.document.get_href()), conditional_escape(attachment.document.title))
html += '<a class="btn btn-primary btn-sm" href="{}">Edit</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_edit_attachment", kwargs={'object_id':attachment.statement.pk,'doc_id':attachment.document.pk})) html += '<a class="btn btn-primary btn-sm" href="{}">Edit</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_edit_attachment", kwargs={'object_id':attachment.statement.pk,'doc_id':attachment.document.pk}))
@ -43,4 +44,4 @@ class ShowAttachmentsWidget(Widget):
else: else:
html += 'No files attached' html += 'No files attached'
html += '</div></div>' html += '</div></div>'
return mark_safe(html) return mark_safe(html)

View file

@ -7,9 +7,7 @@ import datetime
import shutil import shutil
import os import os
import re import re
from unittest import skipIf
import django
from django.utils import timezone from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
from django.db.models import F from django.db.models import F
@ -880,42 +878,6 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.assertNotIn('would-violate-hint', session_elements[4].get_attribute('class'), self.assertNotIn('would-violate-hint', session_elements[4].get_attribute('class'),
'Constraint violation should not be indicated on non-conflicting session') 'Constraint violation should not be indicated on non-conflicting session')
@ifSeleniumEnabled
@skipIf(django.VERSION[0]==2, "Skipping test with race conditions under Django 2")
class ScheduleEditTests(IetfSeleniumTestCase):
def testUnschedule(self):
meeting = make_meeting_test_data()
self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting=meeting, session__group__acronym='mars', schedule__name='test-schedule').count(),1)
ss = list(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule')) # pyflakes:ignore
self.login()
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule',kwargs=dict(num='72',name='test-schedule',owner='plain@example.com'))
self.driver.get(url)
# driver.get() will wait for scripts to finish, but not ajax
# requests. Wait for completion of the permissions check:
read_only_note = self.driver.find_element(By.ID, 'read_only')
WebDriverWait(self.driver, 10).until(expected_conditions.invisibility_of_element(read_only_note), "Read-only schedule")
s1 = Session.objects.filter(group__acronym='mars', meeting=meeting).first()
selector = "#session_{}".format(s1.pk)
WebDriverWait(self.driver, 30).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)), "Did not find %s"%selector)
self.assertEqual(self.driver.find_elements(By.CSS_SELECTOR, "#sortable-list #session_{}".format(s1.pk)), [])
element = self.driver.find_element(By.ID, 'session_{}'.format(s1.pk))
target = self.driver.find_element(By.ID, 'sortable-list')
ActionChains(self.driver).drag_and_drop(element,target).perform()
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, "#sortable-list #session_{}".format(s1.pk)))
time.sleep(0.1) # The API that modifies the database runs async
self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule').count(),0)
@ifSeleniumEnabled @ifSeleniumEnabled
class SlideReorderTests(IetfSeleniumTestCase): class SlideReorderTests(IetfSeleniumTestCase):
@ -1531,7 +1493,7 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
"""Test the timeslot editor""" """Test the timeslot editor"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.meeting: Meeting = MeetingFactory( self.meeting: Meeting = MeetingFactory( # type: ignore[annotation-unchecked]
type_id='ietf', type_id='ietf',
number=120, number=120,
date=date_today() + datetime.timedelta(days=10), date=date_today() + datetime.timedelta(days=10),
@ -1608,13 +1570,13 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
delete_time = delete_time_local.astimezone(datetime.timezone.utc) delete_time = delete_time_local.astimezone(datetime.timezone.utc)
duration = datetime.timedelta(minutes=60) duration = datetime.timedelta(minutes=60)
delete: [TimeSlot] = TimeSlotFactory.create_batch( delete: [TimeSlot] = TimeSlotFactory.create_batch( # type: ignore[annotation-unchecked]
2, 2,
meeting=self.meeting, meeting=self.meeting,
time=delete_time_local, time=delete_time_local,
duration=duration, duration=duration,
) )
keep: [TimeSlot] = [ keep: [TimeSlot] = [ # type: ignore[annotation-unchecked]
TimeSlotFactory( TimeSlotFactory(
meeting=self.meeting, meeting=self.meeting,
time=keep_time, time=keep_time,
@ -1651,14 +1613,14 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
hours = [10, 12] hours = [10, 12]
other_days = [self.meeting.get_meeting_date(d) for d in range(1, 3)] other_days = [self.meeting.get_meeting_date(d) for d in range(1, 3)]
delete: [TimeSlot] = [ delete: [TimeSlot] = [ # type: ignore[annotation-unchecked]
TimeSlotFactory( TimeSlotFactory(
meeting=self.meeting, meeting=self.meeting,
time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=hour), time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=hour),
) for hour in hours ) for hour in hours
] ]
keep: [TimeSlot] = [ keep: [TimeSlot] = [ # type: ignore[annotation-unchecked]
TimeSlotFactory( TimeSlotFactory(
meeting=self.meeting, meeting=self.meeting,
time=datetime_from_date(day, self.meeting.tz()).replace(hour=hour), time=datetime_from_date(day, self.meeting.tz()).replace(hour=hour),

View file

@ -556,7 +556,7 @@ class MeetingTests(BaseMeetingTestCase):
self.assertContains(r, "1. More work items underway") self.assertContains(r, "1. More work items underway")
cont_disp = r._headers.get('content-disposition', ('Content-Disposition', ''))[1] cont_disp = r.headers.get('content-disposition', ('Content-Disposition', ''))[1]
cont_disp = re.split('; ?', cont_disp) cont_disp = re.split('; ?', cont_disp)
cont_disp_settings = dict( e.split('=', 1) for e in cont_disp if '=' in e ) cont_disp_settings = dict( e.split('=', 1) for e in cont_disp if '=' in e )
filename = cont_disp_settings.get('filename', '').strip('"') filename = cont_disp_settings.get('filename', '').strip('"')
@ -2357,7 +2357,7 @@ class EditTimeslotsTests(TestCase):
def test_invalid_edit_timeslot(self): def test_invalid_edit_timeslot(self):
meeting = self.create_bare_meeting() meeting = self.create_bare_meeting()
ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # n.b., colon indicates type hinting ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # type: ignore[annotation-unchecked]
self.login() self.login()
r = self.client.post( r = self.client.post(
self.edit_timeslot_url(ts), self.edit_timeslot_url(ts),
@ -3931,7 +3931,7 @@ class EditTests(TestCase):
'base': meeting.schedule.base_id, 'base': meeting.schedule.base_id,
}) })
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'name', 'Enter a valid value.') self.assertFormError(r.context["form"], 'name', 'Enter a valid value.')
self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created') self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created')
r = self.client.post(url, { r = self.client.post(url, {
@ -3941,7 +3941,7 @@ class EditTests(TestCase):
'base': meeting.schedule.base_id, 'base': meeting.schedule.base_id,
}) })
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'name', 'Enter a valid value.') self.assertFormError(r.context["form"], 'name', 'Enter a valid value.')
self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created') self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created')
# Non-ASCII alphanumeric characters # Non-ASCII alphanumeric characters
@ -3952,7 +3952,7 @@ class EditTests(TestCase):
'base': meeting.schedule.base_id, 'base': meeting.schedule.base_id,
}) })
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'name', 'Enter a valid value.') self.assertFormError(r.context["form"], 'name', 'Enter a valid value.')
self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created') self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created')
def test_edit_session(self): def test_edit_session(self):
@ -4039,9 +4039,9 @@ class EditTests(TestCase):
self.assertIn(return_url_unofficial, r.content.decode()) self.assertIn(return_url_unofficial, r.content.decode())
r = self.client.post(url, {}) r = self.client.post(url, {})
self.assertFormError(r, 'form', 'confirmed', 'This field is required.') self.assertFormError(r.context["form"], 'confirmed', 'This field is required.')
r = self.client.post(url_unofficial, {}) r = self.client.post(url_unofficial, {})
self.assertFormError(r, 'form', 'confirmed', 'This field is required.') self.assertFormError(r.context["form"], 'confirmed', 'This field is required.')
r = self.client.post(url, {'confirmed': 'on'}) r = self.client.post(url, {'confirmed': 'on'})
self.assertRedirects(r, return_url) self.assertRedirects(r, return_url)
@ -6520,8 +6520,8 @@ class ImportNotesTests(TestCase):
r = self.client.get(url) # try to import the same text r = self.client.get(url) # try to import the same text
self.assertContains(r, "This document is identical", status_code=200) self.assertContains(r, "This document is identical", status_code=200)
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(len(q('button:disabled[type="submit"]')), 1) self.assertEqual(len(q('#content button:disabled[type="submit"]')), 1)
self.assertEqual(len(q('button:enabled[type="submit"]')), 0) self.assertEqual(len(q('#content button:enabled[type="submit"]')), 0)
def test_allows_import_on_existing_bad_unicode(self): def test_allows_import_on_existing_bad_unicode(self):
"""Should not be able to import text identical to the current revision""" """Should not be able to import text identical to the current revision"""
@ -6545,8 +6545,8 @@ class ImportNotesTests(TestCase):
r = self.client.get(url) # try to import the same text r = self.client.get(url) # try to import the same text
self.assertNotContains(r, "This document is identical", status_code=200) self.assertNotContains(r, "This document is identical", status_code=200)
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(len(q('button:enabled[type="submit"]')), 1) self.assertEqual(len(q('#content button:enabled[type="submit"]')), 1)
self.assertEqual(len(q('button:disabled[type="submit"]')), 0) self.assertEqual(len(q('#content button:disabled[type="submit"]')), 0)
def test_handles_missing_previous_revision_file(self): def test_handles_missing_previous_revision_file(self):
"""Should still allow import if the file for the previous revision is missing""" """Should still allow import if the file for the previous revision is missing"""
@ -7975,7 +7975,7 @@ class ProceedingsTests(BaseMeetingTestCase):
invalid_file.seek(0) # read the file contents again invalid_file.seek(0) # read the file contents again
r = self.client.post(url, {'file': invalid_file, 'external_url': ''}) r = self.client.post(url, {'file': invalid_file, 'external_url': ''})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'file', 'Found an unexpected extension: .png. Expected one of .pdf') self.assertFormError(r.context["form"], 'file', 'Found an unexpected extension: .png. Expected one of .pdf')
def test_add_proceedings_material_doc_empty(self): def test_add_proceedings_material_doc_empty(self):
"""Upload proceedings materials document without specifying a file""" """Upload proceedings materials document without specifying a file"""
@ -7988,7 +7988,7 @@ class ProceedingsTests(BaseMeetingTestCase):
) )
r = self.client.post(url, {'external_url': ''}) r = self.client.post(url, {'external_url': ''})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'file', 'This field is required') self.assertFormError(r.context["form"], 'file', 'This field is required')
def test_add_proceedings_material_url(self): def test_add_proceedings_material_url(self):
"""Add a URL as proceedings material""" """Add a URL as proceedings material"""
@ -8012,7 +8012,7 @@ class ProceedingsTests(BaseMeetingTestCase):
) )
r = self.client.post(url, {'use_url': 'on', 'external_url': "Ceci n'est pas une URL"}) r = self.client.post(url, {'use_url': 'on', 'external_url': "Ceci n'est pas une URL"})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'external_url', 'Enter a valid URL.') self.assertFormError(r.context["form"], 'external_url', 'Enter a valid URL.')
def test_add_proceedings_material_url_empty(self): def test_add_proceedings_material_url_empty(self):
"""Add proceedings materials URL without specifying the URL""" """Add proceedings materials URL without specifying the URL"""
@ -8025,7 +8025,7 @@ class ProceedingsTests(BaseMeetingTestCase):
) )
r = self.client.post(url, {'use_url': 'on', 'external_url': ''}) r = self.client.post(url, {'use_url': 'on', 'external_url': ''})
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertFormError(r, 'form', 'external_url', 'This field is required') self.assertFormError(r.context["form"], 'external_url', 'This field is required')
@override_settings(MEETING_DOC_HREFS={'procmaterials': '{doc.name}:{doc.rev}'}) @override_settings(MEETING_DOC_HREFS={'procmaterials': '{doc.name}:{doc.rev}'})
def test_replace_proceedings_material(self): def test_replace_proceedings_material(self):

View file

@ -1,8 +1,8 @@
# Copyright The IETF Trust 2007-2020, All Rights Reserved # Copyright The IETF Trust 2007-2020, All Rights Reserved
from django.conf.urls import include
from django.views.generic import RedirectView
from django.conf import settings from django.conf import settings
from django.urls import include
from django.views.generic import RedirectView
from ietf.meeting import views, views_proceedings from ietf.meeting import views, views_proceedings
from ietf.utils.urls import url from ietf.utils.urls import url

View file

@ -15,7 +15,7 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import smart_text from django.utils.encoding import smart_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -699,7 +699,7 @@ def handle_upload_file(file, filename, meeting, subdir, request=None, encoding=N
) )
else: else:
try: try:
text = smart_text(text) text = smart_str(text)
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
return "Failure trying to save '%s'. Hint: Try to upload as UTF-8: %s..." % (filename, str(e)[:120]) return "Failure trying to save '%s'. Hint: Try to upload as UTF-8: %s..." % (filename, str(e)[:120])
# Whole file sanitization; add back what's missing from a complete # Whole file sanitization; add back what's missing from a complete

View file

@ -17,6 +17,7 @@ import tempfile
from calendar import timegm from calendar import timegm
from collections import OrderedDict, Counter, deque, defaultdict, namedtuple from collections import OrderedDict, Counter, deque, defaultdict, namedtuple
from functools import partialmethod
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
from tempfile import mkstemp from tempfile import mkstemp
from wsgiref.handlers import format_date_time from wsgiref.handlers import format_date_time
@ -38,7 +39,6 @@ from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.functional import curry
from django.utils.text import slugify from django.utils.text import slugify
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
@ -2730,7 +2730,7 @@ def upload_session_agenda(request, session_id, num):
}) })
def upload_session_slides(request, session_id, num, name): def upload_session_slides(request, session_id, num, name=None):
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure # num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
session = get_object_or_404(Session,pk=session_id) session = get_object_or_404(Session,pk=session_id)
if not session.can_manage_materials(request.user): if not session.can_manage_materials(request.user):
@ -3210,8 +3210,8 @@ def interim_request(request):
if meeting_type in ('single', 'multi-day'): if meeting_type in ('single', 'multi-day'):
meeting = form.save(date=get_earliest_session_date(formset)) meeting = form.save(date=get_earliest_session_date(formset))
# need to use curry here to pass custom variable to form init # need to use partialmethod here to pass custom variable to form init
SessionFormset.form.__init__ = curry( SessionFormset.form.__init__ = partialmethod(
InterimSessionModelForm.__init__, InterimSessionModelForm.__init__,
user=request.user, user=request.user,
group=group, group=group,
@ -3233,7 +3233,7 @@ def interim_request(request):
# subsequently dealt with individually # subsequently dealt with individually
elif meeting_type == 'series': elif meeting_type == 'series':
series = [] series = []
SessionFormset.form.__init__ = curry( SessionFormset.form.__init__ = partialmethod(
InterimSessionModelForm.__init__, InterimSessionModelForm.__init__,
user=request.user, user=request.user,
group=group, group=group,
@ -3453,7 +3453,7 @@ def interim_request_edit(request, number):
group = Group.objects.get(pk=form.data['group']) group = Group.objects.get(pk=form.data['group'])
is_approved = is_interim_meeting_approved(meeting) is_approved = is_interim_meeting_approved(meeting)
SessionFormset.form.__init__ = curry( SessionFormset.form.__init__ = partialmethod(
InterimSessionModelForm.__init__, InterimSessionModelForm.__init__,
user=request.user, user=request.user,
group=group, group=group,

View file

@ -67,7 +67,7 @@ class Command(BaseCommand):
pprint(connection.queries) pprint(connection.queries)
raise raise
objects = [] # type: List[object] objects: List[object] = [] # type: ignore[annotation-unchecked]
model_objects = {} model_objects = {}
import ietf.name.models import ietf.name.models

View file

@ -3,10 +3,11 @@
import functools import functools
from urllib.parse import quote as urlquote
from django.urls import reverse from django.urls import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils.http import urlquote
def nomcom_private_key_required(view_func): def nomcom_private_key_required(view_func):

View file

@ -66,7 +66,7 @@ Tdb0MiLc+r/zvx8oXtgDjDUa
def provide_private_key_to_test_client(testcase): def provide_private_key_to_test_client(testcase):
session = testcase.client.session session = testcase.client.session
session['NOMCOM_PRIVATE_KEY_%s'%testcase.nc.year()] = key session['NOMCOM_PRIVATE_KEY_%s'%testcase.nc.year()] = key.decode("utf8")
session.save() session.save()
def nomcom_kwargs_for_year(year=None, *args, **kwargs): def nomcom_kwargs_for_year(year=None, *args, **kwargs):

View file

@ -188,8 +188,9 @@ class NomineePosition(models.Model):
def save(self, **kwargs): def save(self, **kwargs):
if not self.pk and not self.state_id: if not self.pk and not self.state_id:
# Don't need to set update_fields because the self.pk test means this is a new instance
self.state = NomineePositionStateName.objects.get(slug='pending') self.state = NomineePositionStateName.objects.get(slug='pending')
super(NomineePosition, self).save(**kwargs) super().save(**kwargs)
def __str__(self): def __str__(self):
return "%s - %s - %s" % (self.nominee, self.state, self.position) return "%s - %s - %s" % (self.nominee, self.state, self.position)

View file

@ -6,7 +6,7 @@ import re
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.template.defaultfilters import linebreaksbr, force_escape from django.template.defaultfilters import linebreaksbr, force_escape
from django.utils.encoding import force_text, DjangoUnicodeDecodeError from django.utils.encoding import force_str, DjangoUnicodeDecodeError
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -55,8 +55,10 @@ def formatted_email(address):
@register.simple_tag @register.simple_tag
def decrypt(string, request, year, plain=False): def decrypt(string, request, year, plain=False):
key = retrieve_nomcom_private_key(request, year) try:
key = retrieve_nomcom_private_key(request, year)
except UnicodeError:
return f"-*- Encrypted text [Error retrieving private key, contact the secretariat ({settings.SECRETARIAT_SUPPORT_EMAIL})]"
if not key: if not key:
return '-*- Encrypted text [No private key provided] -*-' return '-*- Encrypted text [No private key provided] -*-'
@ -68,7 +70,7 @@ def decrypt(string, request, year, plain=False):
code, out, error = pipe(command % (settings.OPENSSL_COMMAND, code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
encrypted_file.name), key) encrypted_file.name), key)
try: try:
out = force_text(out) out = force_str(out)
except DjangoUnicodeDecodeError: except DjangoUnicodeDecodeError:
pass pass
if code != 0: if code != 0:

View file

@ -4,6 +4,7 @@
import datetime import datetime
import io import io
import mock
import random import random
import shutil import shutil
@ -1577,6 +1578,16 @@ class NewActiveNomComTests(TestCase):
login_testing_unauthorized(self,self.chair.user.username,url) login_testing_unauthorized(self,self.chair.user.username,url)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code,200) self.assertEqual(response.status_code,200)
# Check that we get an error if there's an encoding problem talking to openssl
# "\xc3\x28" is an invalid utf8 string
with mock.patch("ietf.nomcom.utils.pipe", return_value=(0, b"\xc3\x28", None)):
response = self.client.post(url, {'key': force_str(key)})
self.assertFormError(
response.context["form"],
None,
"An internal error occurred while adding your private key to your session."
f"Please contact the secretariat for assistance ({settings.SECRETARIAT_SUPPORT_EMAIL})",
)
response = self.client.post(url,{'key': force_str(key)}) response = self.client.post(url,{'key': force_str(key)})
self.assertEqual(response.status_code,302) self.assertEqual(response.status_code,302)
@ -1993,7 +2004,7 @@ class NoPublicKeyTests(TestCase):
text_bits = [x.xpath('.//text()') for x in q('.alert-warning')] text_bits = [x.xpath('.//text()') for x in q('.alert-warning')]
flat_text_bits = [item for sublist in text_bits for item in sublist] flat_text_bits = [item for sublist in text_bits for item in sublist]
self.assertTrue(any(['not yet' in y for y in flat_text_bits])) self.assertTrue(any(['not yet' in y for y in flat_text_bits]))
self.assertEqual(bool(q('form:not(.navbar-form)')),expected_form) self.assertEqual(bool(q('#content form:not(.navbar-form)')),expected_form)
self.client.logout() self.client.logout()
def test_not_yet(self): def test_not_yet(self):

View file

@ -172,6 +172,12 @@ def command_line_safe_secret(secret):
return base64.encodebytes(secret).decode('utf-8').rstrip() return base64.encodebytes(secret).decode('utf-8').rstrip()
def retrieve_nomcom_private_key(request, year): def retrieve_nomcom_private_key(request, year):
"""Retrieve decrypted nomcom private key from the session store
Retrieves encrypted, ascii-armored private key from the session store, encodes
as utf8 bytes, then decrypts. Raises UnicodeError if the value in the session
store cannot be encoded as utf8.
"""
private_key = request.session.get('NOMCOM_PRIVATE_KEY_%s' % year, None) private_key = request.session.get('NOMCOM_PRIVATE_KEY_%s' % year, None)
if not private_key: if not private_key:
@ -183,7 +189,8 @@ def retrieve_nomcom_private_key(request, year):
settings.OPENSSL_COMMAND, settings.OPENSSL_COMMAND,
command_line_safe_secret(settings.NOMCOM_APP_SECRET) command_line_safe_secret(settings.NOMCOM_APP_SECRET)
), ),
private_key # The openssl command expects ascii-armored input, so utf8 encoding should be valid
private_key.encode("utf8")
) )
if code != 0: if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error)) log("openssl error: %s:\n Error %s: %s" %(command, code, error))
@ -191,6 +198,12 @@ def retrieve_nomcom_private_key(request, year):
def store_nomcom_private_key(request, year, private_key): def store_nomcom_private_key(request, year, private_key):
"""Put encrypted nomcom private key in the session store
Encrypts the private key using openssl, then decodes the ascii-armored output
as utf8 and adds to the session store. Raises UnicodeError if the openssl's
output cannot be decoded as utf8.
"""
if not private_key: if not private_key:
request.session['NOMCOM_PRIVATE_KEY_%s' % year] = '' request.session['NOMCOM_PRIVATE_KEY_%s' % year] = ''
else: else:
@ -205,8 +218,9 @@ def store_nomcom_private_key(request, year, private_key):
if code != 0: if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error)) log("openssl error: %s:\n Error %s: %s" %(command, code, error))
if error and error!=b"*** WARNING : deprecated key derivation used.\nUsing -iter or -pbkdf2 would be better.\n": if error and error!=b"*** WARNING : deprecated key derivation used.\nUsing -iter or -pbkdf2 would be better.\n":
out = '' out = b''
request.session['NOMCOM_PRIVATE_KEY_%s' % year] = out # The openssl command output in 'out' is an ascii-armored value, so should be utf8-decodable
request.session['NOMCOM_PRIVATE_KEY_%s' % year] = out.decode("utf8")
def validate_private_key(key): def validate_private_key(key):

View file

@ -18,7 +18,7 @@ from django.http import Http404, HttpResponseRedirect, HttpResponse
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_bytes, force_text from django.utils.encoding import force_bytes, force_str
from ietf.dbtemplate.models import DBTemplate from ietf.dbtemplate.models import DBTemplate
@ -158,8 +158,16 @@ def private_key(request, year):
if request.method == 'POST': if request.method == 'POST':
form = PrivateKeyForm(data=request.POST) form = PrivateKeyForm(data=request.POST)
if form.is_valid(): if form.is_valid():
store_nomcom_private_key(request, year, force_bytes(form.cleaned_data.get('key', ''))) try:
return HttpResponseRedirect(back_url) store_nomcom_private_key(request, year, force_bytes(form.cleaned_data.get('key', '')))
except UnicodeError:
form.add_error(
None,
"An internal error occurred while adding your private key to your session."
f"Please contact the secretariat for assistance ({settings.SECRETARIAT_SUPPORT_EMAIL})"
)
else:
return HttpResponseRedirect(back_url)
else: else:
form = PrivateKeyForm() form = PrivateKeyForm()
@ -684,7 +692,7 @@ def private_questionnaire(request, year):
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(request, 'The questionnaire response has been registered.') messages.success(request, 'The questionnaire response has been registered.')
questionnaire_response = force_text(form.cleaned_data['comment_text']) questionnaire_response = force_str(form.cleaned_data['comment_text'])
form = QuestionnaireForm(nomcom=nomcom, user=request.user) form = QuestionnaireForm(nomcom=nomcom, user=request.user)
else: else:
form = QuestionnaireForm(nomcom=nomcom, user=request.user) form = QuestionnaireForm(nomcom=nomcom, user=request.user)

View file

@ -16,7 +16,7 @@ from unicodedata import normalize
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.encoding import force_text from django.utils.encoding import force_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -68,7 +68,7 @@ class PersonFactory(factory.django.DjangoModelFactory):
# Some i18n names, e.g., "शिला के.सी." have a dot at the end that is also part of the ASCII, e.g., "Shilaa Kesii." # Some i18n names, e.g., "शिला के.सी." have a dot at the end that is also part of the ASCII, e.g., "Shilaa Kesii."
# That trailing dot breaks extract_authors(). Avoid this issue by stripping the dot from the ASCII. # That trailing dot breaks extract_authors(). Avoid this issue by stripping the dot from the ASCII.
# Some others have a trailing semicolon (e.g., "உயிரோவியம் தங்கராஐ;") - strip those, too. # Some others have a trailing semicolon (e.g., "உயிரோவியம் தங்கராஐ;") - strip those, too.
ascii = factory.LazyAttribute(lambda p: force_text(unidecode_name(p.name)).rstrip(".;")) ascii = factory.LazyAttribute(lambda p: force_str(unidecode_name(p.name)).rstrip(".;"))
class Params: class Params:
with_bio = factory.Trait(biography = "\n\n".join(fake.paragraphs())) # type: ignore with_bio = factory.Trait(biography = "\n\n".join(fake.paragraphs())) # type: ignore

View file

@ -183,9 +183,12 @@ class AbstractReviewerQueuePolicy:
role__group=review_req.team role__group=review_req.team
).exclude( person_id__in=rejecting_reviewer_ids ) ).exclude( person_id__in=rejecting_reviewer_ids )
one_assignment = (review_req.reviewassignment_set one_assignment = None
.exclude(state__slug__in=('rejected', 'no-response')) if review_req.pk is not None:
.first()) # cannot use reviewassignment_set relation until review_req has been created
one_assignment = (review_req.reviewassignment_set
.exclude(state__slug__in=('rejected', 'no-response'))
.first())
if one_assignment: if one_assignment:
field.initial = one_assignment.reviewer_id field.initial = one_assignment.reviewer_id

View file

@ -382,7 +382,8 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa
# with a different view on a ReviewAssignment. # with a different view on a ReviewAssignment.
log.assertion('reviewer is not None') log.assertion('reviewer is not None')
if review_req.reviewassignment_set.filter(reviewer=reviewer).exists(): # cannot reference reviewassignment_set relation until pk exists
if review_req.pk is not None and review_req.reviewassignment_set.filter(reviewer=reviewer).exists():
return return
# Note that assigning a review no longer unassigns other reviews # Note that assigning a review no longer unassigns other reviews
@ -598,7 +599,9 @@ def suggested_review_requests_for_team(team):
res.sort(key=lambda r: (r.deadline, r.doc_id), reverse=True) res.sort(key=lambda r: (r.deadline, r.doc_id), reverse=True)
return res return res
def extract_revision_ordered_review_assignments_for_documents_and_replaced(review_assignment_queryset, names): def extract_revision_ordered_review_assignments_for_documents_and_replaced(
review_assignment_queryset, names
):
"""Extracts all review assignments for document names (including replaced ancestors), return them neatly sorted.""" """Extracts all review assignments for document names (including replaced ancestors), return them neatly sorted."""
names = set(names) names = set(names)
@ -607,8 +610,13 @@ def extract_revision_ordered_review_assignments_for_documents_and_replaced(revie
assignments_for_each_doc = defaultdict(list) assignments_for_each_doc = defaultdict(list)
replacement_name_set = set(e for l in replaces.values() for e in l) | names replacement_name_set = set(e for l in replaces.values() for e in l) | names
for r in ( review_assignment_queryset.filter(review_request__doc__name__in=replacement_name_set) for r in (
.order_by("-reviewed_rev","-assigned_on", "-id").iterator()): review_assignment_queryset.filter(
review_request__doc__name__in=replacement_name_set
)
.order_by("-reviewed_rev", "-assigned_on", "-id")
.iterator(chunk_size=2000) # chunk_size not tested, using pre-Django 5 default value
):
assignments_for_each_doc[r.review_request.doc.name].append(r) assignments_for_each_doc[r.review_request.doc.name].append(r)
# now collect in breadth-first order to keep the revision order intact # now collect in breadth-first order to keep the revision order intact
@ -646,7 +654,10 @@ def extract_revision_ordered_review_assignments_for_documents_and_replaced(revie
return res return res
def extract_revision_ordered_review_requests_for_documents_and_replaced(review_request_queryset, names):
def extract_revision_ordered_review_requests_for_documents_and_replaced(
review_request_queryset, names
):
"""Extracts all review requests for document names (including replaced ancestors), return them neatly sorted.""" """Extracts all review requests for document names (including replaced ancestors), return them neatly sorted."""
names = set(names) names = set(names)
@ -654,7 +665,13 @@ def extract_revision_ordered_review_requests_for_documents_and_replaced(review_r
replaces = extract_complete_replaces_ancestor_mapping_for_docs(names) replaces = extract_complete_replaces_ancestor_mapping_for_docs(names)
requests_for_each_doc = defaultdict(list) requests_for_each_doc = defaultdict(list)
for r in review_request_queryset.filter(doc__name__in=set(e for l in replaces.values() for e in l) | names).order_by("-time", "-id").iterator(): for r in (
review_request_queryset.filter(
doc__name__in=set(e for l in replaces.values() for e in l) | names
)
.order_by("-time", "-id")
.iterator(chunk_size=2000) # chunk_size not tested, using pre-Django 5 default value
):
requests_for_each_doc[r.doc.name].append(r) requests_for_each_doc[r.doc.name].append(r)
# now collect in breadth-first order to keep the revision order intact # now collect in breadth-first order to keep the revision order intact

View file

@ -4,10 +4,11 @@
import datetime import datetime
from functools import partialmethod
from django.contrib import messages from django.contrib import messages
from django.forms.formsets import formset_factory from django.forms.formsets import formset_factory
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.utils.functional import curry
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -215,7 +216,7 @@ def doc_detail(request, date, name):
initial_state = {'state':doc.get_state(state_type).pk, initial_state = {'state':doc.get_state(state_type).pk,
'substate':tag} 'substate':tag}
# need to use curry here to pass custom variable to form init # need to use partialmethod here to pass custom variable to form init
if doc.active_ballot(): if doc.active_ballot():
ballot_type = doc.active_ballot().ballot_type ballot_type = doc.active_ballot().ballot_type
elif doc.type.slug == 'draft': elif doc.type.slug == 'draft':
@ -223,7 +224,7 @@ def doc_detail(request, date, name):
else: else:
ballot_type = BallotType.objects.get(doc_type=doc.type) ballot_type = BallotType.objects.get(doc_type=doc.type)
BallotFormset = formset_factory(BallotForm, extra=0) BallotFormset = formset_factory(BallotForm, extra=0)
BallotFormset.form.__init__ = curry(BallotForm.__init__, ballot_type=ballot_type) BallotFormset.form.__init__ = partialmethod(BallotForm.__init__, ballot_type=ballot_type)
agenda = agenda_data(date=date) agenda = agenda_data(date=date)
header = get_section_header(doc, agenda) header = get_section_header(doc, agenda)

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Announcement{% endblock %} {% block title %}Announcement{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Areas{% endblock %} {% block title %}Areas{% endblock %}
{% block extrahead %}{{ block.super }} {% block extrahead %}{{ block.super }}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Areas - People{% endblock %} {% block title %}Areas - People{% endblock %}
{% block extrahead %}{{ block.super }} {% block extrahead %}{{ block.super }}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Areas - View{% endblock %} {% block title %}Areas - View{% endblock %}
{% block extrahead %}{{ block.super }} {% block extrahead %}{{ block.super }}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
{% load staticfiles %} {% load static %}
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
{% load staticfiles %} {% load static %}
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">

View file

@ -1,7 +1,7 @@
{% extends "base_secr.html" %} {% extends "base_secr.html" %}
{% load i18n %} {% load i18n %}
{% load ietf_filters %} {% load ietf_filters %}
{% load staticfiles %} {% load static %}
{% block title %}{{ title }}{% if user|has_role:"Secretariat" %} Secretariat Dashboard {% else %} IETF Dashboard {% endif %}{% endblock %} {% block title %}{{ title }}{% if user|has_role:"Secretariat" %} Secretariat Dashboard {% else %} IETF Dashboard {% endif %}{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "base_secr_bootstrap.html" %} {% extends "base_secr_bootstrap.html" %}
{% load i18n %} {% load i18n %}
{% load ietf_filters %} {% load ietf_filters %}
{% load staticfiles %} {% load static %}
{% block title %}{{ title }}{% if user|has_role:"Secretariat" %} Secretariat Dashboard {% else %} WG Chair Dashboard {% endif %}{% endblock %} {% block title %}{{ title }}{% if user|has_role:"Secretariat" %} Secretariat Dashboard {% else %} WG Chair Dashboard {% endif %}{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Confirm Cancel{% endblock %} {% block title %}Confirm Cancel{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Confirm Delete{% endblock %} {% block title %}Confirm Delete{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Meetings - Add{% endblock %} {% block title %}Meetings - Add{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site_bootstrap.html" %} {% extends "base_site_bootstrap.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Meetings{% endblock %} {% block title %}Meetings{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Meetings - Blue Sheet{% endblock %} {% block title %}Meetings - Blue Sheet{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Meetings - Edit{% endblock %} {% block title %}Meetings - Edit{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Meetings{% endblock %} {% block title %}Meetings{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Meetings{% endblock %} {% block title %}Meetings{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles tz %} {% load static tz %}
{% block title %}Meetings{% endblock %} {% block title %}Meetings{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Meetings{% endblock %} {% block title %}Meetings{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Rolodex - Add{% endblock %} {% block title %}Rolodex - Add{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Rolodex - Edit{% endblock %} {% block title %}Rolodex - Edit{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Rolodex - Search{% endblock %} {% block title %}Rolodex - Search{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Sessions - Confirm{% endblock %} {% block title %}Sessions - Confirm{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Sessions - Edit{% endblock %} {% block title %}Sessions - Edit{% endblock %}
{% block extrahead %}{{ block.super }} {% block extrahead %}{{ block.super }}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Sessions{% endblock %} {% block title %}Sessions{% endblock %}

View file

@ -1,6 +1,6 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load ietf_filters %} {% load ietf_filters %}
{% load staticfiles %} {% load static %}
{% block title %}Sessions{% endblock %} {% block title %}Sessions{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Sessions- New{% endblock %} {% block title %}Sessions- New{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Sessions{% endblock %} {% block title %}Sessions{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Sessions - View{% endblock %} {% block title %}Sessions - View{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_site.html" %} {% extends "base_site.html" %}
{% load staticfiles %} {% load static %}
{% block title %}Telechat{% endblock %} {% block title %}Telechat{% endblock %}

View file

@ -1,13 +1,13 @@
from django.conf.urls import url, include from django.urls import re_path, include
from django.views.generic import TemplateView from django.views.generic import TemplateView
urlpatterns = [ urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='main.html')), re_path(r'^$', TemplateView.as_view(template_name='main.html')),
url(r'^announcement/', include('ietf.secr.announcement.urls')), re_path(r'^announcement/', include('ietf.secr.announcement.urls')),
url(r'^areas/', include('ietf.secr.areas.urls')), re_path(r'^areas/', include('ietf.secr.areas.urls')),
url(r'^console/', include('ietf.secr.console.urls')), re_path(r'^console/', include('ietf.secr.console.urls')),
url(r'^meetings/', include('ietf.secr.meetings.urls')), re_path(r'^meetings/', include('ietf.secr.meetings.urls')),
url(r'^rolodex/', include('ietf.secr.rolodex.urls')), re_path(r'^rolodex/', include('ietf.secr.rolodex.urls')),
url(r'^sreq/', include('ietf.secr.sreq.urls')), re_path(r'^sreq/', include('ietf.secr.sreq.urls')),
url(r'^telechat/', include('ietf.secr.telechat.urls')), re_path(r'^telechat/', include('ietf.secr.telechat.urls')),
] ]

View file

@ -1,12 +1,12 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved # Copyright The IETF Trust 2013-2020, All Rights Reserved
from functools import wraps from functools import wraps
from urllib.parse import quote as urlquote
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.utils.http import urlquote
from ietf.ietfauth.utils import has_role from ietf.ietfauth.utils import has_role
from ietf.doc.models import Document from ietf.doc.models import Document

View file

@ -13,12 +13,18 @@ import warnings
from typing import Any, Dict, List, Tuple # pyflakes:ignore from typing import Any, Dict, List, Tuple # pyflakes:ignore
warnings.simplefilter("always", DeprecationWarning) warnings.simplefilter("always", DeprecationWarning)
warnings.filterwarnings("ignore", message="'urllib3\[secure\]' extra is deprecated") warnings.filterwarnings("ignore", message="pkg_resources is deprecated as an API")
warnings.filterwarnings("ignore", message="The logout\(\) view is superseded by") warnings.filterwarnings("ignore", "Log out via GET requests is deprecated") # happens in oidc_provider
warnings.filterwarnings("ignore", module="tastypie", message="The django.utils.datetime_safe module is deprecated.")
warnings.filterwarnings("ignore", module="oidc_provider", message="The django.utils.timezone.utc alias is deprecated.")
warnings.filterwarnings("ignore", message="The USE_DEPRECATED_PYTZ setting,") # https://github.com/ietf-tools/datatracker/issues/5635
warnings.filterwarnings("ignore", message="The USE_L10N setting is deprecated.") # https://github.com/ietf-tools/datatracker/issues/5648
warnings.filterwarnings("ignore", message="django.contrib.auth.hashers.CryptPasswordHasher is deprecated.") # https://github.com/ietf-tools/datatracker/issues/5663
warnings.filterwarnings("ignore", message="'urllib3\\[secure\\]' extra is deprecated")
warnings.filterwarnings("ignore", message="The logout\\(\\) view is superseded by")
warnings.filterwarnings("ignore", message="Report.file_reporters will no longer be available in Coverage.py 4.2", module="coverage.report") warnings.filterwarnings("ignore", message="Report.file_reporters will no longer be available in Coverage.py 4.2", module="coverage.report")
warnings.filterwarnings("ignore", message="{% load staticfiles %} is deprecated")
warnings.filterwarnings("ignore", message="Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated", module="bleach") warnings.filterwarnings("ignore", message="Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated", module="bleach")
warnings.filterwarnings("ignore", message="HTTPResponse.getheader\(\) is deprecated", module='selenium.webdriver') warnings.filterwarnings("ignore", message="HTTPResponse.getheader\\(\\) is deprecated", module='selenium.webdriver')
try: try:
import syslog import syslog
syslog.openlog(str("datatracker"), syslog.LOG_PID, syslog.LOG_USER) syslog.openlog(str("datatracker"), syslog.LOG_PID, syslog.LOG_USER)
@ -101,7 +107,23 @@ SITE_ID = 1
# to load the internationalization machinery. # to load the internationalization machinery.
USE_I18N = False USE_I18N = False
# Django 4.0 changed the default setting of USE_L10N to True. The setting
# is deprecated and will be removed in Django 5.0.
USE_L10N = False
USE_TZ = True USE_TZ = True
USE_DEPRECATED_PYTZ = True # supported until Django 5
# The DjangoDivFormRenderer is a transitional class that opts in to defaulting to the div.html
# template for formsets. This will become the default behavior in Django 5.0. This configuration
# can be removed at that point.
# See https://docs.djangoproject.com/en/4.2/releases/4.1/#forms
FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
# Default primary key field type to use for models that dont have a field with primary_key=True.
# In the future (relative to 4.2), the default will become 'django.db.models.BigAutoField.'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
if SERVER_MODE == 'production': if SERVER_MODE == 'production':
MEDIA_ROOT = '/a/www/www6s/lib/dt/media/' MEDIA_ROOT = '/a/www/www6s/lib/dt/media/'
@ -317,10 +339,10 @@ UTILS_LOGGER_LEVELS: Dict[str, str] = {
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
'ietf.org', "https://ietf.org",
'*.ietf.org', "https://*.ietf.org",
'meetecho.com', 'https://meetecho.com',
'*.meetecho.com', 'https://*.meetecho.com',
] ]
CSRF_COOKIE_SAMESITE = 'None' CSRF_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
@ -331,11 +353,7 @@ SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_EXPIRE_AT_BROWSER_CLOSE = False
# We want to use the JSON serialisation, as it's safer -- but there is /secr/ SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer"
# code which stashes objects in the session that can't be JSON serialized.
# Switch when that code is rewritten.
#SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer"
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_SAVE_EVERY_REQUEST = True SESSION_SAVE_EVERY_REQUEST = True
SESSION_CACHE_ALIAS = 'sessions' SESSION_CACHE_ALIAS = 'sessions'
@ -435,8 +453,6 @@ INSTALLED_APPS = [
'django_celery_beat', 'django_celery_beat',
'corsheaders', 'corsheaders',
'django_markup', 'django_markup',
'django_password_strength',
'form_utils',
'oidc_provider', 'oidc_provider',
'simple_history', 'simple_history',
'tastypie', 'tastypie',
@ -523,6 +539,8 @@ SECURE_HSTS_SECONDS = 3600
#SECURE_REDIRECT_EXEMPT #SECURE_REDIRECT_EXEMPT
#SECURE_SSL_HOST #SECURE_SSL_HOST
#SECURE_SSL_REDIRECT = True #SECURE_SSL_REDIRECT = True
# Relax the COOP policy to allow Meetecho authentication pop-up
SECURE_CROSS_ORIGIN_OPENER_POLICY = "unsafe-none"
# Override this in your settings_local with the IP addresses relevant for you: # Override this in your settings_local with the IP addresses relevant for you:
INTERNAL_IPS = ( INTERNAL_IPS = (
@ -708,13 +726,13 @@ CACHE_MIDDLEWARE_KEY_PREFIX = ''
# This setting is possibly overridden further down, after the import of settings_local # This setting is possibly overridden further down, after the import of settings_local
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211', 'LOCATION': '127.0.0.1:11211',
'VERSION': __version__, 'VERSION': __version__,
'KEY_PREFIX': 'ietf:dt', 'KEY_PREFIX': 'ietf:dt',
}, },
'sessions': { 'sessions': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211', 'LOCATION': '127.0.0.1:11211',
# No release-specific VERSION setting. # No release-specific VERSION setting.
'KEY_PREFIX': 'ietf:dt', 'KEY_PREFIX': 'ietf:dt',
@ -1123,14 +1141,13 @@ ACCOUNT_REQUEST_EMAIL = 'account-request@ietf.org'
SILENCED_SYSTEM_CHECKS = [ SILENCED_SYSTEM_CHECKS = [
"fields.W342", # Setting unique=True on a ForeignKey has the same effect as using a OneToOneField. "fields.W342", # Setting unique=True on a ForeignKey has the same effect as using a OneToOneField.
"fields.W905", # django.contrib.postgres.fields.CICharField is deprecated. (see https://github.com/ietf-tools/datatracker/issues/5660)
] ]
CHECKS_LIBRARY_PATCHES_TO_APPLY = [ CHECKS_LIBRARY_PATCHES_TO_APPLY = [
'patch/change-oidc-provider-field-sizes-228.patch', 'patch/change-oidc-provider-field-sizes-228.patch',
'patch/fix-oidc-access-token-post.patch', 'patch/fix-oidc-access-token-post.patch',
'patch/fix-jwkest-jwt-logging.patch', 'patch/fix-jwkest-jwt-logging.patch',
'patch/fix-django-password-strength-kwargs.patch',
'patch/add-django-http-cookie-value-none.patch',
'patch/django-cookie-delete-with-all-settings.patch', 'patch/django-cookie-delete-with-all-settings.patch',
'patch/tastypie-django22-fielderror-response.patch', 'patch/tastypie-django22-fielderror-response.patch',
] ]

View file

@ -39,7 +39,7 @@ DATABASES = {
'HOST': 'db', 'HOST': 'db',
'PORT': '5432', 'PORT': '5432',
'NAME': 'test.db', 'NAME': 'test.db',
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql',
'USER': 'django', 'USER': 'django',
'PASSWORD': 'RkTkDPFnKpko', 'PASSWORD': 'RkTkDPFnKpko',
}, },

View file

@ -24,7 +24,8 @@ class AffiliationAlias(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.alias = self.alias.lower() self.alias = self.alias.lower()
super(AffiliationAlias, self).save(*args, **kwargs) update_fields = {"alias"}.union(kwargs.pop("update_fields", set()))
super(AffiliationAlias, self).save(update_fields=update_fields, *args, **kwargs)
class Meta: class Meta:
verbose_name_plural = "affiliation aliases" verbose_name_plural = "affiliation aliases"

View file

@ -13,7 +13,7 @@ from django.urls import reverse as urlreverse
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.encoding import force_text, force_str from django.utils.encoding import force_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -202,7 +202,7 @@ def get_reply_to():
address with "plus addressing" using a random string. Guaranteed to be unique""" address with "plus addressing" using a random string. Guaranteed to be unique"""
local,domain = get_base_submission_message_address().split('@') local,domain = get_base_submission_message_address().split('@')
while True: while True:
rand = force_text(base64.urlsafe_b64encode(os.urandom(12))) rand = force_str(base64.urlsafe_b64encode(os.urandom(12)))
address = "{}+{}@{}".format(local,rand,domain) address = "{}+{}@{}".format(local,rand,domain)
q = Message.objects.filter(reply_to=address) q = Message.objects.filter(reply_to=address)
if not q: if not q:

View file

@ -24,7 +24,7 @@ from django.test import override_settings
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_str, force_text from django.utils.encoding import force_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames, from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames,
@ -750,10 +750,10 @@ class SubmitTests(BaseSubmitTestCase):
self.assertTrue("New Version Notification" in outbox[-2]["Subject"]) self.assertTrue("New Version Notification" in outbox[-2]["Subject"])
self.assertTrue(name in get_payload_text(outbox[-2])) self.assertTrue(name in get_payload_text(outbox[-2]))
interesting_address = {'ietf':'mars', 'irtf':'irtf-chair', 'iab':'iab-chair', 'ise':'rfc-ise'}[draft.stream_id] interesting_address = {'ietf':'mars', 'irtf':'irtf-chair', 'iab':'iab-chair', 'ise':'rfc-ise'}[draft.stream_id]
self.assertTrue(interesting_address in force_text(outbox[-2].as_string())) self.assertTrue(interesting_address in force_str(outbox[-2].as_string()))
if draft.stream_id == 'ietf': if draft.stream_id == 'ietf':
self.assertTrue(draft.ad.role_email("ad").address in force_text(outbox[-2].as_string())) self.assertTrue(draft.ad.role_email("ad").address in force_str(outbox[-2].as_string()))
self.assertTrue(ballot_position.balloter.role_email("ad").address in force_text(outbox[-2].as_string())) self.assertTrue(ballot_position.balloter.role_email("ad").address in force_str(outbox[-2].as_string()))
self.assertTrue("New Version Notification" in outbox[-1]["Subject"]) self.assertTrue("New Version Notification" in outbox[-1]["Subject"])
self.assertTrue(name in get_payload_text(outbox[-1])) self.assertTrue(name in get_payload_text(outbox[-1]))
r = self.client.get(urlreverse('ietf.doc.views_search.recent_drafts')) r = self.client.get(urlreverse('ietf.doc.views_search.recent_drafts'))
@ -1204,15 +1204,15 @@ class SubmitTests(BaseSubmitTestCase):
Unlike some other tests in this module, does not confirm draft if this would be required. Unlike some other tests in this module, does not confirm draft if this would be required.
""" """
orig_draft = DocumentFactory( orig_draft: Document = DocumentFactory( # type: ignore[annotation-unchecked]
type_id='draft', type_id='draft',
group=GroupFactory(type_id=group_type) if group_type else None, group=GroupFactory(type_id=group_type) if group_type else None,
stream_id=stream_type, stream_id=stream_type,
) # type: Document )
name = orig_draft.name name = orig_draft.name
group = orig_draft.group group = orig_draft.group
new_rev = '%02d' % (int(orig_draft.rev) + 1) new_rev = '%02d' % (int(orig_draft.rev) + 1)
author = PersonFactory() # type: Person author: Person = PersonFactory() # type: ignore[annotation-unchecked]
DocumentAuthor.objects.create(person=author, document=orig_draft) DocumentAuthor.objects.create(person=author, document=orig_draft)
orig_draft.docextresource_set.create(name_id='faq', value='https://faq.example.com/') orig_draft.docextresource_set.create(name_id='faq', value='https://faq.example.com/')
orig_draft.docextresource_set.create(name_id='wiki', value='https://wiki.example.com', display_name='Test Wiki') orig_draft.docextresource_set.create(name_id='wiki', value='https://wiki.example.com', display_name='Test Wiki')
@ -1982,7 +1982,7 @@ class SubmitTests(BaseSubmitTestCase):
group = GroupFactory() group = GroupFactory()
# someone to be notified of resource suggestion when permission not granted # someone to be notified of resource suggestion when permission not granted
RoleFactory(group=group, person=PersonFactory(), name_id='chair') RoleFactory(group=group, person=PersonFactory(), name_id='chair')
submission = SubmissionFactory(state_id='grp-appr', group=group) # type: Submission submission: Submission = SubmissionFactory(state_id='grp-appr', group=group) # type: ignore[annotation-unchecked]
SubmissionExtResourceFactory(submission=submission) SubmissionExtResourceFactory(submission=submission)
# use secretary user to ensure we have permission to approve # use secretary user to ensure we have permission to approve
@ -2000,7 +2000,7 @@ class SubmitTests(BaseSubmitTestCase):
group = GroupFactory() group = GroupFactory()
# someone to be notified of resource suggestion when permission not granted # someone to be notified of resource suggestion when permission not granted
RoleFactory(group=group, person=PersonFactory(), name_id='chair') RoleFactory(group=group, person=PersonFactory(), name_id='chair')
submission = SubmissionFactory(state_id=state, group=group) # type: Submission submission: Submission = SubmissionFactory(state_id=state, group=group) # type: ignore[annotation-unchecked]
SubmissionExtResourceFactory(submission=submission) SubmissionExtResourceFactory(submission=submission)
url = urlreverse( url = urlreverse(
@ -2052,7 +2052,7 @@ class SubmitTests(BaseSubmitTestCase):
def test_forcepost_with_extresources(self): def test_forcepost_with_extresources(self):
# state needs to be one that has 'posted' as a next state # state needs to be one that has 'posted' as a next state
submission = SubmissionFactory(state_id='grp-appr') # type: Submission submission: Submission = SubmissionFactory(state_id='grp-appr') # type: ignore[annotation-unchecked]
SubmissionExtResourceFactory(submission=submission) SubmissionExtResourceFactory(submission=submission)
url = urlreverse( url = urlreverse(

View file

@ -10,11 +10,11 @@ import re
import requests import requests
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from urllib.parse import quote as urlquote
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import smart_bytes, force_str from django.utils.encoding import smart_bytes, force_str
from django.utils.http import urlquote
import debug # pyflakes:ignore import debug # pyflakes:ignore

View file

@ -12,7 +12,7 @@ from xml.dom import pulldom, Node
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import smart_bytes, force_str, force_text from django.utils.encoding import smart_bytes, force_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -583,7 +583,7 @@ def post_approved_draft(url, name):
if r.status_code != 200: if r.status_code != 200:
raise RuntimeError("Status code is not 200 OK (it's %s)." % r.status_code) raise RuntimeError("Status code is not 200 OK (it's %s)." % r.status_code)
if force_text(r.text) != "OK": if force_str(r.text) != "OK":
raise RuntimeError('Response is not "OK" (it\'s "%s").' % r.text) raise RuntimeError('Response is not "OK" (it\'s "%s").' % r.text)
except Exception as e: except Exception as e:

View file

@ -32,11 +32,13 @@
{% else %} {% else %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li> <li>
<a class="dropdown-item {% if flavor != 'top' %} text-wrap link-primary{% endif %}" <form id="logout-form" method="post" action="{% url 'django.contrib.auth.views.logout' %}">
rel="nofollow" {% csrf_token %}
href="{% url 'django.contrib.auth.views.logout' %}"> <button class="dropdown-item {% if flavor != 'top' %} text-wrap link-primary{% endif %}"
Sign out type="submit">
</a> Sign out
</button>
</form>
</li> </li>
<li> <li>
<a class="dropdown-item {% if flavor != 'top' %} text-wrap link-primary{% endif %}" <a class="dropdown-item {% if flavor != 'top' %} text-wrap link-primary{% endif %}"

View file

@ -64,7 +64,7 @@
</a> </a>
</td> </td>
<td> <td>
{% ifequal ref.source.get_state.slug 'rfc' %} {% if ref.source.get_state.slug == 'rfc' %}
{% with ref.source.std_level as lvl %} {% with ref.source.std_level as lvl %}
{% if lvl %}{{ lvl }}{% endif %} {% if lvl %}{{ lvl }}{% endif %}
{% endwith %} {% endwith %}
@ -72,7 +72,7 @@
{% with ref.source.intended_std_level as lvl %} {% with ref.source.intended_std_level as lvl %}
{% if lvl %}{{ lvl }}{% endif %} {% if lvl %}{{ lvl }}{% endif %}
{% endwith %} {% endwith %}
{% endifequal %} {% endif %}
</td> </td>
<td>{{ ref.relationship.name }}</td> <td>{{ ref.relationship.name }}</td>
<td>{{ ref.is_downref|default:'' }}</td> <td>{{ ref.is_downref|default:'' }}</td>

View file

@ -51,7 +51,7 @@
</a> </a>
</td> </td>
<td> <td>
{% ifequal ref.target.document.get_state.slug 'rfc' %} {% if ref.target.document.get_state.slug == 'rfc' %}
{% with ref.target.document.std_level as lvl %} {% with ref.target.document.std_level as lvl %}
{% if lvl %}{{ lvl }}{% endif %} {% if lvl %}{{ lvl }}{% endif %}
{% endwith %} {% endwith %}
@ -59,7 +59,7 @@
{% with ref.target.document.intended_std_level as lvl %} {% with ref.target.document.intended_std_level as lvl %}
{% if lvl %}{{ lvl }}{% endif %} {% if lvl %}{{ lvl }}{% endif %}
{% endwith %} {% endwith %}
{% endifequal %} {% endif %}
</td> </td>
<td>{{ ref.relationship.name }}</td> <td>{{ ref.relationship.name }}</td>
<td>{{ ref.is_downref|default:'' }}</td> <td>{{ ref.is_downref|default:'' }}</td>

View file

@ -47,21 +47,43 @@
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"
data-edit-form="{{ form.edit }}" data-edit-form="{{ form.edit }}"
data-ajax-info-url="{% url "ietf.liaisons.views.ajax_get_liaison_info" %}"> data-ajax-info-url="{% url 'ietf.liaisons.views.ajax_get_liaison_info' %}">
{% csrf_token %} {% csrf_token %}
{% for fieldset in form.fieldsets %} <h2>From</h2>
<h2>{{ fieldset.name }}</h2> {% bootstrap_field form.from_groups layout="horizontal" %}
{% for field in fieldset %} {% bootstrap_field form.from_contact layout="horizontal" %}
{% if field.id_for_label != "id_attachments" %} {% bootstrap_field form.response_contacts layout="horizontal" %}
{% bootstrap_field field layout="horizontal" %} {% if form.approved %}
{% else %} {% bootstrap_field form.approved layout="horizontal" %}
<div class="row mb-3"> {% endif %}
<p class="col-md-2 fw-bold col-form-label">{{ field.label }}</p> <h2>To</h2>
<div class="col-md-10">{{ field }}</div> {% bootstrap_field form.to_groups layout="horizontal" %}
</div> {% bootstrap_field form.to_contacts layout="horizontal" %}
{% endif %} <h2>Other email addresses</h2>
{% endfor %} {% bootstrap_field form.technical_contacts layout="horizontal" %}
{% endfor %} {% if form.action_holder_contacts %}
{% bootstrap_field form.action_holder_contacts layout="horizontal" %}
{% endif %}
{% bootstrap_field form.cc_contacts layout="horizontal" %}
<h2>Purpose</h2>
{% bootstrap_field form.purpose layout="horizontal" %}
{% bootstrap_field form.deadline layout="horizontal" %}
<h2>Reference</h2>
{% bootstrap_field form.other_identifiers layout="horizontal" %}
{% bootstrap_field form.related_to layout="horizontal" %}
<h2>Liaison Statement</h2>
{% bootstrap_field form.title layout="horizontal" %}
{% bootstrap_field form.submitted_date layout="horizontal" %}
{% bootstrap_field form.body layout="horizontal" %}
<div class="row mb-3">
<p class="col-md-2 fw-bold col-form-label">{{ form.attachments.label }}</p>
<div class="col-md-10">{{ form.attachments }}</div>
</div>
<h2>Add attachment</h2>
{% bootstrap_field form.attach_title layout="horizontal" %}
{% bootstrap_field form.attach_file layout="horizontal" %}
{% bootstrap_field form.attach_button layout="horizontal" %}
<a class="btn btn-danger float-end" <a class="btn btn-danger float-end"
href="{% if liaison %}{% url 'ietf.liaisons.views.liaison_detail' object_id=liaison.pk %}{% else %}{% url 'ietf.liaisons.views.liaison_list' %}{% endif %}"> href="{% if liaison %}{% url 'ietf.liaisons.views.liaison_detail' object_id=liaison.pk %}{% else %}{% url 'ietf.liaisons.views.liaison_list' %}{% endif %}">
Cancel Cancel

View file

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #} {# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %} {% load origin %}
{% load staticfiles %} {% load static %}
{% load ietf_filters %} {% load ietf_filters %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %} {% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #} {# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %} {% load origin %}
{% load staticfiles %} {% load static %}
{% load ietf_filters %} {% load ietf_filters %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% block content %} {% block content %}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{# Copyright The IETF Trust 2020, All Rights Reserved #} {# Copyright The IETF Trust 2020, All Rights Reserved #}
{% load origin staticfiles django_bootstrap5 %} {% load origin static django_bootstrap5 %}
{% block title %} {% block title %}
Approved Slides for {{ submission.session.meeting }} : {{ submission.session.group.acronym }} Approved Slides for {{ submission.session.meeting }} : {{ submission.session.group.acronym }}
{% endblock %} {% endblock %}

View file

@ -6,8 +6,7 @@
{% block title %}Change password{% endblock %} {% block title %}Change password{% endblock %}
{% block js %} {% block js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'ietf/js/zxcvbn.js' %}"></script> {{ form.media.js }}
<script src="{% static 'ietf/js/password_strength.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% origin %} {% origin %}

Some files were not shown because too many files have changed in this diff Show more