diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cd9e625c..a5d1c5bba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,6 +139,8 @@ jobs: run: | echo "Running checks..." ./ietf/manage.py check + ./ietf/manage.py migrate || true + echo "USE_TZ = True" >> ./ietf/settings_local.py ./ietf/manage.py migrate echo "Validating migrations..." if ! ( ietf/manage.py makemigrations --dry-run --check --verbosity 3 ) ; then diff --git a/bin/add-old-drafts-from-archive.py b/bin/add-old-drafts-from-archive.py index e169ecdcf..239ba7837 100755 --- a/bin/add-old-drafts-from-archive.py +++ b/bin/add-old-drafts-from-archive.py @@ -16,6 +16,7 @@ from django.conf import settings from django.core.validators import validate_email, ValidationError from ietf.utils.draft import PlaintextDraft from ietf.submit.utils import update_authors +from ietf.utils.timezone import date_today import debug # pyflakes:ignore @@ -66,9 +67,9 @@ for name in sorted(names): print name, rev, "Can't parse", p,":",e continue if draft.errors and draft.errors.keys()!=['draftname',]: - print "Errors - could not process", name, rev, datetime.datetime.fromtimestamp(p.stat().st_mtime), draft.errors, draft.get_title().encode('utf8') + print "Errors - could not process", name, rev, datetime.datetime.fromtimestamp(p.stat().st_mtime, datetime.timezone.utc), draft.errors, draft.get_title().encode('utf8') else: - time = datetime.datetime.fromtimestamp(p.stat().st_mtime) + time = datetime.datetime.fromtimestamp(p.stat().st_mtime, datetime.timezone.utc) if not doc: doc = Document.objects.create(name=name, time=time, @@ -140,7 +141,7 @@ for name in sorted(names): doc = doc, rev = rev, by = system, - desc = "Revision added from id-archive on %s by %s"%(datetime.date.today(),sys.argv[0]), + desc = "Revision added from id-archive on %s by %s"%(date_today(),sys.argv[0]), time=time, ) events.append(e) diff --git a/bin/check-copyright b/bin/check-copyright index 6698e3fda..13cbcd858 100755 --- a/bin/check-copyright +++ b/bin/check-copyright @@ -162,7 +162,7 @@ def get_first_commit(path): else: pass except OSError: - rev, who, when = None, None, datetime.datetime.now() + rev, who, when = None, None, datetime.datetime.now(datetime.timezone.utc) return { path: { 'rev': rev, 'who': who, 'date': when.strftime('%Y-%m-%d %H:%M:%S'), }, } diff --git a/dev/diff/prepare.sh b/dev/diff/prepare.sh index c18684fc7..2e80a8e43 100644 --- a/dev/diff/prepare.sh +++ b/dev/diff/prepare.sh @@ -12,5 +12,24 @@ yarn legacy:build echo "Creating data directories..." chmod +x ./docker/scripts/app-create-dirs.sh ./docker/scripts/app-create-dirs.sh + ./ietf/manage.py check -./ietf/manage.py migrate +if ./ietf/manage.py showmigrations | grep "\[ \] 0003_pause_to_change_use_tz"; then + if grep "USE_TZ" ./ietf/settings_local.py; then + cat ./ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = False/' > /tmp/settings_local.py && mv /tmp/settings_local.py ./ietf/settings_local.py + else + echo "USE_TZ = False" >> ./ietf/settings_local.py + fi + # This is expected to exit non-zero at the pause + /usr/local/bin/python ./ietf/manage.py migrate || true + cat ./ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py ./ietf/settings_local.py + /usr/local/bin/python ./ietf/manage.py migrate + +else + if grep "USE_TZ" ./ietf/settings_local.py; then + cat ./ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py ./ietf/settings_local.py + else + echo "USE_TZ = True" >> ./ietf/settings_local.py + /usr/local/bin/python ./ietf/manage.py migrate + fi +fi diff --git a/docker/scripts/app-init.sh b/docker/scripts/app-init.sh index bd3d51a98..7557e8f45 100755 --- a/docker/scripts/app-init.sh +++ b/docker/scripts/app-init.sh @@ -100,7 +100,28 @@ echo "Starting memcached..." echo "Running initial checks..." /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check --settings=settings_local -# /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local + +# Migrate, adjusting to what the current state of the underlying database might be: + +if ietf/manage.py showmigrations | grep "\[ \] 0003_pause_to_change_use_tz"; then + if grep "USE_TZ" $WORKSPACEDIR/ietf/settings_local.py; then + cat $WORKSPACEDIR/ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = False/' > /tmp/settings_local.py && mv /tmp/settings_local.py $WORKSPACEDIR/ietf/settings_local.py + else + echo "USE_TZ = False" >> $WORKSPACEDIR/ietf/settings_local.py + fi + # This is expected to exit non-zero at the pause + /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local || true + cat $WORKSPACEDIR/ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py $WORKSPACEDIR/ietf/settings_local.py + /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local + +else + if grep "USE_TZ" $WORKSPACEDIR/ietf/settings_local.py; then + cat $WORKSPACEDIR/ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py $WORKSPACEDIR/ietf/settings_local.py + else + echo "USE_TZ = True" >> $WORKSPACEDIR/ietf/settings_local.py + /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local + fi +fi echo "-----------------------------------------------------------------" echo "Done!" diff --git a/ietf/api/management/commands/makeresources.py b/ietf/api/management/commands/makeresources.py index 07f8402e7..889b2cdfb 100644 --- a/ietf/api/management/commands/makeresources.py +++ b/ietf/api/management/commands/makeresources.py @@ -3,7 +3,6 @@ import os -import datetime import collections import io @@ -14,6 +13,7 @@ import debug # pyflakes:ignore from django.core.management.base import AppCommand from django.db import models from django.template import Template, Context +from django.utils import timezone from tastypie.resources import ModelResource @@ -89,7 +89,7 @@ class Command(AppCommand): info = dict( app=app.name, app_label=app.label, - date=datetime.datetime.now() + date=timezone.now() ) new_models = {} for model, rclass_name in missing_resources: diff --git a/ietf/api/tests.py b/ietf/api/tests.py index ab565a917..1be220f0f 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -32,6 +32,7 @@ from ietf.person.models import User from ietf.person.models import PersonalApiKey from ietf.stats.models import MeetingRegistration from ietf.utils.mail import outbox, get_payload_text +from ietf.utils.models import DumpInfo from ietf.utils.test_utils import TestCase, login_testing_unauthorized OMITTED_APPS = ( @@ -508,10 +509,17 @@ class CustomApiTests(TestCase): self.assertEqual(set(missing_fields), set(drop_fields)) 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') url = urlreverse('ietf.api.views.version') r = self.client.get(url) data = r.json() self.assertEqual(data['version'], ietf.__version__+ietf.__patch__) + self.assertEqual(data['dumptime'], "2022-08-31 07:10:01 +0000") + DumpInfo.objects.update(tz='PST8PDT') + r = self.client.get(url) + data = r.json() + self.assertEqual(data['dumptime'], "2022-08-31 07:10:01 -0700") + def test_api_appauth(self): url = urlreverse('ietf.api.views.app_auth') diff --git a/ietf/api/views.py b/ietf/api/views.py index 97a551789..a6a51f667 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -203,8 +203,13 @@ def api_new_meeting_registration(request): def version(request): + dumpdate = None dumpinfo = DumpInfo.objects.order_by('-date').first() - dumptime = pytz.timezone(dumpinfo.tz).localize(dumpinfo.date).strftime('%Y-%m-%d %H:%M:%S %z') if dumpinfo else None + if dumpinfo: + dumpdate = dumpinfo.date + if dumpinfo.tz != "UTC": + dumpdate = pytz.timezone(dumpinfo.tz).localize(dumpinfo.date.replace(tzinfo=None)) + dumptime = dumpdate.strftime('%Y-%m-%d %H:%M:%S %z') if dumpinfo else None return HttpResponse( json.dumps({ 'version': ietf.__version__+ietf.__patch__, diff --git a/ietf/bin/create-charter-newrevisiondocevents b/ietf/bin/create-charter-newrevisiondocevents index c7ce9c522..d91c0b5b7 100755 --- a/ietf/bin/create-charter-newrevisiondocevents +++ b/ietf/bin/create-charter-newrevisiondocevents @@ -28,7 +28,7 @@ def warn(string): # ------------------------------------------------------------------------------ import re -from datetime import datetime as Datetime +import datetime import django django.setup() @@ -44,7 +44,7 @@ system_entity = Person.objects.get(name="(System)") charterdir = Path(settings.CHARTER_PATH) for file in charterdir.files("charter-ietf-*.txt"): fname = file.name - ftime = Datetime.fromtimestamp(file.mtime) + ftime = datetime.datetime.fromtimestamp(file.mtime, datetime.timezone.utc) match = re.search("^(?P<name>[a-z0-9-]+)-(?P<rev>\d\d-\d\d)\.txt$", fname) if match: name = match.group("name") diff --git a/ietf/bin/iana-changes-updates b/ietf/bin/iana-changes-updates index 817f7e533..b0ea6712e 100755 --- a/ietf/bin/iana-changes-updates +++ b/ietf/bin/iana-changes-updates @@ -18,6 +18,7 @@ django.setup() from django.conf import settings from optparse import OptionParser +from zoneinfo import ZoneInfo parser = OptionParser() parser.add_option("-f", "--from", dest="start", @@ -38,13 +39,16 @@ CLOCK_SKEW_COMPENSATION = 5 # seconds MAX_INTERVAL_ACCEPTED_BY_IANA = datetime.timedelta(hours=23) +local_tzinfo = ZoneInfo(settings.TIME_ZONE) start = datetime.datetime.now() - datetime.timedelta(hours=23) + datetime.timedelta(seconds=CLOCK_SKEW_COMPENSATION) if options.start: start = datetime.datetime.strptime(options.start, "%Y-%m-%d %H:%M:%S") +start = start.replace(tzinfo=local_tzinfo).astimezone(datetime.timezone.utc) end = start + datetime.timedelta(hours=23) if options.end: - end = datetime.datetime.strptime(options.end, "%Y-%m-%d %H:%M:%S") + end = datetime.datetime.strptime(options.end, "%Y-%m-%d %H:%M:%S").replace(tzinfo=local_tzinfo) +end = end.astimezone(datetime.timezone.utc) syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER) @@ -52,7 +56,13 @@ syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER) from ietf.sync.iana import fetch_changes_json, parse_changes_json, update_history_with_changes -syslog.syslog("Updating history log with new changes from IANA from %s, period %s - %s" % (settings.IANA_SYNC_CHANGES_URL, start, end)) +syslog.syslog( + "Updating history log with new changes from IANA from %s, period %s - %s" % ( + settings.IANA_SYNC_CHANGES_URL, + start.astimezone(local_tzinfo), + end.astimezone(local_tzinfo), + ) +) t = start while t < end: diff --git a/ietf/bin/rfc-editor-index-updates b/ietf/bin/rfc-editor-index-updates index 4ff3bf373..dc7abe26b 100755 --- a/ietf/bin/rfc-editor-index-updates +++ b/ietf/bin/rfc-editor-index-updates @@ -29,6 +29,7 @@ from django.core.mail import mail_admins from ietf.doc.utils import rebuild_reference_relations from ietf.utils.log import log from ietf.utils.pipe import pipe +from ietf.utils.timezone import date_today import ietf.sync.rfceditor @@ -39,7 +40,7 @@ parser.add_option("-d", dest="skip_date", options, args = parser.parse_args() -skip_date = datetime.date.today() - datetime.timedelta(days=365) +skip_date = date_today() - datetime.timedelta(days=365) if options.skip_date: skip_date = datetime.datetime.strptime(options.skip_date, "%Y-%m-%d").date() diff --git a/ietf/bin/send-review-reminders b/ietf/bin/send-review-reminders index e74694c8a..317db4376 100755 --- a/ietf/bin/send-review-reminders +++ b/ietf/bin/send-review-reminders @@ -26,8 +26,9 @@ from ietf.review.utils import ( send_unavailability_period_ending_reminder, send_reminder_all_open_reviews, send_review_reminder_overdue_assignment, send_reminder_unconfirmed_assignments) from ietf.utils.log import log +from ietf.utils.timezone import date_today, DEADLINE_TZINFO -today = datetime.date.today() +today = date_today(DEADLINE_TZINFO) for assignment in review_assignments_needing_reviewer_reminder(today): email_reviewer_reminder(assignment) diff --git a/ietf/bin/send-scheduled-mail b/ietf/bin/send-scheduled-mail index c6a2dbf50..d3474db09 100755 --- a/ietf/bin/send-scheduled-mail +++ b/ietf/bin/send-scheduled-mail @@ -3,7 +3,7 @@ # This script requires that the proper virtual python environment has been # invoked before start -import datetime, os, sys +import os, sys import syslog # boilerplate @@ -16,6 +16,7 @@ syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER) import django django.setup() +from django.utils import timezone from ietf.utils.mail import log_smtp_exception, send_error_email from smtplib import SMTPException @@ -32,7 +33,7 @@ from ietf.message.models import SendQueue mode = sys.argv[1] -now = datetime.datetime.now() +now = timezone.now() needs_sending = SendQueue.objects.filter(sent_at=None).select_related("message") if mode == "specific": diff --git a/ietf/community/views.py b/ietf/community/views.py index c67f992d6..b0646424a 100644 --- a/ietf/community/views.py +++ b/ietf/community/views.py @@ -10,6 +10,7 @@ import uuid from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404, render from django.contrib.auth.decorators import login_required +from django.utils import timezone from django.utils.html import strip_tags import debug # pyflakes:ignore @@ -218,7 +219,7 @@ def feed(request, username=None, acronym=None, group_type=None): significant = request.GET.get('significant', '') == '1' documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True) - since = datetime.datetime.now() - datetime.timedelta(days=14) + since = timezone.now() - datetime.timedelta(days=14) events = DocEvent.objects.filter( doc__id__in=documents, @@ -243,7 +244,7 @@ def feed(request, username=None, acronym=None, group_type=None): 'title': title, 'subtitle': subtitle, 'id': feed_id.urn, - 'updated': datetime.datetime.now(), + 'updated': timezone.now(), }, content_type='text/xml') diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py index b780c73bd..af48827cf 100644 --- a/ietf/doc/expire.py +++ b/ietf/doc/expire.py @@ -4,6 +4,7 @@ from django.conf import settings +from django.utils import timezone import datetime, os, shutil, glob, re from pathlib import Path @@ -17,6 +18,7 @@ from ietf.person.models import Person from ietf.meeting.models import Meeting from ietf.doc.utils import add_state_change_event, update_action_holders from ietf.mailtrigger.utils import gather_address_lists +from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO nonexpirable_states: Optional[List[State]] = None @@ -52,17 +54,17 @@ def expirable_drafts(queryset=None): def get_soon_to_expire_drafts(days_of_warning): - start_date = datetime.date.today() - datetime.timedelta(1) + start_date = datetime_today(DEADLINE_TZINFO) - datetime.timedelta(1) end_date = start_date + datetime.timedelta(days_of_warning) return expirable_drafts().filter(expires__gte=start_date, expires__lt=end_date) def get_expired_drafts(): - return expirable_drafts().filter(expires__lt=datetime.date.today() + datetime.timedelta(1)) + return expirable_drafts().filter(expires__lt=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(1)) def in_draft_expire_freeze(when=None): if when == None: - when = datetime.datetime.now() + when = timezone.now() meeting = Meeting.objects.filter(type='ietf', date__gte=when-datetime.timedelta(days=7)).order_by('date').first() @@ -171,7 +173,7 @@ def expire_draft(doc): def clean_up_draft_files(): """Move unidentified and old files out of the Internet Draft directory.""" - cut_off = datetime.date.today() + cut_off = date_today() pattern = os.path.join(settings.INTERNET_DRAFT_PATH, "draft-*.*") filename_re = re.compile(r'^(.*)-(\d\d)$') diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py index 8fd6feca7..40e0f506b 100644 --- a/ietf/doc/factories.py +++ b/ietf/doc/factories.py @@ -10,6 +10,7 @@ import datetime from typing import Optional # pyflakes:ignore from django.conf import settings +from django.utils import timezone from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor, StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent, @@ -18,6 +19,7 @@ from ietf.group.models import Group from ietf.person.factories import PersonFactory from ietf.group.factories import RoleFactory from ietf.utils.text import xslugify +from ietf.utils.timezone import date_today def draft_name_generator(type_id,group,n): @@ -38,7 +40,7 @@ class BaseDocumentFactory(factory.django.DjangoModelFactory): rev = '00' std_level_id = None # type: Optional[str] intended_std_level_id = None - time = datetime.datetime.now() + time = timezone.now() expires = factory.LazyAttribute(lambda o: o.time+datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)) pages = factory.fuzzy.FuzzyInteger(2,400) @@ -320,7 +322,7 @@ class ConflictReviewFactory(BaseDocumentFactory): # This is very skeletal. It is enough for the tests that use it now, but when it's needed, it will need to be improved with, at least, a group generator that backs the object with a review team. class ReviewFactory(BaseDocumentFactory): type_id = 'review' - name = factory.LazyAttribute(lambda o: 'review-doesnotexist-00-%s-%s'%(o.group.acronym,datetime.date.today().isoformat())) + name = factory.LazyAttribute(lambda o: 'review-doesnotexist-00-%s-%s'%(o.group.acronym,date_today().isoformat())) group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review') class DocAliasFactory(factory.django.DjangoModelFactory): @@ -357,7 +359,8 @@ class TelechatDocEventFactory(DocEventFactory): class Meta: model = TelechatDocEvent - telechat_date = datetime.datetime.today()+datetime.timedelta(days=14) + # note: this is evaluated at import time and not updated - all events will have the same telechat_date + telechat_date = timezone.now()+datetime.timedelta(days=14) type = 'scheduled_for_telechat' class NewRevisionDocEventFactory(DocEventFactory): @@ -410,7 +413,7 @@ class IRSGBallotDocEventFactory(BallotDocEventFactory): class Meta: model = IRSGBallotDocEvent - duedate = datetime.datetime.now() + datetime.timedelta(days=14) + duedate = timezone.now() + datetime.timedelta(days=14) ballot_type = factory.SubFactory(BallotTypeFactory, slug='irsg-approve') class BallotPositionDocEventFactory(DocEventFactory): diff --git a/ietf/doc/feeds.py b/ietf/doc/feeds.py index 1169db105..7885e75e3 100644 --- a/ietf/doc/feeds.py +++ b/ietf/doc/feeds.py @@ -10,11 +10,13 @@ from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed from django.urls import reverse as urlreverse from django.template.defaultfilters import truncatewords, truncatewords_html, date as datefilter from django.template.defaultfilters import linebreaks # type: ignore +from django.utils import timezone from django.utils.html import strip_tags from ietf.doc.models import Document, State, LastCallDocEvent, DocEvent from ietf.doc.utils import augment_events_with_revision from ietf.doc.templatetags.ietf_filters import format_textarea +from ietf.utils.timezone import RPC_TZINFO def strip_control_characters(s): @@ -133,9 +135,16 @@ class RfcFeed(Feed): def items(self): if self.year: - rfc_events = DocEvent.objects.filter(type='published_rfc',time__year=self.year).order_by('-time') + # Find published RFCs based on their official publication year + start_of_year = datetime.datetime(int(self.year), 1, 1, tzinfo=RPC_TZINFO) + start_of_next_year = datetime.datetime(int(self.year) + 1, 1, 1, tzinfo=RPC_TZINFO) + rfc_events = DocEvent.objects.filter( + type='published_rfc', + time__gte=start_of_year, + time__lt=start_of_next_year, + ).order_by('-time') else: - cutoff = datetime.datetime.now() - datetime.timedelta(days=8) + cutoff = timezone.now() - datetime.timedelta(days=8) rfc_events = DocEvent.objects.filter(type='published_rfc',time__gte=cutoff).order_by('-time') results = [(e.doc, e.time) for e in rfc_events] for doc,time in results: diff --git a/ietf/doc/forms.py b/ietf/doc/forms.py index d209eb0a2..372c43ab0 100644 --- a/ietf/doc/forms.py +++ b/ietf/doc/forms.py @@ -15,6 +15,7 @@ from ietf.person.fields import SearchablePersonField, SearchablePersonsField from ietf.person.models import Email, Person from ietf.name.models import ExtResourceName +from ietf.utils.timezone import date_today from ietf.utils.validators import validate_external_resource_value class TelechatForm(forms.Form): @@ -34,7 +35,7 @@ class TelechatForm(forms.Form): for d in dates: self.page_count[d] = telechat_page_count(date=d).for_approval choice_display[d] = '%s (%s pages)' % (d.strftime("%Y-%m-%d"),self.page_count[d]) - if d-datetime.date.today() < datetime.timedelta(days=13): + if d - date_today() < datetime.timedelta(days=13): choice_display[d] += ' : WARNING - this may not leave enough time for directorate reviews!' self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, choice_display[d]) for d in dates] diff --git a/ietf/doc/lastcall.py b/ietf/doc/lastcall.py index fab3001b1..1c490c64c 100644 --- a/ietf/doc/lastcall.py +++ b/ietf/doc/lastcall.py @@ -1,7 +1,5 @@ # helpers for handling last calls on Internet Drafts -import datetime - from django.db.models import Q from ietf.doc.models import Document, State, DocEvent, LastCallDocEvent, WriteupDocEvent @@ -10,6 +8,8 @@ from ietf.person.models import Person from ietf.doc.utils import add_state_change_event, update_action_holders from ietf.doc.mails import generate_ballot_writeup, generate_approval_mail, generate_last_call_announcement from ietf.doc.mails import send_last_call_request, email_last_call_expired, email_last_call_expired_with_downref +from ietf.utils.timezone import date_today, DEADLINE_TZINFO + def request_last_call(request, doc): if not doc.latest_event(type="changed_ballot_writeup_text"): @@ -33,7 +33,7 @@ def request_last_call(request, doc): e.save() def get_expired_last_calls(): - today = datetime.date.today() + today = date_today(DEADLINE_TZINFO) for d in Document.objects.filter(Q(states__type="draft-iesg", states__slug="lc") | Q(states__type="statchg", states__slug="in-lc")): e = d.latest_event(LastCallDocEvent, type="sent_last_call") diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 0f344f015..54e0f47e2 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -10,6 +10,7 @@ from django.template.loader import render_to_string from django.utils.html import strip_tags from django.conf import settings from django.urls import reverse as urlreverse +from django.utils import timezone from django.utils.encoding import force_text import debug # pyflakes:ignore @@ -24,6 +25,7 @@ from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible from ietf.group.models import Role from ietf.doc.models import Document from ietf.mailtrigger.utils import gather_address_lists +from ietf.utils.timezone import date_today, DEADLINE_TZINFO def email_state_changed(request, doc, text, mailtrigger_id=None): @@ -191,7 +193,7 @@ def generate_ballot_rfceditornote(request, doc): return e def generate_last_call_announcement(request, doc): - expiration_date = datetime.date.today() + datetime.timedelta(days=14) + expiration_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=14) if doc.group.type_id in ("individ", "area"): group = "an individual submitter" expiration_date += datetime.timedelta(days=14) @@ -418,7 +420,7 @@ def generate_issue_ballot_mail(request, doc, ballot): e = doc.latest_event(LastCallDocEvent, type="sent_last_call") last_call_expires = e.expires if e else None - last_call_has_expired = last_call_expires and last_call_expires < datetime.datetime.now() + last_call_has_expired = last_call_expires and last_call_expires < timezone.now() return render_to_string("doc/mail/issue_iesg_ballot_mail.txt", dict(doc=doc, @@ -437,7 +439,7 @@ def _send_irsg_ballot_email(request, doc, ballot, subject, template): (to, cc) = gather_address_lists('irsg_ballot_issued', doc=doc) sender = 'IESG Secretary <iesg-secretary@ietf.org>' - ballot_expired = ballot.duedate < datetime.datetime.now() + ballot_expired = ballot.duedate < timezone.now() active_ballot = doc.active_ballot() if active_ballot is None: needed_bps = '' diff --git a/ietf/doc/management/commands/generate_draft_aliases.py b/ietf/doc/management/commands/generate_draft_aliases.py index 796e3db63..88f4aa98c 100755 --- a/ietf/doc/management/commands/generate_draft_aliases.py +++ b/ietf/doc/management/commands/generate_draft_aliases.py @@ -16,6 +16,7 @@ from tempfile import mkstemp from django.conf import settings from django.core.management.base import BaseCommand +from django.utils import timezone import debug # pyflakes:ignore @@ -101,7 +102,7 @@ class Command(BaseCommand): 'that have seen activity in the last %s years.' % (DEFAULT_YEARS)) def handle(self, *args, **options): - show_since = datetime.datetime.now() - datetime.timedelta(DEFAULT_YEARS*365) + show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365) date = time.strftime("%Y-%m-%d_%H:%M:%S") signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date) diff --git a/ietf/doc/management/commands/generate_draft_bibxml_files.py b/ietf/doc/management/commands/generate_draft_bibxml_files.py index 9ac90523d..f2dc508b9 100644 --- a/ietf/doc/management/commands/generate_draft_bibxml_files.py +++ b/ietf/doc/management/commands/generate_draft_bibxml_files.py @@ -10,6 +10,7 @@ import sys from django.conf import settings from django.core.management.base import BaseCommand +from django.utils import timezone import debug # pyflakes:ignore @@ -68,7 +69,7 @@ class Command(BaseCommand): if process_all: doc_events = NewRevisionDocEvent.objects.filter(type='new_revision', doc__type_id='draft') else: - start = datetime.datetime.now() - datetime.timedelta(days=days) + start = timezone.now() - datetime.timedelta(days=days) doc_events = NewRevisionDocEvent.objects.filter(type='new_revision', doc__type_id='draft', time__gte=start) doc_events = doc_events.order_by('time') diff --git a/ietf/doc/migrations/0046_use_timezone_now_for_doc_models.py b/ietf/doc/migrations/0046_use_timezone_now_for_doc_models.py new file mode 100644 index 000000000..3749fd973 --- /dev/null +++ b/ietf/doc/migrations/0046_use_timezone_now_for_doc_models.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.28 on 2022-07-12 11:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('doc', '0045_docstates_chatlogs_polls'), + ] + + operations = [ + migrations.AlterField( + model_name='deletedevent', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='docevent', + name='time', + field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='When the event happened'), + ), + migrations.AlterField( + model_name='dochistory', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='document', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='documentactionholder', + name='time_added', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/ietf/doc/migrations/0047_tzaware_deletedevents.py b/ietf/doc/migrations/0047_tzaware_deletedevents.py new file mode 100644 index 000000000..bf258de6e --- /dev/null +++ b/ietf/doc/migrations/0047_tzaware_deletedevents.py @@ -0,0 +1,61 @@ +# Generated by Django 2.2.28 on 2022-08-31 20:26 + +import datetime +import json + +from zoneinfo import ZoneInfo + +from django.db import migrations + + +TZ_BEFORE = ZoneInfo('PST8PDT') + + +def forward(apps, schema_editor): + DeletedEvent = apps.get_model('doc', 'DeletedEvent') + for deleted_event in DeletedEvent.objects.all(): + fields = json.loads(deleted_event.json) + replacements = {} + for k, v in fields.items(): + if isinstance(v, str): + try: + dt = datetime.datetime.strptime(v, '%Y-%m-%d %H:%M:%S') + except: + pass + else: + replacements[k] = dt.replace(tzinfo=TZ_BEFORE).astimezone(datetime.timezone.utc).isoformat() + if len(replacements) > 0: + fields.update(replacements) + deleted_event.json = json.dumps(fields) + deleted_event.save() + + +def reverse(apps, schema_editor): + DeletedEvent = apps.get_model('doc', 'DeletedEvent') + for deleted_event in DeletedEvent.objects.all(): + fields = json.loads(deleted_event.json) + replacements = {} + for k, v in fields.items(): + if isinstance(v, str) and 'T' in v: + try: + dt = datetime.datetime.fromisoformat(v) + except: + pass + else: + replacements[k] = dt.astimezone(TZ_BEFORE).replace(tzinfo=None).strftime('%Y-%m-%d %H:%M:%S') + if len(replacements) > 0: + fields.update(replacements) + deleted_event.json = json.dumps(fields) + deleted_event.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('doc', '0046_use_timezone_now_for_doc_models'), + ('utils', '0003_pause_to_change_use_tz'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 6512d4b54..c6e06fecd 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -18,6 +18,7 @@ from django.core.validators import URLValidator, RegexValidator from django.urls import reverse as urlreverse from django.contrib.contenttypes.models import ContentType from django.conf import settings +from django.utils import timezone from django.utils.encoding import force_text from django.utils.html import mark_safe # type:ignore @@ -35,6 +36,7 @@ from ietf.utils.decorators import memoize from ietf.utils.validators import validate_no_control_chars from ietf.utils.mail import formataddr from ietf.utils.models import ForeignKey +from ietf.utils.timezone import date_today, RPC_TZINFO if TYPE_CHECKING: # importing other than for type checking causes errors due to cyclic imports from ietf.meeting.models import ProceedingsMaterial, Session @@ -85,7 +87,7 @@ IESG_SUBSTATE_TAGS = ('ad-f-up', 'need-rev', 'extpty') class DocumentInfo(models.Model): """Any kind of document. Draft, RFC, Charter, IPR Statement, Liaison Statement""" - time = models.DateTimeField(default=datetime.datetime.now) # should probably have auto_now=True + time = models.DateTimeField(default=timezone.now) # should probably have auto_now=True type = ForeignKey(DocTypeName, blank=True, null=True) # Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki, External ... title = models.CharField(max_length=255, validators=[validate_no_control_chars, ]) @@ -682,7 +684,7 @@ class DocumentActionHolder(models.Model): """Action holder for a document""" document = ForeignKey('Document') person = ForeignKey(Person) - time_added = models.DateTimeField(default=datetime.datetime.now) + time_added = models.DateTimeField(default=timezone.now) CLEAR_ACTION_HOLDERS_STATES = ['approved', 'ann', 'rfcqueue', 'pub', 'dead'] # draft-iesg state slugs GROUP_ROLES_OF_INTEREST = ['chair', 'techadv', 'editor', 'secr'] @@ -829,16 +831,20 @@ class Document(DocumentInfo): def telechat_date(self, e=None): if not e: e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat") - return e.telechat_date if e and e.telechat_date and e.telechat_date >= datetime.date.today() else None + return e.telechat_date if e and e.telechat_date and e.telechat_date >= date_today(settings.TIME_ZONE) else None def past_telechat_date(self): "Return the latest telechat date if it isn't in the future; else None" e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat") - return e.telechat_date if e and e.telechat_date and e.telechat_date < datetime.date.today() else None + return e.telechat_date if e and e.telechat_date and e.telechat_date < date_today(settings.TIME_ZONE) else None def previous_telechat_date(self): "Return the most recent telechat date in the past, if any (even if there's another in the future)" - e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat", telechat_date__lt=datetime.datetime.now()) + e = self.latest_event( + TelechatDocEvent, + type="scheduled_for_telechat", + telechat_date__lt=date_today(settings.TIME_ZONE), + ) return e.telechat_date if e else None def request_closed_time(self, review_req): @@ -904,14 +910,21 @@ class Document(DocumentInfo): def future_presentations(self): """ returns related SessionPresentation objects for meetings that have not yet ended. This implementation allows for 2 week meetings """ - candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__gte=datetime.date.today()-datetime.timedelta(days=15)) - return sorted([pres for pres in candidate_presentations if pres.session.meeting.end_date()>=datetime.date.today()], key=lambda x:x.session.meeting.date) + candidate_presentations = self.sessionpresentation_set.filter( + session__meeting__date__gte=date_today() - datetime.timedelta(days=15) + ) + return sorted( + [pres for pres in candidate_presentations + if pres.session.meeting.end_date() >= date_today()], + key=lambda x:x.session.meeting.date, + ) def last_presented(self): """ returns related SessionPresentation objects for the most recent meeting in the past""" # Assumes no two meetings have the same start date - if the assumption is violated, one will be chosen arbitrariy - candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__lte=datetime.date.today()) - candidate_meetings = set([p.session.meeting for p in candidate_presentations if p.session.meeting.end_date()<datetime.date.today()]) + today = date_today() + candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__lte=today) + candidate_meetings = set([p.session.meeting for p in candidate_presentations if p.session.meeting.end_date()<today]) if candidate_meetings: mtg = sorted(list(candidate_meetings),key=lambda x:x.date,reverse=True)[0] return self.sessionpresentation_set.filter(session__meeting=mtg) @@ -924,13 +937,18 @@ class Document(DocumentInfo): return s def pub_date(self): - """This is the rfc publication date (datetime) for RFCs, - and the new-revision datetime for other documents.""" + """Get the publication date for this document + + This is the rfc publication date for RFCs, and the new-revision date for other documents. + """ if self.get_state_slug() == "rfc": + # As of Sept 2022, in ietf.sync.rfceditor.update_docs_from_rfc_index() `published_rfc` events are + # created with a timestamp whose date *in the PST8PDT timezone* is the official publication date + # assigned by the RFC editor. event = self.latest_event(type='published_rfc') else: event = self.latest_event(type='new_revision') - return event.time + return event.time.astimezone(RPC_TZINFO).date() if event else None def is_dochistory(self): return False @@ -955,7 +973,7 @@ class Document(DocumentInfo): elif rev_events.exists(): time = rev_events.first().time else: - time = datetime.datetime.fromtimestamp(0) + time = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) dh = DocHistory(name=self.name, rev=rev, doc=self, time=time, type=self.type, title=self.title, stream=self.stream, group=self.group) @@ -1208,7 +1226,7 @@ EVENT_TYPES = [ class DocEvent(models.Model): """An occurrence for a document, used for tracking who, when and what.""" - time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened", db_index=True) + time = models.DateTimeField(default=timezone.now, help_text="When the event happened", db_index=True) type = models.CharField(max_length=50, choices=EVENT_TYPES) by = ForeignKey(Person) doc = ForeignKey(Document) @@ -1388,7 +1406,7 @@ class DeletedEvent(models.Model): content_type = ForeignKey(ContentType) json = models.TextField(help_text="Deleted object in JSON format, with attribute names chosen to be suitable for passing into the relevant create method.") by = ForeignKey(Person) - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) def __str__(self): return u"%s by %s %s" % (self.content_type, self.by, self.time) diff --git a/ietf/doc/templatetags/ballot_icon.py b/ietf/doc/templatetags/ballot_icon.py index ee6be139c..8f9aa9ce8 100644 --- a/ietf/doc/templatetags/ballot_icon.py +++ b/ietf/doc/templatetags/ballot_icon.py @@ -38,6 +38,7 @@ import debug # pyflakes:ignore from django import template from django.urls import reverse as urlreverse from django.db.models import Q +from django.utils import timezone from django.utils.safestring import mark_safe from ietf.ietfauth.utils import user_is_person, has_role @@ -173,17 +174,17 @@ def state_age_colored(doc): if iesg_state in ["dead", "watching", "pub", "idexists"]: return "" try: - state_date = ( + state_datetime = ( doc.docevent_set.filter( Q(type="started_iesg_process") | Q(type="changed_state", statedocevent__state_type="draft-iesg") ) .order_by("-time")[0] - .time.date() + .time ) except IndexError: - state_date = datetime.date(1990, 1, 1) - days = (datetime.date.today() - state_date).days + state_datetime = datetime.datetime(1990, 1, 1, tzinfo=datetime.timezone.utc) + days = (timezone.now() - state_datetime).days # loosely based on # https://trac.ietf.org/trac/iesg/wiki/PublishPath if iesg_state == "lc": diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py index c309a7ebd..e7e5a4117 100644 --- a/ietf/doc/templatetags/ietf_filters.py +++ b/ietf/doc/templatetags/ietf_filters.py @@ -18,6 +18,7 @@ from django.urls import reverse as urlreverse from django.core.cache import cache from django.core.exceptions import ValidationError from django.urls import NoReverseMatch +from django.utils import timezone import debug # pyflakes:ignore @@ -318,7 +319,7 @@ def timesince_days(date): """Returns the number of days since 'date' (relative to now)""" if date.__class__ is not datetime.datetime: date = datetime.datetime(date.year, date.month, date.day) - delta = datetime.datetime.now() - date + delta = timezone.now() - date return delta.days @register.filter @@ -637,19 +638,19 @@ def action_holder_badge(action_holder): >>> action_holder_badge(DocumentActionHolderFactory()) '' - >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=15))) + >>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=15))) '' - >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=16))) + >>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=16))) '<span class="badge rounded-pill bg-danger" title="In state for 16 days; goal is <15 days."><i class="bi bi-clock-fill"></i> 16</span>' - >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=30))) + >>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=30))) '<span class="badge rounded-pill bg-danger" title="In state for 30 days; goal is <15 days."><i class="bi bi-clock-fill"></i> 30</span>' >>> settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = old_limit """ age_limit = settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS - age = (datetime.datetime.now() - action_holder.time_added).days + age = (timezone.now() - action_holder.time_added).days if age > age_limit: return mark_safe( '<span class="badge rounded-pill bg-danger" title="In state for %d day%s; goal is <%d days."><i class="bi bi-clock-fill"></i> %d</span>' diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index e7f8ad21c..19e3669fb 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -18,6 +18,7 @@ from pyquery import PyQuery from urllib.parse import urlparse, parse_qs from tempfile import NamedTemporaryFile from collections import defaultdict +from zoneinfo import ZoneInfo from django.core.management import call_command from django.urls import reverse as urlreverse @@ -25,6 +26,7 @@ from django.conf import settings from django.forms import Form from django.utils.html import escape from django.test import override_settings +from django.utils import timezone from django.utils.text import slugify from tastypie.test import ResourceTestCaseMixin @@ -56,6 +58,8 @@ from ietf.utils.mail import outbox, empty_outbox from ietf.utils.test_utils import login_testing_unauthorized, unicontent, reload_db_objects from ietf.utils.test_utils import TestCase from ietf.utils.text import normalize_text +from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO, RPC_TZINFO + class SearchTests(TestCase): def test_search(self): @@ -385,13 +389,13 @@ class SearchTests(TestCase): # Three drafts to show with various warnings drafts = WgDraftFactory.create_batch(3,states=[('draft','active'),('draft-iesg','ad-eval')]) for index, draft in enumerate(drafts): - StateDocEventFactory(doc=draft, state=('draft-iesg','ad-eval'), time=datetime.datetime.now()-datetime.timedelta(days=[1,15,29][index])) + StateDocEventFactory(doc=draft, state=('draft-iesg','ad-eval'), time=timezone.now()-datetime.timedelta(days=[1,15,29][index])) draft.action_holders.set([PersonFactory()]) # And one draft that should not show (with the default of 7 days to view) old = WgDraftFactory() - old.docevent_set.filter(newrevisiondocevent__isnull=False).update(time=datetime.datetime.now()-datetime.timedelta(days=8)) - StateDocEventFactory(doc=old, time=datetime.datetime.now()-datetime.timedelta(days=8)) + old.docevent_set.filter(newrevisiondocevent__isnull=False).update(time=timezone.now()-datetime.timedelta(days=8)) + StateDocEventFactory(doc=old, time=timezone.now()-datetime.timedelta(days=8)) url = urlreverse('ietf.doc.views_search.recent_drafts') r = self.client.get(url) @@ -764,7 +768,7 @@ Man Expires September 22, 2015 [Page 3] replacement = WgDraftFactory( name="draft-ietf-replacement", - time=datetime.datetime.now(), + time=timezone.now(), title="Replacement Draft", stream_id=draft.stream_id, group_id=draft.group_id, abstract=draft.abstract,stream=draft.stream, rev=draft.rev, pages=draft.pages, intended_std_level_id=draft.intended_std_level_id, @@ -1427,6 +1431,8 @@ Man Expires September 22, 2015 [Page 3] def test_draft_group_link(self): """Link to group 'about' page should have correct format""" + event_datetime = datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO) + for group_type_id in ['wg', 'rg', 'ag']: group = GroupFactory(type_id=group_type_id) draft = WgDraftFactory(name='draft-document-%s' % group_type_id, group=group) @@ -1435,7 +1441,7 @@ Man Expires September 22, 2015 [Page 3] self.assert_correct_wg_group_link(r, group) rfc = WgRfcFactory(name='draft-rfc-document-%s' % group_type_id, group=group) - DocEventFactory.create(doc=rfc, type='published_rfc', time = '2010-10-10') + DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime) # get the rfc name to avoid a redirect rfc_name = rfc.docalias.filter(name__startswith='rfc').first().name r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc_name))) @@ -1450,7 +1456,7 @@ Man Expires September 22, 2015 [Page 3] self.assert_correct_non_wg_group_link(r, group) rfc = WgRfcFactory(name='draft-rfc-document-%s' % group_type_id, group=group) - DocEventFactory.create(doc=rfc, type='published_rfc', time = '2010-10-10') + DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime) # get the rfc name to avoid a redirect rfc_name = rfc.docalias.filter(name__startswith='rfc').first().name r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc_name))) @@ -1586,7 +1592,7 @@ class DocTestCase(TestCase): name = "session-72-mars-1", meeting = Meeting.objects.get(number='72'), group = Group.objects.get(acronym='mars'), - modified = datetime.datetime.now(), + modified = timezone.now(), add_to_schedule=False, ) SchedulingEvent.objects.create( @@ -1616,7 +1622,7 @@ class DocTestCase(TestCase): type="changed_ballot_position", pos_id="yes", comment="Looks fine to me", - comment_time=datetime.datetime.now(), + comment_time=timezone.now(), balloter=Person.objects.get(user__username="ad"), by=Person.objects.get(name="(System)")) @@ -1650,7 +1656,7 @@ class DocTestCase(TestCase): type="changed_ballot_position", pos_id="noobj", comment="Still looks okay to me", - comment_time=datetime.datetime.now(), + comment_time=timezone.now(), balloter=Person.objects.get(user__username="ad"), by=Person.objects.get(name="(System)")) @@ -1672,7 +1678,7 @@ class DocTestCase(TestCase): type="changed_ballot_position", pos_id="yes", comment="Looks fine to me", - comment_time=datetime.datetime.now(), + comment_time=timezone.now(), balloter=Person.objects.get(user__username="ad"), by=Person.objects.get(name="(System)")) @@ -1842,7 +1848,7 @@ class DocTestCase(TestCase): desc="Last call\x0b", # include a control character to be sure it does not break anything type="sent_last_call", by=Person.objects.get(user__username="secretary"), - expires=datetime.date.today() + datetime.timedelta(days=7)) + expires=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=7)) r = self.client.get("/feed/last-call/") self.assertEqual(r.status_code, 200) @@ -1890,10 +1896,14 @@ class DocTestCase(TestCase): #other_aliases = ['rfc6020',], states = [('draft','rfc'),('draft-iesg','pub')], std_level_id = 'ps', - time = datetime.datetime(2010,10,10), + time = datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)), ) num = rfc.rfc_number() - DocEventFactory.create(doc=rfc, type='published_rfc', time = '2010-10-10') + DocEventFactory.create( + doc=rfc, + type='published_rfc', + time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO), + ) # url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=rfc.name)) r = self.client.get(url) @@ -1911,10 +1921,14 @@ class DocTestCase(TestCase): stream_id = 'ise', states = [('draft','rfc'),('draft-iesg','pub')], std_level_id = 'inf', - time = datetime.datetime(1990,0o4,0o1), + time = datetime.datetime(1990, 4, 1, tzinfo=ZoneInfo(settings.TIME_ZONE)), ) num = april1.rfc_number() - DocEventFactory.create(doc=april1, type='published_rfc', time = '1990-04-01') + DocEventFactory.create( + doc=april1, + type='published_rfc', + time=datetime.datetime(1990, 4, 1, tzinfo=RPC_TZINFO), + ) # url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=april1.name)) r = self.client.get(url) @@ -2049,7 +2063,8 @@ class GenerateDraftAliasesTests(TestCase): super().tearDown() def testManagementCommand(self): - a_month_ago = datetime.datetime.now() - datetime.timedelta(30) + a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(RPC_TZINFO) + a_month_ago = a_month_ago.replace(hour=0, minute=0, second=0, microsecond=0) ad = RoleFactory(name_id='ad', group__type_id='area', group__state_id='active').person shepherd = PersonFactory() author1 = PersonFactory() @@ -2064,9 +2079,9 @@ class GenerateDraftAliasesTests(TestCase): doc1 = IndividualDraftFactory(authors=[author1], shepherd=shepherd.email(), ad=ad) doc2 = WgDraftFactory(name='draft-ietf-mars-test', group__acronym='mars', authors=[author2], ad=ad) doc3 = WgRfcFactory.create(name='draft-ietf-mars-finished', group__acronym='mars', authors=[author3], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=a_month_ago) - DocEventFactory.create(doc=doc3, type='published_rfc', time=a_month_ago.strftime("%Y-%m-%d")) - doc4 = WgRfcFactory.create(authors=[author4,author5], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=datetime.datetime(2010,10,10)) - DocEventFactory.create(doc=doc4, type='published_rfc', time = '2010-10-10') + DocEventFactory.create(doc=doc3, type='published_rfc', time=a_month_ago) + doc4 = WgRfcFactory.create(authors=[author4,author5], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=datetime.datetime(2010,10,10, tzinfo=ZoneInfo(settings.TIME_ZONE))) + DocEventFactory.create(doc=doc4, type='published_rfc', time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO)) doc5 = IndividualDraftFactory(authors=[author6]) args = [ ] @@ -2217,7 +2232,7 @@ class DocumentMeetingTests(TestCase): self.other_chair = PersonFactory() self.other_group.role_set.create(name_id='chair',person=self.other_chair,email=self.other_chair.email()) - today = datetime.date.today() + today = date_today() cut_days = settings.MEETING_MATERIALS_DEFAULT_SUBMISSION_CORRECTION_DAYS self.past_cutoff = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1+cut_days)) self.past = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=cut_days/2)) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 317e4e3a1..8b8d1c1b1 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -12,6 +12,7 @@ import debug # pyflakes:ignore from django.test import RequestFactory from django.utils.text import slugify from django.urls import reverse as urlreverse +from django.utils import timezone from ietf.doc.models import (Document, State, DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, TelechatDocEvent) @@ -30,6 +31,7 @@ from ietf.person.utils import get_active_ads from ietf.utils.test_utils import TestCase, login_testing_unauthorized from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.text import unwrap +from ietf.utils.timezone import date_today class EditPositionTests(TestCase): @@ -105,7 +107,7 @@ class EditPositionTests(TestCase): draft = WgDraftFactory(ad=ad) url = urlreverse('ietf.doc.views_ballot.api_set_position') create_ballot_if_not_open(None, draft, ad, 'approve') - ad.user.last_login = datetime.datetime.now() + ad.user.last_login = timezone.now() ad.user.save() apikey = PersonalApiKey.objects.create(endpoint=url, person=ad) @@ -238,9 +240,9 @@ class EditPositionTests(TestCase): doc=draft, rev=draft.rev, type="changed_ballot_position", by=ad, balloter=ad, ballot=ballot, pos=BallotPositionName.objects.get(slug="discuss"), discuss="This draft seems to be lacking a clearer title?", - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), comment="Test!", - comment_time=datetime.datetime.now()) + comment_time=timezone.now()) url = urlreverse('ietf.doc.views_ballot.send_ballot_comment', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) @@ -466,7 +468,7 @@ class BallotWriteupsTests(TestCase): doc=draft, rev=draft.rev, desc='issued last call', - expires = datetime.datetime.now()+datetime.timedelta(days = 1 if case=='future' else -1) + expires = timezone.now()+datetime.timedelta(days = 1 if case=='future' else -1) ) url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "ad", url) @@ -791,7 +793,7 @@ class ApproveBallotTests(TestCase): doc=draft, rev=draft.rev, desc='issued last call', - expires = datetime.datetime.now()-datetime.timedelta(days=14) ) + expires = timezone.now()-datetime.timedelta(days=14) ) WriteupDocEvent.objects.create( by=Person.objects.get(name='(System)'), doc=draft, @@ -898,7 +900,7 @@ class MakeLastCallTests(TestCase): mailbox_before = len(outbox) - last_call_sent_date = datetime.date.today() + last_call_sent_date = date_today() expire_date = last_call_sent_date+datetime.timedelta(days=14) r = self.client.post(url, @@ -1117,7 +1119,7 @@ class RegenerateLastCallTestCase(TestCase): class BallotContentTests(TestCase): def test_ballotpositiondocevent_any_email_sent(self): - now = datetime.datetime.now() # be sure event timestamps are at distinct times + now = timezone.now() # be sure event timestamps are at distinct times bpde_with_null_send_email = BallotPositionDocEventFactory( time=now - datetime.timedelta(minutes=30), send_email=None, @@ -1219,7 +1221,7 @@ class BallotContentTests(TestCase): balloter=balloters[0], pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=True, ) BallotPositionDocEventFactory( @@ -1227,7 +1229,7 @@ class BallotContentTests(TestCase): balloter=balloters[1], pos_id='noobj', comment='Commentary', - comment_time=datetime.datetime.now(), + comment_time=timezone.now(), send_email=True, ) @@ -1237,7 +1239,7 @@ class BallotContentTests(TestCase): balloter=balloters[2], pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=False, ) BallotPositionDocEventFactory( @@ -1245,7 +1247,7 @@ class BallotContentTests(TestCase): balloter=balloters[3], pos_id='noobj', comment='Commentary', - comment_time=datetime.datetime.now(), + comment_time=timezone.now(), send_email=False, ) @@ -1255,7 +1257,7 @@ class BallotContentTests(TestCase): balloter=balloters[4], pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now() - datetime.timedelta(days=1), + discuss_time=timezone.now() - datetime.timedelta(days=1), send_email=True, ) BallotPositionDocEventFactory( @@ -1263,7 +1265,7 @@ class BallotContentTests(TestCase): balloter=balloters[4], pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=False, ) BallotPositionDocEventFactory( @@ -1271,7 +1273,7 @@ class BallotContentTests(TestCase): balloter=balloters[5], pos_id='noobj', comment='Commentary', - comment_time=datetime.datetime.now() - datetime.timedelta(days=1), + comment_time=timezone.now() - datetime.timedelta(days=1), send_email=True, ) BallotPositionDocEventFactory( @@ -1279,7 +1281,7 @@ class BallotContentTests(TestCase): balloter=balloters[5], pos_id='noobj', comment='Commentary', - comment_time=datetime.datetime.now(), + comment_time=timezone.now(), send_email=False, ) @@ -1296,7 +1298,7 @@ class BallotContentTests(TestCase): balloter__plain='plain name1', pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=False, ).balloter send_email_balloter = BallotPositionDocEventFactory( @@ -1304,7 +1306,7 @@ class BallotContentTests(TestCase): balloter__plain='plain name2', pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=True, ).balloter prev_send_email_balloter = BallotPositionDocEventFactory( @@ -1312,7 +1314,7 @@ class BallotContentTests(TestCase): balloter__plain='plain name3', pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now() - datetime.timedelta(days=1), + discuss_time=timezone.now() - datetime.timedelta(days=1), send_email=True, ).balloter BallotPositionDocEventFactory( @@ -1320,7 +1322,7 @@ class BallotContentTests(TestCase): balloter=prev_send_email_balloter, pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=False, ) @@ -1351,7 +1353,7 @@ class BallotContentTests(TestCase): balloter=balloters[0], pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=None, ) BallotPositionDocEventFactory( @@ -1359,7 +1361,7 @@ class BallotContentTests(TestCase): balloter=balloters[1], pos_id='noobj', comment='Commentary', - comment_time=datetime.datetime.now(), + comment_time=timezone.now(), send_email=None, ) old_balloter = BallotPositionDocEventFactory( @@ -1367,7 +1369,7 @@ class BallotContentTests(TestCase): balloter__plain='plain name', # ensure plain name is slugifiable pos_id='discuss', discuss='Discussion text', - discuss_time=datetime.datetime.now(), + discuss_time=timezone.now(), send_email=None, ).balloter diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index 4d02108d0..9925ec3d1 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -14,6 +14,7 @@ from html import unescape from django.conf import settings from django.urls import reverse as urlreverse from django.template.loader import render_to_string +from django.utils import timezone from ietf.group.factories import RoleFactory from ietf.doc.factories import BofreqFactory, NewRevisionDocEventFactory @@ -48,7 +49,7 @@ This test section has some text. states = State.objects.filter(type_id='bofreq') self.assertTrue(states.count()>0) for i in range(3*len(states)): - BofreqFactory(states=[('bofreq',states[i%len(states)].slug)],newrevisiondocevent__time=datetime.datetime.today()-datetime.timedelta(days=randint(0,20))) + BofreqFactory(states=[('bofreq',states[i%len(states)].slug)],newrevisiondocevent__time=timezone.now()-datetime.timedelta(days=randint(0,20))) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index 8732b7701..c420fdd0a 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -26,6 +26,8 @@ from ietf.person.models import Person from ietf.utils.test_utils import TestCase from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import login_testing_unauthorized +from ietf.utils.timezone import datetime_today, date_today, DEADLINE_TZINFO + class ViewCharterTests(TestCase): def test_view_revisions(self): @@ -402,7 +404,7 @@ class EditCharterTests(TestCase): # Make it so that the charter has been through internal review, and passed its external review # ballot on a previous telechat - last_week = datetime.date.today()-datetime.timedelta(days=7) + last_week = datetime_today(DEADLINE_TZINFO) - datetime.timedelta(days=7) BallotDocEvent.objects.create(type='created_ballot',by=login,doc=charter, rev=charter.rev, ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'), time=last_week) @@ -746,7 +748,7 @@ class EditCharterTests(TestCase): charter.set_state(State.objects.get(used=True, type="charter", slug="iesgrev")) - due_date = datetime.date.today() + datetime.timedelta(days=180) + due_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=180) m1 = GroupMilestone.objects.create(group=group, state_id="active", desc="Has been copied", @@ -826,7 +828,7 @@ class EditCharterTests(TestCase): m = GroupMilestone.objects.create(group=charter.group, state_id="active", desc="Test milestone", - due=datetime.date.today(), + due=date_today(DEADLINE_TZINFO), resolved="") url = urlreverse('ietf.doc.views_charter.charter_with_milestones_txt', kwargs=dict(name=charter.name, rev=charter.rev)) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 1adf243e2..518bff377 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -13,6 +13,7 @@ from pyquery import PyQuery from django.urls import reverse as urlreverse from django.conf import settings +from django.utils import timezone from django.utils.html import escape import debug # pyflakes:ignore @@ -33,6 +34,7 @@ from ietf.iesg.models import TelechatDate from ietf.utils.test_utils import login_testing_unauthorized from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase +from ietf.utils.timezone import date_today, datetime_from_date class ChangeStateTests(TestCase): @@ -401,11 +403,11 @@ class EditInfoTests(TestCase): # change to a telechat that should cause returning item to be auto-detected # First, make it appear that the previous telechat has already passed - telechat_event.telechat_date = datetime.date.today()-datetime.timedelta(days=7) + telechat_event.telechat_date = date_today() - datetime.timedelta(days=7) telechat_event.save() ad = Person.objects.get(user__username="ad") ballot = create_ballot_if_not_open(None, draft, ad, 'approve') - ballot.time = telechat_event.telechat_date + ballot.time = datetime_from_date(telechat_event.telechat_date) ballot.save() r = self.client.post(url, data) @@ -428,7 +430,7 @@ class EditInfoTests(TestCase): self.assertTrue("Telechat update" in outbox[-1]['Subject']) # Put it on an agenda that's very soon from now - next_week = datetime.date.today()+datetime.timedelta(days=7) + next_week = date_today() + datetime.timedelta(days=7) td = TelechatDate.objects.active()[0] td.date = next_week td.save() @@ -618,7 +620,7 @@ class ResurrectTests(DraftFileMixin, TestCase): self.assertEqual(draft.docevent_set.count(), events_before + 1) self.assertEqual(draft.latest_event().type, "completed_resurrect") self.assertEqual(draft.get_state_slug(), "active") - self.assertTrue(draft.expires >= datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1)) + self.assertTrue(draft.expires >= timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1)) self.assertEqual(len(outbox), mailbox_before + 1) self.assertTrue('Resurrection Completed' in outbox[-1]['Subject']) self.assertTrue('iesg-secretary' in outbox[-1]['To']) @@ -638,7 +640,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase): meeting = Meeting.objects.create(number="123", type=MeetingTypeName.objects.get(slug="ietf"), - date=datetime.date.today()) + date=date_today()) second_cut_off = meeting.get_second_cut_off() ietf_monday = meeting.get_ietf_monday() @@ -659,7 +661,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase): # hack into expirable state draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) - draft.expires = datetime.datetime.now() + datetime.timedelta(days=10) + draft.expires = timezone.now() + datetime.timedelta(days=10) draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) self.assertEqual(len(list(get_soon_to_expire_drafts(14))), 1) @@ -698,7 +700,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase): # hack into expirable state draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists')) - draft.expires = datetime.datetime.now() + draft.expires = timezone.now() draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) self.assertEqual(len(list(get_expired_drafts())), 1) @@ -741,7 +743,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase): draft.delete() - rgdraft = RgDraftFactory(expires=datetime.datetime.now()) + rgdraft = RgDraftFactory(expires=timezone.now()) self.assertEqual(len(list(get_expired_drafts())), 1) for slug in ('iesg-rev','irsgpoll'): rgdraft.set_state(State.objects.get(type_id='draft-stream-irtf',slug=slug)) @@ -791,7 +793,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase): # expire draft draft.set_state(State.objects.get(used=True, type="draft", slug="expired")) - draft.expires = datetime.datetime.now() - datetime.timedelta(days=1) + draft.expires = timezone.now() - datetime.timedelta(days=1) draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) e = DocEvent(doc=draft, rev=draft.rev, type= "expired_document", time=draft.expires, @@ -824,7 +826,7 @@ class ExpireLastCallTests(TestCase): e = LastCallDocEvent(doc=draft, rev=draft.rev, type="sent_last_call", by=secretary) e.text = "Last call sent" - e.expires = datetime.datetime.now() + datetime.timedelta(days=14) + e.expires = timezone.now() + datetime.timedelta(days=14) e.save() self.assertEqual(len(list(get_expired_last_calls())), 0) @@ -832,7 +834,7 @@ class ExpireLastCallTests(TestCase): # test expired e = LastCallDocEvent(doc=draft, rev=draft.rev, type="sent_last_call", by=secretary) e.text = "Last call sent" - e.expires = datetime.datetime.now() + e.expires = timezone.now() e.save() drafts = list(get_expired_last_calls()) @@ -866,7 +868,7 @@ class ExpireLastCallTests(TestCase): e = LastCallDocEvent(doc=draft, rev=draft.rev, type="sent_last_call", by=secretary) e.text = "Last call sent" e.desc = "Blah, blah, blah.\n\nThis document makes the following downward references (downrefs):\n ** Downref: Normative reference to an Experimental RFC: RFC 4764" - e.expires = datetime.datetime.now() + e.expires = timezone.now() e.save() drafts = list(get_expired_last_calls()) @@ -1730,7 +1732,7 @@ class ChangeStreamStateTests(TestCase): self.assertEqual(draft.docevent_set.count() - events_before, 2) reminder = DocReminder.objects.filter(event__doc=draft, type="stream-s") self.assertEqual(len(reminder), 1) - due = datetime.datetime.now() + datetime.timedelta(weeks=10) + due = timezone.now() + datetime.timedelta(weeks=10) self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1)) self.assertEqual(len(outbox), 1) self.assertTrue("state changed" in outbox[0]["Subject"].lower()) @@ -1775,7 +1777,7 @@ class ChangeStreamStateTests(TestCase): self.assertEqual(draft.docevent_set.count() - events_before, 2) reminder = DocReminder.objects.filter(event__doc=draft, type="stream-s") self.assertEqual(len(reminder), 1) - due = datetime.datetime.now() + datetime.timedelta(weeks=10) + due = timezone.now() + datetime.timedelta(weeks=10) self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1)) self.assertEqual(len(outbox), 1) self.assertTrue("state changed" in outbox[0]["Subject"].lower()) @@ -1826,7 +1828,7 @@ class ChangeReplacesTests(TestCase): name="draft-test-base-b", title="Base B", group=mars_wg, - expires = datetime.datetime.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), + expires = timezone.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), ) p = PersonFactory(name="baseb_author") e = Email.objects.create(address="baseb_author@example.com", person=p, origin=p.user.username) diff --git a/ietf/doc/tests_irsg_ballot.py b/ietf/doc/tests_irsg_ballot.py index f178bb4e5..da1b48fc6 100644 --- a/ietf/doc/tests_irsg_ballot.py +++ b/ietf/doc/tests_irsg_ballot.py @@ -19,6 +19,7 @@ from ietf.doc.utils import create_ballot_if_not_open, close_ballot from ietf.person.utils import get_active_irsg, get_active_ads from ietf.group.factories import RoleFactory from ietf.person.models import Person +from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO class IssueIRSGBallotTests(TestCase): @@ -254,7 +255,7 @@ class IssueIRSGBallotTests(TestCase): irsgmember = get_active_irsg()[0] secr = RoleFactory(group__acronym='secretariat',name_id='secr') wg_ballot = create_ballot_if_not_open(None, wg_draft, ad.person, 'approve') - due = datetime.date.today()+datetime.timedelta(days=14) + due = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=14) rg_ballot = create_ballot_if_not_open(None, rg_draft, secr.person, 'irsg-approve', due) url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=wg_draft.name, ballot_id=wg_ballot.pk)) @@ -323,7 +324,7 @@ class BaseManipulationTests(): def test_issue_ballot(self): draft = RgDraftFactory() url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot',kwargs=dict(name=draft.name)) - due = datetime.date.today()+datetime.timedelta(days=14) + due = date_today(DEADLINE_TZINFO)+datetime.timedelta(days=14) empty_outbox() login_testing_unauthorized(self, self.username , url) @@ -444,7 +445,7 @@ class IRSGMemberTests(TestCase): def test_cant_issue_irsg_ballot(self): draft = RgDraftFactory() - due = datetime.date.today()+datetime.timedelta(days=14) + due = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=14) url = urlreverse('ietf.doc.views_ballot.close_irsg_ballot', kwargs=dict(name=draft.name)) self.client.login(username = self.username, password = self.username+'+password') diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py index 1e922197d..05bbc2078 100644 --- a/ietf/doc/tests_material.py +++ b/ietf/doc/tests_material.py @@ -4,7 +4,6 @@ import os import shutil -import datetime import io from pathlib import Path @@ -14,6 +13,7 @@ import debug # pyflakes:ignore from django.conf import settings from django.urls import reverse as urlreverse +from django.utils import timezone from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent from ietf.group.factories import RoleFactory @@ -155,7 +155,7 @@ class GroupMaterialTests(TestCase): name = "session-42-mars-1", meeting = Meeting.objects.get(number='42'), group = Group.objects.get(acronym='mars'), - modified = datetime.datetime.now(), + modified = timezone.now(), ) SchedulingEvent.objects.create( session=session, diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py index 7e902514d..8e3fdcf22 100644 --- a/ietf/doc/tests_review.py +++ b/ietf/doc/tests_review.py @@ -10,10 +10,10 @@ import email.mime.multipart, email.mime.text, email.utils from mock import patch from requests import Response - from django.apps import apps from django.urls import reverse as urlreverse from django.conf import settings +from django.utils import timezone from pyquery import PyQuery @@ -38,6 +38,7 @@ from ietf.utils.mail import outbox, empty_outbox, parseaddr, on_behalf_of, get_p from ietf.utils.test_utils import login_testing_unauthorized, reload_db_objects from ietf.utils.test_utils import TestCase from ietf.utils.text import strip_prefix, xslugify +from ietf.utils.timezone import date_today, DEADLINE_TZINFO from django.utils.html import escape class ReviewTests(TestCase): @@ -67,7 +68,7 @@ class ReviewTests(TestCase): RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr') RoleFactory(group=review_team3,person__user__username='reviewsecretary3',person__user__email='reviewsecretary3@example.com',name_id='secr') - req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) ReviewAssignmentFactory(review_request = req, reviewer = rev_role.person.email_set.first(), state_id='accepted') url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name }) @@ -77,7 +78,7 @@ class ReviewTests(TestCase): r = self.client.get(url) self.assertEqual(r.status_code, 200) - deadline = datetime.date.today() + datetime.timedelta(days=10) + deadline = date_today() + datetime.timedelta(days=10) empty_outbox() @@ -145,7 +146,7 @@ class ReviewTests(TestCase): doc = WgDraftFactory(group__acronym='mars',rev='01') review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) ReviewAssignmentFactory(review_request=review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted') # move the review request to a doubly-replaced document to @@ -166,7 +167,7 @@ class ReviewTests(TestCase): doc = WgDraftFactory(group__acronym='mars',rev='01', authors=[author]) review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted') url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk }) @@ -195,7 +196,7 @@ class ReviewTests(TestCase): rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr') RoleFactory(group=review_team,person__user__username='reviewsecretary2',person__user__email='reviewsecretary2@example.com',name_id='secr') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) ReviewAssignmentFactory(review_request=review_req, state_id='accepted', reviewer=rev_role.person.email_set.first()) close_url = urlreverse('ietf.doc.views_review.close_request', kwargs={ "name": doc.name, "request_id": review_req.pk }) @@ -260,14 +261,14 @@ class ReviewTests(TestCase): # previous review req = ReviewRequestFactory( - time=datetime.datetime.now() - datetime.timedelta(days=100), + time=timezone.now() - datetime.timedelta(days=100), requested_by=Person.objects.get(name="(System)"), doc=doc, type_id='early', team=review_req.team, state_id='assigned', requested_rev="01", - deadline=datetime.date.today() - datetime.timedelta(days=80), + deadline=date_today() - datetime.timedelta(days=80), ) ReviewAssignmentFactory( review_request = req, @@ -372,7 +373,7 @@ class ReviewTests(TestCase): doc = WgDraftFactory(group__acronym='mars',rev='01') review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) assignment = ReviewAssignmentFactory(review_request=review_req, state_id='assigned', reviewer=rev_role.person.email_set.first()) url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk }) @@ -395,7 +396,7 @@ class ReviewTests(TestCase): review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) assignment = ReviewAssignmentFactory(review_request = review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted') reject_url = urlreverse('ietf.doc.views_review.reject_reviewer_assignment', kwargs={ "name": doc.name, "assignment_id": assignment.pk }) @@ -495,7 +496,7 @@ class ReviewTests(TestCase): review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) assignment = ReviewAssignmentFactory(review_request=review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted') # test URL construction @@ -525,7 +526,7 @@ class ReviewTests(TestCase): messages = r.json()["messages"] self.assertEqual(len(messages), 2) - today = datetime.date.today() + today = date_today() self.assertEqual(messages[0]["url"], "https://www.example.com/testmessage") self.assertTrue("John Doe" in messages[0]["content"]) @@ -587,7 +588,7 @@ class ReviewTests(TestCase): review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) assignment = ReviewAssignmentFactory(review_request=review_req, state_id='accepted', reviewer=rev_role.person.email_set.first()) for r in ReviewResultName.objects.filter(slug__in=("issues", "ready")): review_req.team.reviewteamsettings.review_results.add(r) @@ -699,7 +700,7 @@ class ReviewTests(TestCase): assignment = reload_db_objects(assignment) self.assertEqual(assignment.state_id, "completed") # Completed time should be close to now, but will not be exactly, so check within 10s margin - completed_time_diff = datetime.datetime.now() - assignment.completed_on + completed_time_diff = timezone.now() - assignment.completed_on self.assertLess(completed_time_diff, datetime.timedelta(seconds=10)) with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: @@ -733,15 +734,15 @@ class ReviewTests(TestCase): # The secretary is allowed to set a custom completion date (#2590) assignment = reload_db_objects(assignment) self.assertEqual(assignment.state_id, "completed") - self.assertEqual(assignment.completed_on, datetime.datetime(2012, 12, 24, 12, 13, 14)) + self.assertEqual(assignment.completed_on, datetime.datetime(2012, 12, 24, 12, 13, 14, tzinfo=DEADLINE_TZINFO)) # There should be two events: # - the event logging when the change when it was entered, i.e. very close to now. # - the completion of the review, set to the provided date/time events = ReviewAssignmentDocEvent.objects.filter(doc=assignment.review_request.doc).order_by('-time') - event0_time_diff = datetime.datetime.now() - events[0].time + event0_time_diff = timezone.now() - events[0].time self.assertLess(event0_time_diff, datetime.timedelta(seconds=10)) - self.assertEqual(events[1].time, datetime.datetime(2012, 12, 24, 12, 13, 14)) + self.assertEqual(events[1].time, datetime.datetime(2012, 12, 24, 12, 13, 14, tzinfo=DEADLINE_TZINFO)) with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: self.assertEqual(f.read(), "This is a review\nwith two lines") @@ -898,7 +899,7 @@ class ReviewTests(TestCase): assignment.review_request.team.acronym, assignment.review_request.type.slug, xslugify(assignment.reviewer.person.ascii_parts()[3]), - datetime.date.today().isoformat(), + date_today().isoformat(), ] review_name = "-".join(c for c in name_components if c).lower() Document.objects.create(name=review_name,type_id='review',group=assignment.review_request.team) @@ -985,7 +986,7 @@ class ReviewTests(TestCase): self.assertEqual(assignment.state_id, "completed") # The revision event time should be the date the revision was submitted, i.e. not backdated event1 = assignment.review_request.doc.latest_event(ReviewAssignmentDocEvent) - event_time_diff = datetime.datetime.now() - event1.time + event_time_diff = timezone.now() - event1.time self.assertLess(event_time_diff, datetime.timedelta(seconds=10)) self.assertTrue('revised' in event1.desc.lower()) @@ -1012,7 +1013,7 @@ class ReviewTests(TestCase): assignment = reload_db_objects(assignment) self.assertEqual(assignment.review.rev, "01") event2 = assignment.review_request.doc.latest_event(ReviewAssignmentDocEvent) - event_time_diff = datetime.datetime.now() - event2.time + event_time_diff = timezone.now() - event2.time self.assertLess(event_time_diff, datetime.timedelta(seconds=10)) # Ensure that a new event was created for the new revision (#2590) self.assertNotEqual(event1.id, event2.id) @@ -1024,7 +1025,7 @@ class ReviewTests(TestCase): review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted') url = urlreverse('ietf.doc.views_review.edit_comment', kwargs={ "name": doc.name, "request_id": review_req.pk }) @@ -1046,7 +1047,7 @@ class ReviewTests(TestCase): review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr') - review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20)) + review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20)) ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted') url = urlreverse('ietf.doc.views_review.edit_deadline', kwargs={ "name": doc.name, "request_id": review_req.pk }) diff --git a/ietf/doc/tests_utils.py b/ietf/doc/tests_utils.py index aef6eb69a..f5f2fdd6b 100644 --- a/ietf/doc/tests_utils.py +++ b/ietf/doc/tests_utils.py @@ -16,6 +16,7 @@ from ietf.doc.models import State, DocumentActionHolder, DocumentAuthor, Documen from ietf.doc.utils import (update_action_holders, add_state_change_event, update_documentauthors, fuzzy_find_documents, rebuild_reference_relations) from ietf.utils.draft import Draft, PlaintextDraft +from ietf.utils.timezone import date_today from ietf.utils.xmldraft import XMLDraft @@ -143,12 +144,13 @@ class ActionHoldersTests(TestCase): doc = self.doc_in_iesg_state('pub-req') doc.action_holders.set([self.ad]) dah = doc.documentactionholder_set.get(person=self.ad) - dah.time_added = datetime.datetime(2020, 1, 1) # arbitrary date in the past + dah.time_added = datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc) # arbitrary date in the past dah.save() - self.assertNotEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today()) + today = date_today() + self.assertNotEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), today) self.update_doc_state(doc, State.objects.get(slug='ad-eval')) - self.assertEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today()) + self.assertEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), today) def test_update_action_holders_add_tag_need_rev(self): """Adding need-rev tag adds authors as action holders""" diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 01b529c5e..65666ab6c 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -13,12 +13,14 @@ import textwrap from collections import defaultdict, namedtuple from urllib.parse import quote +from zoneinfo import ZoneInfo from django.conf import settings from django.contrib import messages from django.forms import ValidationError from django.http import Http404 from django.template.loader import render_to_string +from django.utils import timezone from django.utils.html import escape from django.urls import reverse as urlreverse @@ -39,6 +41,7 @@ from ietf.review.models import ReviewWish from ietf.utils import draft, log from ietf.utils.mail import send_mail from ietf.mailtrigger.utils import gather_address_lists +from ietf.utils.timezone import date_today, datetime_from_date, datetime_today, DEADLINE_TZINFO from ietf.utils.xmldraft import XMLDraft @@ -637,11 +640,22 @@ def has_same_ballot(doc, date1, date2=None): """ Test if the most recent ballot created before the end of date1 is the same as the most recent ballot created before the end of date 2. """ + datetime1 = datetime_from_date(date1, DEADLINE_TZINFO) if date2 is None: - date2 = datetime.date.today() - ballot1 = doc.latest_event(BallotDocEvent,type='created_ballot',time__lt=date1+datetime.timedelta(days=1)) - ballot2 = doc.latest_event(BallotDocEvent,type='created_ballot',time__lt=date2+datetime.timedelta(days=1)) - return ballot1==ballot2 + datetime2 = datetime_today(DEADLINE_TZINFO) + else: + datetime2 = datetime_from_date(date2, DEADLINE_TZINFO) + ballot1 = doc.latest_event( + BallotDocEvent, + type='created_ballot', + time__lt=datetime1 + datetime.timedelta(days=1), + ) + ballot2 = doc.latest_event( + BallotDocEvent, + type='created_ballot', + time__lt=datetime2 + datetime.timedelta(days=1), + ) + return ballot1 == ballot2 def make_notify_changed_event(request, doc, by, new_notify, time=None): @@ -687,7 +701,7 @@ def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None and on_agenda and prev_agenda and new_telechat_date != prev_telechat - and prev_telechat < datetime.date.today() + and prev_telechat < date_today(DEADLINE_TZINFO) and has_same_ballot(doc,prev.telechat_date) ): returning = True @@ -718,7 +732,7 @@ def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None e.save() - has_short_fuse = doc.type_id=='draft' and new_telechat_date and (( new_telechat_date - datetime.date.today() ) < datetime.timedelta(days=13)) + has_short_fuse = doc.type_id=='draft' and new_telechat_date and (( new_telechat_date - date_today() ) < datetime.timedelta(days=13)) from ietf.doc.mails import email_update_telechat @@ -808,7 +822,7 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, com cc.update(other_addrs.cc) RelatedDocument.objects.filter(source=doc, target=d, relationship=relationship).delete() if not RelatedDocument.objects.filter(target=d, relationship=relationship): - s = 'active' if d.document.expires > datetime.datetime.now() else 'expired' + s = 'active' if d.document.expires > timezone.now() else 'expired' d.document.set_state(State.objects.get(type='draft', slug=s)) for d in new_replaces: @@ -956,6 +970,7 @@ def make_rev_history(doc): history[url]['pages'] = d.history_set.filter(rev=e.newrevisiondocevent.rev).first().pages if doc.type_id == "draft": + # e.time.date() agrees with RPC publication date when shown in the RPC_TZINFO time zone e = doc.latest_event(type='published_rfc') else: e = doc.latest_event(type='iesg_approved') @@ -1119,7 +1134,7 @@ def build_doc_meta_block(doc, path): lines[i] = line return lines # - now = datetime.datetime.now() + now = timezone.now() draft_state = doc.get_state('draft') block = '' meta = {} @@ -1337,14 +1352,15 @@ def bibxml_for_draft(doc, rev=None): latest_revision_event = doc.latest_event(NewRevisionDocEvent, type="new_revision") latest_revision_rev = latest_revision_event.rev if latest_revision_event else None best_events = NewRevisionDocEvent.objects.filter(doc__name=doc.name, rev=(rev or latest_revision_rev)) + tzinfo = ZoneInfo(settings.TIME_ZONE) if best_events.exists(): # There was a period where it was possible to get more than one NewRevisionDocEvent for a revision. # A future data cleanup would allow this to be simplified best_event = best_events.order_by('time').first() log.assertion('doc.rev == best_event.rev') - doc.date = best_event.time.date() + doc.date = best_event.time.astimezone(tzinfo).date() else: - doc.date = doc.time.date() # Even if this may be incoreect, what would be better? + doc.date = doc.time.astimezone(tzinfo).date() # Even if this may be incorrect, what would be better? return render_to_string('doc/bibxml.xml', {'name':doc.name, 'doc': doc, 'doc_bibtype':'I-D'}) diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index ce9552106..d14684d42 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -11,6 +11,7 @@ import shutil from django.conf import settings from django.urls import reverse as urlreverse from django.template.loader import render_to_string +from django.utils import timezone from django.utils.encoding import smart_text, force_text import debug # pyflakes:ignore @@ -24,6 +25,8 @@ from ietf.utils.mail import parse_preformatted from ietf.mailtrigger.utils import gather_address_lists from ietf.utils.log import log from ietf.group.utils import save_group_in_history +from ietf.utils.timezone import date_today + def charter_name_for_group(group): if group.type_id == "rg": @@ -73,7 +76,7 @@ def change_group_state_after_charter_approval(group, by): save_group_in_history(group) group.state = new_state - group.time = datetime.datetime.now() + group.time = timezone.now() group.save() # create an event for the group state change, too @@ -132,7 +135,7 @@ def historic_milestones_for_charter(charter, rev): # revision (when approving a charter) just_before_next_rev = e[0].time - datetime.timedelta(seconds=5) else: - just_before_next_rev = datetime.datetime.now() + just_before_next_rev = timezone.now() res = [] if hasattr(charter, 'chartered_group'): @@ -197,7 +200,7 @@ def derive_new_work_text(review_text,group): return smart_text(m.as_string()) def default_review_text(group, charter, by): - now = datetime.datetime.now() + now = timezone.now() addrs = gather_address_lists('charter_external_review',group=group).as_strings(compact=False) e1 = WriteupDocEvent(doc=charter, rev=charter.rev, by=by) @@ -215,7 +218,7 @@ def default_review_text(group, charter, by): parent_ads=group.parent.role_set.filter(name='ad'), techadv=group.role_set.filter(name="techadv"), milestones=group.groupmilestone_set.filter(state="charter"), - review_date=(datetime.date.today() + datetime.timedelta(weeks=1)).isoformat(), + review_date=(date_today() + datetime.timedelta(weeks=1)).isoformat(), review_type="new" if group.state_id in ["proposed","bof"] else "recharter", to=addrs.to, cc=addrs.cc, diff --git a/ietf/doc/utils_search.py b/ietf/doc/utils_search.py index 47532fdb3..0353d5296 100644 --- a/ietf/doc/utils_search.py +++ b/ietf/doc/utils_search.py @@ -5,11 +5,17 @@ import re import datetime import debug # pyflakes:ignore +from zoneinfo import ZoneInfo + +from django.conf import settings + from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent from ietf.doc.expire import expirable_drafts from ietf.doc.utils import augment_docs_and_user_with_user_info from ietf.meeting.models import SessionPresentation, Meeting, Session from ietf.review.utils import review_assignments_to_list_for_docs +from ietf.utils.timezone import date_today + def wrap_value(v): return lambda: v @@ -30,8 +36,9 @@ def fill_in_telechat_date(docs, doc_dict=None, doc_ids=None): seen.add(e.doc_id) def fill_in_document_sessions(docs, doc_dict, doc_ids): - beg_date = datetime.date.today()-datetime.timedelta(days=7) - end_date = datetime.date.today()+datetime.timedelta(days=30) + today = date_today() + beg_date = today-datetime.timedelta(days=7) + end_date = today+datetime.timedelta(days=30) meetings = Meeting.objects.filter(date__gte=beg_date, date__lte=end_date).prefetch_related('session_set') # get sessions sessions = Session.objects.filter(meeting_id__in=[ m.id for m in meetings ]) @@ -204,7 +211,7 @@ def prepare_document_table(request, docs, query=None, max_results=200): if sort_key == "title": res.append(d.title) elif sort_key == "date": - res.append(str(d.latest_revision_date)) + res.append(str(d.latest_revision_date.astimezone(ZoneInfo(settings.TIME_ZONE)))) elif sort_key == "status": if rfc_num != None: res.append(num(rfc_num)) diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 7a4e34571..207b8b972 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -40,6 +40,8 @@ from ietf.person.models import Person from ietf.utils.mail import send_mail_text, send_mail_preformatted from ietf.utils.decorators import require_api_key from ietf.utils.response import permission_denied +from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO + BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), @@ -1055,9 +1057,11 @@ def make_last_call(request, name): e.desc = "The following Last Call announcement was sent out (ends %s):<br><br>" % expiration_date e.desc += announcement - if form.cleaned_data['last_call_sent_date'] != e.time.date(): - e.time = datetime.datetime.combine(form.cleaned_data['last_call_sent_date'], e.time.time()) - e.expires = expiration_date + e_production_time = e.time.astimezone(DEADLINE_TZINFO) + if form.cleaned_data['last_call_sent_date'] != e_production_time.date(): + lcsd = form.cleaned_data['last_call_sent_date'] + e.time = e_production_time.replace(year=lcsd.year, month=lcsd.month, day=lcsd.day) # preserves tzinfo + e.expires = datetime_from_date(expiration_date, DEADLINE_TZINFO) e.save() events.append(e) @@ -1080,7 +1084,7 @@ def make_last_call(request, name): return HttpResponseRedirect(doc.get_absolute_url()) else: initial = {} - initial["last_call_sent_date"] = datetime.date.today() + initial["last_call_sent_date"] = date_today() if doc.type.slug == 'draft': # This logic is repeated in the code that edits last call text - why? expire_days = 14 @@ -1091,7 +1095,7 @@ def make_last_call(request, name): expire_days=28 templ = 'doc/status_change/make_last_call.html' - initial["last_call_expiration_date"] = datetime.date.today() + datetime.timedelta(days=expire_days) + initial["last_call_expiration_date"] = date_today() + datetime.timedelta(days=expire_days) form = MakeLastCallForm(initial=initial) @@ -1108,7 +1112,7 @@ def issue_irsg_ballot(request, name): raise Http404 by = request.user.person - fillerdate = datetime.date.today() + datetime.timedelta(weeks=2) + fillerdate = date_today(DEADLINE_TZINFO) + datetime.timedelta(weeks=2) if request.method == 'POST': button = request.POST.get("irsg_button") @@ -1117,7 +1121,7 @@ def issue_irsg_ballot(request, name): e = IRSGBallotDocEvent(doc=doc, rev=doc.rev, by=request.user.person) if (duedate == None or duedate==""): duedate = str(fillerdate) - e.duedate = datetime.datetime.strptime(duedate, '%Y-%m-%d') + e.duedate = datetime_from_date(datetime.datetime.strptime(duedate, '%Y-%m-%d'), DEADLINE_TZINFO) e.type = "created_ballot" e.desc = "Created IRSG Ballot" ballot_type = BallotType.objects.get(doc_type=doc.type, slug="irsg-approve") @@ -1188,7 +1192,10 @@ def irsg_ballot_status(request): ballot = doc.active_ballot() if ballot: doc.ballot = ballot - doc.duedate=datetime.datetime.strftime(ballot.irsgballotdocevent.duedate, '%Y-%m-%d') + doc.duedate=datetime.datetime.strftime( + ballot.irsgballotdocevent.duedate.astimezone(DEADLINE_TZINFO), + '%Y-%m-%d', + ) docs.append(doc) diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index c2b88ac47..3f85a19ce 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -16,6 +16,7 @@ from django.utils.safestring import mark_safe from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.utils import timezone from django.utils.encoding import force_text from django.utils.html import escape @@ -77,7 +78,7 @@ def change_state(request, name, option=None): chartering_type = get_chartering_type(charter) initial_review = charter.latest_event(InitialReviewDocEvent, type="initial_review") - if charter.get_state_slug() != "infrev" or (initial_review and initial_review.expires < datetime.datetime.now()) or chartering_type == "rechartering": + if charter.get_state_slug() != "infrev" or (initial_review and initial_review.expires < timezone.now()) or chartering_type == "rechartering": initial_review = None by = request.user.person @@ -183,7 +184,7 @@ def change_state(request, name, option=None): if charter_state.slug == "infrev" and clean["initial_time"] and clean["initial_time"] != 0: e = InitialReviewDocEvent(type="initial_review", by=by, doc=charter, rev=charter.rev) - e.expires = datetime.datetime.now() + datetime.timedelta(weeks=clean["initial_time"]) + e.expires = timezone.now() + datetime.timedelta(weeks=clean["initial_time"]) e.desc = "Initial review time expires %s" % e.expires.strftime("%Y-%m-%d") e.save() @@ -506,7 +507,7 @@ def review_announcement_text(request, name): existing_new_work.type = "changed_new_work_text" existing_new_work.desc = "%s review text was changed" % group.type.name existing_new_work.text = derive_new_work_text(existing.text,group) - existing_new_work.time = datetime.datetime.now() + existing_new_work.time = timezone.now() form = ReviewAnnouncementTextForm(initial=dict(announcement_text=escape(existing.text),new_work_text=escape(existing_new_work.text))) @@ -514,7 +515,7 @@ def review_announcement_text(request, name): form = ReviewAnnouncementTextForm(request.POST) if "save_text" in request.POST and form.is_valid(): - now = datetime.datetime.now() + now = timezone.now() events = [] t = form.cleaned_data['announcement_text'] diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 7475c6b1f..bdf3b6c1a 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -34,7 +34,6 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import datetime import glob import io import json @@ -85,6 +84,7 @@ from ietf.utils import markup_txt, log, markdown from ietf.utils.draft import PlaintextDraft from ietf.utils.response import permission_denied from ietf.utils.text import maybe_split +from ietf.utils.timezone import date_today def render_document_top(request, doc, tab, name): @@ -186,7 +186,7 @@ def document_main(request, name, rev=None): telechat = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat") - if telechat and (not telechat.telechat_date or telechat.telechat_date < datetime.date.today()): + if telechat and (not telechat.telechat_date or telechat.telechat_date < date_today(settings.TIME_ZONE)): telechat = None @@ -990,7 +990,7 @@ def document_bibtex(request, name, rev=None): latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision") replaced_by = [d.name for d in doc.related_that("replaces")] - published = doc.latest_event(type="published_rfc") + published = doc.latest_event(type="published_rfc") is not None rfc = latest_revision.doc if latest_revision and latest_revision.doc.get_state_slug() == "rfc" else None if rev != None and rev != doc.rev: @@ -1408,11 +1408,12 @@ def telechat_date(request, name): warnings = [] if e and e.telechat_date and doc.type.slug != 'charter': - if e.telechat_date==datetime.date.today(): + today = date_today(settings.TIME_ZONE) + if e.telechat_date == today: warnings.append( "This document is currently scheduled for today's telechat. " +"Please set the returning item bit carefully.") - elif e.telechat_date<datetime.date.today() and has_same_ballot(doc,e.telechat_date): + elif e.telechat_date < today and has_same_ballot(doc,e.telechat_date): initial_returning_item = True warnings.append( "This document appears to have been on a previous telechat with the same ballot, " +"so the returning item bit has been set. Clear it if that is not appropriate.") diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index f3a307178..01cb4ea36 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -19,6 +19,7 @@ from django.shortcuts import render, get_object_or_404, redirect from django.template.loader import render_to_string from django.forms.utils import ErrorList from django.template.defaultfilters import pluralize +from django.utils import timezone import debug # pyflakes:ignore @@ -52,6 +53,8 @@ from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils import log from ietf.utils.response import permission_denied +from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO + class ChangeStateForm(forms.Form): state = forms.ModelChoiceField(State.objects.filter(used=True, type="draft-iesg"), empty_label=None, required=True) @@ -857,7 +860,7 @@ def resurrect(request, name): events.append(e) doc.set_state(State.objects.get(used=True, type="draft", slug="active")) - doc.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) + doc.expires = timezone.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) doc.save_with_history(events) restore_draft_file(request, doc) @@ -1480,7 +1483,7 @@ def adopt_draft(request, name): due_date = None if form.cleaned_data["weeks"] != None: - due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"]) + due_date = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(weeks=form.cleaned_data["weeks"]) update_reminder(doc, "stream-s", e, due_date) @@ -1671,7 +1674,7 @@ def change_stream_state(request, name, state_type): due_date = None if form.cleaned_data["weeks"] != None: - due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"]) + due_date = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(weeks=form.cleaned_data["weeks"]) update_reminder(doc, "stream-s", e, due_date) diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py index c7d8e5804..29cf67b8d 100644 --- a/ietf/doc/views_review.py +++ b/ietf/doc/views_review.py @@ -10,7 +10,9 @@ import datetime import requests import email.utils +from django.utils import timezone from django.utils.http import is_safe_url + from simple_history.utils import update_change_reason import debug # pyflakes:ignore @@ -52,6 +54,8 @@ from ietf.utils.mail import send_mail_message from ietf.mailtrigger.utils import gather_address_lists from ietf.utils.fields import MultiEmailField from ietf.utils.response import permission_denied +from ietf.utils.timezone import date_today, DEADLINE_TZINFO + def clean_doc_revision(doc, rev): if rev: @@ -92,7 +96,7 @@ class RequestReviewForm(forms.ModelForm): def clean_deadline(self): v = self.cleaned_data.get('deadline') - if v < datetime.date.today(): + if v < date_today(DEADLINE_TZINFO): raise forms.ValidationError("Select today or a date in the future.") return v @@ -117,7 +121,7 @@ def request_review(request, name): if not can_request_review_of_doc(request.user, doc): permission_denied(request, "You do not have permission to perform this action") - now = datetime.datetime.now() + now = timezone.now() lc_ends = None e = doc.latest_event(LastCallDocEvent, type="sent_last_call") @@ -348,7 +352,7 @@ class RejectReviewerAssignmentForm(forms.Form): def reject_reviewer_assignment(request, name, assignment_id): doc = get_object_or_404(Document, name=name) review_assignment = get_object_or_404(ReviewAssignment, pk=assignment_id, state__in=["assigned", "accepted"]) - review_request_past_deadline = review_assignment.review_request.deadline < datetime.date.today() + review_request_past_deadline = review_assignment.review_request.deadline < date_today(DEADLINE_TZINFO) if not review_assignment.reviewer: return redirect(review_request, name=review_assignment.review_request.doc.name, request_id=review_assignment.review_request.pk) @@ -364,7 +368,7 @@ def reject_reviewer_assignment(request, name, assignment_id): if form.is_valid(): # reject the assignment review_assignment.state = ReviewAssignmentStateName.objects.get(slug="rejected") - review_assignment.completed_on = datetime.datetime.now() + review_assignment.completed_on = timezone.now() review_assignment.save() descr = "Assignment of request for {} review by {} to {} was rejected".format( @@ -531,7 +535,7 @@ class CompleteReviewForm(forms.Form): review_url = forms.URLField(label="Link to message", required=False) review_file = forms.FileField(label="Text file to upload", required=False) review_content = forms.CharField(widget=forms.Textarea, required=False, strip=False) - completion_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1" }, initial=datetime.date.today, help_text="Date of announcement of the results of this review") + completion_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1" }, initial=date_today, help_text="Date of announcement of the results of this review") completion_time = forms.TimeField(widget=forms.HiddenInput, initial=datetime.time.min) cc = MultiEmailField(required=False, help_text="Email addresses to send to in addition to the review team list") email_ad = forms.BooleanField(label="Send extra email to the responsible AD suggesting early attention", required=False) @@ -704,7 +708,7 @@ def complete_review(request, name, assignment_id=None, acronym=None): team.acronym, request_type.slug, xslugify(reviewer.person.ascii_parts()[3]), - datetime.date.today().isoformat(), + date_today().isoformat(), ] review_name = "-".join(c for c in name_components if c).lower() if not Document.objects.filter(name=review_name).exists(): @@ -723,7 +727,7 @@ def complete_review(request, name, assignment_id=None, acronym=None): type=form.cleaned_data['review_type'], doc=doc, team=team, - deadline=datetime.date.today(), + deadline=date_today(DEADLINE_TZINFO), requested_by=Person.objects.get(user=request.user), requested_rev=form.cleaned_data['reviewed_rev'], ) @@ -731,13 +735,13 @@ def complete_review(request, name, assignment_id=None, acronym=None): review_request=review_request, state_id='assigned', reviewer=form.cleaned_data['reviewer'].role_email('reviewer', group=team), - assigned_on=datetime.datetime.now(), + assigned_on=timezone.now(), review = review, ) review.rev = "00" if not review.rev else "{:02}".format(int(review.rev) + 1) review.title = "{} Review of {}-{}".format(assignment.review_request.type.name, assignment.review_request.doc.name, form.cleaned_data["reviewed_rev"]) - review.time = datetime.datetime.now() + review.time = timezone.now() if review_submission == "link": review.external_url = form.cleaned_data['review_url'] @@ -764,9 +768,13 @@ def complete_review(request, name, assignment_id=None, acronym=None): with io.open(filename, 'w', encoding='utf-8') as destination: destination.write(content) - completion_datetime = datetime.datetime.now() + completion_datetime = timezone.now() if "completion_date" in form.cleaned_data: - completion_datetime = datetime.datetime.combine(form.cleaned_data["completion_date"], form.cleaned_data.get("completion_time") or datetime.time.min) + completion_datetime = datetime.datetime.combine( + form.cleaned_data["completion_date"], + form.cleaned_data.get("completion_time") or datetime.time.min, + tzinfo=DEADLINE_TZINFO, + ) # complete assignment assignment.state = form.cleaned_data["state"] @@ -778,7 +786,7 @@ def complete_review(request, name, assignment_id=None, acronym=None): need_to_email_review = review_submission != "link" and assignment.review_request.team.list_email and not revising_review - submitted_on_different_date = completion_datetime.date() != datetime.date.today() + submitted_on_different_date = completion_datetime.date() != date_today(DEADLINE_TZINFO) desc = "Request for {} review by {} {}: {}. Reviewer: {}.".format( assignment.review_request.type.name, assignment.review_request.team.acronym.upper(), @@ -799,7 +807,7 @@ def complete_review(request, name, assignment_id=None, acronym=None): close_event.by = request.user.person close_event.desc = desc close_event.state = assignment.state - close_event.time = datetime.datetime.now() + close_event.time = timezone.now() close_event.save() # If the completion date is different, record when the initial review was made too. @@ -894,8 +902,13 @@ def complete_review(request, name, assignment_id=None, acronym=None): } try: - initial['review_content'] = render_to_string('/group/%s/review/content_templates/%s.txt' % (assignment.review_request.team.acronym, - request_type.slug), {'assignment':assignment, 'today':datetime.date.today()}) + initial['review_content'] = render_to_string( + f'/group/{assignment.review_request.team.acronym}/review/content_templates/{request_type.slug}.txt', + { + 'assignment': assignment, + 'today': date_today(settings.TIME_ZONE), + }, + ) except (TemplateDoesNotExist, AttributeError): pass @@ -984,7 +997,7 @@ class EditReviewRequestDeadlineForm(forms.ModelForm): def clean_deadline(self): v = self.cleaned_data.get('deadline') - if v < datetime.date.today(): + if v < date_today(DEADLINE_TZINFO): raise forms.ValidationError("Select today or a date in the future.") return v diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index efe24d02c..ca2413785 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -453,7 +453,7 @@ def ad_dashboard_sort_key(doc): ageseconds = 0 changetime= doc.latest_event(type='changed_document') if changetime: - ad = (datetime.datetime.now()-doc.latest_event(type='changed_document').time) + ad = (timezone.now()-doc.latest_event(type='changed_document').time) ageseconds = (ad.microseconds + (ad.seconds + ad.days * 24 * 3600) * 10**6) / 10**6 return "1%d%s%s%010d" % (state[0].order,seed,doc.type.slug,ageseconds) @@ -761,7 +761,7 @@ def recent_drafts(request, days=7): cache_key = f'recentdraftsview{days}' cached_val = slowcache.get(cache_key) if not cached_val: - since = datetime.datetime.now()-datetime.timedelta(days=days) + since = timezone.now()-datetime.timedelta(days=days) state = State.objects.get(type='draft', slug='active') events = NewRevisionDocEvent.objects.filter(time__gt=since) names = [ e.doc.name for e in events ] diff --git a/ietf/doc/views_stats.py b/ietf/doc/views_stats.py index 414d04bbe..912d4a57c 100644 --- a/ietf/doc/views_stats.py +++ b/ietf/doc/views_stats.py @@ -19,6 +19,8 @@ from ietf.doc.utils import get_search_cache_key from ietf.doc.views_search import SearchForm, retrieve_search_results from ietf.name.models import DocTypeName from ietf.person.models import Person +from ietf.utils.timezone import date_today + epochday = datetime.datetime.utcfromtimestamp(0).date().toordinal() @@ -47,7 +49,7 @@ def model_to_timeline_data(model, field='time', **kwargs): # 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 = datetime.date.today() + today = date_today() if not obj_list[-1]['date'] == today: obj_list += [ {'date': today, 'count': 0} ] data = [ ((e['date'].toordinal()-epochday)*1000*60*60*24, e['count']) for e in obj_list ] diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index b64aadfd0..d4868215a 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -35,6 +35,7 @@ from ietf.name.models import DocRelationshipName, StdLevelName from ietf.person.models import Person from ietf.utils.mail import send_mail_preformatted from ietf.utils.textupload import get_cleaned_text_file_content +from ietf.utils.timezone import date_today, DEADLINE_TZINFO class ChangeStateForm(forms.Form): @@ -638,7 +639,7 @@ def generate_last_call_text(request, doc): # and when groups are set, vary the expiration time accordingly requester = "an individual participant" - expiration_date = datetime.date.today() + datetime.timedelta(days=28) + expiration_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=28) cc = [] new_text = render_to_string("doc/status_change/last_call_announcement.txt", diff --git a/ietf/group/factories.py b/ietf/group/factories.py index d8d927d80..e7fbef59a 100644 --- a/ietf/group/factories.py +++ b/ietf/group/factories.py @@ -5,6 +5,8 @@ import factory from typing import List # pyflakes:ignore +from django.utils import timezone + from ietf.group.models import Group, Role, GroupEvent, GroupMilestone, \ GroupHistory, RoleHistory from ietf.review.factories import ReviewTeamSettingsFactory @@ -66,7 +68,7 @@ class BaseGroupMilestoneFactory(factory.django.DjangoModelFactory): class DatedGroupMilestoneFactory(BaseGroupMilestoneFactory): group = factory.SubFactory(GroupFactory, uses_milestone_dates=True) - due = datetime.datetime.today()+datetime.timedelta(days=180) + due = timezone.now()+datetime.timedelta(days=180) class DatelessGroupMilestoneFactory(BaseGroupMilestoneFactory): group = factory.SubFactory(GroupFactory, uses_milestone_dates=False) diff --git a/ietf/group/forms.py b/ietf/group/forms.py index 0b8fc8e45..c93ca1d63 100644 --- a/ietf/group/forms.py +++ b/ietf/group/forms.py @@ -3,7 +3,6 @@ # Stdlib imports -import datetime import re import debug # pyflakes:ignore @@ -27,6 +26,7 @@ from ietf.utils import log from ietf.utils.textupload import get_cleaned_text_file_content #from ietf.utils.ordereddict import insert_after_in_ordered_dict from ietf.utils.fields import DatepickerDateField, MultiEmailField +from ietf.utils.timezone import date_today from ietf.utils.validators import validate_external_resource_value # --- Constants -------------------------------------------------------- @@ -364,7 +364,7 @@ class AddUnavailablePeriodForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(AddUnavailablePeriodForm, self).__init__(*args, **kwargs) - self.fields["start_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["start_date"].label, help_text=self.fields["start_date"].help_text, required=self.fields["start_date"].required, initial=datetime.date.today()) + self.fields["start_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["start_date"].label, help_text=self.fields["start_date"].help_text, required=self.fields["start_date"].required, initial=date_today()) self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["end_date"].label, help_text=self.fields["end_date"].help_text, required=self.fields["end_date"].required) self.fields['availability'].widget = forms.RadioSelect(choices=UnavailablePeriod.LONG_AVAILABILITY_CHOICES) diff --git a/ietf/group/management/commands/generate_group_aliases.py b/ietf/group/management/commands/generate_group_aliases.py index 9efbb68ab..2610ba5ad 100755 --- a/ietf/group/management/commands/generate_group_aliases.py +++ b/ietf/group/management/commands/generate_group_aliases.py @@ -15,6 +15,7 @@ from tempfile import mkstemp from django.conf import settings from django.core.management.base import BaseCommand +from django.utils import timezone import debug # pyflakes:ignore @@ -39,7 +40,7 @@ class Command(BaseCommand): 'have seen activity in the last %s years.' % (DEFAULT_YEARS)) def handle(self, *args, **options): - show_since = datetime.datetime.now() - datetime.timedelta(DEFAULT_YEARS*365) + show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365) date = time.strftime("%Y-%m-%d_%H:%M:%S") signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date) diff --git a/ietf/group/migrations/0059_use_timezone_now_for_group_models.py b/ietf/group/migrations/0059_use_timezone_now_for_group_models.py new file mode 100644 index 000000000..24c083855 --- /dev/null +++ b/ietf/group/migrations/0059_use_timezone_now_for_group_models.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.28 on 2022-07-12 11:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('group', '0058_alter_has_default_chat'), + ] + + operations = [ + migrations.AlterField( + model_name='group', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='groupevent', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now, help_text='When the event happened'), + ), + migrations.AlterField( + model_name='grouphistory', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/ietf/group/models.py b/ietf/group/models.py index 899c88241..8183f35a0 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- -import datetime import email.utils import jsonfield import os @@ -13,6 +12,7 @@ from django.core.validators import RegexValidator from django.db import models from django.db.models.deletion import CASCADE, PROTECT from django.dispatch import receiver +from django.utils import timezone import debug # pyflakes:ignore @@ -27,7 +27,7 @@ from ietf.utils.validators import JSONForeignKeyListValidator class GroupInfo(models.Model): - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) name = models.CharField(max_length=80) state = ForeignKey(GroupStateName, null=True) type = ForeignKey(GroupTypeName, null=True) @@ -180,11 +180,15 @@ class Group(GroupInfo): return self.role_set.none() def status_for_meeting(self,meeting): - end_date = meeting.end_date()+datetime.timedelta(days=1) previous_meeting = meeting.previous_meeting() - status_events = self.groupevent_set.filter(type='status_update',time__lte=end_date).order_by('-time') + status_events = self.groupevent_set.filter( + type='status_update', + time__lt=meeting.end_datetime(), + ).order_by('-time') if previous_meeting: - status_events = status_events.filter(time__gte=previous_meeting.end_date()+datetime.timedelta(days=1)) + status_events = status_events.filter( + time__gte=previous_meeting.end_datetime() + ) return status_events.first() def get_description(self): @@ -353,7 +357,7 @@ GROUP_EVENT_CHOICES = [ class GroupEvent(models.Model): """An occurrence for a group, used for tracking who, when and what.""" group = ForeignKey(Group) - time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened") + time = models.DateTimeField(default=timezone.now, help_text="When the event happened") type = models.CharField(max_length=50, choices=GROUP_EVENT_CHOICES) by = ForeignKey(Person) desc = models.TextField() diff --git a/ietf/group/tests.py b/ietf/group/tests.py index b168e4d3b..2c63ff95d 100644 --- a/ietf/group/tests.py +++ b/ietf/group/tests.py @@ -13,6 +13,7 @@ from django.conf import settings from django.urls import reverse as urlreverse from django.db.models import Q from django.test import Client +from django.utils import timezone import debug # pyflakes:ignore @@ -115,8 +116,8 @@ class GenerateGroupAliasesTests(TestCase): super().tearDown() def testManagementCommand(self): - a_month_ago = datetime.datetime.now() - datetime.timedelta(30) - a_decade_ago = datetime.datetime.now() - datetime.timedelta(3650) + a_month_ago = timezone.now() - datetime.timedelta(30) + a_decade_ago = timezone.now() - datetime.timedelta(3650) role1 = RoleFactory(name_id='ad', group__type_id='area', group__acronym='myth', group__state_id='active') area = role1.group ad = role1.person diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 0b3ead2c7..9d5328d51 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -42,6 +42,8 @@ from ietf.person.factories import PersonFactory, EmailFactory from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent, reload_db_objects +from ietf.utils.timezone import date_today, DEADLINE_TZINFO + def group_urlreverse_list(group, viewname): return [ @@ -269,7 +271,7 @@ class GroupPagesTests(TestCase): group=group, state_id="active", desc="Get Work Done", - due=datetime.date.today() + datetime.timedelta(days=100)) + due=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=100)) milestone.docs.add(draft) for url in [group.about_url(),] + group_urlreverse_list(group, 'ietf.group.views.group_about'): @@ -876,7 +878,7 @@ class GroupEditTests(TestCase): self.assertEqual(r.status_code, 302) review_assignment.state_id = 'accepted' review_assignment.save() - review_req.deadline = datetime.date.today() - datetime.timedelta(days=1) + review_req.deadline = date_today(DEADLINE_TZINFO) - datetime.timedelta(days=1) review_req.save() r = self.client.post(url, post_data) @@ -1194,7 +1196,7 @@ class MilestoneTests(TestCase): m1 = GroupMilestone.objects.create(id=1, group=group, desc="Test 1", - due=datetime.date.today(), + due=date_today(DEADLINE_TZINFO), resolved="", state_id="active") m1.docs.set([draft]) @@ -1202,7 +1204,7 @@ class MilestoneTests(TestCase): m2 = GroupMilestone.objects.create(id=2, group=group, desc="Test 2", - due=datetime.date.today(), + due=date_today(DEADLINE_TZINFO), resolved="", state_id="charter") m2.docs.set([draft]) @@ -1246,7 +1248,7 @@ class MilestoneTests(TestCase): events_before = group.groupevent_set.count() doc_pks = pklist(Document.objects.filter(type="draft")) - due = self.last_day_of_month(datetime.date.today() + datetime.timedelta(days=365)) + due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365)) # faulty post r = self.client.post(url, { 'prefix': "m-1", @@ -1302,7 +1304,7 @@ class MilestoneTests(TestCase): milestones_before = GroupMilestone.objects.filter(group=group).count() events_before = group.groupevent_set.count() - due = self.last_day_of_month(datetime.date.today() + datetime.timedelta(days=365)) + due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365)) # add mailbox_before = len(outbox) @@ -1393,7 +1395,7 @@ class MilestoneTests(TestCase): events_before = group.groupevent_set.count() doc_pks = pklist(Document.objects.filter(type="draft")) - due = self.last_day_of_month(datetime.date.today() + datetime.timedelta(days=365)) + due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365)) # faulty post r = self.client.post(url, { 'prefix': "m1", @@ -1776,7 +1778,7 @@ class MeetingInfoTests(TestCase): def setUp(self): super().setUp() self.group = GroupFactory.create(type_id='wg') - today = datetime.date.today() + today = date_today() SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=14)) self.inprog = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1)) SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today+datetime.timedelta(days=90)) @@ -1900,7 +1902,7 @@ class StatusUpdateTests(TestCase): def test_view_status_update_for_meeting(self): chair = RoleFactory(name_id='chair',group__type_id='wg') GroupEventFactory(type='status_update',group=chair.group) - sess = SessionFactory.create(meeting__type_id='ietf',group=chair.group,meeting__date=datetime.datetime.today()-datetime.timedelta(days=1)) + sess = SessionFactory.create(meeting__type_id='ietf',group=chair.group,meeting__date=date_today()-datetime.timedelta(days=1)) url = urlreverse('ietf.group.views.group_about_status_meeting',kwargs={'acronym':chair.group.acronym,'num':sess.meeting.number}) response = self.client.get(url) self.assertEqual(response.status_code,200) diff --git a/ietf/group/tests_js.py b/ietf/group/tests_js.py index 49dd33983..1c7f9fc9a 100644 --- a/ietf/group/tests_js.py +++ b/ietf/group/tests_js.py @@ -7,6 +7,7 @@ import debug # pyflakes:ignore from ietf.doc.factories import WgDraftFactory from ietf.group.factories import GroupFactory, RoleFactory, DatedGroupMilestoneFactory from ietf.utils.jstest import IetfSeleniumTestCase, ifSeleniumEnabled, selenium_enabled +from ietf.utils.timezone import date_today if selenium_enabled(): from selenium.common.exceptions import TimeoutException @@ -68,7 +69,7 @@ class MilestoneTests(IetfSeleniumTestCase): draft = WgDraftFactory() WgDraftFactory.create_batch(3) # some drafts to ignore description = 'some description' - due_date = datetime.date.today() + datetime.timedelta(days=60) + due_date = date_today() + datetime.timedelta(days=60) assert(len(draft.name) > 5) draft_search_string = draft.name[-5:] diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py index ee4ae96fe..fc09e27e0 100644 --- a/ietf/group/tests_review.py +++ b/ietf/group/tests_review.py @@ -7,7 +7,9 @@ import debug # pyflakes:ignore from pyquery import PyQuery +from django.conf import settings from django.urls import reverse as urlreverse +from django.utils import timezone from ietf.review.policies import get_reviewer_queue_policy from ietf.utils.test_utils import login_testing_unauthorized, TestCase, reload_db_objects @@ -25,6 +27,7 @@ from ietf.person.factories import PersonFactory, EmailFactory from ietf.doc.factories import DocumentFactory from ietf.group.factories import RoleFactory, ReviewTeamFactory, GroupFactory from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory +from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO from django.utils.html import escape class ReviewTests(TestCase): @@ -131,7 +134,7 @@ class ReviewTests(TestCase): doc.states.add(State.objects.get(type="draft-iesg", slug="lc", used=True)) LastCallDocEvent.objects.create( doc=doc, - expires=datetime.datetime.now() + datetime.timedelta(days=365), + expires=timezone.now() + datetime.timedelta(days=365), by=Person.objects.get(name="(System)"), rev=doc.rev ) @@ -155,7 +158,7 @@ class ReviewTests(TestCase): review_request__doc=review_req1.doc, review_request__team=review_req1.team, review_request__type_id="early", - review_request__deadline=datetime.date.today() + datetime.timedelta(days=30), + review_request__deadline=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=30), review_request__state_id="assigned", review_request__requested_by=Person.objects.get(user__username="reviewer"), state_id = "accepted", @@ -165,7 +168,7 @@ class ReviewTests(TestCase): UnavailablePeriod.objects.create( team=review_req1.team, person=reviewer, - start_date=datetime.date.today() - datetime.timedelta(days=10), + start_date=date_today() - datetime.timedelta(days=10), availability="unavailable", ) @@ -210,7 +213,7 @@ class ReviewTests(TestCase): review_request__doc=review_req2.doc, review_request__team=review_req2.team, review_request__type_id="lc", - review_request__deadline=datetime.date.today() - datetime.timedelta(days=30), + review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=30), review_request__state_id="assigned", review_request__requested_by=Person.objects.get(user__username="reviewer"), state_id = "no-response", @@ -231,15 +234,15 @@ class ReviewTests(TestCase): review_req3 = ReviewRequestFactory(state_id='completed', team=team) ReviewAssignmentFactory( review_request__doc=review_req3.doc, - review_request__time=datetime.date.today() - datetime.timedelta(days=30), + review_request__time=datetime_today() - datetime.timedelta(days=30), review_request__team=review_req3.team, review_request__type_id="telechat", - review_request__deadline=datetime.date.today() - datetime.timedelta(days=25), + review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=25), review_request__state_id="completed", review_request__requested_by=Person.objects.get(user__username="reviewer"), state_id = "completed", reviewer=reviewer.email_set.first(), - assigned_on=datetime.date.today() - datetime.timedelta(days=30) + assigned_on=datetime_today() - datetime.timedelta(days=30) ) r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -252,15 +255,15 @@ class ReviewTests(TestCase): for i in range(10): ReviewAssignmentFactory( review_request__doc=reqs[i].doc, - review_request__time=datetime.date.today() - datetime.timedelta(days=i*30), + review_request__time=datetime_today() - datetime.timedelta(days=i*30), review_request__team=reqs[i].team, review_request__type_id="telechat", - review_request__deadline=datetime.date.today() - datetime.timedelta(days=i*20), + review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=i*20), review_request__state_id="completed", review_request__requested_by=Person.objects.get(user__username="reviewer"), state_id = "completed", reviewer=reviewer.email_set.first(), - assigned_on=datetime.date.today() - datetime.timedelta(days=i*30) + assigned_on=datetime_today() - datetime.timedelta(days=i*30) ) r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -304,28 +307,28 @@ class ReviewTests(TestCase): review_req4 = ReviewRequestFactory(state_id='completed', team=team) ReviewAssignmentFactory( review_request__doc=review_req4.doc, - review_request__time=datetime.date.today() - datetime.timedelta(days=80), + review_request__time=datetime_today() - datetime.timedelta(days=80), review_request__team=review_req4.team, review_request__type_id="lc", - review_request__deadline=datetime.date.today() - datetime.timedelta(days=60), + review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=60), review_request__state_id="assigned", review_request__requested_by=Person.objects.get(user__username="reviewer"), state_id = "accepted", reviewer=reviewer.email_set.first(), - assigned_on=datetime.date.today() - datetime.timedelta(days=80) + assigned_on=datetime_today() - datetime.timedelta(days=80) ) review_req5 = ReviewRequestFactory(state_id='completed', team=team) ReviewAssignmentFactory( review_request__doc=review_req5.doc, - review_request__time=datetime.date.today() - datetime.timedelta(days=120), + review_request__time=datetime_today() - datetime.timedelta(days=120), review_request__team=review_req5.team, review_request__type_id="lc", - review_request__deadline=datetime.date.today() - datetime.timedelta(days=100), + review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=100), review_request__state_id="assigned", review_request__requested_by=Person.objects.get(user__username="reviewer"), state_id = "accepted", reviewer=reviewer.email_set.first(), - assigned_on=datetime.date.today() - datetime.timedelta(days=120) + assigned_on=datetime_today() - datetime.timedelta(days=120) ) r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -429,7 +432,7 @@ class ReviewTests(TestCase): doc.states.add(State.objects.get(type="draft-iesg", slug="lc", used=True)) LastCallDocEvent.objects.create( doc=doc, - expires=datetime.datetime.now() + datetime.timedelta(days=365), + expires=timezone.now() + datetime.timedelta(days=365), by=Person.objects.get(name="(System)"), rev=doc.rev ) @@ -475,7 +478,7 @@ class ReviewTests(TestCase): review_req1 = ReviewRequestFactory() review_assignment_completed = ReviewAssignmentFactory(review_request=review_req1,reviewer=EmailFactory(person__user__username='marschairman'), state_id='completed', reviewed_rev=0) ReviewAssignmentFactory(review_request=review_req1,reviewer=review_assignment_completed.reviewer) - TelechatDocEvent.objects.create(telechat_date=datetime.date.today(), type='scheduled_for_telechat', by=review_assignment_completed.reviewer.person, doc=review_req1.doc, rev=0) + TelechatDocEvent.objects.create(telechat_date=date_today(settings.TIME_ZONE), type='scheduled_for_telechat', by=review_assignment_completed.reviewer.person, doc=review_req1.doc, rev=0) DBTemplateFactory.create(path='/group/defaults/email/open_assignments.txt', type_id='django', @@ -555,7 +558,7 @@ class ReviewTests(TestCase): # get r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.context['period_form']['start_date'].initial, datetime.date.today()) + self.assertEqual(r.context['period_form']['start_date'].initial, date_today()) # set settings empty_outbox() @@ -596,7 +599,7 @@ class ReviewTests(TestCase): self.assertEqual(settings.skip_next, 0) # add unavailable period - start_date = datetime.date.today() + datetime.timedelta(days=10) + start_date = date_today() + datetime.timedelta(days=10) empty_outbox() r = self.client.post(url, { "action": "add_period", diff --git a/ietf/group/views.py b/ietf/group/views.py index 3a4b9f3e0..4bc31d09f 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -53,6 +53,7 @@ from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonRespons from django.shortcuts import render, redirect, get_object_or_404 from django.template.loader import render_to_string from django.urls import reverse as urlreverse +from django.utils import timezone from django.utils.html import escape from django.views.decorators.cache import cache_page, cache_control @@ -118,6 +119,7 @@ from ietf.settings import MAILING_LIST_INFO_URL from ietf.utils.response import permission_denied from ietf.utils.text import strip_suffix from ietf.utils import markdown +from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO # --- Helpers ---------------------------------------------------------- @@ -566,7 +568,7 @@ def all_status(request): if e: wg_reports.append(e) - wg_reports.sort(key=lambda x: (x.group.parent.acronym,datetime.datetime.now()-x.time)) + wg_reports.sort(key=lambda x: (x.group.parent.acronym,timezone.now()-x.time)) rg_reports = [] for rg in rgs: @@ -808,7 +810,7 @@ def email_aliases(request, acronym=None, group_type=None): def meetings(request, acronym=None, group_type=None): group = get_group_or_404(acronym,group_type) if acronym else None - four_years_ago = datetime.datetime.now()-datetime.timedelta(days=4*365) + four_years_ago = timezone.now()-datetime.timedelta(days=4*365) sessions = add_event_info_to_session_qs( group.session_set.filter( @@ -972,7 +974,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None): try: group = Group.objects.get(acronym=clean["acronym"]) save_group_in_history(group) - group.time = datetime.datetime.now() + group.time = timezone.now() group.save() except Group.DoesNotExist: group = Group.objects.create(name=clean["name"], @@ -1026,7 +1028,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None): change_text=title + ' deleted: ' + ", ".join(x.name_and_email() for x in deleted) personnel_change_text+=change_text+"\n" - today = datetime.date.today() + today = date_today() for deleted_email in deleted: # Verify the person doesn't have a separate reviewer role for the group with a different address if not group.role_set.filter(name_id='reviewer',person=deleted_email.person).exists(): @@ -1071,7 +1073,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None): ) )) - group.time = datetime.datetime.now() + group.time = timezone.now() if changes and not new_group: for attr, new, desc in changes: @@ -1387,7 +1389,7 @@ def review_requests(request, acronym, group_type=None): unassigned_review_requests.sort(key=lambda r: r.doc.name) open_review_assignments = list(ReviewAssignment.objects.filter(review_request__team=group, state_id__in=('assigned','accepted')).order_by('-assigned_on')) - today = datetime.date.today() + today = date_today(DEADLINE_TZINFO) unavailable_periods = current_unavailable_periods_for_reviewers(group) for a in open_review_assignments: a.reviewer_unavailable = any(p.availability == "unavailable" @@ -1420,11 +1422,14 @@ def review_requests(request, acronym, group_type=None): }[since] closed_review_requests = closed_review_requests.filter( - Q(reviewrequestdocevent__type='closed_review_request', reviewrequestdocevent__time__gte=datetime.date.today() - date_limit) - | Q(reviewrequestdocevent__isnull=True, time__gte=datetime.date.today() - date_limit) + Q(reviewrequestdocevent__type='closed_review_request', + reviewrequestdocevent__time__gte=datetime_today(DEADLINE_TZINFO) - date_limit) + | Q(reviewrequestdocevent__isnull=True, time__gte=datetime_today(DEADLINE_TZINFO) - date_limit) ).distinct() - closed_review_assignments = closed_review_assignments.filter(completed_on__gte = datetime.date.today() - date_limit) + closed_review_assignments = closed_review_assignments.filter( + completed_on__gte = datetime_today(DEADLINE_TZINFO) - date_limit, + ) return render(request, 'group/review_requests.html', construct_group_menu_context(request, group, "review requests", group_type, { @@ -1455,7 +1460,7 @@ def reviewer_overview(request, acronym, group_type=None): unavailable_periods[p.person_id].append(p) reviewer_roles = { r.person_id: r for r in Role.objects.filter(group=group, name="reviewer").select_related("email") } - today = datetime.date.today() + today = date_today() max_closed_reqs = settings.GROUP_REVIEW_MAX_ITEMS_TO_SHOW_IN_REVIEWER_LIST days_back = settings.GROUP_REVIEW_DAYS_TO_SHOW_IN_REVIEWER_LIST @@ -1509,7 +1514,7 @@ def reviewer_overview(request, acronym, group_type=None): int(math.ceil(d.assignment_to_closure_days)) if d.assignment_to_closure_days is not None else None)) if d.state in ["completed", "completed_in_time", "completed_late"]: if d.assigned_time is not None: - delta = datetime.datetime.now() - d.assigned_time + delta = timezone.now() - d.assigned_time if d.assignment_to_closure_days is not None: days = int(delta.days - d.assignment_to_closure_days) if days_since > days: days_since = days @@ -1833,7 +1838,7 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None): period.save() update_change_reason(period, "Added unavailability period: {}".format(period)) - today = datetime.date.today() + today = date_today() in_the_past = period.end_date and period.end_date < today @@ -1874,7 +1879,7 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None): period.delete() update_change_reason(period, "Removed unavailability period: {}".format(period)) - today = datetime.date.today() + today = date_today() in_the_past = period.end_date and period.end_date < today diff --git a/ietf/idindex/index.py b/ietf/idindex/index.py index 3d17b197c..d864905e4 100644 --- a/ietf/idindex/index.py +++ b/ietf/idindex/index.py @@ -7,10 +7,10 @@ import datetime import os -import pytz from django.conf import settings from django.template.loader import render_to_string +from django.utils import timezone import debug # pyflakes:ignore @@ -296,6 +296,6 @@ def id_index_txt(with_abstracts=False): return render_to_string("idindex/id_index.txt", { 'groups': groups, - 'time': datetime.datetime.now(pytz.UTC).strftime("%Y-%m-%d %H:%M:%S %Z"), + 'time': timezone.now().astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"), 'with_abstracts': with_abstracts, }) diff --git a/ietf/idindex/tests.py b/ietf/idindex/tests.py index 413e3a4ed..f207fa562 100644 --- a/ietf/idindex/tests.py +++ b/ietf/idindex/tests.py @@ -7,6 +7,7 @@ import datetime from pathlib import Path from django.conf import settings +from django.utils import timezone import debug # pyflakes:ignore @@ -120,7 +121,7 @@ class IndexTests(TestCase): draft.set_state(State.objects.get(type="draft", slug="active")) draft.set_state(State.objects.get(type="draft-iesg", slug="lc")) - e = LastCallDocEvent.objects.create(doc=draft, rev=draft.rev, type="sent_last_call", expires=datetime.datetime.now() + datetime.timedelta(days=14), by=draft.ad) + e = LastCallDocEvent.objects.create(doc=draft, rev=draft.rev, type="sent_last_call", expires=timezone.now() + datetime.timedelta(days=14), by=draft.ad) t = get_fields(all_id2_txt()) self.assertEqual(t[11], e.expires.strftime("%Y-%m-%d")) diff --git a/ietf/iesg/agenda.py b/ietf/iesg/agenda.py index e923c1bab..113c3c5d1 100644 --- a/ietf/iesg/agenda.py +++ b/ietf/iesg/agenda.py @@ -17,13 +17,14 @@ from ietf.doc.models import Document, LastCallDocEvent, ConsensusDocEvent from ietf.doc.utils_search import fill_in_telechat_date from ietf.iesg.models import TelechatDate, TelechatAgendaItem from ietf.review.utils import review_assignments_to_list_for_docs +from ietf.utils.timezone import date_today def get_agenda_date(date=None): if not date: try: return TelechatDate.objects.active().order_by('date')[0].date except IndexError: - return datetime.date.today() + return date_today() else: try: return TelechatDate.objects.active().get(date=datetime.datetime.strptime(date, "%Y-%m-%d").date()).date diff --git a/ietf/iesg/feeds.py b/ietf/iesg/feeds.py index 6e8d290c6..a8b5c2869 100644 --- a/ietf/iesg/feeds.py +++ b/ietf/iesg/feeds.py @@ -2,12 +2,13 @@ # -*- coding: utf-8 -*- -import datetime - +from django.conf import settings from django.contrib.syndication.views import Feed from django.utils.feedgenerator import Atom1Feed from ietf.doc.models import Document, TelechatDocEvent +from ietf.utils.timezone import date_today + class IESGAgendaFeed(Feed): title = "Documents on Future IESG Telechat Agendas" @@ -16,7 +17,7 @@ class IESGAgendaFeed(Feed): description_template = "iesg/feed_item_description.html" def items(self): - docs = Document.objects.filter(docevent__telechatdocevent__telechat_date__gte=datetime.date.today()).distinct() + docs = Document.objects.filter(docevent__telechatdocevent__telechat_date__gte=date_today(settings.TIME_ZONE)).distinct() for d in docs: d.latest_telechat_event = d.latest_event(TelechatDocEvent, type="scheduled_for_telechat") docs = [d for d in docs if d.latest_telechat_event.telechat_date] diff --git a/ietf/iesg/models.py b/ietf/iesg/models.py index f3690d4c7..6a622e27e 100644 --- a/ietf/iesg/models.py +++ b/ietf/iesg/models.py @@ -36,8 +36,12 @@ import datetime +from django.conf import settings from django.db import models +from ietf.utils.timezone import date_today + + class TelechatAgendaItem(models.Model): TYPE_CHOICES = ( (1, "Any Other Business (WG News, New Proposals, etc.)"), @@ -72,11 +76,11 @@ def next_telechat_date(): dates = TelechatDate.objects.order_by("-date") if dates: return dates[0].date + datetime.timedelta(days=14) - return datetime.date.today() + return date_today(settings.TIME_ZONE) class TelechatDateManager(models.Manager): def active(self): - return self.get_queryset().filter(date__gte=datetime.date.today()) + return self.get_queryset().filter(date__gte=date_today(settings.TIME_ZONE)) class TelechatDate(models.Model): objects = TelechatDateManager() diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index f317ad32b..5ecb4ed9d 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -28,6 +28,7 @@ from ietf.name.models import StreamName from ietf.person.models import Person from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent from ietf.iesg.factories import IESGMgmtItemFactory +from ietf.utils.timezone import date_today, DEADLINE_TZINFO class IESGTests(TestCase): @@ -58,7 +59,7 @@ class IESGTests(TestCase): m = GroupMilestone.objects.create(group=draft.group, state_id="review", desc="Test milestone", - due=datetime.date.today()) + due=date_today(DEADLINE_TZINFO)) url = urlreverse("ietf.iesg.views.milestones_needing_review") login_testing_unauthorized(self, "ad", url) @@ -142,7 +143,7 @@ class IESGAgendaTests(TestCase): mgmtitem = self.mgmt_items # put on agenda - date = datetime.date.today() + datetime.timedelta(days=50) + date = date_today(settings.TIME_ZONE) + datetime.timedelta(days=50) TelechatDate.objects.create(date=date) telechat_event = TelechatDocEvent.objects.create( type="scheduled_for_telechat", @@ -430,7 +431,7 @@ class IESGAgendaTests(TestCase): self.assertNotIn(d.title, unicontent(r)) # Add the documents to a past telechat by = Person.objects.get(name="Areað Irector") - date = datetime.date.today() - datetime.timedelta(days=14) + date = date_today(settings.TIME_ZONE) - datetime.timedelta(days=14) approved = State.objects.get(type='draft-iesg', slug='approved') iesg_eval = State.objects.get(type='draft-iesg', slug='iesg-eva') for d in list(self.telechat_docs.values()): @@ -485,7 +486,7 @@ class IESGAgendaTests(TestCase): def test_admin_change(self): draft = Document.objects.get(name="draft-ietf-mars-test") - today = datetime.date.today() + today = date_today(settings.TIME_ZONE) telechat_date = TelechatDate.objects.get(date=draft.telechat_date()) url = urlreverse('admin:iesg_telechatdate_change', args=(telechat_date.id,)) self.client.login(username="secretary", password="secretary+password") diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index 5d1457cee..5334e8a85 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -63,6 +63,7 @@ from ietf.iesg.utils import telechat_page_count from ietf.ietfauth.utils import has_role, role_required, user_is_person from ietf.person.models import Person from ietf.doc.utils_search import fill_in_document_table_attributes, fill_in_telechat_date +from ietf.utils.timezone import date_today, datetime_from_date def review_decisions(request, year=None): events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved")) @@ -73,9 +74,9 @@ def review_decisions(request, year=None): year = int(year) events = events.filter(time__year=year) else: - d = datetime.date.today() - datetime.timedelta(days=185) + d = date_today() - datetime.timedelta(days=185) d = datetime.date(d.year, d.month, 1) - events = events.filter(time__gte=d) + events = events.filter(time__gte=datetime_from_date(d)) events = events.select_related("doc", "doc__intended_std_level").order_by("-time", "-id") diff --git a/ietf/ietfauth/management/commands/send_apikey_usage_emails.py b/ietf/ietfauth/management/commands/send_apikey_usage_emails.py index 4aa4e5524..d3fce1bcc 100644 --- a/ietf/ietfauth/management/commands/send_apikey_usage_emails.py +++ b/ietf/ietfauth/management/commands/send_apikey_usage_emails.py @@ -8,6 +8,7 @@ from textwrap import dedent from django.conf import settings from django.core.management.base import BaseCommand +from django.utils import timezone import debug # pyflakes:ignore @@ -37,7 +38,7 @@ class Command(BaseCommand): keys = PersonalApiKey.objects.filter(valid=True) for key in keys: - earliest = datetime.datetime.now() - datetime.timedelta(days=days) + earliest = timezone.now() - datetime.timedelta(days=days) events = PersonApiKeyEvent.objects.filter(key=key, time__gt=earliest) count = events.count() events = events[:32] diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index b6c145e68..983283ae7 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -28,6 +28,7 @@ from django.urls import reverse as urlreverse from django.contrib.auth.models import User from django.conf import settings from django.template.loader import render_to_string +from django.utils import timezone import debug # pyflakes:ignore @@ -46,6 +47,8 @@ from ietf.stats.models import MeetingRegistration from ietf.utils.decorators import skip_coverage from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized +from ietf.utils.timezone import date_today + import ietf.ietfauth.views @@ -390,7 +393,7 @@ class IetfAuthTests(TestCase): self.assertFalse(q('#volunteer-button')) self.assertFalse(q('#volunteered')) - year = datetime.date.today().year + year = date_today().year nomcom = NomComFactory(group__acronym=f'nomcom{year}',is_accepting_volunteers=True) r = self.client.get(url) self.assertEqual(r.status_code,200) @@ -505,7 +508,7 @@ class IetfAuthTests(TestCase): UnavailablePeriod.objects.create( team=review_req.team, person=reviewer, - start_date=datetime.date.today() - datetime.timedelta(days=10), + start_date=date_today() - datetime.timedelta(days=10), availability="unavailable", ) @@ -750,11 +753,11 @@ class IetfAuthTests(TestCase): self.assertContains(r, 'Invalid apikey', status_code=403) # too long since regular login - person.user.last_login = datetime.datetime.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS+1) + person.user.last_login = timezone.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS+1) person.user.save() r = self.client.post(key.endpoint, {'apikey':key.hash(), 'dummy':'dummy',}) self.assertContains(r, 'Too long since last regular login', status_code=400) - person.user.last_login = datetime.datetime.now() + person.user.last_login = timezone.now() person.user.save() # endpoint mismatch @@ -783,12 +786,12 @@ class IetfAuthTests(TestCase): # apikey usage will be registered) count = 2 # avoid usage across dates - if datetime.datetime.now().time() > datetime.time(hour=23, minute=59, second=58): + if timezone.now().time() > datetime.time(hour=23, minute=59, second=58): time.sleep(2) for i in range(count): for key in person.apikeys.all(): self.client.post(key.endpoint, {'apikey':key.hash(), 'dummy': 'dummy', }) - date = str(datetime.date.today()) + date = str(date_today()) empty_outbox() cmd = Command() @@ -905,7 +908,7 @@ class OpenIDConnectTests(TestCase): # an additional email EmailFactory(person=person) email_list = person.email_set.all().values_list('address', flat=True) - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) MeetingRegistration.objects.create( meeting=meeting, person=None, first_name=person.first_name(), last_name=person.last_name(), email=email_list[0], ticket_type='full_week', reg_type='remote', affiliation='Some Company', diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index e405d6620..f9db86d76 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -34,9 +34,9 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import datetime import importlib -from datetime import date as Date, datetime as DateTime # needed if we revert to higher barrier for account creation #from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date from collections import defaultdict @@ -78,6 +78,7 @@ from ietf.doc.fields import SearchableDocumentField from ietf.utils.decorators import person_required from ietf.utils.mail import send_mail from ietf.utils.validators import validate_external_resource_value +from ietf.utils.timezone import date_today, DEADLINE_TZINFO # These are needed if we revert to the higher bar for account creation @@ -223,7 +224,7 @@ def profile(request): emails = Email.objects.filter(person=person).exclude(address__startswith='unknown-email-').order_by('-active','-time') new_email_forms = [] - nc = NomCom.objects.filter(group__acronym__icontains=Date.today().year).first() + nc = NomCom.objects.filter(group__acronym__icontains=date_today().year).first() if nc and nc.volunteer_set.filter(person=person).exists(): volunteer_status = 'volunteered' elif nc and nc.is_accepting_volunteers: @@ -455,7 +456,7 @@ def confirm_password_reset(request, auth): password = data['password'] last_login = None if data['last_login']: - last_login = DateTime.fromtimestamp(data['last_login']) + last_login = datetime.datetime.fromtimestamp(data['last_login'], datetime.timezone.utc) except django.core.signing.BadSignature: raise Http404("Invalid or expired auth") @@ -557,7 +558,7 @@ def review_overview(request): reviewer__person__user=request.user, state__in=["assigned", "accepted"], ) - today = Date.today() + today = date_today(DEADLINE_TZINFO) for r in open_review_assignments: r.due = max(0, (today - r.review_request.deadline).days) diff --git a/ietf/ipr/factories.py b/ietf/ipr/factories.py index da1262912..e32090a36 100644 --- a/ietf/ipr/factories.py +++ b/ietf/ipr/factories.py @@ -5,6 +5,7 @@ import datetime import factory +from django.utils import timezone from ietf.ipr.models import ( IprDisclosureBase, HolderIprDisclosure, ThirdPartyIprDisclosure, NonDocSpecificIprDisclosure, @@ -13,7 +14,7 @@ from ietf.ipr.models import ( def _fake_patent_info(): return "Date: %s\nNotes: %s\nTitle: %s\nNumber: %s\nInventor: %s\n" % ( - (datetime.datetime.today()-datetime.timedelta(days=365)).strftime("%Y-%m-%d"), + (timezone.now()-datetime.timedelta(days=365)).strftime("%Y-%m-%d"), factory.Faker('paragraph'), factory.Faker('sentence', nb_words=8), 'US9999999', diff --git a/ietf/ipr/mail.py b/ietf/ipr/mail.py index ea3551d56..f1d8039db 100644 --- a/ietf/ipr/mail.py +++ b/ietf/ipr/mail.py @@ -3,13 +3,14 @@ import base64 -import email import datetime from dateutil.tz import tzoffset import os -import pytz import re +from email import message_from_bytes +from email.utils import parsedate_tz + from django.template.loader import render_to_string from django.utils.encoding import force_text, force_bytes @@ -50,7 +51,7 @@ def parsedate_to_datetime(date): http://python.readthedocs.org/en/latest/library/email.util.html """ try: - tuple = email.utils.parsedate_tz(date) + tuple = parsedate_tz(date) if not tuple: return None tz = tuple[-1] @@ -62,10 +63,12 @@ def parsedate_to_datetime(date): def utc_from_string(s): date = parsedate_to_datetime(s) - if is_aware(date): - return date.astimezone(pytz.utc).replace(tzinfo=None) + if date is None: + return None + elif is_aware(date): + return date.astimezone(datetime.timezone.utc) else: - return date + return date.replace(tzinfo=datetime.timezone.utc) # ---------------------------------------------------------------- # Email Functions @@ -174,7 +177,7 @@ def process_response_email(msg): a matching value in the reply_to field, associated to an IPR disclosure through IprEvent. Create a Message object for the incoming message and associate it to the original message via new IprEvent""" - message = email.message_from_bytes(force_bytes(msg)) + message = message_from_bytes(force_bytes(msg)) to = message.get('To', '') # exit if this isn't a response we're interested in (with plus addressing) diff --git a/ietf/ipr/models.py b/ietf/ipr/models.py index 282f33568..2eb588b8b 100644 --- a/ietf/ipr/models.py +++ b/ietf/ipr/models.py @@ -2,11 +2,10 @@ # -*- coding: utf-8 -*- -import datetime - from django.conf import settings from django.db import models from django.urls import reverse +from django.utils import timezone from ietf.doc.models import DocAlias, DocEvent from ietf.name.models import DocRelationshipName,IprDisclosureStateName,IprLicenseTypeName,IprEventTypeName @@ -220,7 +219,7 @@ class IprEvent(models.Model): """Returns true if it's beyond the response_due date and no response has been received""" qs = IprEvent.objects.filter(disclosure=self.disclosure,in_reply_to=self.message) - if not qs and datetime.datetime.now().date() > self.response_due.date(): + if not qs and timezone.now().date() > self.response_due.date(): return True else: return False diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index db9c1371e..ab6b01a5b 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -8,8 +8,9 @@ import datetime from pyquery import PyQuery from urllib.parse import quote, urlparse -from django.urls import reverse as urlreverse from django.conf import settings +from django.urls import reverse as urlreverse +from django.utils import timezone import debug # pyflakes:ignore @@ -27,6 +28,7 @@ from ietf.message.models import Message from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized from ietf.utils.text import text_to_dict +from ietf.utils.timezone import date_today def make_data_from_content(content): @@ -572,7 +574,7 @@ I would like to revoke this declaration. self.assertEqual(r.status_code,302) self.assertEqual(len(outbox),len_before+2) self.assertTrue('george@acme.com' in outbox[len_before]['To']) - self.assertIn('posted on '+datetime.date.today().strftime("%Y-%m-%d"), get_payload_text(outbox[len_before]).replace('\n',' ')) + self.assertIn('posted on '+date_today().strftime("%Y-%m-%d"), get_payload_text(outbox[len_before]).replace('\n',' ')) self.assertTrue('draft-ietf-mars-test@ietf.org' in outbox[len_before+1]['To']) self.assertTrue('mars-wg@ietf.org' in outbox[len_before+1]['Cc']) self.assertIn('Secretariat on '+ipr.get_latest_event_submitted().time.strftime("%Y-%m-%d"), get_payload_text(outbox[len_before+1]).replace('\n',' ')) @@ -600,7 +602,7 @@ I would like to revoke this declaration. ipr = HolderIprDisclosureFactory() url = urlreverse('ietf.ipr.views.email',kwargs={ "id": ipr.id }) self.client.login(username="secretary", password="secretary+password") - yesterday = datetime.date.today() - datetime.timedelta(1) + yesterday = date_today() - datetime.timedelta(1) data = dict( to='joe@test.com', frm='ietf-ipr@ietf.org', @@ -640,7 +642,7 @@ I would like to revoke this declaration. message_string.format( to=addrs.to, cc=addrs.cc, - date=datetime.datetime.now().ctime() + date=timezone.now().ctime() ) ) self.assertIsNone(result) @@ -650,7 +652,7 @@ I would like to revoke this declaration. From: joe@test.com Date: {} Subject: test -""".format(reply_to, datetime.datetime.now().ctime()) +""".format(reply_to, timezone.now().ctime()) result = process_response_email(message_string) self.assertIsInstance(result, Message) @@ -664,7 +666,7 @@ Subject: test From: joe@test.com Date: {} Subject: test -""".format(reply_to, datetime.datetime.now().ctime()) +""".format(reply_to, timezone.now().ctime()) message_bytes = message_string.encode('utf8') + b'\nInvalid stuff: \xfe\xff\n' result = process_response_email(message_bytes) self.assertIsInstance(result, Message) @@ -680,7 +682,7 @@ Subject: test message_bytes = message_string.format( to=addrs.to, cc=addrs.cc, - date=datetime.datetime.now().ctime(), + date=timezone.now().ctime(), ).encode('utf8') + b'\nInvalid stuff: \xfe\xff\n' result = process_response_email(message_bytes) self.assertIsNone(result) diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 163a858cc..d587eaecd 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -43,6 +43,7 @@ from ietf.utils.draft_search import normalize_draftname from ietf.utils.mail import send_mail, send_mail_message from ietf.utils.response import permission_denied from ietf.utils.text import text_to_dict +from ietf.utils.timezone import datetime_from_date, datetime_today, DEADLINE_TZINFO # ---------------------------------------------------------------- # Globals @@ -147,15 +148,14 @@ def ipr_rfc_number(disclosureDate, thirdPartyDisclosureFlag): # made on 1993-07-23, which is more than a year after RFC 1310. # RFC publication date comes from the RFC Editor announcement - # TODO: These times are tzinfo=pytz.utc, but disclosure times are offset-naive ipr_rfc_pub_datetime = { - 1310 : datetime.datetime(1992, 3, 13, 0, 0), - 1802 : datetime.datetime(1994, 3, 23, 0, 0), - 2026 : datetime.datetime(1996, 10, 29, 0, 0), - 3668 : datetime.datetime(2004, 2, 18, 0, 0), - 3979 : datetime.datetime(2005, 3, 2, 2, 23), - 4879 : datetime.datetime(2007, 4, 10, 18, 21), - 8179 : datetime.datetime(2017, 5, 31, 23, 1), + 1310 : datetime.datetime(1992, 3, 13, 0, 0, tzinfo=datetime.timezone.utc), + 1802 : datetime.datetime(1994, 3, 23, 0, 0, tzinfo=datetime.timezone.utc), + 2026 : datetime.datetime(1996, 10, 29, 0, 0, tzinfo=datetime.timezone.utc), + 3668 : datetime.datetime(2004, 2, 18, 0, 0, tzinfo=datetime.timezone.utc), + 3979 : datetime.datetime(2005, 3, 2, 2, 23, tzinfo=datetime.timezone.utc), + 4879 : datetime.datetime(2007, 4, 10, 18, 21, tzinfo=datetime.timezone.utc), + 8179 : datetime.datetime(2017, 5, 31, 23, 1, tzinfo=datetime.timezone.utc), } if disclosureDate < ipr_rfc_pub_datetime[1310]: @@ -396,7 +396,7 @@ def email(request, id): type_id = 'msgout', by = request.user.person, disclosure = ipr, - response_due = form.cleaned_data['response_due'], + response_due = datetime_from_date(form.cleaned_data['response_due'], DEADLINE_TZINFO), message = msg, ) @@ -590,7 +590,7 @@ def notify(request, id, type): type_id = form.cleaned_data['type'], by = request.user.person, disclosure = ipr, - response_due = datetime.datetime.now().date() + datetime.timedelta(days=30), + response_due = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=30), message = message, ) messages.success(request,'Notifications sent') diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py index c66c27593..b76551953 100644 --- a/ietf/liaisons/forms.py +++ b/ietf/liaisons/forms.py @@ -3,7 +3,7 @@ import io -import datetime, os +import os import operator from typing import Union # pyflakes:ignore @@ -34,6 +34,7 @@ from ietf.person.models import Email from ietf.person.fields import SearchableEmailField from ietf.doc.models import Document, DocAlias from ietf.utils.fields import DatepickerDateField +from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO from functools import reduce ''' @@ -185,9 +186,12 @@ class SearchLiaisonForm(forms.Form): end_date = self.cleaned_data.get('end_date') events = None if start_date: - events = LiaisonStatementEvent.objects.filter(type='posted', time__gte=start_date) + events = LiaisonStatementEvent.objects.filter( + type='posted', + time__gte=datetime_from_date(start_date, DEADLINE_TZINFO), + ) if end_date: - events = events.filter(time__lte=end_date) + events = events.filter(time__lte=datetime_from_date(end_date, DEADLINE_TZINFO)) elif end_date: events = LiaisonStatementEvent.objects.filter(type='posted', time__lte=end_date) if events: @@ -222,7 +226,7 @@ class LiaisonModelForm(BetterModelForm): to_groups.widget.attrs['data-minimum-input-length'] = 0 deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True) related_to = SearchableLiaisonStatementsField(label='Related Liaison Statement', required=False) - submitted_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Submission date', required=True, initial=datetime.date.today()) + submitted_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Submission date', required=True, initial=lambda: date_today(DEADLINE_TZINFO)) attachments = CustomModelMultipleChoiceField(queryset=Document.objects,label='Attachments', widget=ShowAttachmentsWidget, required=False) attach_title = forms.CharField(label='Title', required=False) attach_file = forms.FileField(label='File', required=False) @@ -538,7 +542,7 @@ class EditLiaisonForm(LiaisonModelForm): super(EditLiaisonForm, self).save(*args,**kwargs) if self.has_changed() and 'submitted_date' in self.changed_data: event = self.instance.liaisonstatementevent_set.filter(type='submitted').first() - event.time = self.cleaned_data.get('submitted_date') + event.time = datetime_from_date(self.cleaned_data.get('submitted_date'), DEADLINE_TZINFO) event.save() return self.instance diff --git a/ietf/liaisons/mails.py b/ietf/liaisons/mails.py index 47f18c75b..8708c8a07 100644 --- a/ietf/liaisons/mails.py +++ b/ietf/liaisons/mails.py @@ -2,14 +2,14 @@ # -*- coding: utf-8 -*- -import datetime - from django.conf import settings from django.template.loader import render_to_string from ietf.utils.mail import send_mail_text from ietf.group.models import Role from ietf.mailtrigger.utils import gather_address_lists +from ietf.utils.timezone import date_today, DEADLINE_TZINFO + def send_liaison_by_email(request, liaison): subject = 'New Liaison Statement, "%s"' % (liaison.title) @@ -61,7 +61,7 @@ def possibly_send_deadline_reminder(liaison): 0: 'today' } - days_to_go = (liaison.deadline - datetime.date.today()).days + days_to_go = (liaison.deadline - date_today(DEADLINE_TZINFO)).days if not (days_to_go < 0 or days_to_go in list(PREVIOUS_DAYS.keys())): return None # no reminder diff --git a/ietf/liaisons/management/commands/check_liaison_deadlines.py b/ietf/liaisons/management/commands/check_liaison_deadlines.py index 84faa5fed..1fe76029a 100644 --- a/ietf/liaisons/management/commands/check_liaison_deadlines.py +++ b/ietf/liaisons/management/commands/check_liaison_deadlines.py @@ -8,13 +8,14 @@ from django.core.management.base import BaseCommand from ietf.liaisons.models import LiaisonStatement from ietf.liaisons.mails import possibly_send_deadline_reminder +from ietf.utils.timezone import date_today, DEADLINE_TZINFO class Command(BaseCommand): help = ("Check liaison deadlines and send a reminder if we are close to a deadline") def handle(self, *args, **options): - today = datetime.date.today() + today = date_today(DEADLINE_TZINFO) cutoff = today - datetime.timedelta(14) msgs = [] diff --git a/ietf/liaisons/tests.py b/ietf/liaisons/tests.py index b08832ae5..845271db7 100644 --- a/ietf/liaisons/tests.py +++ b/ietf/liaisons/tests.py @@ -14,6 +14,8 @@ from django.conf import settings from django.contrib.auth.models import User from django.urls import reverse as urlreverse from django.db.models import Q +from django.utils import timezone + from io import StringIO from pyquery import PyQuery @@ -29,6 +31,8 @@ from ietf.person.models import Person from ietf.group.models import Group from ietf.liaisons.mails import send_sdo_reminder, possibly_send_deadline_reminder from ietf.liaisons.views import contacts_from_roles, contact_email_from_role +from ietf.utils.timezone import date_today, DEADLINE_TZINFO + # ------------------------------------------------- # Helper Functions @@ -50,7 +54,7 @@ def get_liaison_post_data(type='incoming'): to_contacts='to_contacts@example.com', purpose="info", title="title", - submitted_date=datetime.datetime.today().strftime("%Y-%m-%d"), + submitted_date=timezone.now().strftime("%Y-%m-%d"), body="body", send="1" ) @@ -242,7 +246,7 @@ class ManagementCommandTests(TestCase): def test_check_liaison_deadlines(self): from django.core.management import call_command - LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1)) + LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO)+datetime.timedelta(days=1)) out = io.StringIO() mailbox_before = len(outbox) @@ -310,7 +314,7 @@ class LiaisonManagementTests(TestCase): self.assertNotContains(r, 'Private comment') def test_taken_care_of(self): - liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1)) + liaison = LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO)+datetime.timedelta(days=1)) url = urlreverse('ietf.liaisons.views.liaison_detail', kwargs=dict(object_id=liaison.pk)) # normal get @@ -384,8 +388,8 @@ class LiaisonManagementTests(TestCase): self.assertTrue(liaison.liaisonstatementevent_set.filter(type='posted')) def test_edit_liaison(self): - liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1)) - LiaisonStatementEventFactory(statement=liaison,type_id='submitted', time=datetime.datetime.now()-datetime.timedelta(days=1)) + liaison = LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=1)) + LiaisonStatementEventFactory(statement=liaison,type_id='submitted', time=timezone.now()-datetime.timedelta(days=1)) LiaisonStatementEventFactory(statement=liaison,type_id='posted') from_group = liaison.from_groups.first() to_group = liaison.to_groups.first() @@ -696,7 +700,7 @@ class LiaisonManagementTests(TestCase): from_groups = [ str(g.pk) for g in Group.objects.filter(type="sdo") ] to_group = Group.objects.get(acronym="mars") submitter = Person.objects.get(user__username="marschairman") - today = datetime.date.today() + today = date_today() related_liaison = liaison r = self.client.post(url, dict(from_groups=from_groups, @@ -775,7 +779,7 @@ class LiaisonManagementTests(TestCase): from_group = Group.objects.get(acronym="mars") to_group = Group.objects.filter(type="sdo")[0] submitter = Person.objects.get(user__username="marschairman") - today = datetime.date.today() + today = date_today() related_liaison = liaison r = self.client.post(url, dict(from_groups=str(from_group.pk), @@ -843,7 +847,7 @@ class LiaisonManagementTests(TestCase): from_group = Group.objects.get(acronym="mars") to_group = Group.objects.filter(type="sdo")[0] submitter = Person.objects.get(user__username="marschairman") - today = datetime.date.today() + today = date_today() r = self.client.post(url, dict(from_groups=str(from_group.pk), from_contact=submitter.email_address(), @@ -862,7 +866,7 @@ class LiaisonManagementTests(TestCase): self.assertEqual(len(outbox), mailbox_before + 1) def test_liaison_add_attachment(self): - liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1)) + liaison = LiaisonStatementFactory(deadline=date_today()+datetime.timedelta(days=1)) LiaisonStatementEventFactory(statement=liaison,type_id='submitted') self.assertEqual(liaison.attachments.count(),0) @@ -1021,7 +1025,7 @@ class LiaisonManagementTests(TestCase): LiaisonStatementEventFactory(type_id='posted', statement__body="Has recently in its body",statement__from_groups=[GroupFactory(type_id='sdo',acronym='ulm'),]) # Statement 2 s2 = LiaisonStatementEventFactory(type_id='posted', statement__body="That word does not occur here", statement__title="Nor does it occur here") - s2.time=datetime.datetime(2010,1,1) + s2.time=datetime.datetime(2010, 1, 1, tzinfo=datetime.timezone.utc) s2.save() # test list only, no search filters @@ -1148,7 +1152,7 @@ class LiaisonManagementTests(TestCase): self.assertTrue('ulm-liaiman@' in outbox[-1]['To']) def test_send_liaison_deadline_reminder(self): - liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1)) + liaison = LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=1)) mailbox_before = len(outbox) possibly_send_deadline_reminder(liaison) diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py index e59b559f4..cf3c87e7c 100644 --- a/ietf/meeting/factories.py +++ b/ietf/meeting/factories.py @@ -187,7 +187,9 @@ class TimeSlotFactory(factory.django.DjangoModelFactory): @factory.lazy_attribute def time(self): - return datetime.datetime.combine(self.meeting.date,datetime.time(11,0)) + return self.meeting.tz().localize( + datetime.datetime.combine(self.meeting.date, datetime.time(11, 0)) + ) @factory.lazy_attribute def duration(self): diff --git a/ietf/meeting/fixtures/proceedings_templates.json b/ietf/meeting/fixtures/proceedings_templates.json index 1594debff..97d38f566 100644 --- a/ietf/meeting/fixtures/proceedings_templates.json +++ b/ietf/meeting/fixtures/proceedings_templates.json @@ -32,7 +32,7 @@ "comments": "", "list_subscribe": "", "state": "active", - "time": "2012-02-26T00:21:36", + "time": "2012-02-26T00:21:36Z", "unused_tags": [], "list_archive": "", "type": "ietf", diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 4fd4579c9..99be2dd89 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -17,6 +17,7 @@ from django.contrib.auth.models import AnonymousUser from django.urls import reverse from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string +from django.utils import timezone import debug # pyflakes:ignore @@ -42,7 +43,7 @@ def get_meeting(num=None,type_in=['ietf',],days=28): if type_in: meetings = meetings.filter(type__in=type_in) if num == None: - meetings = meetings.filter(date__gte=datetime.datetime.today()-datetime.timedelta(days=days)).order_by('date') + meetings = meetings.filter(date__gte=timezone.now()-datetime.timedelta(days=days)).order_by('date') else: meetings = meetings.filter(number=num) if meetings.exists(): @@ -51,7 +52,7 @@ def get_meeting(num=None,type_in=['ietf',],days=28): raise Http404("No such meeting found: %s" % num) def get_current_ietf_meeting(): - meetings = Meeting.objects.filter(type='ietf',date__gte=datetime.datetime.today()-datetime.timedelta(days=31)).order_by('date') + meetings = Meeting.objects.filter(type='ietf',date__gte=timezone.now()-datetime.timedelta(days=31)).order_by('date') return meetings.first() def get_current_ietf_meeting_num(): @@ -117,7 +118,10 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe # assignments = list(assignments_queryset) # make sure we're set in stone assignments = assignments_queryset - meeting_time = datetime.datetime.combine(meeting.date, datetime.time()) + # meeting_time is meeting-local midnight at the start of the meeting date + meeting_time = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time()) + ) # replace groups with historic counterparts groups = [ ] @@ -1148,11 +1152,15 @@ def sessions_post_cancel(request, sessions): def update_interim_session_assignment(form): - """Helper function to create / update timeslot assigned to interim session""" - time = datetime.datetime.combine( - form.cleaned_data['date'], - form.cleaned_data['time']) + """Helper function to create / update timeslot assigned to interim session + + form is an InterimSessionModelForm + """ session = form.instance + meeting = session.meeting + time = meeting.tz().localize( + datetime.datetime.combine(form.cleaned_data['date'], form.cleaned_data['time']) + ) if session.official_timeslotassignment(): slot = session.official_timeslotassignment().timeslot slot.time = time @@ -1160,14 +1168,14 @@ def update_interim_session_assignment(form): slot.save() else: slot = TimeSlot.objects.create( - meeting=session.meeting, + meeting=meeting, type_id='regular', duration=session.requested_duration, time=time) SchedTimeSessAssignment.objects.create( timeslot=slot, session=session, - schedule=session.meeting.schedule) + schedule=meeting.schedule) def populate_important_dates(meeting): assert ImportantDate.objects.filter(meeting=meeting).exists() is False diff --git a/ietf/meeting/management/commands/create_dummy_meeting.py b/ietf/meeting/management/commands/create_dummy_meeting.py index 79b1e5db6..2a4b1dad7 100644 --- a/ietf/meeting/management/commands/create_dummy_meeting.py +++ b/ietf/meeting/management/commands/create_dummy_meeting.py @@ -48,7 +48,7 @@ import socket import datetime import pytz -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.db.models import Q @@ -75,10 +75,12 @@ class Command(BaseCommand): def _meeting_datetime(self, day, *time_args): """Generate a datetime on a meeting day""" - return datetime.datetime.combine( - self.start_date, - datetime.time(*time_args) - ) + datetime.timedelta(days=day) + return self.meeting_tz.localize( + datetime.datetime.combine( + self.start_date, + datetime.time(*time_args) + ) + datetime.timedelta(days=day) + ) def handle(self, *args, **options): if socket.gethostname().split('.')[0] in ['core3', 'ietfa', 'ietfb', 'ietfc', ]: @@ -87,10 +89,7 @@ class Command(BaseCommand): opt_delete = options.get('delete', False) opt_use_old_conflicts = options.get('old_conflicts', False) self.start_date = options['start_date'] - meeting_tz = options['tz'] - if not opt_delete and (meeting_tz not in pytz.common_timezones): - self.stderr.write("Warning: {} is not a recognized time zone.".format(meeting_tz)) - + meeting_tzname = options['tz'] if opt_delete: if Meeting.objects.filter(number='999').exists(): Meeting.objects.filter(number='999').delete() @@ -98,6 +97,11 @@ class Command(BaseCommand): else: self.stderr.write("Dummy meeting IETF 999 does not exist; nothing to do.\n") else: + try: + self.meeting_tz = pytz.timezone(meeting_tzname) + except pytz.UnknownTimeZoneError: + raise CommandError("{} is not a recognized time zone.".format(meeting_tzname)) + if Meeting.objects.filter(number='999').exists(): self.stderr.write("Dummy meeting IETF 999 already exists; nothing to do.\n") else: @@ -111,7 +115,7 @@ class Command(BaseCommand): type_id='IETF', date=self._meeting_datetime(0).date(), days=7, - time_zone=meeting_tz, + time_zone=meeting_tzname, ) # Set enabled constraints diff --git a/ietf/meeting/management/commands/meetecho_conferences.py b/ietf/meeting/management/commands/meetecho_conferences.py index e6525d7dc..8a7eb0ebc 100644 --- a/ietf/meeting/management/commands/meetecho_conferences.py +++ b/ietf/meeting/management/commands/meetecho_conferences.py @@ -1,7 +1,5 @@ # Copyright The IETF Trust 2022, All Rights Reserved # -*- coding: utf-8 -*- -import datetime - from textwrap import dedent from django.conf import settings @@ -9,6 +7,7 @@ from django.core.management.base import BaseCommand, CommandError from ietf.meeting.models import Session from ietf.utils.meetecho import ConferenceManager, MeetechoAPIError +from ietf.utils.timezone import date_today class Command(BaseCommand): @@ -85,7 +84,7 @@ class Command(BaseCommand): for conf in confs: conf_sessions[conf.id] = Session.objects.filter( group__acronym=group, - meeting__date__gte=datetime.date.today(), + meeting__date__gte=date_today(), remote_instructions__contains=conf.url, ) return confs, conf_sessions diff --git a/ietf/meeting/management/commands/update_important_dates.py b/ietf/meeting/management/commands/update_important_dates.py index c1a6ad424..9a06e850a 100644 --- a/ietf/meeting/management/commands/update_important_dates.py +++ b/ietf/meeting/management/commands/update_important_dates.py @@ -11,6 +11,8 @@ import debug # pyflakes:ignore from ietf.name.models import ImportantDateName from ietf.meeting.helpers import update_important_dates from ietf.meeting.models import Meeting, ImportantDate +from ietf.utils.timezone import date_today + class Command(BaseCommand): @@ -29,7 +31,7 @@ class Command(BaseCommand): if not meeting: self.stderr.write("\nMeeting not found: %s\n" % (m, )) continue - if meeting.date < datetime.date.today() + datetime.timedelta(days=max_offset): + if meeting.date < date_today(meeting.tz()) + datetime.timedelta(days=max_offset): self.stderr.write("\nMeeting %s: Won't change dates for meetings in the past or close future\n" % (meeting, )) continue self.stdout.write('\n%s\n\n' % (meeting, )) diff --git a/ietf/meeting/migrations/0056_use_timezone_now_for_meeting_models.py b/ietf/meeting/migrations/0056_use_timezone_now_for_meeting_models.py new file mode 100644 index 000000000..83d20fd57 --- /dev/null +++ b/ietf/meeting/migrations/0056_use_timezone_now_for_meeting_models.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.28 on 2022-07-12 11:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0055_pytz_2022_2_1'), + ] + + operations = [ + migrations.AlterField( + model_name='schedulingevent', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now, help_text='When the event happened'), + ), + ] diff --git a/ietf/meeting/migrations/0057_fill_in_empty_meeting_time_zone.py b/ietf/meeting/migrations/0057_fill_in_empty_meeting_time_zone.py new file mode 100644 index 000000000..f009b08f3 --- /dev/null +++ b/ietf/meeting/migrations/0057_fill_in_empty_meeting_time_zone.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.28 on 2022-08-08 11:37 + +import datetime + +from django.db import migrations + + +# date of last meeting with an empty time_zone before this migration +LAST_EMPTY_TZ = datetime.date(2022, 7, 1) + + +def forward(apps, schema_editor): + Meeting = apps.get_model('meeting', 'Meeting') + + # Check that we will be able to identify the migrated meetings later + old_meetings_in_pst8pdt = Meeting.objects.filter(type_id='interim', time_zone='PST8PDT', date__lte=LAST_EMPTY_TZ) + assert old_meetings_in_pst8pdt.count() == 0, 'not expecting interim meetings in PST8PDT time_zone' + + meetings_with_empty_tz = Meeting.objects.filter(time_zone='') + # check our expected conditions + for mtg in meetings_with_empty_tz: + assert mtg.type_id == 'interim', 'was not expecting non-interim meetings to be affected' + assert mtg.date <= LAST_EMPTY_TZ, 'affected meeting outside expected date range' + mtg.time_zone = 'PST8PDT' + + # commit the changes + Meeting.objects.bulk_update(meetings_with_empty_tz, ['time_zone']) + + +def reverse(apps, schema_editor): + Meeting = apps.get_model('meeting', 'Meeting') + meetings_to_restore = Meeting.objects.filter(time_zone='PST8PDT', date__lte=LAST_EMPTY_TZ) + for mtg in meetings_to_restore: + mtg.time_zone = '' + # commit the changes + Meeting.objects.bulk_update(meetings_to_restore, ['time_zone']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0056_use_timezone_now_for_meeting_models'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/meeting/migrations/0058_meeting_time_zone_not_blank.py b/ietf/meeting/migrations/0058_meeting_time_zone_not_blank.py new file mode 100644 index 000000000..0adec77a2 --- /dev/null +++ b/ietf/meeting/migrations/0058_meeting_time_zone_not_blank.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2022-08-25 12:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0057_fill_in_empty_meeting_time_zone'), + ] + + operations = [ + migrations.AlterField( + model_name='meeting', + name='time_zone', + field=models.CharField(choices=[('', '---------'), ('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('UTC', 'UTC')], default='UTC', max_length=255), + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 0265dad4c..83b245f76 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -24,6 +24,7 @@ from django.db.models import Max, Subquery, OuterRef, TextField, Value, Q from django.db.models.functions import Coalesce from django.conf import settings from django.urls import reverse as urlreverse +from django.utils import timezone from django.utils.text import slugify from django.utils.safestring import mark_safe @@ -40,7 +41,7 @@ from ietf.person.models import Person from ietf.utils.decorators import memoize from ietf.utils.storage import NoLocationMigrationFileSystemStorage from ietf.utils.text import xslugify -from ietf.utils.timezone import date2datetime +from ietf.utils.timezone import datetime_from_date, date_today from ietf.utils.models import ForeignKey from ietf.utils.validators import ( MaxImageSizeValidator, WrappedValidator, validate_file_size, validate_mime_type, @@ -85,7 +86,7 @@ class Meeting(models.Model): # We can't derive time-zone from country, as there are some that have # more than one timezone, and the pytz module doesn't provide timezone # lookup information for all relevant city/country combinations. - time_zone = models.CharField(blank=True, max_length=255, choices=timezones) + time_zone = models.CharField(max_length=255, choices=timezones, default='UTC') idsubmit_cutoff_day_offset_00 = models.IntegerField(blank=True, default=settings.IDSUBMIT_DEFAULT_CUTOFF_DAY_OFFSET_00, help_text = "The number of days before the meeting start date when the submission of -00 drafts will be closed.") @@ -137,8 +138,21 @@ class Meeting(models.Model): def end_date(self): return self.get_meeting_date(self.days-1) + def start_datetime(self): + """Start-of-day on meeting.date in meeting time zone""" + return datetime_from_date(self.date, self.tz()) + + def end_datetime(self): + """Datetime of the first instant _after_ the meeting's last day in meeting time zone""" + return datetime_from_date(self.get_meeting_date(self.days), self.tz()) + def get_00_cutoff(self): - start_date = datetime.datetime(year=self.date.year, month=self.date.month, day=self.date.day, tzinfo=pytz.utc) + start_date = datetime.datetime( + year=self.date.year, + month=self.date.month, + day=self.date.day, + tzinfo=datetime.timezone.utc, + ) importantdate = self.importantdate_set.filter(name_id='idcutoff').first() if not importantdate: importantdate = self.importantdate_set.filter(name_id='00cutoff').first() @@ -146,7 +160,7 @@ class Meeting(models.Model): cutoff_date = importantdate.date else: cutoff_date = start_date + datetime.timedelta(days=ImportantDateName.objects.get(slug='idcutoff').default_offset_days) - cutoff_time = date2datetime(cutoff_date) + self.idsubmit_cutoff_time_utc + cutoff_time = datetime_from_date(cutoff_date) + self.idsubmit_cutoff_time_utc return cutoff_time def get_01_cutoff(self): @@ -158,7 +172,7 @@ class Meeting(models.Model): cutoff_date = importantdate.date else: cutoff_date = start_date + datetime.timedelta(days=ImportantDateName.objects.get(slug='idcutoff').default_offset_days) - cutoff_time = date2datetime(cutoff_date) + self.idsubmit_cutoff_time_utc + cutoff_time = datetime_from_date(cutoff_date) + self.idsubmit_cutoff_time_utc return cutoff_time def get_reopen_time(self): @@ -176,7 +190,7 @@ class Meeting(models.Model): @classmethod def get_current_meeting(cls, type="ietf"): - return cls.objects.filter(type=type, date__gte=datetime.datetime.today()-datetime.timedelta(days=7) ).order_by('date').first() + return cls.objects.filter(type=type, date__gte=timezone.now()-datetime.timedelta(days=7) ).order_by('date').first() def get_first_cut_off(self): return self.get_00_cutoff() @@ -338,7 +352,7 @@ class Meeting(models.Model): for ts in self.timeslot_set.all(): if ts.location_id is None: continue - ymd = ts.time.date() + ymd = ts.local_start_time().date() if ymd not in time_slices: time_slices[ymd] = [] slots[ymd] = [] @@ -346,15 +360,15 @@ class Meeting(models.Model): if ymd in time_slices: # only keep unique entries - if [ts.time, ts.time + ts.duration, ts.duration.seconds] not in time_slices[ymd]: - time_slices[ymd].append([ts.time, ts.time + ts.duration, ts.duration.seconds]) + if [ts.local_start_time(), ts.local_end_time(), ts.duration.seconds] not in time_slices[ymd]: + time_slices[ymd].append([ts.local_start_time(), ts.local_end_time(), ts.duration.seconds]) slots[ymd].append(ts) days.sort() for ymd in time_slices: # Make sure these sort the same way time_slices[ymd].sort() - slots[ymd].sort(key=lambda x: (x.time, x.duration)) + slots[ymd].sort(key=lambda x: (x.local_start_time(), x.duration)) return days,time_slices,slots # this functions makes a list of timeslices and rooms, and @@ -370,20 +384,24 @@ class Meeting(models.Model): # SchedTimeSessAssignment.objects.create(schedule = sched, # timeslot = ts) + def tz(self): + if not hasattr(self, '_cached_tz'): + self._cached_tz = pytz.timezone(self.time_zone) + return self._cached_tz + def vtimezone(self): - if self.time_zone: - try: - tzfn = os.path.join(settings.TZDATA_ICS_PATH, self.time_zone + ".ics") - if os.path.exists(tzfn): - with io.open(tzfn) as tzf: - icstext = tzf.read() - vtimezone = re.search("(?sm)(\nBEGIN:VTIMEZONE.*\nEND:VTIMEZONE\n)", icstext).group(1).strip() - if vtimezone: - vtimezone += "\n" - return vtimezone - except IOError: - pass - return '' + try: + tzfn = os.path.join(settings.TZDATA_ICS_PATH, self.time_zone + ".ics") + if os.path.exists(tzfn): + with io.open(tzfn) as tzf: + icstext = tzf.read() + vtimezone = re.search("(?sm)(\nBEGIN:VTIMEZONE.*\nEND:VTIMEZONE\n)", icstext).group(1).strip() + if vtimezone: + vtimezone += "\n" + return vtimezone + except IOError: + pass + return None def set_official_schedule(self, schedule): if self.schedule != schedule: @@ -391,16 +409,14 @@ class Meeting(models.Model): self.save() def updated(self): - min_time = datetime.datetime(1970, 1, 1, 0, 0, 0) # should be Meeting.modified, but we don't have that + # should be Meeting.modified, but we don't have that + min_time = pytz.utc.localize(datetime.datetime(1970, 1, 1, 0, 0, 0)) timeslots_updated = self.timeslot_set.aggregate(Max('modified'))["modified__max"] or min_time sessions_updated = self.session_set.aggregate(Max('modified'))["modified__max"] or min_time assignments_updated = min_time if self.schedule: assignments_updated = SchedTimeSessAssignment.objects.filter(schedule__in=[self.schedule, self.schedule.base if self.schedule else None]).aggregate(Max('modified'))["modified__max"] or min_time - ts = max(timeslots_updated, sessions_updated, assignments_updated) - tz = pytz.timezone(settings.PRODUCTION_TIMEZONE) - ts = tz.localize(ts) - return ts + return max(timeslots_updated, sessions_updated, assignments_updated) @memoize def previous_meeting(self): @@ -621,41 +637,22 @@ class TimeSlot(models.Model): return self._cached_html_location def tz(self): - if not hasattr(self, '_cached_tz'): - if self.meeting.time_zone: - self._cached_tz = pytz.timezone(self.meeting.time_zone) - else: - self._cached_tz = None - return self._cached_tz + return self.meeting.tz() def tzname(self): - if self.tz(): - return self.tz().tzname(self.time) - else: - return "" + return self.tz().tzname(self.time) def utc_start_time(self): - if self.tz(): - local_start_time = self.tz().localize(self.time) - return local_start_time.astimezone(pytz.utc) - else: - return None + return self.time.astimezone(pytz.utc) # USE_TZ is True, so time is aware def utc_end_time(self): - utc_start = self.utc_start_time() - # Add duration after converting start time, otherwise errors creep in around DST change - return None if utc_start is None else utc_start + self.duration + return self.time.astimezone(pytz.utc) + self.duration # USE_TZ is True, so time is aware def local_start_time(self): - if self.tz(): - return self.tz().localize(self.time) - else: - return None + return self.time.astimezone(self.tz()) def local_end_time(self): - local_start = self.local_start_time() - # Add duration after converting start time, otherwise errors creep in around DST change - return None if local_start is None else local_start + self.duration + return (self.time.astimezone(pytz.utc) + self.duration).astimezone(self.tz()) @property def js_identifier(self): @@ -753,7 +750,7 @@ class Schedule(models.Model): @property def is_official_record(self): return (self.is_official and - self.meeting.end_date() <= datetime.date.today() ) + self.meeting.end_date() <= date_today() ) # returns a dictionary {group -> [schedtimesessassignment+]} # and it has [] if the session is not placed. @@ -1170,7 +1167,7 @@ class Session(models.Model): return can_manage_materials(user,self.group) def is_material_submission_cutoff(self): - return datetime.date.today() > self.meeting.get_submission_correction_date() + return date_today(self.meeting.tz()) > self.meeting.get_submission_correction_date() def joint_with_groups_acronyms(self): return [group.acronym for group in self.joint_with_groups.all()] @@ -1297,7 +1294,7 @@ class Session(models.Model): class SchedulingEvent(models.Model): session = ForeignKey(Session) - time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened") + time = models.DateTimeField(default=timezone.now, help_text="When the event happened") status = ForeignKey(SessionStatusName) by = ForeignKey(Person) diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index e5fdd71c5..5ecb494df 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -20,11 +20,15 @@ from ietf.name.models import RoomResourceName from ietf.person.factories import PersonFactory from ietf.person.models import Person from ietf.utils.test_data import make_test_data +from ietf.utils.timezone import date_today -def make_interim_meeting(group,date,status='sched'): + +def make_interim_meeting(group,date,status='sched',tz='UTC'): system_person = Person.objects.get(name="(System)") - time = datetime.datetime.combine(date, datetime.time(9)) - meeting = create_interim_meeting(group=group,date=date) + meeting = create_interim_meeting(group=group,date=date,timezone=tz) + time = meeting.tz().localize( + datetime.datetime.combine(date, datetime.time(9)) + ) session = SessionFactory(meeting=meeting, group=group, attendees=10, requested_duration=datetime.timedelta(minutes=20), @@ -102,24 +106,37 @@ def make_meeting_test_data(meeting=None, create_interims=False): # slots session_date = meeting.date + datetime.timedelta(days=1) + tz = meeting.tz() slot1 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=60), - time=datetime.datetime.combine(session_date, datetime.time(9, 30))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(9, 30)) + )) slot2 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=60), - time=datetime.datetime.combine(session_date, datetime.time(10, 50))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(10, 50)) + )) breakfast_slot = TimeSlot.objects.create(meeting=meeting, type_id="lead", location=breakfast_room, duration=datetime.timedelta(minutes=90), - time=datetime.datetime.combine(session_date, datetime.time(7,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(7,0)) + )) reg_slot = TimeSlot.objects.create(meeting=meeting, type_id="reg", location=reg_room, duration=datetime.timedelta(minutes=480), - time=datetime.datetime.combine(session_date, datetime.time(9,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(9,0)) + )) break_slot = TimeSlot.objects.create(meeting=meeting, type_id="break", location=break_room, duration=datetime.timedelta(minutes=90), - time=datetime.datetime.combine(session_date, datetime.time(7,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(7,0)) + )) plenary_slot = TimeSlot.objects.create(meeting=meeting, type_id="plenary", location=room, duration=datetime.timedelta(minutes=60), - time=datetime.datetime.combine(session_date, datetime.time(11,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(11,0)) + )) # mars WG mars = Group.objects.get(acronym='mars') mars_session = SessionFactory(meeting=meeting, group=mars, @@ -201,8 +218,8 @@ def make_meeting_test_data(meeting=None, create_interims=False): mars_session.sessionpresentation_set.add(pres) # Future Interim Meetings - date = datetime.date.today() + datetime.timedelta(days=365) - date2 = datetime.date.today() + datetime.timedelta(days=1000) + date = date_today() + datetime.timedelta(days=365) + date2 = date_today() + datetime.timedelta(days=1000) ames = Group.objects.get(acronym="ames") if create_interims: @@ -213,9 +230,9 @@ def make_meeting_test_data(meeting=None, create_interims=False): return meeting -def make_interim_test_data(): - date = datetime.date.today() + datetime.timedelta(days=365) - date2 = datetime.date.today() + datetime.timedelta(days=1000) +def make_interim_test_data(meeting_tz='UTC'): + date = date_today() + datetime.timedelta(days=365) + date2 = date_today() + datetime.timedelta(days=1000) PersonFactory(user__username='plain') area = GroupFactory(type_id='area') ad = Person.objects.get(user__username='ad') @@ -225,10 +242,10 @@ def make_interim_test_data(): RoleFactory(group=mars,person__user__username='marschairman',name_id='chair') RoleFactory(group=ames,person__user__username='ameschairman',name_id='chair') - make_interim_meeting(group=mars,date=date,status='sched') - make_interim_meeting(group=mars,date=date2,status='apprw') - make_interim_meeting(group=ames,date=date,status='canceled') - make_interim_meeting(group=ames,date=date2,status='apprw') + make_interim_meeting(group=mars,date=date,status='sched',tz=meeting_tz) + make_interim_meeting(group=mars,date=date2,status='apprw',tz=meeting_tz) + make_interim_meeting(group=ames,date=date,status='canceled',tz=meeting_tz) + make_interim_meeting(group=ames,date=date2,status='apprw',tz=meeting_tz) return diff --git a/ietf/meeting/tests_helpers.py b/ietf/meeting/tests_helpers.py index 3160f9388..d77610baa 100644 --- a/ietf/meeting/tests_helpers.py +++ b/ietf/meeting/tests_helpers.py @@ -18,6 +18,7 @@ from ietf.meeting.models import SchedTimeSessAssignment, Session from ietf.meeting.test_data import make_meeting_test_data from ietf.utils.meetecho import Conference from ietf.utils.test_utils import TestCase +from ietf.utils.timezone import date_today # override the legacy office hours setting to guarantee consistency with the tests @@ -623,12 +624,13 @@ class HelperTests(TestCase): def test_get_ietf_meeting(self): """get_ietf_meeting() should only return IETF meetings""" # put the IETF far in the past so it's not "current" - ietf = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=5 * 365)) + today = date_today() + ietf = MeetingFactory(type_id='ietf', date=today- datetime.timedelta(days=5 * 365)) # put the interim meeting now so it will be picked up as "current" if there's a bug - interim = MeetingFactory(type_id='interim', date=datetime.date.today()) + interim = MeetingFactory(type_id='interim', date=today) self.assertEqual(get_ietf_meeting(ietf.number), ietf, 'Return IETF meeting by number') self.assertIsNone(get_ietf_meeting(interim.number), 'Ignore non-IETF meetings') self.assertIsNone(get_ietf_meeting(), 'Return None if there is no current IETF meeting') - ietf.date = datetime.date.today() + ietf.date = today ietf.save() self.assertEqual(get_ietf_meeting(), ietf, 'Return current meeting if there is one') diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 2d221994a..d93835ac6 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -10,8 +10,8 @@ import re from unittest import skipIf import django +from django.utils import timezone from django.utils.text import slugify -from django.utils.timezone import now from django.db.models import F import pytz @@ -34,6 +34,7 @@ from ietf.meeting.utils import add_event_info_to_session_qs from ietf.utils.test_utils import assert_ical_response_is_valid from ietf.utils.jstest import ( IetfSeleniumTestCase, ifSeleniumEnabled, selenium_enabled, presence_of_element_child_by_css_selector ) +from ietf.utils.timezone import datetime_today, datetime_from_date, date_today if selenium_enabled(): from selenium.webdriver.common.action_chains import ActionChains @@ -268,14 +269,32 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): # modal_open.click() self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed()) - self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click() + self.driver.find_element( + By.CSS_SELECTOR, + "#timeslot-group-toggles-modal [value=\"{}\"]".format( + "ts-group-{}-{}".format( + slot2.time.astimezone(slot2.tz()).strftime("%Y%m%d-%H%M"), + int(slot2.duration.total_seconds() / 60), + ), + ), + ).click() self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [data-bs-dismiss=\"modal\"]").click() self.assertTrue(not self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed()) # swap days - self.driver.find_element(By.CSS_SELECTOR, ".day .swap-days[data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click() + self.driver.find_element( + By.CSS_SELECTOR, + ".day .swap-days[data-dayid=\"{}\"]".format( + slot4.time.astimezone(slot4.tz()).date().isoformat(), + ), + ).click() self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal").is_displayed()) - self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click() + self.driver.find_element( + By.CSS_SELECTOR, + "#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format( + slot1.time.astimezone(slot1.tz()).date().isoformat(), + ), + ).click() self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal button[type=\"submit\"]").click() self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot4.pk, s1.pk)), @@ -305,7 +324,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): room = RoomFactory(meeting=meeting) # get current time in meeting time zone - right_now = now().astimezone( + right_now = timezone.now().astimezone( pytz.timezone(meeting.time_zone) ) if not settings.USE_TZ: @@ -392,11 +411,11 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): def test_past_swap_days_buttons(self): """Swap days buttons should be hidden for past items""" wait = WebDriverWait(self.driver, 2) - meeting = MeetingFactory(type_id='ietf', date=datetime.datetime.today() - datetime.timedelta(days=3), days=7) + meeting = MeetingFactory(type_id='ietf', date=timezone.now() - datetime.timedelta(days=3), days=7) room = RoomFactory(meeting=meeting) # get current time in meeting time zone - right_now = now().astimezone( + right_now = timezone.now().astimezone( pytz.timezone(meeting.time_zone) ) if not settings.USE_TZ: @@ -427,21 +446,24 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): past_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( - '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots + '.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat()) + for ts in past_timeslots ) ) self.assertEqual(len(past_swap_days_buttons), len(past_timeslots), 'Missing past swap days buttons') future_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( - '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in future_timeslots + '.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat()) + for ts in future_timeslots ) ) self.assertEqual(len(future_swap_days_buttons), len(future_timeslots), 'Missing future swap days buttons') now_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( - '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots + '.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat()) + for ts in now_timeslots ) ) # only one "now" button because both sessions are on the same day @@ -492,7 +514,8 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.assertFalse( any(radio.is_enabled() for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( - 'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots) + 'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat()) + for ts in past_timeslots) )), 'Past day is enabled in swap-days modal for official schedule', ) @@ -501,14 +524,16 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.assertTrue( all(radio.is_enabled() for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( - 'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in enabled_timeslots) + 'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat()) + for ts in enabled_timeslots) )), 'Future day is not enabled in swap-days modal for official schedule', ) self.assertFalse( any(radio.is_enabled() for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( - 'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots) + 'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat()) + for ts in now_timeslots) )), '"Now" day is enabled in swap-days modal for official schedule', ) @@ -516,11 +541,11 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): def test_past_swap_timeslot_col_buttons(self): """Swap timeslot column buttons should be hidden for past items""" wait = WebDriverWait(self.driver, 2) - meeting = MeetingFactory(type_id='ietf', date=datetime.datetime.today() - datetime.timedelta(days=3), days=7) + meeting = MeetingFactory(type_id='ietf', date=timezone.now() - datetime.timedelta(days=3), days=7) room = RoomFactory(meeting=meeting) # get current time in meeting time zone - right_now = now().astimezone( + right_now = timezone.now().astimezone( pytz.timezone(meeting.time_zone) ) if not settings.USE_TZ: @@ -806,7 +831,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): To test for recurrence of https://trac.ietf.org/trac/ietfdb/ticket/3327 need to have some constraints that do not conflict. Testing with only violated constraints does not exercise the code adequately. """ - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), populate_schedule=False) + meeting = MeetingFactory(type_id='ietf', date=date_today(), populate_schedule=False) TimeSlotFactory.create_batch(5, meeting=meeting) schedule = ScheduleFactory(meeting=meeting) sessions = SessionFactory.create_batch(5, meeting=meeting, add_to_schedule=False) @@ -928,7 +953,7 @@ class InterimTests(IetfSeleniumTestCase): # Create a group with a plenary interim session for testing type filters somegroup = GroupFactory(acronym='sg', name='Some Group') - sg_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20)) + sg_interim = make_interim_meeting(somegroup, date_today() + datetime.timedelta(days=20)) sg_sess = sg_interim.session_set.first() sg_slot = sg_sess.timeslotassignments.first().timeslot sg_sess.purpose_id = 'plenary' @@ -960,7 +985,7 @@ class InterimTests(IetfSeleniumTestCase): Session.objects.filter( meeting__type_id='interim', timeslotassignments__schedule=F('meeting__schedule'), - timeslotassignments__timeslot__time__gte=datetime.datetime.today() + timeslotassignments__timeslot__time__gte=timezone.now() ) ).filter(current_status__in=('sched','canceled')) meetings = [] @@ -973,7 +998,7 @@ class InterimTests(IetfSeleniumTestCase): def all_ietf_meetings(self): meetings = Meeting.objects.filter( type_id='ietf', - date__gte=datetime.datetime.today()-datetime.timedelta(days=7) + date__gte=timezone.now()-datetime.timedelta(days=7) ) for m in meetings: m.calendar_label = 'IETF %s' % m.number @@ -1103,7 +1128,7 @@ class InterimTests(IetfSeleniumTestCase): expected_assignments = list(SchedTimeSessAssignment.objects.filter( schedule__in=expected_schedules, session__in=expected_interim_sessions, - timeslot__time__gte=datetime.date.today(), + timeslot__time__gte=datetime_today(), )) # The UID formats should match those in the upcoming.ics template expected_uids = [ @@ -1419,7 +1444,7 @@ class ProceedingsMaterialTests(IetfSeleniumTestCase): def setUp(self): super().setUp() self.wait = WebDriverWait(self.driver, 2) - self.meeting = MeetingFactory(type_id='ietf', number='123', date=datetime.date.today()) + self.meeting = MeetingFactory(type_id='ietf', number='123', date=date_today()) def test_add_proceedings_material(self): url = self.absreverse( @@ -1521,7 +1546,7 @@ class EditTimeslotsTests(IetfSeleniumTestCase): self.meeting: Meeting = MeetingFactory( type_id='ietf', number=120, - date=datetime.datetime.today() + datetime.timedelta(days=10), + date=date_today() + datetime.timedelta(days=10), populate_schedule=False, ) self.edit_timeslot_url = self.absreverse( @@ -1591,22 +1616,23 @@ class EditTimeslotsTests(IetfSeleniumTestCase): self.do_delete_timeslot_test(cancel=True) def do_delete_time_interval_test(self, cancel=False): - delete_day = self.meeting.date.date() + delete_day = self.meeting.date delete_time = datetime.time(hour=10) - other_day = self.meeting.get_meeting_date(1).date() + other_day = self.meeting.get_meeting_date(1) other_time = datetime.time(hour=12) duration = datetime.timedelta(minutes=60) delete: [TimeSlot] = TimeSlotFactory.create_batch( 2, meeting=self.meeting, - time=datetime.datetime.combine(delete_day, delete_time), - duration=duration) + time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=delete_time.hour), + duration=duration, + ) keep: [TimeSlot] = [ TimeSlotFactory( meeting=self.meeting, - time=datetime.datetime.combine(day, time), + time=datetime_from_date(day, self.meeting.tz()).replace(hour=time.hour), duration=duration ) for (day, time) in ( @@ -1623,7 +1649,9 @@ class EditTimeslotsTests(IetfSeleniumTestCase): '[data-col-id="{}T{}-{}"]'.format( delete_day.isoformat(), delete_time.strftime('%H:%M'), - (datetime.datetime.combine(delete_day, delete_time) + duration).strftime( + self.meeting.tz().localize( + datetime.datetime.combine(delete_day, delete_time) + duration + ).strftime( '%H:%M' )) ) @@ -1638,22 +1666,22 @@ class EditTimeslotsTests(IetfSeleniumTestCase): self.do_delete_time_interval_test(cancel=True) def do_delete_day_test(self, cancel=False): - delete_day = self.meeting.date.date() - times = [datetime.time(hour=10), datetime.time(hour=12)] - other_days = [self.meeting.get_meeting_date(d).date() for d in range(1, 3)] + delete_day = self.meeting.date + hours = [10, 12] + other_days = [self.meeting.get_meeting_date(d) for d in range(1, 3)] delete: [TimeSlot] = [ TimeSlotFactory( meeting=self.meeting, - time=datetime.datetime.combine(delete_day, time), - ) for time in times + time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=hour), + ) for hour in hours ] keep: [TimeSlot] = [ TimeSlotFactory( meeting=self.meeting, - time=datetime.datetime.combine(day, time), - ) for day in other_days for time in times + time=datetime_from_date(day, self.meeting.tz()).replace(hour=hour), + ) for day in other_days for hour in hours ] selector = ( diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index 71f5b9361..175236611 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -3,6 +3,8 @@ """Tests of models in the Meeting application""" import datetime +from mock import patch + from ietf.meeting.factories import MeetingFactory, SessionFactory, AttendedFactory from ietf.stats.factories import MeetingRegistrationFactory from ietf.utils.test_utils import TestCase @@ -86,6 +88,22 @@ class MeetingTests(TestCase): self.assertEqual(att.onsite, 1) self.assertEqual(att.remote, 0) + def test_vtimezone(self): + # normal time zone that should have a zoneinfo file + meeting = MeetingFactory(type_id='ietf', time_zone='America/Los_Angeles', populate_schedule=False) + vtz = meeting.vtimezone() + self.assertIsNotNone(vtz) + self.assertGreater(len(vtz), 0) + # time zone that does not have a zoneinfo file should return None + meeting = MeetingFactory(type_id='ietf', time_zone='Fake/Time_Zone', populate_schedule=False) + vtz = meeting.vtimezone() + self.assertIsNone(vtz) + # ioerror trying to read zoneinfo should return None + meeting = MeetingFactory(type_id='ietf', time_zone='America/Los_Angeles', populate_schedule=False) + with patch('ietf.meeting.models.io.open', side_effect=IOError): + vtz = meeting.vtimezone() + self.assertIsNone(vtz) + class SessionTests(TestCase): def test_chat_archive_url_with_jabber(self): diff --git a/ietf/meeting/tests_schedule_generator.py b/ietf/meeting/tests_schedule_generator.py index d414805d3..a88280208 100644 --- a/ietf/meeting/tests_schedule_generator.py +++ b/ietf/meeting/tests_schedule_generator.py @@ -1,6 +1,7 @@ # Copyright The IETF Trust 2020, All Rights Reserved import calendar import datetime +import pytz from io import StringIO from django.core.management.base import CommandError @@ -36,9 +37,11 @@ class ScheduleGeneratorTest(TestCase): t = TimeSlotFactory( meeting=self.meeting, location=room, - time=datetime.datetime.combine( - self.meeting.date + datetime.timedelta(days=day), - datetime.time(hour, 0), + time=self.meeting.tz().localize( + datetime.datetime.combine( + self.meeting.date + datetime.timedelta(days=day), + datetime.time(hour, 0), + ) ), duration=datetime.timedelta(minutes=60), ) @@ -306,8 +309,11 @@ class ScheduleGeneratorTest(TestCase): add_to_schedule=False ) # use a timeslot not on Sunday + meeting_date = pytz.utc.localize( + datetime.datetime.combine(self.meeting.get_meeting_date(1), datetime.time()) + ) ts = self.meeting.timeslot_set.filter( - time__gt=self.meeting.date + datetime.timedelta(days=1), + time__gt=meeting_date, location__capacity__lt=base_reg_session.attendees, ).order_by( 'time' diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 94a52c82b..c8f209bdc 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -21,6 +21,7 @@ from urllib.parse import urlparse, urlsplit from PIL import Image from pathlib import Path from tempfile import NamedTemporaryFile +from zoneinfo import ZoneInfo from django.urls import reverse as urlreverse from django.conf import settings @@ -29,8 +30,8 @@ from django.test import Client, override_settings from django.db.models import F, Max from django.http import QueryDict, FileResponse from django.template import Context, Template +from django.utils import timezone from django.utils.text import slugify -from django.utils.timezone import now import debug # pyflakes:ignore @@ -51,6 +52,7 @@ from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, Pro from ietf.utils.decorators import skip_coverage from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent +from ietf.utils.timezone import date_today, time_now from ietf.person.factories import PersonFactory from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory @@ -154,7 +156,7 @@ class MeetingTests(BaseMeetingTestCase): # self.write_materials_files(meeting, session) # - future_year = datetime.date.today().year+1 + future_year = date_today().year+1 future_num = (future_year-1984)*3 # valid for the mid-year meeting future_meeting = Meeting.objects.create(date=datetime.date(future_year, 7, 22), number=future_num, type_id='ietf', city="Panama City", country="PA", time_zone='America/Panama') @@ -213,7 +215,10 @@ class MeetingTests(BaseMeetingTestCase): ) # plain - time_interval = r"%s<span.*/span>-%s" % (slot.time.strftime("%H:%M").lstrip("0"), (slot.time + slot.duration).strftime("%H:%M").lstrip("0")) + time_interval = r"{}<span.*/span>-{}".format( + slot.time.astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"), + slot.end_time().astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"), + ) # text # the rest of the results don't have as nicely formatted times @@ -351,7 +356,7 @@ class MeetingTests(BaseMeetingTestCase): def test_interim_materials(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') - date = datetime.datetime.today() - datetime.timedelta(days=10) + date = timezone.now() - datetime.timedelta(days=10) meeting = make_interim_meeting(group=group, date=date, status='sched') session = meeting.session_set.first() @@ -548,7 +553,12 @@ class MeetingTests(BaseMeetingTestCase): a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session - t2 = TimeSlotFactory.create(meeting=meeting, time=datetime.datetime.combine(meeting.date, datetime.time(11, 30))) + t2 = TimeSlotFactory.create( + meeting=meeting, + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(11, 30)) + ) + ) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # @@ -558,16 +568,16 @@ class MeetingTests(BaseMeetingTestCase): r, expected_event_summaries=['mars - Martian Special Interest Group'], expected_event_count=2) - self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) - self.assertContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, }) r = self.client.get(url) assert_ical_response_is_valid(self, r, expected_event_summaries=['mars - Martian Special Interest Group'], expected_event_count=1) - self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) - self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) + self.assertNotContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) def test_parse_agenda_filter_params(self): def _r(show=(), hide=(), showtypes=(), hidetypes=()): @@ -673,7 +683,7 @@ class MeetingTests(BaseMeetingTestCase): url = urlreverse('ietf.meeting.views.current_materials') response = self.client.get(url) self.assertEqual(response.status_code, 404) - MeetingFactory(type_id='ietf', date=datetime.date.today()) + MeetingFactory(type_id='ietf', date=date_today()) response = self.client.get(url) self.assertEqual(response.status_code, 302) @@ -818,7 +828,9 @@ class EditMeetingScheduleTests(TestCase): TimeSlotFactory( meeting=meeting, location=room, - time=datetime.datetime.combine(meeting.date, time), + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date, time) + ), duration=datetime.timedelta(minutes=duration), ) @@ -889,7 +901,7 @@ class EditMeetingScheduleTests(TestCase): # Meeting must be in the future so it can be edited meeting = MeetingFactory( type_id='ietf', - date=datetime.date.today() + datetime.timedelta(days=7), + date=date_today() + datetime.timedelta(days=7), populate_schedule=False, ) meeting.schedule = ScheduleFactory(meeting=meeting) @@ -902,7 +914,11 @@ class EditMeetingScheduleTests(TestCase): ] # Set up different sets of timeslots - t0 = datetime.datetime.combine(meeting.date, datetime.time(11, 0)) + # Work with t0 in UTC for arithmetic. This does not change the results but is cleaner if someone looks + # at intermediate results which may be misleading until passed through tz.normalize(). + t0 = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(11, 0)) + ).astimezone(pytz.utc) dur = datetime.timedelta(hours=2) for room in room_groups[0]: TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0) @@ -997,7 +1013,7 @@ class EditMeetingScheduleTests(TestCase): self.client.login(username=username, password=username + '+password') # Swap group 0's first and last sessions, first in the past - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) for room in room_groups[0]: ts = room.timeslot_set.last() ts.time = right_now - datetime.timedelta(minutes=5) @@ -1179,12 +1195,18 @@ class EditMeetingScheduleTests(TestCase): self.client.login(username=username, password=username + '+password') # Swap group 0's first and last sessions, first in the past - right_now = self._right_now_in(meeting.time_zone) - yesterday = (right_now - datetime.timedelta(days=1)).date() - day_before = (right_now - datetime.timedelta(days=2)).date() + right_now = self._right_now_in(meeting.tz()) + yesterday = right_now.date() - datetime.timedelta(days=1) + day_before = right_now.date() - datetime.timedelta(days=2) for room in room_groups[0]: ts = room.timeslot_set.last() - ts.time = datetime.datetime.combine(yesterday, ts.time.time()) + # Calculation keeps local clock time, shifted to a different day. + ts.time = meeting.tz().localize( + datetime.datetime.combine( + yesterday, + ts.time.astimezone(meeting.tz()).time() + ), + ) ts.save() # timeslot_set is ordered by -time, so check that we know which is past/future self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now) @@ -1218,7 +1240,12 @@ class EditMeetingScheduleTests(TestCase): # now with both in the past for room in room_groups[0]: ts = room.timeslot_set.first() - ts.time = datetime.datetime.combine(day_before, ts.time.time()) + ts.time = meeting.tz().localize( + datetime.datetime.combine( + day_before, + ts.time.astimezone(meeting.tz()).time(), + ) + ) ts.save() past_slots = room_groups[0][0].timeslot_set.filter(time__lt=right_now) self.assertEqual(len(past_slots), 2, 'Need two timeslots in the past!') @@ -1241,20 +1268,18 @@ class EditMeetingScheduleTests(TestCase): self.fail('Response was not valid JSON: {}'.format(err)) @staticmethod - def _right_now_in(tzname): - right_now = now().astimezone(pytz.timezone(tzname)) - if not settings.USE_TZ: - right_now = right_now.replace(tzinfo=None) + def _right_now_in(tzinfo): + right_now = timezone.now().astimezone(tzinfo) return right_now def test_assign_session(self): """Allow assignment to future timeslots only for official schedule""" meeting = MeetingFactory( type_id='ietf', - date=(datetime.datetime.today() - datetime.timedelta(days=1)).date(), + date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, @@ -1311,10 +1336,10 @@ class EditMeetingScheduleTests(TestCase): """Do not allow assignment of past sessions for official schedule""" meeting = MeetingFactory( type_id='ietf', - date=(datetime.datetime.today() - datetime.timedelta(days=1)).date(), + date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, @@ -1446,10 +1471,10 @@ class EditMeetingScheduleTests(TestCase): """Allow unassignment only of future timeslots for official schedule""" meeting = MeetingFactory( type_id='ietf', - date=(datetime.datetime.today() - datetime.timedelta(days=1)).date(), + date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, @@ -1531,7 +1556,7 @@ class EditMeetingScheduleTests(TestCase): """Schedule editor should not crash when there are no timeslots""" meeting = MeetingFactory( type_id='ietf', - date=datetime.date.today() + datetime.timedelta(days=7), + date=date_today() + datetime.timedelta(days=7), populate_schedule=False, ) meeting.schedule = ScheduleFactory(meeting=meeting) @@ -1545,6 +1570,58 @@ class EditMeetingScheduleTests(TestCase): self.assertContains(r, 'No timeslots exist') self.assertContains(r, urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number})) + def test_editor_time_zone(self): + """Agenda editor should show meeting time zone""" + time_zone = 'Etc/GMT+8' + meeting_tz = ZoneInfo(time_zone) + meeting = MeetingFactory( + type_id='ietf', + date=date_today(meeting_tz) + datetime.timedelta(days=7), + populate_schedule=False, + time_zone=time_zone, + ) + meeting.schedule = ScheduleFactory(meeting=meeting) + meeting.save() + timeslot = TimeSlotFactory(meeting=meeting) + ts_start = timeslot.time.astimezone(meeting_tz) + ts_end = timeslot.end_time().astimezone(meeting_tz) + url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}) + self.assertTrue(self.client.login(username='secretary', password='secretary+password')) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + pq = PyQuery(r.content) + + day_header = pq('.day-flow .day-label') + self.assertIn(ts_start.strftime('%A'), day_header.text()) + + day_swap = day_header.find('.swap-days') + self.assertEqual(day_swap.attr('data-dayid'), ts_start.date().isoformat()) + self.assertEqual(day_swap.attr('data-start'), ts_start.date().isoformat()) + + time_label = pq('.day-flow .time-header .time-label') + self.assertEqual(len(time_label), 1) + # strftime() does not seem to support hours without leading 0, so do this manually + time_label_string = f'{ts_start.hour:d}:{ts_start.minute:02d} - {ts_end.hour:d}:{ts_end.minute:02d}' + self.assertIn(time_label_string, time_label.text()) + self.assertEqual(time_label.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat()) + self.assertEqual(time_label.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat()) + + ts_swap = time_label.find('.swap-timeslot-col') + origin_label = ts_swap.attr('data-origin-label') + # testing the exact date in origin_label is hard because Django's date filter uses + # different month formats than Python's strftime, so just check a couple parts. + self.assertIn(ts_start.strftime('%A'), origin_label) + self.assertIn(f'{ts_start.hour:d}:{ts_start.minute:02d}-{ts_end.hour:d}:{ts_end.minute:02d}', origin_label) + + timeslot_elt = pq(f'#timeslot{timeslot.pk}') + self.assertEqual(len(timeslot_elt), 1) + self.assertEqual(timeslot_elt.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat()) + self.assertEqual(timeslot_elt.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat()) + + timeslot_label = pq(f'#timeslot{timeslot.pk} .time-label') + self.assertEqual(len(timeslot_label), 1) + self.assertIn(time_label_string, timeslot_label.text()) + class EditTimeslotsTests(TestCase): def login(self, username='secretary'): @@ -1570,7 +1647,7 @@ class EditTimeslotsTests(TestCase): return MeetingFactory( type_id='ietf', number=number, - date=datetime.datetime.today() + datetime.timedelta(days=10), + date=date_today() + datetime.timedelta(days=10), populate_schedule=False, ) @@ -1602,7 +1679,8 @@ class EditTimeslotsTests(TestCase): meeting = self.create_bare_meeting(number=number) RoomFactory.create_batch(8, meeting=meeting) self.create_initial_schedule(meeting) - return meeting + # retrieve meeting from DB so it goes through Django's processing + return Meeting.objects.get(pk=meeting.pk) def test_view_permissions(self): """Only the secretary should be able to edit timeslots""" @@ -1771,7 +1849,7 @@ class EditTimeslotsTests(TestCase): meeting = self.create_meeting() # add some timeslots times = [datetime.time(hour=h) for h in (11, 14)] - days = [meeting.get_meeting_date(ii).date() for ii in range(meeting.days)] + days = [meeting.get_meeting_date(ii) for ii in range(meeting.days)] timeslots = [] duration = datetime.timedelta(minutes=90) @@ -1781,7 +1859,7 @@ class EditTimeslotsTests(TestCase): TimeSlotFactory( meeting=meeting, location=room, - time=datetime.datetime.combine(day, t), + time=meeting.tz().localize(datetime.datetime.combine(day, t)), duration=duration, ) for t in times @@ -1862,17 +1940,21 @@ class EditTimeslotsTests(TestCase): TimeSlotFactory( meeting=meeting, location=meeting.room_set.first(), - time=datetime.datetime.combine( - meeting.get_meeting_date(day).date(), - datetime.time(hour=11) + time=meeting.tz().localize( + datetime.datetime.combine( + meeting.get_meeting_date(day), + datetime.time(hour=11), + ) ), ) TimeSlotFactory( meeting=meeting, location=meeting.room_set.first(), - time=datetime.datetime.combine( - meeting.get_meeting_date(day).date(), - datetime.time(hour=14) + time=meeting.tz().localize( + datetime.datetime.combine( + meeting.get_meeting_date(day), + datetime.time(hour=14), + ) ), ) @@ -1971,10 +2053,8 @@ class EditTimeslotsTests(TestCase): name_before = 'Name Classic (tm)' type_before = 'regular' - time_before = datetime.datetime.combine( - meeting.date, - datetime.time(hour=10), - ) + time_utc = pytz.utc.localize(datetime.datetime.combine(meeting.date, datetime.time(hour=10))) + time_before = time_utc.astimezone(meeting.tz()) duration_before = datetime.timedelta(minutes=60) show_location_before = True location_before = meeting.room_set.first() @@ -1991,7 +2071,7 @@ class EditTimeslotsTests(TestCase): self.login() name_after = 'New Name (tm)' type_after = 'plenary' - time_after = time_before + datetime.timedelta(days=1, hours=2) + time_after = (time_utc + datetime.timedelta(days=1, hours=2)).astimezone(meeting.tz()) duration_after = duration_before * 2 show_location_after = False location_after = meeting.room_set.last() @@ -2171,8 +2251,8 @@ class EditTimeslotsTests(TestCase): ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1 self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) - self.assertEqual(str(ts.time.date().toordinal()), post_data['days']) - self.assertEqual(ts.time.strftime('%H:%M'), post_data['time']) + self.assertEqual(str(ts.local_start_time().date().toordinal()), post_data['days']) + self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) self.assertEqual(str(ts.location.pk), post_data['locations']) @@ -2181,7 +2261,7 @@ class EditTimeslotsTests(TestCase): """Creating a single timeslot outside the official meeting days should work""" meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) - other_date = meeting.get_meeting_date(-7).date() + other_date = meeting.get_meeting_date(-7) post_data = dict( name='some name', type='regular', @@ -2204,8 +2284,8 @@ class EditTimeslotsTests(TestCase): ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1 self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) - self.assertEqual(ts.time.date(), other_date) - self.assertEqual(ts.time.strftime('%H:%M'), post_data['time']) + self.assertEqual(ts.local_start_time().date(), other_date) + self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) self.assertEqual(str(ts.location.pk), post_data['locations']) @@ -2419,8 +2499,8 @@ class EditTimeslotsTests(TestCase): """Creating multiple timeslots should work""" meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) - days = [meeting.get_meeting_date(n).date() for n in range(meeting.days)] - other_date = meeting.get_meeting_date(-1).date() # date before start of meeting + days = [meeting.get_meeting_date(n) for n in range(meeting.days)] + other_date = meeting.get_meeting_date(-1) # date before start of meeting self.assertNotIn(other_date, days) locations = meeting.room_set.all() post_data = dict( @@ -2450,10 +2530,10 @@ class EditTimeslotsTests(TestCase): for ts in meeting.timeslot_set.exclude(pk__in=timeslots_before): self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) - self.assertEqual(ts.time.strftime('%H:%M'), post_data['time']) + self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) - self.assertIn(ts.time.date(), days) + self.assertIn(ts.local_start_time().date(), days) self.assertIn(ts.location, locations) self.assertIn((ts.time.date(), ts.location), day_locs, 'Duplicated day / location found') @@ -2593,7 +2673,7 @@ class ReorderSlidesTests(TestCase): def test_add_slides_to_session(self): for type_id in ('ietf','interim'): chair_role = RoleFactory(name_id='chair') - session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90), meeting__type_id=type_id) + session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90), meeting__type_id=type_id) slides = DocumentFactory(type_id='slides') url = urlreverse('ietf.meeting.views.ajax_add_slides_to_session', kwargs={'session_id':session.pk, 'num':session.meeting.number}) @@ -2609,7 +2689,7 @@ class ReorderSlidesTests(TestCase): self.assertEqual(r.status_code, 403) self.assertIn('materials cutoff', unicontent(r)) - session.meeting.date = datetime.date.today() + session.meeting.date = date_today() session.meeting.save() # Invalid order @@ -2696,7 +2776,7 @@ class ReorderSlidesTests(TestCase): def test_remove_slides_from_session(self): for type_id in ['ietf','interim']: chair_role = RoleFactory(name_id='chair') - session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90), meeting__type_id=type_id) + session = SessionFactory(group=chair_role.group, meeting__date=date_today()-datetime.timedelta(days=90), meeting__type_id=type_id) slides = DocumentFactory(type_id='slides') url = urlreverse('ietf.meeting.views.ajax_remove_slides_from_session', kwargs={'session_id':session.pk, 'num':session.meeting.number}) @@ -2712,7 +2792,7 @@ class ReorderSlidesTests(TestCase): self.assertEqual(r.status_code, 403) self.assertIn('materials cutoff', unicontent(r)) - session.meeting.date = datetime.date.today() + session.meeting.date = date_today() session.meeting.save() # Invalid order @@ -2807,7 +2887,7 @@ class ReorderSlidesTests(TestCase): def test_reorder_slides_in_session(self): chair_role = RoleFactory(name_id='chair') - session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90)) + session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90)) sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session) for num, sp in enumerate(sp_list, start=1): sp.order = num @@ -2817,7 +2897,7 @@ class ReorderSlidesTests(TestCase): for type_id in ['ietf','interim']: session.meeting.type_id = type_id - session.meeting.date = datetime.date.today()-datetime.timedelta(days=90) + session.meeting.date = date_today()-datetime.timedelta(days=90) session.meeting.save() # Not a valid user @@ -2832,7 +2912,7 @@ class ReorderSlidesTests(TestCase): self.assertEqual(r.status_code, 403) self.assertIn('materials cutoff', unicontent(r)) - session.meeting.date = datetime.date.today() + session.meeting.date = date_today() session.meeting.save() # Bad index values @@ -2899,7 +2979,7 @@ class ReorderSlidesTests(TestCase): def test_slide_order_reconditioning(self): chair_role = RoleFactory(name_id='chair') - session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90)) + session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90)) sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session) for num, sp in enumerate(sp_list, start=1): sp.order = 2*num @@ -2918,7 +2998,7 @@ class EditTests(TestCase): def test_official_record_schedule_is_read_only(self): def _set_date_offset_and_retrieve_page(meeting, days_offset, client): - meeting.date = datetime.date.today() + datetime.timedelta(days=days_offset) + meeting.date = date_today() + datetime.timedelta(days=days_offset) meeting.save() client.login(username="secretary", password="secretary+password") url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number)) @@ -3001,7 +3081,9 @@ class EditTests(TestCase): room = Room.objects.get(meeting=meeting, session_types='regular') base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=50), - time=datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30))) + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30)) + )) timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time')) @@ -3223,7 +3305,12 @@ class EditTests(TestCase): self.assertIn("#scroll=1234", r['Location']) test_timeslot = TimeSlot.objects.get(meeting=meeting, name="IETF Testing") - self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(8, 30))) + self.assertEqual( + test_timeslot.time, + meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(8, 30)) + ), + ) self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1, minutes=30)) self.assertEqual(test_timeslot.location_id, break_room.pk) self.assertEqual(test_timeslot.show_location, True) @@ -3265,7 +3352,12 @@ class EditTests(TestCase): }) self.assertNoFormPostErrors(r) test_timeslot.refresh_from_db() - self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(9, 30))) + self.assertEqual( + test_timeslot.time, + meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(9, 30)) + ), + ) self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1)) self.assertEqual(test_timeslot.location_id, breakfast_room.pk) self.assertEqual(test_timeslot.show_location, False) @@ -3710,7 +3802,7 @@ class SessionDetailsTests(TestCase): def test_session_details(self): group = GroupFactory.create(type_id='wg',state_id='active') - session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=datetime.date.today()+datetime.timedelta(days=90)) + session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90)) SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None) SessionPresentationFactory.create(session=session,document__type_id='minutes') SessionPresentationFactory.create(session=session,document__type_id='slides') @@ -3729,7 +3821,7 @@ class SessionDetailsTests(TestCase): session = SessionFactory.create( meeting__type_id='ietf', group=group, - meeting__date=datetime.date.today() + datetime.timedelta(days=90), + meeting__date=date_today() + datetime.timedelta(days=90), ) session_details_url = urlreverse( 'ietf.meeting.views.session_details', @@ -3781,7 +3873,7 @@ class SessionDetailsTests(TestCase): def test_session_details_past_interim(self): group = GroupFactory.create(type_id='wg',state_id='active') chair = RoleFactory(name_id='chair',group=group) - session = SessionFactory.create(meeting__type_id='interim',group=group, meeting__date=datetime.date.today()-datetime.timedelta(days=90)) + session = SessionFactory.create(meeting__type_id='interim',group=group, meeting__date=date_today() - datetime.timedelta(days=90)) SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None) SessionPresentationFactory.create(session=session,document__type_id='minutes') SessionPresentationFactory.create(session=session,document__type_id='slides') @@ -3800,7 +3892,7 @@ class SessionDetailsTests(TestCase): group = GroupFactory.create(type_id='wg',state_id='active') group_chair = PersonFactory.create() group.role_set.create(name_id='chair',person = group_chair, email = group_chair.email()) - session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=datetime.date.today()+datetime.timedelta(days=90)) + session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90)) SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None) old_draft = session.sessionpresentation_set.filter(document__type='draft').first().document new_draft = DocumentFactory(type_id='draft') @@ -3966,7 +4058,7 @@ class InterimTests(TestCase): def do_interim_skip_announcement_test(self, base_session=False, extra_session=False, canceled_session=False): make_meeting_test_data() group = Group.objects.get(acronym='irg') - date = datetime.date.today() + datetime.timedelta(days=30) + date = date_today() + datetime.timedelta(days=30) meeting = make_interim_meeting(group=group, date=date, status='scheda') session = meeting.session_set.first() if base_session: @@ -4036,12 +4128,10 @@ class InterimTests(TestCase): self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True, base_session=True) def do_interim_send_announcement_test(self, base_session=False, extra_session=False, canceled_session=False): - make_interim_test_data() + make_interim_test_data(meeting_tz='America/Los_Angeles') session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first() meeting = session.meeting - meeting.time_zone = 'America/Los_Angeles' - meeting.save() if base_session: base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False) @@ -4247,7 +4337,7 @@ class InterimTests(TestCase): self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True, base_session=True) def test_past(self): - today = datetime.date.today() + today = date_today() last_week = today - datetime.timedelta(days=7) ietf = SessionFactory(meeting__type_id='ietf',meeting__date=last_week,group__state_id='active',group__parent=GroupFactory(state_id='active')) SessionFactory(meeting__type_id='interim',meeting__date=last_week,status_id='canceled',group__state_id='active',group__parent=GroupFactory(state_id='active')) @@ -4266,7 +4356,7 @@ class InterimTests(TestCase): if querystring is not None: url += '?' + querystring - today = datetime.date.today() + today = date_today() interims = dict( mars=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting, ames=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting, @@ -4386,9 +4476,9 @@ class InterimTests(TestCase): def do_interim_request_single_virtual(self, emails_expected): make_meeting_test_data() group = Group.objects.get(acronym='mars') - date = datetime.date.today() + datetime.timedelta(days=30) - time = datetime.datetime.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) + date = date_today() + datetime.timedelta(days=30) + time = time_now().replace(microsecond=0,second=0) + dt = pytz.utc.localize(datetime.datetime.combine(date, time)) duration = datetime.timedelta(hours=3) remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' @@ -4457,13 +4547,14 @@ class InterimTests(TestCase): def test_interim_request_single_in_person(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') - date = datetime.date.today() + datetime.timedelta(days=30) - time = datetime.datetime.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) + date = date_today() + datetime.timedelta(days=30) + time = time_now().replace(microsecond=0,second=0) + time_zone = 'America/Los_Angeles' + tz = pytz.timezone(time_zone) + dt = tz.localize(datetime.datetime.combine(date, time)) duration = datetime.timedelta(hours=3) city = 'San Francisco' country = 'US' - time_zone = 'America/Los_Angeles' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' @@ -4504,16 +4595,17 @@ class InterimTests(TestCase): def test_interim_request_multi_day(self): make_meeting_test_data() - date = datetime.date.today() + datetime.timedelta(days=30) + date = date_today() + datetime.timedelta(days=30) date2 = date + datetime.timedelta(days=1) - time = datetime.datetime.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) - dt2 = datetime.datetime.combine(date2, time) + time = time_now().replace(microsecond=0,second=0) + time_zone = 'America/Los_Angeles' + tz = pytz.timezone(time_zone) + dt = tz.localize(datetime.datetime.combine(date, time)) + dt2 = tz.localize(datetime.datetime.combine(date2, time)) duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = 'San Francisco' country = 'US' - time_zone = 'America/Los_Angeles' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' @@ -4570,9 +4662,9 @@ class InterimTests(TestCase): def test_interim_request_multi_day_non_consecutive(self): make_meeting_test_data() - date = datetime.date.today() + datetime.timedelta(days=30) + date = date_today() + datetime.timedelta(days=30) date2 = date + datetime.timedelta(days=2) - time = datetime.datetime.now().time().replace(microsecond=0,second=0) + time = timezone.now().time().replace(microsecond=0,second=0) group = Group.objects.get(acronym='mars') city = 'San Francisco' country = 'US' @@ -4607,7 +4699,7 @@ class InterimTests(TestCase): def test_interim_request_multi_day_cancel(self): """All sessions of a multi-day interim request should be canceled""" length_before = len(outbox) - date = datetime.date.today()+datetime.timedelta(days=15) + date = date_today() + datetime.timedelta(days=15) # Set up an interim request with several sessions num_sessions = 3 @@ -4630,7 +4722,7 @@ class InterimTests(TestCase): def test_interim_request_series(self): make_meeting_test_data() meeting_count_before = Meeting.objects.filter(type='interim').count() - date = datetime.date.today() + datetime.timedelta(days=30) + date = date_today() + datetime.timedelta(days=30) if (date.month, date.day) == (12, 31): # Avoid date and date2 in separate years # (otherwise the test will fail if run on December 1st) @@ -4640,14 +4732,15 @@ class InterimTests(TestCase): if date.year != date2.year: date += datetime.timedelta(days=1) date2 += datetime.timedelta(days=1) - time = datetime.datetime.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) - dt2 = datetime.datetime.combine(date2, time) + time = time_now().replace(microsecond=0,second=0) + time_zone = 'America/Los_Angeles' + tz = pytz.timezone(time_zone) + dt = tz.localize(datetime.datetime.combine(date, time)) + dt2 = tz.localize(datetime.datetime.combine(date2, time)) duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = '' country = '' - time_zone = 'America/Los_Angeles' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' @@ -4789,14 +4882,14 @@ class InterimTests(TestCase): self.assertFalse(can_manage_group(user=user,group=group)) def test_interim_request_details(self): - make_interim_test_data() + make_interim_test_data(meeting_tz='America/Chicago') meeting = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) - start_time = meeting.session_set.first().official_timeslotassignment().timeslot.time.strftime('%H:%M') + start_time = meeting.session_set.first().official_timeslotassignment().timeslot.local_start_time().strftime('%H:%M') utc_start_time = meeting.session_set.first().official_timeslotassignment().timeslot.utc_start_time().strftime('%H:%M') self.assertIn(start_time, unicontent(r)) self.assertIn(utc_start_time, unicontent(r)) @@ -4804,7 +4897,7 @@ class InterimTests(TestCase): def test_interim_request_details_announcement(self): '''Test access to Announce / Skip Announce features''' make_meeting_test_data() - date = datetime.date.today() + datetime.timedelta(days=30) + date = date_today() + datetime.timedelta(days=30) group = Group.objects.get(acronym='mars') meeting = make_interim_meeting(group=group, date=date, status='scheda') url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) @@ -5322,7 +5415,7 @@ class InterimTests(TestCase): def test_send_interim_minutes_reminder(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') - date = datetime.datetime.today() - datetime.timedelta(days=10) + date = timezone.now() - datetime.timedelta(days=10) meeting = make_interim_meeting(group=group, date=date, status='sched') length_before = len(outbox) send_interim_minutes_reminder(meeting=meeting) @@ -5337,7 +5430,11 @@ class InterimTests(TestCase): a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session - t2 = TimeSlotFactory.create(meeting=meeting, time=datetime.datetime.combine(meeting.date, datetime.time(11, 30))) + t2 = TimeSlotFactory.create( + meeting=meeting, + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(11, 30)) + )) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # @@ -5347,8 +5444,8 @@ class InterimTests(TestCase): self.assertContains(r, 'BEGIN:VEVENT') self.assertEqual(r.content.count(b'UID'), 2) self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group') - self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) - self.assertContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) self.assertContains(r, 'END:VEVENT') # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, }) @@ -5539,7 +5636,7 @@ class MaterialsTests(TestCase): def test_upload_bluesheets_interim_chair_access(self): make_meeting_test_data() mars = Group.objects.get(acronym='mars') - session=SessionFactory(meeting__type_id='interim',group=mars, meeting__date = datetime.date.today()) + session=SessionFactory(meeting__type_id='interim',group=mars, meeting__date = date_today()) url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id}) self.client.login(username="marschairman", password="marschairman+password") r = self.client.get(url) @@ -5782,7 +5879,7 @@ class MaterialsTests(TestCase): for type_id in ['ietf','interim']: session = SessionFactory(meeting__type_id=type_id) chair = RoleFactory(group=session.group,name_id='chair').person - session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) + session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) newperson = PersonFactory() session_overview_url = urlreverse('ietf.meeting.views.session_details',kwargs={'num':session.meeting.number,'acronym':session.group.acronym}) @@ -5830,7 +5927,7 @@ class MaterialsTests(TestCase): def test_disapprove_proposed_slides(self): submission = SlideSubmissionFactory() - submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) + submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 1) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) @@ -5848,7 +5945,7 @@ class MaterialsTests(TestCase): def test_approve_proposed_slides(self): submission = SlideSubmissionFactory() session = submission.session - session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) + session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) @@ -5873,7 +5970,7 @@ class MaterialsTests(TestCase): submission = SlideSubmissionFactory(session__meeting__type_id='ietf') session1 = submission.session session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting) - submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) + submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) @@ -5890,7 +5987,7 @@ class MaterialsTests(TestCase): submission = SlideSubmissionFactory(session__meeting__type_id='ietf') session1 = submission.session session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting) - submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) + submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) @@ -5904,7 +6001,7 @@ class MaterialsTests(TestCase): def test_submit_and_approve_multiple_versions(self): session = SessionFactory(meeting__type_id='ietf') chair = RoleFactory(group=session.group,name_id='chair').person - session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) + session.meeting.importantdate_set.create(name_id='revsub',date=date_today()+datetime.timedelta(days=20)) newperson = PersonFactory() propose_url = urlreverse('ietf.meeting.views.propose_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number}) @@ -6285,8 +6382,8 @@ class HasMeetingsTests(TestCase): self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('#id_group option[value="%d"]'%group.pk)) - date = datetime.date.today() + datetime.timedelta(days=30+meeting_count) - time = datetime.datetime.now().time().replace(microsecond=0,second=0) + date = date_today() + datetime.timedelta(days=30+meeting_count) + time = time_now().replace(microsecond=0,second=0) remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' @@ -6382,7 +6479,7 @@ class HasMeetingsTests(TestCase): session = SessionFactory( group__type_id = gf.type_id, meeting__type_id='interim', - meeting__date = datetime.datetime.today()+datetime.timedelta(days=30), + meeting__date = timezone.now()+datetime.timedelta(days=30), status_id='sched', ) sessions.append(session) @@ -6398,7 +6495,7 @@ class HasMeetingsTests(TestCase): sessions=[] for gf in GroupFeatures.objects.filter(has_meetings=True): group = GroupFactory(type_id=gf.type_id) - meeting_date = datetime.datetime.today() + datetime.timedelta(days=30) + meeting_date = timezone.now() + datetime.timedelta(days=30) session = SessionFactory( group=group, meeting__type_id='interim', @@ -6419,7 +6516,7 @@ class HasMeetingsTests(TestCase): sessions=[] for gf in GroupFeatures.objects.filter(has_meetings=True): group = GroupFactory(type_id=gf.type_id) - meeting_date = datetime.datetime.today() + datetime.timedelta(days=30) + meeting_date = timezone.now() + datetime.timedelta(days=30) session = SessionFactory( group=group, meeting__type_id='interim', @@ -7145,7 +7242,7 @@ class ProceedingsTests(BaseMeetingTestCase): def test_proceedings_no_agenda(self): # Meeting number must be larger than the last special-cased proceedings (currently 96) - meeting = MeetingFactory(type_id='ietf',populate_schedule=False,date=datetime.date.today(), number='100') + meeting = MeetingFactory(type_id='ietf',populate_schedule=False,date=date_today(), number='100') url = urlreverse('ietf.meeting.views.proceedings') r = self.client.get(url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials')) @@ -7254,7 +7351,7 @@ class ProceedingsTests(BaseMeetingTestCase): """Generate a meeting for proceedings material test""" # meeting number 123 avoids various legacy cases that affect these tests # (as of Aug 2021, anything above 96 is probably ok) - return MeetingFactory(type_id='ietf', number='123', date=datetime.date.today()) + return MeetingFactory(type_id='ietf', number='123', date=date_today()) def _secretary_only_permission_test(self, url, include_post=True): self.client.logout() diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index aae286682..e8efb92ad 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import datetime import itertools +import pytz import requests import subprocess @@ -12,6 +13,7 @@ from urllib.error import HTTPError from django.conf import settings from django.contrib import messages from django.template.loader import render_to_string +from django.utils import timezone from django.utils.encoding import smart_text import debug # pyflakes:ignore @@ -26,6 +28,7 @@ from ietf.person.models import Person from ietf.secr.proceedings.proc_utils import import_audio_files from ietf.utils.html import sanitize_document from ietf.utils.log import log +from ietf.utils.timezone import date_today def session_time_for_sorting(session, use_meeting_date): @@ -33,13 +36,17 @@ def session_time_for_sorting(session, use_meeting_date): if official_timeslot: return official_timeslot.time elif use_meeting_date and session.meeting.date: - return datetime.datetime.combine(session.meeting.date, datetime.time.min) + return session.meeting.tz().localize( + datetime.datetime.combine(session.meeting.date, datetime.time.min) + ) else: first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first() if first_event: return first_event.time else: - return datetime.datetime.min + # n.b. cannot interpret this in timezones west of UTC. That is not expected to be necessary, + # but could probably safely add a day to the minimum datetime to make that possible. + return pytz.utc.localize(datetime.datetime.min) def session_requested_by(session): first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first() @@ -64,12 +71,12 @@ def group_sessions(sessions): sessions = sorted(sessions,key=lambda s:s.time) - today = datetime.date.today() future = [] in_progress = [] recent = [] past = [] for s in sessions: + today = date_today(s.meeting.tz()) if s.meeting.date > today: future.append(s) elif s.meeting.end_date() >= today: @@ -101,7 +108,7 @@ def get_upcoming_manageable_sessions(user): # .filter(date__gte=today - F('days')), but unfortunately, it # doesn't work correctly with Django 1.11 and MySQL/SQLite - today = datetime.date.today() + today = date_today() candidate_sessions = add_event_info_to_session_qs( Session.objects.filter(meeting__date__gte=today - datetime.timedelta(days=15)) @@ -158,7 +165,12 @@ def create_proceedings_templates(meeting): def finalize(meeting): end_date = meeting.end_date() - end_time = datetime.datetime.combine(end_date, datetime.datetime.min.time())+datetime.timedelta(days=1) + end_time = meeting.tz().localize( + datetime.datetime.combine( + end_date, + datetime.time.min, + ) + ).astimezone(pytz.utc) + datetime.timedelta(days=1) for session in meeting.session_set.all(): for sp in session.sessionpresentation_set.filter(document__type='draft',rev=None): rev_before_end = [e for e in sp.document.docevent_set.filter(newrevisiondocevent__isnull=False).order_by('-time') if e.time <= end_time ] @@ -322,7 +334,9 @@ def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions): # synthesize AD constraints - we can treat them as a special kind of 'bethere' responsible_ad_for_group = {} session_groups = set(s.group for s in sessions if s.group and s.group.parent and s.group.parent.type_id == 'area') - meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0)) + meeting_time = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0)) + ) # dig up historic AD names for group_id, history_time, pk in Person.objects.filter(rolehistory__name='ad', rolehistory__group__group__in=session_groups, rolehistory__group__time__lte=meeting_time).values_list('rolehistory__group__group', 'rolehistory__group__time', 'pk').order_by('rolehistory__group__time'): @@ -511,7 +525,7 @@ def swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, targe if max_overlap > datetime.timedelta(minutes=5): for a in lts_assignments: a.timeslot = most_overlapping_rts - a.modified = datetime.datetime.now() + a.modified = timezone.now() a.save() swapped = True diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index a50ba1ad9..c9e22cdf1 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -36,10 +36,10 @@ from django.db.models import F, Max, Q from django.forms.models import modelform_factory, inlineformset_factory from django.template import TemplateDoesNotExist from django.template.loader import render_to_string +from django.utils import timezone from django.utils.encoding import force_str from django.utils.functional import curry from django.utils.text import slugify -from django.utils.timezone import now from django.views.decorators.cache import cache_page from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt from django.views.generic import RedirectView @@ -97,6 +97,7 @@ from ietf.utils.pipe import pipe from ietf.utils.pdf import pdf_pages from ietf.utils.response import permission_denied from ietf.utils.text import xslugify +from ietf.utils.timezone import datetime_today, date_today from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm, InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm, @@ -128,22 +129,23 @@ def materials(request, num=None): begin_date = meeting.get_submission_start_date() cut_off_date = meeting.get_submission_cut_off_date() cor_cut_off_date = meeting.get_submission_correction_date() - now = datetime.date.today() - old = datetime.datetime.now() - datetime.timedelta(days=1) + now = date_today() + old = timezone.now() - datetime.timedelta(days=1) if settings.SERVER_MODE != 'production' and '_testoverride' in request.GET: pass elif now > cor_cut_off_date: if meeting.number.isdigit() and int(meeting.number) > 96: return redirect('ietf.meeting.views.proceedings', num=meeting.number) else: - return render(request, "meeting/materials_upload_closed.html", { - 'meeting_num': meeting.number, - 'begin_date': begin_date, - 'cut_off_date': cut_off_date, - 'cor_cut_off_date': cor_cut_off_date - }) + with timezone.override(meeting.tz()): + return render(request, "meeting/materials_upload_closed.html", { + 'meeting_num': meeting.number, + 'begin_date': begin_date, + 'cut_off_date': cut_off_date, + 'cor_cut_off_date': cor_cut_off_date + }) - past_cutoff_date = datetime.date.today() > meeting.get_submission_correction_date() + past_cutoff_date = date_today() > meeting.get_submission_correction_date() schedule = get_schedule(meeting, None) @@ -177,23 +179,24 @@ def materials(request, num=None): for type_name in ProceedingsMaterialTypeName.objects.all() ] - return render(request, "meeting/materials.html", { - 'meeting': meeting, - 'proceedings_materials': proceedings_materials, - 'plenaries': plenaries, - 'ietf': ietf, - 'training': training, - 'irtf': irtf, - 'iab': iab, - 'other': other, - 'cut_off_date': cut_off_date, - 'cor_cut_off_date': cor_cut_off_date, - 'submission_started': now > begin_date, - 'old': old, - }) + with timezone.override(meeting.tz()): + return render(request, "meeting/materials.html", { + 'meeting': meeting, + 'proceedings_materials': proceedings_materials, + 'plenaries': plenaries, + 'ietf': ietf, + 'training': training, + 'irtf': irtf, + 'iab': iab, + 'other': other, + 'cut_off_date': cut_off_date, + 'cor_cut_off_date': cor_cut_off_date, + 'submission_started': now > begin_date, + 'old': old, + }) def current_materials(request): - today = datetime.date.today() + today = date_today() meetings = Meeting.objects.exclude(number__startswith='interim-').filter(date__lte=today).order_by('-date') if meetings: return redirect(materials, meetings[0].number) @@ -282,60 +285,61 @@ def materials_editable_groups(request, num=None): def edit_timeslots(request, num=None): meeting = get_meeting(num) + with timezone.override(meeting.tz()): + if request.method == 'POST': + # handle AJAX requests + action = request.POST.get('action') + if action == 'delete': + # delete a timeslot + # Parameters: + # slot_id: comma-separated list of TimeSlot PKs to delete + slot_id = request.POST.get('slot_id') + if slot_id is None: + return HttpResponseBadRequest('missing slot_id') + slot_ids = [id.strip() for id in slot_id.split(',')] + try: + timeslots = meeting.timeslot_set.filter(pk__in=slot_ids) + except ValueError: + return HttpResponseBadRequest('invalid slot_id specification') + missing_ids = set(slot_ids).difference(str(ts.pk) for ts in timeslots) + if len(missing_ids) != 0: + return HttpResponseNotFound('TimeSlot ids not found in meeting {}: {}'.format( + meeting.number, + ', '.join(sorted(missing_ids)) + )) + timeslots.delete() + return HttpResponse(content='; '.join('Deleted TimeSlot {}'.format(id) for id in slot_ids)) + else: + return HttpResponseBadRequest('unknown action') - if request.method == 'POST': - # handle AJAX requests - action = request.POST.get('action') - if action == 'delete': - # delete a timeslot - # Parameters: - # slot_id: comma-separated list of TimeSlot PKs to delete - slot_id = request.POST.get('slot_id') - if slot_id is None: - return HttpResponseBadRequest('missing slot_id') - slot_ids = [id.strip() for id in slot_id.split(',')] - try: - timeslots = meeting.timeslot_set.filter(pk__in=slot_ids) - except ValueError: - return HttpResponseBadRequest('invalid slot_id specification') - missing_ids = set(slot_ids).difference(str(ts.pk) for ts in timeslots) - if len(missing_ids) != 0: - return HttpResponseNotFound('TimeSlot ids not found in meeting {}: {}'.format( - meeting.number, - ', '.join(sorted(missing_ids)) - )) - timeslots.delete() - return HttpResponse(content='; '.join('Deleted TimeSlot {}'.format(id) for id in slot_ids)) - else: - return HttpResponseBadRequest('unknown action') + # Labels here differ from those in the build_timeslices() method. The labels here are + # relative to the table: time_slices are the row headings (ie, days), date_slices are + # the column headings (i.e., time intervals), and slots are the per-day list of timeslots + # (with only one timeslot per unique time/duration) + time_slices, date_slices, slots = meeting.build_timeslices() - # Labels here differ from those in the build_timeslices() method. The labels here are - # relative to the table: time_slices are the row headings (ie, days), date_slices are - # the column headings (i.e., time intervals), and slots are the per-day list of timeslots - # (with only one timeslot per unique time/duration) - time_slices, date_slices, slots = meeting.build_timeslices() + ts_list = deque() + rooms = meeting.room_set.order_by("capacity","name","id") + for room in rooms: + for day in time_slices: + for slice in date_slices[day]: + ts_list.append(room.timeslot_set.filter(time=slice[0],duration=datetime.timedelta(seconds=slice[2]))) - ts_list = deque() - rooms = meeting.room_set.order_by("capacity","name","id") - for room in rooms: - for day in time_slices: - for slice in date_slices[day]: - ts_list.append(room.timeslot_set.filter(time=slice[0],duration=datetime.timedelta(seconds=slice[2]))) + # Grab these in one query each to identify sessions that are in use and should be handled with care + ts_with_official_assignments = meeting.timeslot_set.filter(sessionassignments__schedule=meeting.schedule) + ts_with_any_assignments = meeting.timeslot_set.filter(sessionassignments__isnull=False) + + return render(request, "meeting/timeslot_edit.html", + {"rooms":rooms, + "time_slices":time_slices, + "slot_slices": slots, + "date_slices":date_slices, + "meeting":meeting, + "ts_list":ts_list, + "ts_with_official_assignments": ts_with_official_assignments, + "ts_with_any_assignments": ts_with_any_assignments, + }) - # Grab these in one query each to identify sessions that are in use and should be handled with care - ts_with_official_assignments = meeting.timeslot_set.filter(sessionassignments__schedule=meeting.schedule) - ts_with_any_assignments = meeting.timeslot_set.filter(sessionassignments__isnull=False) - - return render(request, "meeting/timeslot_edit.html", - {"rooms":rooms, - "time_slices":time_slices, - "slot_slices": slots, - "date_slices":date_slices, - "meeting":meeting, - "ts_list":ts_list, - "ts_with_official_assignments": ts_with_official_assignments, - "ts_with_any_assignments": ts_with_any_assignments, - }) class NewScheduleForm(forms.ModelForm): class Meta: @@ -443,9 +447,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): lock_time = settings.MEETING_SESSION_LOCK_TIME def timeslot_locked(ts): - meeting_now = now().astimezone(pytz.timezone(meeting.time_zone)) - if not settings.USE_TZ: - meeting_now = meeting_now.replace(tzinfo=None) + meeting_now = timezone.now().astimezone(meeting.tz()) return schedule.is_official and (ts.time - meeting_now < lock_time) if not can_see: @@ -642,7 +644,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): rd = room_data[t.location_id] rd['timeslot_count'] += 1 rd['start_and_duration'].append((t.time, t.duration)) - ttd = t.time.date() + ttd = t.local_start_time().date() # date in meeting timezone all_days.add(ttd) if ttd not in rd['timeslots_by_day']: rd['timeslots_by_day'][ttd] = [] @@ -786,7 +788,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): timeslot=old_timeslot, ) - existing_assignments.update(timeslot=timeslot, modified=datetime.datetime.now()) + existing_assignments.update(timeslot=timeslot, modified=timezone.now()) else: SchedTimeSessAssignment.objects.create( session=session, @@ -827,8 +829,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): source_day = swap_days_form.cleaned_data['source_day'] target_day = swap_days_form.cleaned_data['target_day'] - source_timeslots = [ts for ts in timeslots_qs if ts.time.date() == source_day] - target_timeslots = [ts for ts in timeslots_qs if ts.time.date() == target_day] + source_timeslots = [ts for ts in timeslots_qs if ts.local_start_time().date() == source_day] + target_timeslots = [ts for ts in timeslots_qs if ts.local_start_time().date() == target_day] if any(timeslot_locked(ts) for ts in source_timeslots + target_timeslots): return HttpResponseBadRequest("Can't swap these days.") @@ -885,8 +887,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): # possible timeslot start/ends timeslot_groups = defaultdict(set) for ts in timeslots_qs: - ts.start_end_group = "ts-group-{}-{}".format(ts.time.strftime("%Y%m%d-%H%M"), int(ts.duration.total_seconds() / 60)) - timeslot_groups[ts.time.date()].add((ts.time, ts.end_time(), ts.start_end_group)) + ts.start_end_group = "ts-group-{}-{}".format(ts.local_start_time().strftime("%Y%m%d-%H%M"), int(ts.duration.total_seconds() / 60)) + timeslot_groups[ts.local_start_time().date()].add((ts.local_start_time(), ts.local_end_time(), ts.start_end_group)) # prepare sessions prepare_sessions_for_display(sessions) @@ -967,22 +969,23 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): if ts_type.slug in session_data.get('enabled_timeslot_types', []) ] - return render(request, "meeting/edit_meeting_schedule.html", { - 'meeting': meeting, - 'schedule': schedule, - 'can_edit': can_edit, - 'can_edit_properties': can_edit or secretariat, - 'secretariat': secretariat, - 'days': days, - 'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()), - 'unassigned_sessions': unassigned_sessions, - 'session_parents': session_parents, - 'session_purposes': session_purposes, - 'timeslot_types': timeslot_types, - 'hide_menu': True, - 'lock_time': lock_time, - 'enabled_timeslot_types': enabled_timeslot_types, - }) + with timezone.override(meeting.tz()): + return render(request, "meeting/edit_meeting_schedule.html", { + 'meeting': meeting, + 'schedule': schedule, + 'can_edit': can_edit, + 'can_edit_properties': can_edit or secretariat, + 'secretariat': secretariat, + 'days': days, + 'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()), + 'unassigned_sessions': unassigned_sessions, + 'session_parents': session_parents, + 'session_purposes': session_purposes, + 'timeslot_types': timeslot_types, + 'hide_menu': True, + 'lock_time': lock_time, + 'enabled_timeslot_types': enabled_timeslot_types, + }) class RoomNameModelChoiceField(forms.ModelChoiceField): @@ -990,7 +993,7 @@ class RoomNameModelChoiceField(forms.ModelChoiceField): return obj.name class TimeSlotForm(forms.Form): - day = forms.TypedChoiceField(coerce=lambda t: datetime.datetime.strptime(t, "%Y-%m-%d").date()) + day = forms.TypedChoiceField(coerce=lambda t: datetime.datetime.strptime(t, "%Y-%m-%d").date()) # all dates, no tz time = forms.TimeField() duration = CustomDurationField() # this is just to make 1:30 turn into 1.5 hours instead of 1.5 minutes location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)") @@ -1030,8 +1033,8 @@ class TimeSlotForm(forms.Form): if timeslot: self.initial = { - 'day': timeslot.time.date(), - 'time': timeslot.time.time(), + 'day': timeslot.local_start_time().date(), + 'time': timeslot.local_start_time().time(), 'duration': timeslot.duration, 'location': timeslot.location_id, 'show_location': timeslot.show_location, @@ -1110,230 +1113,239 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name can_edit = has_role(request.user, 'Secretariat') - if request.method == 'GET' and request.GET.get('action') == "edit-timeslot": - timeslot_pk = request.GET.get('timeslot') - if not timeslot_pk or not timeslot_pk.isdecimal(): - raise Http404 - timeslot = get_object_or_404(timeslot_qs, pk=timeslot_pk) + with timezone.override(meeting.tz()): + if request.method == 'GET' and request.GET.get('action') == "edit-timeslot": + timeslot_pk = request.GET.get('timeslot') + if not timeslot_pk or not timeslot_pk.isdecimal(): + raise Http404 + timeslot = get_object_or_404(timeslot_qs, pk=timeslot_pk) - assigned_session = add_event_info_to_session_qs(Session.objects.filter( - timeslotassignments__schedule__in=[schedule, schedule.base], - timeslotassignments__timeslot=timeslot, - )).first() + assigned_session = add_event_info_to_session_qs(Session.objects.filter( + timeslotassignments__schedule__in=[schedule, schedule.base], + timeslotassignments__timeslot=timeslot, + )).first() - timeslot.can_cancel = not assigned_session or assigned_session.current_status not in ['canceled', 'canceled', 'resched'] + timeslot.can_cancel = not assigned_session or assigned_session.current_status not in ['canceled', 'canceled', 'resched'] - return JsonResponse({ - 'form': render_to_string("meeting/edit_timeslot_form.html", { - 'timeslot_form_action': 'edit', - 'timeslot_form': TimeSlotForm(meeting, schedule, timeslot=timeslot), - 'timeslot': timeslot, - 'schedule': schedule, - 'meeting': meeting, - 'can_edit': can_edit, - }, request=request) - }) + return JsonResponse({ + 'form': render_to_string("meeting/edit_timeslot_form.html", { + 'timeslot_form_action': 'edit', + 'timeslot_form': TimeSlotForm(meeting, schedule, timeslot=timeslot), + 'timeslot': timeslot, + 'schedule': schedule, + 'meeting': meeting, + 'can_edit': can_edit, + }, request=request) + }) - scroll = request.POST.get('scroll') + scroll = request.POST.get('scroll') - def redirect_with_scroll(): - url = request.get_full_path() - if scroll and scroll.isdecimal(): - url += "#scroll={}".format(scroll) - return HttpResponseRedirect(url) + def redirect_with_scroll(): + url = request.get_full_path() + if scroll and scroll.isdecimal(): + url += "#scroll={}".format(scroll) + return HttpResponseRedirect(url) - add_timeslot_form = None - if request.method == 'POST' and request.POST.get('action') == 'add-timeslot' and can_edit: - add_timeslot_form = TimeSlotForm(meeting, schedule, request.POST) - if add_timeslot_form.is_valid(): - c = add_timeslot_form.cleaned_data + add_timeslot_form = None + if request.method == 'POST' and request.POST.get('action') == 'add-timeslot' and can_edit: + add_timeslot_form = TimeSlotForm(meeting, schedule, request.POST) + if add_timeslot_form.is_valid(): + c = add_timeslot_form.cleaned_data - timeslot, created = TimeSlot.objects.get_or_create( - meeting=meeting, - type=c['type'], - name=c['name'], - time=datetime.datetime.combine(c['day'], c['time']), - duration=c['duration'], - location=c['location'], - show_location=c['show_location'], - ) + timeslot, created = TimeSlot.objects.get_or_create( + meeting=meeting, + type=c['type'], + name=c['name'], + time=meeting.tz().localize(datetime.datetime.combine(c['day'], c['time'])), + duration=c['duration'], + location=c['location'], + show_location=c['show_location'], + ) + + if timeslot.type_id != 'regular': + if not created: + Session.objects.filter(timeslotassignments__timeslot=timeslot).delete() + + session = Session.objects.create( + meeting=meeting, + name=c['name'], + short=c['short'], + group=c['group'], + type=c['type'], + purpose=c['purpose'], + agenda_note=c.get('agenda_note') or "", + ) + + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='sched'), + by=request.user.person, + ) + + SchedTimeSessAssignment.objects.create( + timeslot=timeslot, + session=session, + schedule=schedule + ) + + return redirect_with_scroll() + + edit_timeslot_form = None + if request.method == 'POST' and request.POST.get('action') == 'edit-timeslot' and can_edit: + timeslot_pk = request.POST.get('timeslot') + if not timeslot_pk or not timeslot_pk.isdecimal(): + raise Http404 + + timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk) + + edit_timeslot_form = TimeSlotForm(meeting, schedule, request.POST, timeslot=timeslot) + if edit_timeslot_form.is_valid() and edit_timeslot_form.active_assignment.schedule_id == schedule.pk: + + c = edit_timeslot_form.cleaned_data + + timeslot.type = c['type'] + timeslot.name = c['name'] + timeslot.time = meeting.tz().localize(datetime.datetime.combine(c['day'], c['time'])) + timeslot.duration = c['duration'] + timeslot.location = c['location'] + timeslot.show_location = c['show_location'] + timeslot.save() + + session = Session.objects.filter( + timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None], + timeslotassignments__timeslot=timeslot, + ).select_related('group').first() + + if session: + if timeslot.type_id != 'regular': + session.name = c['name'] + session.short = c['short'] + session.group = c['group'] + session.type = c['type'] + session.agenda_note = c.get('agenda_note') or "" + session.save() + + return redirect_with_scroll() + + if request.method == 'POST' and request.POST.get('action') == 'cancel-timeslot' and can_edit: + timeslot_pk = request.POST.get('timeslot') + if not timeslot_pk or not timeslot_pk.isdecimal(): + raise Http404 + + timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk) + if timeslot.type_id != 'break': + sessions = add_event_info_to_session_qs( + Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot), + ).exclude(current_status__in=['canceled', 'resched']) + for session in sessions: + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='canceled'), + by=request.user.person, + ) + + return redirect_with_scroll() + + if request.method == 'POST' and request.POST.get('action') == 'delete-timeslot' and can_edit: + timeslot_pk = request.POST.get('timeslot') + if not timeslot_pk or not timeslot_pk.isdecimal(): + raise Http404 + + timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk) if timeslot.type_id != 'regular': - if not created: - Session.objects.filter(timeslotassignments__timeslot=timeslot).delete() + for session in Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot): + for doc in session.materials.all(): + doc.set_state(State.objects.get(type=doc.type_id, slug='deleted')) + e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='deleted') + e.desc = "Deleted meeting session" + e.save() - session = Session.objects.create( + session.delete() + + timeslot.delete() + + return redirect_with_scroll() + + sessions_by_pk = { + s.pk: s for s in + add_event_info_to_session_qs( + Session.objects.filter( meeting=meeting, - name=c['name'], - short=c['short'], - group=c['group'], - type=c['type'], - purpose=c['purpose'], - agenda_note=c.get('agenda_note') or "", - ) + ).order_by('pk'), + requested_time=True, + requested_by=True, + ).filter( + current_status__in=['appr', 'schedw', 'scheda', 'sched', 'canceled', 'canceledpa', 'resched'] + ).prefetch_related( + 'group', 'group', 'group__type', + ) + } - SchedulingEvent.objects.create( - session=session, - status=SessionStatusName.objects.get(slug='sched'), - by=request.user.person, - ) + assignments_by_timeslot = defaultdict(list) + for a in SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]): + assignments_by_timeslot[a.timeslot_id].append(a) - SchedTimeSessAssignment.objects.create( - timeslot=timeslot, - session=session, - schedule=schedule - ) + days = [meeting.date + datetime.timedelta(days=i) for i in range(meeting.days)] - return redirect_with_scroll() + timeslots_by_day_and_room = defaultdict(list) + for t in timeslot_qs: + timeslots_by_day_and_room[(t.time.date(), t.location_id)].append(t) - edit_timeslot_form = None - if request.method == 'POST' and request.POST.get('action') == 'edit-timeslot' and can_edit: - timeslot_pk = request.POST.get('timeslot') - if not timeslot_pk or not timeslot_pk.isdecimal(): - raise Http404 + # Calculate full time range for display in meeting-local time, always showing at least 8am to 10pm + min_time = min([t.local_start_time().time() for t in timeslot_qs] + [datetime.time(8)]) + max_time = max([t.local_end_time().time() for t in timeslot_qs] + [datetime.time(22)]) + min_max_delta = datetime.datetime.combine(meeting.date, max_time) - datetime.datetime.combine(meeting.date, min_time) - timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk) + day_grid = [] + for d in days: + room_timeslots = [] + for r in rooms: + ts = [] + for t in timeslots_by_day_and_room.get((d, r.pk), []): + # FIXME: the database (as of 2020) contains spurious + # regular timeslots in rooms not intended for regular + # sessions - once those are gone, this filter can go + # away + if t.type_id == 'regular' and not any(t.slug == 'regular' for t in r.session_types.all()): + continue - edit_timeslot_form = TimeSlotForm(meeting, schedule, request.POST, timeslot=timeslot) - if edit_timeslot_form.is_valid() and edit_timeslot_form.active_assignment.schedule_id == schedule.pk: + t.assigned_sessions = [] + for a in assignments_by_timeslot.get(t.pk, []): + s = sessions_by_pk.get(a.session_id) + if s: + t.assigned_sessions.append(s) - c = edit_timeslot_form.cleaned_data + local_start_dt = t.local_start_time() + local_min_dt = local_start_dt.replace( + hour=min_time.hour, + minute=min_time.minute, + second=min_time.second, + microsecond=min_time.microsecond, + ) + t.left_offset = 100.0 * (local_start_dt - local_min_dt) / min_max_delta + t.layout_width = min(100.0 * t.duration / min_max_delta, 100 - t.left_offset) + ts.append(t) - timeslot.type = c['type'] - timeslot.name = c['name'] - timeslot.time = datetime.datetime.combine(c['day'], c['time']) - timeslot.duration = c['duration'] - timeslot.location = c['location'] - timeslot.show_location = c['show_location'] - timeslot.save() + room_timeslots.append((r, ts)) - session = Session.objects.filter( - timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None], - timeslotassignments__timeslot=timeslot, - ).select_related('group').first() + day_grid.append({ + 'day': d, + 'room_timeslots': room_timeslots + }) - if session: - if timeslot.type_id != 'regular': - session.name = c['name'] - session.short = c['short'] - session.group = c['group'] - session.type = c['type'] - session.agenda_note = c.get('agenda_note') or "" - session.save() - - return redirect_with_scroll() - - if request.method == 'POST' and request.POST.get('action') == 'cancel-timeslot' and can_edit: - timeslot_pk = request.POST.get('timeslot') - if not timeslot_pk or not timeslot_pk.isdecimal(): - raise Http404 - - timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk) - if timeslot.type_id != 'break': - sessions = add_event_info_to_session_qs( - Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot), - ).exclude(current_status__in=['canceled', 'resched']) - for session in sessions: - SchedulingEvent.objects.create( - session=session, - status=SessionStatusName.objects.get(slug='canceled'), - by=request.user.person, - ) - - return redirect_with_scroll() - - if request.method == 'POST' and request.POST.get('action') == 'delete-timeslot' and can_edit: - timeslot_pk = request.POST.get('timeslot') - if not timeslot_pk or not timeslot_pk.isdecimal(): - raise Http404 - - timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk) - - if timeslot.type_id != 'regular': - for session in Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot): - for doc in session.materials.all(): - doc.set_state(State.objects.get(type=doc.type_id, slug='deleted')) - e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='deleted') - e.desc = "Deleted meeting session" - e.save() - - session.delete() - - timeslot.delete() - - return redirect_with_scroll() - - sessions_by_pk = { - s.pk: s for s in - add_event_info_to_session_qs( - Session.objects.filter( - meeting=meeting, - ).order_by('pk'), - requested_time=True, - requested_by=True, - ).filter( - current_status__in=['appr', 'schedw', 'scheda', 'sched', 'canceled', 'canceledpa', 'resched'] - ).prefetch_related( - 'group', 'group', 'group__type', - ) - } - - assignments_by_timeslot = defaultdict(list) - for a in SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]): - assignments_by_timeslot[a.timeslot_id].append(a) - - days = [meeting.date + datetime.timedelta(days=i) for i in range(meeting.days)] - - timeslots_by_day_and_room = defaultdict(list) - for t in timeslot_qs: - timeslots_by_day_and_room[(t.time.date(), t.location_id)].append(t) - - min_time = min([t.time.time() for t in timeslot_qs] + [datetime.time(8)]) - max_time = max([t.end_time().time() for t in timeslot_qs] + [datetime.time(22)]) - min_max_delta = datetime.datetime.combine(meeting.date, max_time) - datetime.datetime.combine(meeting.date, min_time) - - day_grid = [] - for d in days: - room_timeslots = [] - for r in rooms: - ts = [] - for t in timeslots_by_day_and_room.get((d, r.pk), []): - # FIXME: the database (as of 2020) contains spurious - # regular timeslots in rooms not intended for regular - # sessions - once those are gone, this filter can go - # away - if t.type_id == 'regular' and not any(t.slug == 'regular' for t in r.session_types.all()): - continue - - t.assigned_sessions = [] - for a in assignments_by_timeslot.get(t.pk, []): - s = sessions_by_pk.get(a.session_id) - if s: - t.assigned_sessions.append(s) - - t.left_offset = 100.0 * (t.time - datetime.datetime.combine(t.time.date(), min_time)) / min_max_delta - t.layout_width = min(100.0 * t.duration / min_max_delta, 100 - t.left_offset) - ts.append(t) - - room_timeslots.append((r, ts)) - - day_grid.append({ - 'day': d, - 'room_timeslots': room_timeslots + return render(request, "meeting/edit_meeting_timeslots_and_misc_sessions.html", { + 'meeting': meeting, + 'schedule': schedule, + 'can_edit': can_edit, + 'day_grid': day_grid, + 'empty_timeslot_form': TimeSlotForm(meeting, schedule), + 'add_timeslot_form': add_timeslot_form, + 'edit_timeslot_form': edit_timeslot_form, + 'scroll': scroll, + 'hide_menu': True, }) - return render(request, "meeting/edit_meeting_timeslots_and_misc_sessions.html", { - 'meeting': meeting, - 'schedule': schedule, - 'can_edit': can_edit, - 'day_grid': day_grid, - 'empty_timeslot_form': TimeSlotForm(meeting, schedule), - 'add_timeslot_form': add_timeslot_form, - 'edit_timeslot_form': edit_timeslot_form, - 'scroll': scroll, - 'hide_menu': True, - }) - class SchedulePropertiesForm(forms.ModelForm): class Meta: @@ -1553,19 +1565,26 @@ def agenda_plain(request, num=None, name=None, base=None, ext=None, owner=None, is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num()) - rendered_page = render(request, "meeting/"+base+ext, { - "personalize": False, - "schedule": schedule, - "filtered_assignments": filtered_assignments, - "updated": updated, - "filter_categories": filter_organizer.get_filter_categories(), - "non_area_keywords": filter_organizer.get_non_area_keywords(), - "now": datetime.datetime.now().astimezone(pytz.UTC), - "timezone": meeting.time_zone, - "is_current_meeting": is_current_meeting, - "use_codimd": True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False, - "cache_time": 150 if is_current_meeting else 3600, - }, content_type=mimetype[ext]) + display_timezone = 'UTC' if utc else meeting.time_zone + with timezone.override(display_timezone): + rendered_page = render( + request, + "meeting/" + base + ext, + { + "personalize": False, + "schedule": schedule, + "filtered_assignments": filtered_assignments, + "updated": updated, + "filter_categories": filter_organizer.get_filter_categories(), + "non_area_keywords": filter_organizer.get_non_area_keywords(), + "now": timezone.now().astimezone(meeting.tz()), + "display_timezone": display_timezone, + "is_current_meeting": is_current_meeting, + "use_codimd": True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False, + "cache_time": 150 if is_current_meeting else 3600, + }, + content_type=mimetype[ext], + ) return rendered_page @@ -2191,16 +2210,14 @@ def agenda_json(request, num=None): meetinfo.sort(key=lambda x: x['modified'],reverse=True) last_modified = meetinfo and meetinfo[0]['modified'] - tz = pytz.timezone(settings.PRODUCTION_TIMEZONE) - for obj in meetinfo: - obj['modified'] = tz.localize(obj['modified']).astimezone(pytz.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + obj['modified'] = obj['modified'].astimezone(pytz.utc).strftime('%Y-%m-%dT%H:%M:%SZ') data = {"%s"%num: meetinfo} response = HttpResponse(json.dumps(data, indent=2, sort_keys=True), content_type='application/json;charset=%s'%settings.DEFAULT_CHARSET) if last_modified: - last_modified = tz.localize(last_modified).astimezone(pytz.utc) + last_modified = last_modified.astimezone(pytz.utc) response['Last-Modified'] = format_date_time(timegm(last_modified.timetuple())) return response @@ -2275,7 +2292,9 @@ def session_details(request, num, acronym): # Find the time of the meeting, so that we can look back historically # for what the group was called at the time. - meeting_time = datetime.datetime.combine(meeting.date, datetime.time()) + meeting_time = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time()) + ) groups = list(set([ s.group for s in sessions ])) group_replacements = find_history_replacements_active_at(groups, meeting_time) @@ -2343,8 +2362,8 @@ def session_details(request, num, acronym): 'is_materials_manager' : session.group.has_role(request.user, session.group.features.matman_roles), 'can_manage_materials' : can_manage, 'can_view_request': can_view_request, - 'thisweek': datetime.date.today()-datetime.timedelta(days=7), - 'now': datetime.datetime.now(), + 'thisweek': datetime_today()-datetime.timedelta(days=7), + 'now': timezone.now(), 'use_codimd': True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False, }) @@ -3432,7 +3451,7 @@ def interim_request_edit(request, number): @cache_page(60*60) def past(request): '''List of past meetings''' - today = datetime.datetime.today() + today = timezone.now() meetings = data_for_meetings_overview(Meeting.objects.filter(date__lte=today).order_by('-date')) @@ -3442,7 +3461,7 @@ def past(request): def upcoming(request): '''List of upcoming meetings''' - today = datetime.date.today() + today = datetime_today() # Get ietf meetings starting 7 days ago, and interim meetings starting today ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7)) @@ -3505,8 +3524,8 @@ def upcoming(request): 'menu_actions': actions, 'menu_entries': menu_entries, 'selected_menu_entry': selected_menu_entry, - 'now': datetime.datetime.now(), - 'use_codimd': True if datetime.date.today()>=settings.MEETING_USES_CODIMD_DATE else False, + 'now': timezone.now(), + 'use_codimd': (date_today() >= settings.MEETING_USES_CODIMD_DATE), }) @@ -3520,7 +3539,7 @@ def upcoming_ical(request): except ValueError as e: return HttpResponseBadRequest(str(e)) - today = datetime.date.today() + today = datetime_today() # get meetings starting 7 days ago -- we'll filter out sessions in the past further down meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).prefetch_related('schedule').order_by('date')) @@ -3552,9 +3571,12 @@ def upcoming_ical(request): ietfs = [m for m in meetings if m.type_id == 'ietf'] preprocess_meeting_important_dates(ietfs) + meeting_vtz = {meeting.vtimezone() for meeting in meetings} + meeting_vtz.discard(None) + # icalendar response file should have '\r\n' line endings per RFC5545 response = render_to_string('meeting/upcoming.ics', { - 'vtimezones': ''.join(sorted(list({meeting.vtimezone() for meeting in meetings if meeting.vtimezone()}))), + 'vtimezones': ''.join(sorted(meeting_vtz)), 'assignments': assignments, 'ietfs': ietfs, }, request=request) @@ -3567,7 +3589,7 @@ def upcoming_ical(request): def upcoming_json(request): '''Return Upcoming meetings in json format''' - today = datetime.date.today() + today = date_today() # get meetings starting 7 days ago -- we'll filter out sessions in the past further down meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).order_by('date')) @@ -3598,7 +3620,7 @@ def proceedings(request, num=None): begin_date = meeting.get_submission_start_date() cut_off_date = meeting.get_submission_cut_off_date() cor_cut_off_date = meeting.get_submission_correction_date() - now = datetime.date.today() + now = date_today() schedule = get_schedule(meeting, None) sessions = add_event_info_to_session_qs( @@ -3627,20 +3649,21 @@ def proceedings(request, num=None): meeting_sessions.append(s) ietf_areas.append((area, meeting_sessions, not_meeting_sessions)) - return render(request, "meeting/proceedings.html", { - 'meeting': meeting, - 'plenaries': plenaries, 'ietf': ietf, 'training': training, 'irtf': irtf, 'iab': iab, - 'ietf_areas': ietf_areas, - 'cut_off_date': cut_off_date, - 'cor_cut_off_date': cor_cut_off_date, - 'submission_started': now > begin_date, - 'cache_version': cache_version, - 'attendance': meeting.get_attendance(), - 'meetinghost_logo': { - 'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT, - 'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH, - } - }) + with timezone.override(meeting.tz()): + return render(request, "meeting/proceedings.html", { + 'meeting': meeting, + 'plenaries': plenaries, 'ietf': ietf, 'training': training, 'irtf': irtf, 'iab': iab, + 'ietf_areas': ietf_areas, + 'cut_off_date': cut_off_date, + 'cor_cut_off_date': cor_cut_off_date, + 'submission_started': now > begin_date, + 'cache_version': cache_version, + 'attendance': meeting.get_attendance(), + 'meetinghost_logo': { + 'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT, + 'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH, + } + }) @role_required('Secretariat') def finalize_proceedings(request, num=None): @@ -3836,7 +3859,7 @@ def api_upload_chatlog(request): try: apidata = json.loads(apidata_post) except json.decoder.JSONDecodeError: - return err(400, "Malformed post") + return err(400, "Malformed post") if not ( 'session_id' in apidata and type(apidata['session_id']) is int ): return err(400, "Malformed post") session_id = apidata['session_id'] @@ -3860,7 +3883,7 @@ def api_upload_chatlog(request): write_doc_for_session(session, 'chatlog', filename, json.dumps(apidata['chatlog'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse("Done", status=200, content_type='text/plain') @require_api_key @role_required('Recording Manager') @@ -3876,7 +3899,7 @@ def api_upload_polls(request): try: apidata = json.loads(apidata_post) except json.decoder.JSONDecodeError: - return err(400, "Malformed post") + return err(400, "Malformed post") if not ( 'session_id' in apidata and type(apidata['session_id']) is int ): return err(400, "Malformed post") session_id = apidata['session_id'] @@ -3900,7 +3923,7 @@ def api_upload_polls(request): write_doc_for_session(session, 'polls', filename, json.dumps(apidata['polls'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse("Done", status=200, content_type='text/plain') @require_api_key @role_required('Recording Manager', 'Secretariat') @@ -3974,7 +3997,7 @@ def important_dates(request, num=None, output_format=None): base_num = int(meeting.number) user = request.user - today = datetime.date.today() + today = date_today() meetings = [] if meeting.show_important_dates or meeting.date < today: meetings.append(meeting) @@ -4026,23 +4049,24 @@ def edit_timeslot(request, num, slot_id): meeting = get_object_or_404(Meeting, number=num) if timeslot.meeting != meeting: raise Http404() - if request.method == 'POST': - form = TimeSlotEditForm(instance=timeslot, data=request.POST) - if form.is_valid(): - form.save() - return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots', kwargs={'num': num})) - else: - form = TimeSlotEditForm(instance=timeslot) + with timezone.override(meeting.tz()): # specifies current_timezone used for rendering and form handling + if request.method == 'POST': + form = TimeSlotEditForm(instance=timeslot, data=request.POST) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots', kwargs={'num': num})) + else: + form = TimeSlotEditForm(instance=timeslot) - sessions = timeslot.sessions.filter( - timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]) + sessions = timeslot.sessions.filter( + timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]) - return render( - request, - 'meeting/edit_timeslot.html', - {'timeslot': timeslot, 'form': form, 'sessions': sessions}, - status=400 if form.errors else 200, - ) + return render( + request, + 'meeting/edit_timeslot.html', + {'timeslot': timeslot, 'form': form, 'sessions': sessions}, + status=400 if form.errors else 200, + ) @role_required('Secretariat') @@ -4053,7 +4077,7 @@ def create_timeslot(request, num): if form.is_valid(): bulk_create_timeslots( meeting, - [datetime.datetime.combine(day, form.cleaned_data['time']) + [meeting.tz().localize(datetime.datetime.combine(day, form.cleaned_data['time'])) for day in form.cleaned_data.get('days', [])], form.cleaned_data['locations'], dict( @@ -4241,7 +4265,7 @@ def approve_proposed_slides(request, slidesubmission_id, num): } form = ApproveSlidesForm(show_apply_to_all_checkbox, initial=initial ) - return render(request, "meeting/approve_proposed_slides.html", + return render(request, "meeting/approve_proposed_slides.html", {'submission': submission, 'session_number': session_number, 'existing_doc' : existing_doc, diff --git a/ietf/message/management/commands/show_messages.py b/ietf/message/management/commands/show_messages.py index b232da481..8741b2129 100644 --- a/ietf/message/management/commands/show_messages.py +++ b/ietf/message/management/commands/show_messages.py @@ -6,6 +6,7 @@ import email import datetime from django.core.management.base import BaseCommand +from django.utils import timezone import debug # pyflakes:ignore @@ -25,7 +26,7 @@ class Command(BaseCommand): """ def add_arguments(self, parser): - default_start = datetime.datetime.now() - datetime.timedelta(days=14) + default_start = timezone.now() - datetime.timedelta(days=14) parser.add_argument( '-t', '--start', '--from', type=str, default=default_start.strftime('%Y-%m-%d %H:%M'), help='Limit the list to messages saved after the given time (default %(default)s).', diff --git a/ietf/message/migrations/0012_use_timezone_now_for_message_models.py b/ietf/message/migrations/0012_use_timezone_now_for_message_models.py new file mode 100644 index 000000000..dbef893ea --- /dev/null +++ b/ietf/message/migrations/0012_use_timezone_now_for_message_models.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.28 on 2022-07-12 11:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('message', '0011_auto_20201109_0439'), + ] + + operations = [ + migrations.AlterField( + model_name='message', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='sendqueue', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/ietf/message/models.py b/ietf/message/models.py index 01162a4b6..fe2c8d325 100644 --- a/ietf/message/models.py +++ b/ietf/message/models.py @@ -2,10 +2,10 @@ # -*- coding: utf-8 -*- -import datetime import email.utils from django.db import models +from django.utils import timezone import debug # pyflakes:ignore @@ -17,7 +17,7 @@ from ietf.utils.models import ForeignKey from ietf.utils.mail import get_email_addresses_from_text class Message(models.Model): - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) by = ForeignKey(Person) subject = models.CharField(max_length=255) @@ -62,7 +62,7 @@ class MessageAttachment(models.Model): class SendQueue(models.Model): - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) by = ForeignKey(Person) message = ForeignKey(Message) diff --git a/ietf/message/tests.py b/ietf/message/tests.py index 4ab790699..a027df447 100644 --- a/ietf/message/tests.py +++ b/ietf/message/tests.py @@ -5,6 +5,7 @@ import datetime from django.urls import reverse as urlreverse +from django.utils import timezone import debug # pyflakes:ignore @@ -14,10 +15,13 @@ from ietf.message.utils import send_scheduled_message_from_send_queue from ietf.person.models import Person from ietf.utils.mail import outbox, send_mail_text, send_mail_message, get_payload_text from ietf.utils.test_utils import TestCase +from ietf.utils.timezone import date_today + + class MessageTests(TestCase): def test_message_view(self): - nomcom = GroupFactory(name="nomcom%s" % datetime.date.today().year, type_id="nomcom") + nomcom = GroupFactory(name="nomcom%s" % date_today().year, type_id="nomcom") msg = Message.objects.create( by=Person.objects.get(name="(System)"), subject="This is a test", @@ -87,7 +91,7 @@ class SendScheduledAnnouncementsTests(TestCase): q = SendQueue.objects.create( by=Person.objects.get(name="(System)"), message=msg, - send_at=datetime.datetime.now() + datetime.timedelta(hours=12) + send_at=timezone.now() + datetime.timedelta(hours=12) ) mailbox_before = len(outbox) @@ -113,7 +117,7 @@ class SendScheduledAnnouncementsTests(TestCase): q = SendQueue.objects.create( by=Person.objects.get(name="(System)"), message=msg, - send_at=datetime.datetime.now() + datetime.timedelta(hours=12) + send_at=timezone.now() + datetime.timedelta(hours=12) ) mailbox_before = len(outbox) diff --git a/ietf/message/utils.py b/ietf/message/utils.py index 65c018a39..2601eccab 100644 --- a/ietf/message/utils.py +++ b/ietf/message/utils.py @@ -2,8 +2,9 @@ # -*- coding: utf-8 -*- -import re, datetime, email +import re, email +from django.utils import timezone from django.utils.encoding import force_str from ietf.utils.mail import send_mail_text, send_mail_mime @@ -52,7 +53,7 @@ def send_scheduled_message_from_send_queue(queue_item): send_mail_mime(None, message.to, message.frm, message.subject, msg, cc=message.cc, bcc=message.bcc) - queue_item.sent_at = datetime.datetime.now() + queue_item.sent_at = timezone.now() queue_item.save() queue_item.message.sent = queue_item.sent_at diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 6208e1fe9..b17cf80ba 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -16116,7 +16116,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2022-10-12T00:09:36.629", + "time": "2022-10-12T00:09:36.629Z", "used": true, "version": "xym 0.5" }, @@ -16127,7 +16127,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2022-10-12T00:09:36.945", + "time": "2022-10-12T00:09:36.945Z", "used": true, "version": "pyang 2.5.3" }, @@ -16138,7 +16138,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2022-10-12T00:09:36.957", + "time": "2022-10-12T00:09:36.957Z", "used": true, "version": "yanglint SO 1.9.2" }, @@ -16149,7 +16149,7 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2022-10-12T00:09:37.890", + "time": "2022-10-12T00:09:37.890Z", "used": true, "version": "xml2rfc 3.14.2" }, diff --git a/ietf/nomcom/management/commands/send_reminders.py b/ietf/nomcom/management/commands/send_reminders.py index 3dce0a361..bc1042543 100644 --- a/ietf/nomcom/management/commands/send_reminders.py +++ b/ietf/nomcom/management/commands/send_reminders.py @@ -2,13 +2,14 @@ # -*- coding: utf-8 -*- -import datetime import syslog from django.core.management.base import BaseCommand from ietf.nomcom.models import NomCom, NomineePosition from ietf.nomcom.utils import send_accept_reminder_to_nominee,send_questionnaire_reminder_to_nominee +from ietf.utils.timezone import date_today + def log(message): syslog.syslog(message) @@ -27,10 +28,10 @@ class Command(BaseCommand): for nomcom in NomCom.objects.filter(group__state__slug='active'): nps = NomineePosition.objects.filter(nominee__nomcom=nomcom,nominee__duplicated__isnull=True) for nominee_position in nps.pending(): - if is_time_to_send(nomcom, datetime.date.today(), nominee_position.time.date()): + if is_time_to_send(nomcom, date_today(), nominee_position.time.date()): send_accept_reminder_to_nominee(nominee_position) log('Sent accept reminder to %s' % nominee_position.nominee.email.address) for nominee_position in nps.accepted().without_questionnaire_response(): - if is_time_to_send(nomcom, datetime.date.today(), nominee_position.time.date()): + if is_time_to_send(nomcom, date_today(), nominee_position.time.date()): send_questionnaire_reminder_to_nominee(nominee_position) log('Sent questionnaire reminder to %s' % nominee_position.nominee.email.address) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 27f55329b..0577ca558 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -17,6 +17,7 @@ from django.conf import settings from django.core.files import File from django.contrib.auth.models import User from django.urls import reverse +from django.utils import timezone from django.utils.encoding import force_str import debug # pyflakes:ignore @@ -49,6 +50,8 @@ from ietf.stats.models import MeetingRegistration from ietf.stats.factories import MeetingRegistrationFactory from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent +from ietf.utils.timezone import date_today, datetime_today, datetime_from_date, DEADLINE_TZINFO + client_test_cert_files = None @@ -1091,7 +1094,7 @@ class ReminderTest(TestCase): rai = Position.objects.get(nomcom=self.nomcom,name='RAI') iab = Position.objects.get(nomcom=self.nomcom,name='IAB') - today = datetime.date.today() + today = datetime_today() t_minus_3 = today - datetime.timedelta(days=3) t_minus_4 = today - datetime.timedelta(days=4) e1 = EmailFactory(address="nominee1@example.org", person=PersonFactory(name="Nominee 1"), origin='test') @@ -1127,7 +1130,7 @@ class ReminderTest(TestCase): def test_is_time_to_send(self): self.nomcom.reminder_interval = 4 - today = datetime.date.today() + today = date_today() self.assertTrue(is_time_to_send(self.nomcom,today+datetime.timedelta(days=4),today)) for delta in range(4): self.assertFalse(is_time_to_send(self.nomcom,today+datetime.timedelta(days=delta),today)) @@ -1233,7 +1236,7 @@ class InactiveNomcomTests(TestCase): self.assertIn( 'closed', q('.alert-warning').text()) def test_acceptance_closed(self): - today = datetime.date.today().strftime('%Y%m%d') + today = date_today().strftime('%Y%m%d') pid = self.nc.position_set.first().nomineeposition_set.order_by('pk').first().id url = reverse('ietf.nomcom.views.process_nomination_status', kwargs = { 'year' : self.nc.year(), @@ -1389,7 +1392,7 @@ class FeedbackLastSeenTests(TestCase): f.nominees.add(self.nominee) f = FeedbackFactory.create(author=self.author,nomcom=self.nc,type_id='comment') f.topics.add(self.topic) - now = datetime.datetime.now() + now = timezone.now() self.hour_ago = now - datetime.timedelta(hours=1) self.half_hour_ago = now - datetime.timedelta(minutes=30) self.second_from_now = now + datetime.timedelta(seconds=1) @@ -1889,7 +1892,7 @@ Junk body for testing assert year >= 1990 return (year-1985)*3+2 # Create meetings to ensure we have the 'last 5' - meeting_start = first_meeting_of_year(datetime.date.today().year-2) + meeting_start = first_meeting_of_year(date_today().year-2) # Populate the meeting registration records for number in range(meeting_start, meeting_start+10): meeting = MeetingFactory.create(type_id='ietf', number=number) @@ -2241,7 +2244,8 @@ class EligibilityUnitTests(TestCase): def test_get_eligibility_date(self): # No Nomcoms exist: - self.assertEqual(get_eligibility_date(), datetime.date(datetime.date.today().year,5,1)) + this_year = date_today().year + self.assertEqual(get_eligibility_date(), datetime.date(this_year,5,1)) # a provided date trumps anything in the database self.assertEqual(get_eligibility_date(date=datetime.date(2001,2,3)), datetime.date(2001,2,3)) @@ -2255,7 +2259,7 @@ class EligibilityUnitTests(TestCase): n.save() self.assertEqual(get_eligibility_date(nomcom=n), datetime.date(2015,5,17)) # No nomcoms in the database with seated members - self.assertEqual(get_eligibility_date(), datetime.date(datetime.date.today().year,5,1)) + self.assertEqual(get_eligibility_date(), datetime.date(this_year,5,1)) RoleFactory(group=n.group,name_id='member') self.assertEqual(get_eligibility_date(),datetime.date(2016,5,1)) @@ -2263,7 +2267,6 @@ class EligibilityUnitTests(TestCase): NomComFactory(group__acronym='nomcom2016', populate_personnel=False, first_call_for_volunteers=datetime.date(2016,5,4)) self.assertEqual(get_eligibility_date(),datetime.date(2016,5,4)) - this_year = datetime.date.today().year NomComFactory(group__acronym=f'nomcom{this_year}', first_call_for_volunteers=datetime.date(this_year,5,6)) self.assertEqual(get_eligibility_date(),datetime.date(this_year,5,6)) @@ -2417,7 +2420,8 @@ class rfc8989EligibilityTests(TestCase): nobody=PersonFactory() for nomcom in self.nomcoms: - before_elig_date = nomcom.first_call_for_volunteers - datetime.timedelta(days=5) + elig_datetime = datetime_from_date(nomcom.first_call_for_volunteers, DEADLINE_TZINFO) + before_elig_date = elig_datetime - datetime.timedelta(days=5) chair = RoleFactory(name_id='chair',group__time=before_elig_date).person @@ -2435,7 +2439,7 @@ class rfc8989EligibilityTests(TestCase): def test_elig_by_office_edge(self): for nomcom in self.nomcoms: - elig_date=get_eligibility_date(nomcom) + elig_date = datetime_from_date(get_eligibility_date(nomcom), DEADLINE_TZINFO) day_after = elig_date + datetime.timedelta(days=1) two_days_after = elig_date + datetime.timedelta(days=2) @@ -2450,15 +2454,15 @@ class rfc8989EligibilityTests(TestCase): def test_elig_by_office_closed_groups(self): for nomcom in self.nomcoms: - elig_date=get_eligibility_date(nomcom) + elig_date=datetime_from_date(get_eligibility_date(nomcom), DEADLINE_TZINFO) day_before = elig_date-datetime.timedelta(days=1) # special case for Feb 29 if elig_date.month == 2 and elig_date.day == 29: - year_before = datetime.date(elig_date.year - 1, 2, 28) - three_years_before = datetime.date(elig_date.year - 3, 2, 28) + year_before = elig_date.replace(year=elig_date.year - 1, day=28) + three_years_before = elig_date.replace(year=elig_date.year - 3, day=28) else: - year_before = datetime.date(elig_date.year - 1, elig_date.month, elig_date.day) - three_years_before = datetime.date(elig_date.year - 3, elig_date.month, elig_date.day) + year_before = elig_date.replace(year=elig_date.year - 1) + three_years_before = elig_date.replace(year=elig_date.year - 3) just_after_three_years_before = three_years_before + datetime.timedelta(days=1) just_before_three_years_before = three_years_before - datetime.timedelta(days=1) @@ -2517,14 +2521,14 @@ class rfc8989EligibilityTests(TestCase): for nomcom in self.nomcoms: elig_date = get_eligibility_date(nomcom) - last_date = elig_date + last_date = datetime_from_date(elig_date, DEADLINE_TZINFO) # special case for Feb 29 if last_date.month == 2 and last_date.day == 29: - first_date = datetime.date(last_date.year - 5, 2, 28) - middle_date = datetime.date(last_date.year - 3, 2, 28) + first_date = last_date.replace(year = last_date.year - 5, day=28) + middle_date = last_date.replace(year=first_date.year - 3, day=28) else: - first_date = datetime.date(last_date.year - 5, last_date.month, last_date.day) - middle_date = datetime.date(last_date.year - 3, last_date.month, last_date.day) + first_date = last_date.replace(year=last_date.year - 5) + middle_date = last_date.replace(year=first_date.year - 3) day_after_last_date = last_date+datetime.timedelta(days=1) day_before_first_date = first_date-datetime.timedelta(days=1) @@ -2632,8 +2636,8 @@ class VolunteerTests(TestCase): r = self.client.get(url) self.assertContains(r, 'NomCom is not accepting volunteers at this time', status_code=200) - year = datetime.date.today().year - nomcom = NomComFactory(group__acronym=f'nomcom{year}', is_accepting_volunteers=False) + this_year = date_today().year + nomcom = NomComFactory(group__acronym=f'nomcom{this_year}', is_accepting_volunteers=False) r = self.client.get(url) self.assertContains(r, 'NomCom is not accepting volunteers at this time', status_code=200) nomcom.is_accepting_volunteers = True @@ -2656,7 +2660,7 @@ class VolunteerTests(TestCase): self.assertContains(r, 'already volunteered', status_code=200) person.volunteer_set.all().delete() - nomcom2 = NomComFactory(group__acronym=f'nomcom{year-1}', is_accepting_volunteers=True) + nomcom2 = NomComFactory(group__acronym=f'nomcom{this_year-1}', is_accepting_volunteers=True) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) @@ -2717,7 +2721,7 @@ class VolunteerDecoratorUnitTests(TestCase): office_person = PersonFactory() RoleHistoryFactory( name_id='chair', - group__time= elig_date - datetime.timedelta(days=365), + group__time=datetime_from_date(elig_date) - datetime.timedelta(days=365), group__group__state_id='conclude', person=office_person, ) @@ -2729,11 +2733,13 @@ class VolunteerDecoratorUnitTests(TestCase): DocEventFactory( type='published_rfc', doc=da.document, - time=datetime.date( + time=datetime.datetime( elig_date.year - 3, elig_date.month, 28 if elig_date.month == 2 and elig_date.day == 29 else elig_date.day, - )) + tzinfo=datetime.timezone.utc, + ) + ) nomcom.volunteer_set.create(person=author_person) volunteers = nomcom.volunteer_set.all() diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 2bf63e194..50712f9aa 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -34,6 +34,7 @@ from ietf.utils.pipe import pipe from ietf.utils.mail import send_mail_text, send_mail, get_payload_text from ietf.utils.log import log from ietf.person.name import unidecode_name +from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO import debug # pyflakes:ignore @@ -240,7 +241,7 @@ def validate_public_key(public_key): def send_accept_reminder_to_nominee(nominee_position): - today = datetime.date.today().strftime('%Y%m%d') + today = date_today().strftime('%Y%m%d') subject = 'Reminder: please accept (or decline) your nomination.' domain = Site.objects.get_current().domain position = nominee_position.position @@ -332,7 +333,7 @@ def make_nomineeposition(nomcom, candidate, position, author): from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year()) (to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=nominee.email.address) domain = Site.objects.get_current().domain - today = datetime.date.today().strftime('%Y%m%d') + today = date_today().strftime('%Y%m%d') hash = get_hash_nominee_position(today, nominee_position.id) accept_url = reverse('ietf.nomcom.views.process_nomination_status', None, @@ -558,34 +559,35 @@ def get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable): base_qs = Person.objects.all() previous_five = previous_five_meetings(date) + date_as_dt = datetime_from_date(date, DEADLINE_TZINFO) three_of_five_qs = three_of_five_callable(previous_five=previous_five, queryset=base_qs) # If date is Feb 29, neither 3 nor 5 years ago has a Feb 29. Use Feb 28 instead. if date.month == 2 and date.day == 29: - three_years_ago = datetime.date(date.year - 3, 2, 28) - five_years_ago = datetime.date(date.year - 5, 2, 28) + three_years_ago = datetime.datetime(date.year - 3, 2, 28, tzinfo=DEADLINE_TZINFO) + five_years_ago = datetime.datetime(date.year - 5, 2, 28, tzinfo=DEADLINE_TZINFO) else: - three_years_ago = datetime.date(date.year - 3, date.month, date.day) - five_years_ago = datetime.date(date.year - 5, date.month, date.day) + three_years_ago = datetime.datetime(date.year - 3, date.month, date.day, tzinfo=DEADLINE_TZINFO) + five_years_ago = datetime.datetime(date.year - 5, date.month, date.day, tzinfo=DEADLINE_TZINFO) officer_qs = base_qs.filter( # is currently an officer Q(role__name_id__in=('chair','secr'), role__group__state_id='active', role__group__type_id='wg', - role__group__time__lte=date, ## TODO - inspect - lots of things affect group__time... + role__group__time__lte=date_as_dt, ## TODO - inspect - lots of things affect group__time... ) # was an officer since the given date (I think this is wrong - it looks at when roles _start_, not when roles end) | Q(rolehistory__group__time__gte=three_years_ago, - rolehistory__group__time__lte=date, + rolehistory__group__time__lte=date_as_dt, rolehistory__name_id__in=('chair','secr'), rolehistory__group__state_id='active', rolehistory__group__type_id='wg', ) ).distinct() - rfc_pks = set(DocEvent.objects.filter(type='published_rfc',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True)) - iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True)) + rfc_pks = set(DocEvent.objects.filter(type='published_rfc', time__gte=five_years_ago, time__lte=date_as_dt).values_list('doc__pk', flat=True)) + iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved', time__gte=five_years_ago, time__lte=date_as_dt).values_list('doc__pk',flat=True)) qualifying_pks = rfc_pks.union(iesgappr_pks.difference(rfc_pks)) author_qs = base_qs.filter( documentauthor__document__pk__in=qualifying_pks @@ -624,7 +626,7 @@ def get_eligibility_date(nomcom=None, date=None): last_seated=Role.objects.filter(group__type_id='nomcom',name_id='member').order_by('-group__acronym').first() if last_seated: last_nomcom_year = int(last_seated.group.acronym[6:]) - if last_nomcom_year == datetime.date.today().year: + if last_nomcom_year == date_today().year: next_nomcom_year = last_nomcom_year else: next_nomcom_year = int(last_seated.group.acronym[6:])+1 @@ -634,11 +636,11 @@ def get_eligibility_date(nomcom=None, date=None): else: return datetime.date(next_nomcom_year,5,1) else: - return datetime.date(datetime.date.today().year,5,1) + return datetime.date(date_today().year,5,1) def previous_five_meetings(date = None): if date is None: - date = datetime.date.today() + date = date_today() return Meeting.objects.filter(type='ietf',date__lte=date).order_by('-date')[:5] def three_of_five_eligible_8713(previous_five, queryset=None): diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 9f752e49e..6f02b16e2 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -45,6 +45,8 @@ from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, sug from ietf.ietfauth.utils import role_required from ietf.person.models import Person from ietf.utils.response import permission_denied +from ietf.utils.timezone import date_today + import debug # pyflakes:ignore @@ -702,7 +704,7 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h expiration_days = getattr(settings, 'DAYS_TO_EXPIRE_NOMINATION_LINK', None) if expiration_days: request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:])) - if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_NOMINATION_LINK)): + if date_today() > (request_date + datetime.timedelta(days=expiration_days)): permission_denied(request, "Link expired.") need_confirmation = True @@ -952,7 +954,7 @@ def view_feedback_topic(request, year, topic_id): feedback_types = FeedbackTypeName.objects.filter(slug__in=['comment',]) last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first() - last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1,month=1,day=1) + last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc) if last_seen: last_seen.save() else: @@ -974,7 +976,7 @@ def view_feedback_nominee(request, year, nominee_id): feedback_types = FeedbackTypeName.objects.filter(slug__in=settings.NOMINEE_FEEDBACK_TYPES) last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first() - last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1,month=1,day=1) + last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc) if last_seen: last_seen.save() else: diff --git a/ietf/person/management/commands/purge_old_personal_api_key_events.py b/ietf/person/management/commands/purge_old_personal_api_key_events.py index e60f784b5..47674e2d5 100644 --- a/ietf/person/management/commands/purge_old_personal_api_key_events.py +++ b/ietf/person/management/commands/purge_old_personal_api_key_events.py @@ -1,9 +1,10 @@ # Copyright The IETF Trust 2021, All Rights Reserved # -*- coding: utf-8 -*- -from datetime import datetime, timedelta +from datetime import timedelta from django.core.management.base import BaseCommand, CommandError from django.db.models import Max, Min +from django.utils import timezone from ietf.person.models import PersonApiKeyEvent @@ -33,7 +34,7 @@ class Command(BaseCommand): self.stdout.write('Finding events older than {}\n'.format(_format_count(keep_days))) self.stdout.flush() - now = datetime.now() + now = timezone.now() old_events = PersonApiKeyEvent.objects.filter( time__lt=now - timedelta(days=keep_days) ) diff --git a/ietf/person/management/commands/tests.py b/ietf/person/management/commands/tests.py index f30a80bf5..291a6ace5 100644 --- a/ietf/person/management/commands/tests.py +++ b/ietf/person/management/commands/tests.py @@ -5,6 +5,7 @@ import datetime from io import StringIO from django.core.management import call_command, CommandError +from django.utils import timezone from ietf.person.factories import PersonApiKeyEventFactory from ietf.person.models import PersonApiKeyEvent, PersonEvent @@ -51,7 +52,7 @@ class CommandTests(TestCase): # Remember how many PersonEvents were present so we can verify they're cleaned up properly. personevents_before = PersonEvent.objects.count() - now = datetime.datetime.now() + now = timezone.now() # The first of these events will be timestamped a fraction of a second more than keep_days # days ago by the time we call the management command, so will just barely chosen for purge. old_events = [ @@ -101,7 +102,7 @@ class CommandTests(TestCase): def test_purge_old_personal_api_key_events_rejects_invalid_arguments(self): """The purge_old_personal_api_key_events command should reject invalid arguments""" - event = PersonApiKeyEventFactory(time=datetime.datetime.now() - datetime.timedelta(days=30)) + event = PersonApiKeyEventFactory(time=timezone.now() - datetime.timedelta(days=30)) with self.assertRaises(CommandError): self._call_command('purge_old_personal_api_key_events') diff --git a/ietf/person/migrations/0026_use_timezone_now_for_person_models.py b/ietf/person/migrations/0026_use_timezone_now_for_person_models.py new file mode 100644 index 000000000..619b42162 --- /dev/null +++ b/ietf/person/migrations/0026_use_timezone_now_for_person_models.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.28 on 2022-07-12 11:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0025_chat_and_polls_apikey'), + ] + + operations = [ + migrations.AlterField( + model_name='historicalperson', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='person', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='personalapikey', + name='created', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='personevent', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now, help_text='When the event happened'), + ), + ] diff --git a/ietf/person/models.py b/ietf/person/models.py index c8982e338..a7e30b2de 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- -import datetime import email.utils import email.header import jsonfield @@ -18,6 +17,7 @@ from django.core.validators import validate_email from django.db import models from django.template.loader import render_to_string from django.urls import reverse as urlreverse +from django.utils import timezone from django.utils.encoding import smart_bytes from django.utils.text import slugify @@ -43,7 +43,7 @@ def name_character_validator(value): class Person(models.Model): history = HistoricalRecords() user = OneToOneField(User, blank=True, null=True, on_delete=models.SET_NULL) - time = models.DateTimeField(default=datetime.datetime.now) # When this Person record entered the system + time = models.DateTimeField(default=timezone.now) # When this Person record entered the system # The normal unicode form of the name. This must be # set to the same value as the ascii-form if equal. name = models.CharField("Full Name (Unicode)", max_length=255, db_index=True, help_text="Preferred long form of name.", validators=[name_character_validator]) @@ -357,7 +357,7 @@ PERSON_API_KEY_ENDPOINTS = sorted(list(set([ (v, n) for (v, n, r) in PERSON_API_ class PersonalApiKey(models.Model): person = ForeignKey(Person, related_name='apikeys') endpoint = models.CharField(max_length=128, null=False, blank=False, choices=PERSON_API_KEY_ENDPOINTS) - created = models.DateTimeField(default=datetime.datetime.now, null=False) + created = models.DateTimeField(default=timezone.now, null=False) valid = models.BooleanField(default=True) salt = models.BinaryField(default=salt, max_length=12, null=False, blank=False) count = models.IntegerField(default=0, null=False, blank=False) @@ -406,7 +406,7 @@ PERSON_EVENT_CHOICES = [ class PersonEvent(models.Model): person = ForeignKey(Person) - time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened") + time = models.DateTimeField(default=timezone.now, help_text="When the event happened") type = models.CharField(max_length=50, choices=PERSON_EVENT_CHOICES) desc = models.TextField() diff --git a/ietf/person/templatetags/person_filters.py b/ietf/person/templatetags/person_filters.py index 96696456f..017b29c63 100644 --- a/ietf/person/templatetags/person_filters.py +++ b/ietf/person/templatetags/person_filters.py @@ -1,13 +1,12 @@ # Copyright The IETF Trust 2017-2020, All Rights Reserved -import datetime - from django import template import debug # pyflakes:ignore from ietf.nomcom.utils import is_eligible from ietf.person.models import Alias +from ietf.utils.timezone import date_today register = template.Library() @@ -15,7 +14,7 @@ register = template.Library() @register.filter def is_nomcom_eligible(person, date=None): if date is None: - date = datetime.date.today() + date = date_today() return is_eligible(person=person, date=date) diff --git a/ietf/person/tests.py b/ietf/person/tests.py index affcb0441..f659b9896 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -14,6 +14,7 @@ from django.core.exceptions import ValidationError from django.http import HttpRequest from django.test import override_settings from django.urls import reverse as urlreverse +from django.utils import timezone from django.utils.encoding import iri_to_uri import debug # pyflakes:ignore @@ -218,7 +219,7 @@ class PersonUtilsTests(TestCase): self.assertEqual(results,(p1,p3)) # both have User - today = datetime.datetime.today() + today = timezone.now() p2.user.last_login = today p2.user.save() p4.user.last_login = today - datetime.timedelta(days=30) diff --git a/ietf/person/views.py b/ietf/person/views.py index fca943e2d..59ab9366c 100644 --- a/ietf/person/views.py +++ b/ietf/person/views.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- -import datetime from io import StringIO, BytesIO from PIL import Image @@ -10,6 +9,7 @@ from django.contrib import messages from django.db.models import Q from django.http import HttpResponse, Http404 from django.shortcuts import render, get_object_or_404, redirect +from django.utils import timezone import debug # pyflakes:ignore @@ -76,7 +76,7 @@ def profile(request, email_or_name): persons = [ p for p in persons if p and p.id ] if not persons: raise Http404 - return render(request, 'person/profile.html', {'persons': persons, 'today':datetime.date.today()}) + return render(request, 'person/profile.html', {'persons': persons, 'today': timezone.now()}) def photo(request, email_or_name): diff --git a/ietf/review/factories.py b/ietf/review/factories.py index cf66f1ed8..d6780fad8 100644 --- a/ietf/review/factories.py +++ b/ietf/review/factories.py @@ -2,6 +2,8 @@ import factory import datetime +from django.utils import timezone + from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewAssignment, ReviewerSettings from ietf.name.models import ReviewTypeName, ReviewResultName @@ -39,7 +41,7 @@ class ReviewRequestFactory(factory.django.DjangoModelFactory): type_id = 'lc' doc = factory.SubFactory('ietf.doc.factories.DocumentFactory',type_id='draft') team = factory.SubFactory('ietf.group.factories.ReviewTeamFactory',type_id='review') - deadline = datetime.datetime.today()+datetime.timedelta(days=14) + deadline = timezone.now()+datetime.timedelta(days=14) requested_by = factory.SubFactory('ietf.person.factories.PersonFactory') class ReviewAssignmentFactory(factory.django.DjangoModelFactory): @@ -49,7 +51,7 @@ class ReviewAssignmentFactory(factory.django.DjangoModelFactory): review_request = factory.SubFactory('ietf.review.factories.ReviewRequestFactory') state_id = 'assigned' reviewer = factory.SubFactory('ietf.person.factories.EmailFactory') - assigned_on = datetime.datetime.now() + assigned_on = timezone.now() class ReviewerSettingsFactory(factory.django.DjangoModelFactory): class Meta: diff --git a/ietf/review/mailarch.py b/ietf/review/mailarch.py index 2ca6c9566..6ef5909a1 100644 --- a/ietf/review/mailarch.py +++ b/ietf/review/mailarch.py @@ -25,6 +25,8 @@ from django.conf import settings from django.utils.encoding import force_bytes, force_str from ietf.utils.mail import get_payload_text +from ietf.utils.timezone import date_today + def list_name_from_email(list_email): if not list_email.endswith("@ietf.org"): @@ -51,7 +53,7 @@ def construct_query_urls(doc, team, query=None): encoded_query = "?" + urlencode({ "qdr": "c", # custom time frame - "start_date": (datetime.date.today() - datetime.timedelta(days=180)).isoformat(), + "start_date": (date_today() - datetime.timedelta(days=180)).isoformat(), "email_list": list_name, "q": "subject:({})".format(query), "as": "1", # this is an advanced search @@ -89,7 +91,7 @@ def retrieve_messages_from_mbox(mbox_fileobj): utcdate = None d = email.utils.parsedate_tz(msg["Date"]) if d: - utcdate = datetime.datetime.fromtimestamp(email.utils.mktime_tz(d)) + utcdate = datetime.datetime.fromtimestamp(email.utils.mktime_tz(d), datetime.timezone.utc) res.append({ "from": msg["From"], diff --git a/ietf/review/migrations/0029_use_timezone_now_for_review_models.py b/ietf/review/migrations/0029_use_timezone_now_for_review_models.py new file mode 100644 index 000000000..a8a0558d4 --- /dev/null +++ b/ietf/review/migrations/0029_use_timezone_now_for_review_models.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.28 on 2022-07-12 11:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0028_auto_20220513_1456'), + ] + + operations = [ + migrations.AlterField( + model_name='historicalreviewrequest', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='reviewrequest', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='reviewwish', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/ietf/review/migrations/0030_use_date_today_helper.py b/ietf/review/migrations/0030_use_date_today_helper.py new file mode 100644 index 000000000..b006d276f --- /dev/null +++ b/ietf/review/migrations/0030_use_date_today_helper.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.28 on 2022-10-18 15:43 + +from django.db import migrations, models +import ietf.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0029_use_timezone_now_for_review_models'), + ] + + operations = [ + migrations.AlterField( + model_name='historicalunavailableperiod', + name='start_date', + field=models.DateField(default=ietf.utils.timezone.date_today, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.", null=True), + ), + migrations.AlterField( + model_name='unavailableperiod', + name='start_date', + field=models.DateField(default=ietf.utils.timezone.date_today, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.", null=True), + ), + ] diff --git a/ietf/review/models.py b/ietf/review/models.py index 96355417d..f0ec78094 100644 --- a/ietf/review/models.py +++ b/ietf/review/models.py @@ -2,11 +2,10 @@ # -*- coding: utf-8 -*- -import datetime - from simple_history.models import HistoricalRecords from django.db import models +from django.utils import timezone import debug # pyflakes:ignore @@ -17,6 +16,8 @@ from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResul ReviewAssignmentStateName, ReviewerQueuePolicyName from ietf.utils.validators import validate_regular_expression_string from ietf.utils.models import ForeignKey, OneToOneField +from ietf.utils.timezone import date_today + class ReviewerSettings(models.Model): """Keeps track of admin data associated with a reviewer in a team.""" @@ -66,7 +67,7 @@ class UnavailablePeriod(models.Model): history = HistoricalRecords(history_change_reason_field=models.TextField(null=True)) team = ForeignKey(Group, limit_choices_to=~models.Q(reviewteamsettings=None)) person = ForeignKey(Person) - start_date = models.DateField(default=datetime.date.today, null=True, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.") + start_date = models.DateField(default=date_today, null=True, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.") end_date = models.DateField(blank=True, null=True, help_text="Leaving the end date blank means that the period continues indefinitely. You can end it later.") AVAILABILITY_CHOICES = [ ("canfinish", "Can do follow-ups"), @@ -80,8 +81,7 @@ class UnavailablePeriod(models.Model): reason = models.TextField(verbose_name="Reason why reviewer is unavailable (Optional)", max_length=2048, blank=True, help_text="Provide (for the secretary's benefit) the reason why the review is unavailable", default='') def state(self): - import datetime - today = datetime.date.today() + today = date_today() if self.start_date is None or self.start_date <= today: if not self.end_date or today <= self.end_date: return "active" @@ -95,7 +95,7 @@ class UnavailablePeriod(models.Model): class ReviewWish(models.Model): """Reviewer wishes to review a document when it becomes available for review.""" - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) team = ForeignKey(Group, limit_choices_to=~models.Q(reviewteamsettings=None)) person = ForeignKey(Person) doc = ForeignKey(Document) @@ -125,7 +125,7 @@ class ReviewRequest(models.Model): # Fields filled in on the initial record creation - these # constitute the request part. - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) type = ForeignKey(ReviewTypeName) doc = ForeignKey(Document, related_name='reviewrequest_set') team = ForeignKey(Group, limit_choices_to=~models.Q(reviewteamsettings=None)) diff --git a/ietf/review/tests.py b/ietf/review/tests.py index 1ecdb4040..43294801e 100644 --- a/ietf/review/tests.py +++ b/ietf/review/tests.py @@ -5,6 +5,7 @@ import datetime from ietf.group.factories import RoleFactory from ietf.utils.mail import empty_outbox, get_payload_text, outbox from ietf.utils.test_utils import TestCase, reload_db_objects +from ietf.utils.timezone import date_today, datetime_from_date from .factories import ReviewAssignmentFactory, ReviewRequestFactory, ReviewerSettingsFactory from .mailarch import hash_list_message_id from .models import ReviewerSettings, ReviewSecretarySettings, ReviewTeamSettings, UnavailablePeriod @@ -74,7 +75,7 @@ class ReviewAssignmentTest(TestCase): class ReviewAssignmentReminderTests(TestCase): - today = datetime.date.today() + today = date_today() deadline = today + datetime.timedelta(days=6) def setUp(self): @@ -345,7 +346,7 @@ class ReviewAssignmentReminderTests(TestCase): def test_send_unavailability_period_ending_reminder(self): secretary = self.make_secretary(username='reviewsecretary') empty_outbox() - today = datetime.date.today() + today = date_today() UnavailablePeriod.objects.create( team=self.team, person=self.reviewer, @@ -408,7 +409,7 @@ class ReviewAssignmentReminderTests(TestCase): review_request__state_id='assigned', review_request__deadline=self.deadline, state_id='assigned', - assigned_on=self.deadline, + assigned_on=datetime_from_date(self.deadline), reviewer=self.reviewer.email_set.first(), ).review_request.team second_team.reviewteamsettings.delete() # prevent it from being sent reminders @@ -420,7 +421,7 @@ class ReviewAssignmentReminderTests(TestCase): review_request__state_id='assigned', review_request__deadline=not_overdue, state_id='assigned', - assigned_on=not_overdue, + assigned_on=datetime_from_date(not_overdue), reviewer=self.reviewer.email_set.first(), ) ReviewAssignmentFactory( @@ -428,7 +429,7 @@ class ReviewAssignmentReminderTests(TestCase): review_request__state_id='assigned', review_request__deadline=not_overdue, state_id='assigned', - assigned_on=not_overdue, + assigned_on=datetime_from_date(not_overdue), reviewer=self.reviewer.email_set.first(), ) @@ -439,7 +440,7 @@ class ReviewAssignmentReminderTests(TestCase): review_request__state_id='assigned', review_request__deadline=in_grace_period, state_id='assigned', - assigned_on=in_grace_period, + assigned_on=datetime_from_date(in_grace_period), reviewer=self.reviewer.email_set.first(), ) ReviewAssignmentFactory( @@ -447,7 +448,7 @@ class ReviewAssignmentReminderTests(TestCase): review_request__state_id='assigned', review_request__deadline=in_grace_period, state_id='assigned', - assigned_on=in_grace_period, + assigned_on=datetime_from_date(in_grace_period), reviewer=self.reviewer.email_set.first(), ) @@ -469,7 +470,7 @@ class ReviewAssignmentReminderTests(TestCase): self.assertIn('1 overdue review', log[0]) def test_send_reminder_all_open_reviews(self): - today = datetime.date.today() + today = date_today() self.make_secretary(username='reviewsecretary') ReviewerSettingsFactory(team=self.team, person=self.reviewer, remind_days_open_reviews=1) diff --git a/ietf/review/utils.py b/ietf/review/utils.py index e94dfb2a4..15312dd93 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -12,6 +12,8 @@ from django.template.defaultfilters import pluralize from django.template.loader import render_to_string from django.urls import reverse as urlreverse from django.contrib.sites.models import Site +from django.utils import timezone + from simple_history.utils import update_change_reason import debug # pyflakes:ignore @@ -30,6 +32,8 @@ from ietf.review.models import (ReviewRequest, ReviewAssignment, ReviewRequestSt from ietf.utils.mail import send_mail from ietf.doc.utils import extract_complete_replaces_ancestor_mapping_for_docs from ietf.utils import log +from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO + # The origin date is used to have a single reference date for "every X days". # This date is arbitrarily chosen and has no special meaning, but should be consistent. @@ -89,12 +93,12 @@ def no_review_from_teams_on_doc(doc, rev): def unavailable_periods_to_list(past_days=14): return UnavailablePeriod.objects.filter( - Q(end_date=None) | Q(end_date__gte=datetime.date.today() - datetime.timedelta(days=past_days)), + Q(end_date=None) | Q(end_date__gte=date_today() - datetime.timedelta(days=past_days)), ).order_by("start_date") def current_unavailable_periods_for_reviewers(team): """Return dict with currently active unavailable periods for reviewers.""" - today = datetime.date.today() + today = date_today() unavailable_period_qs = UnavailablePeriod.objects.filter( Q(end_date__gte=today) | Q(end_date=None), @@ -119,7 +123,7 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team): min_intervals = dict(ReviewerSettings.objects.filter(team=team).values_list("person_id", "min_interval")) - now = datetime.datetime.now() + now = timezone.now() res = {} for person_id, latest_assignment_time in latest_assignments.items(): @@ -192,7 +196,10 @@ def extract_review_assignment_data(teams=None, reviewers=None, time_from=None, t assigned_time = assigned_on closed_time = completed_on - late_days = positive_days(datetime.datetime.combine(deadline, datetime.time.max), closed_time) + late_days = positive_days( + datetime.datetime.combine(deadline, datetime.time.max, tzinfo=DEADLINE_TZINFO), + closed_time, + ) request_to_assignment_days = positive_days(requested_time, assigned_time) assignment_to_closure_days = positive_days(assigned_time, closed_time) request_to_closure_days = positive_days(requested_time, closed_time) @@ -283,7 +290,7 @@ def latest_review_assignments_for_reviewers(team, days_back=365): extracted_data = extract_review_assignment_data( teams=[team], - time_from=datetime.date.today() - datetime.timedelta(days=days_back), + time_from=datetime_today(DEADLINE_TZINFO) - datetime.timedelta(days=days_back), ordering=["reviewer"], ) @@ -495,7 +502,7 @@ def suggested_review_requests_for_team(team): requests = {} - now = datetime.datetime.now() + now = timezone.now() reviewable_docs_qs = Document.objects.filter(type="draft").exclude(stream="ise") @@ -870,7 +877,7 @@ def email_reviewer_reminder(assignment): review_request = assignment.review_request team = review_request.team - deadline_days = (review_request.deadline - datetime.date.today()).days + deadline_days = (review_request.deadline - date_today(DEADLINE_TZINFO)).days subject = "Reminder: deadline for review of {} in {} is {}".format(review_request.doc.name, team.acronym, review_request.deadline.isoformat()) @@ -936,7 +943,7 @@ def email_secretary_reminder(assignment, secretary_role): review_request = assignment.review_request team = review_request.team - deadline_days = (review_request.deadline - datetime.date.today()).days + deadline_days = (review_request.deadline - date_today(DEADLINE_TZINFO)).days subject = "Reminder: deadline for review of {} in {} is {}".format(review_request.doc.name, team.acronym, review_request.deadline.isoformat()) diff --git a/ietf/secr/meetings/forms.py b/ietf/secr/meetings/forms.py index 50ccddbb7..92c751418 100644 --- a/ietf/secr/meetings/forms.py +++ b/ietf/secr/meetings/forms.py @@ -166,6 +166,15 @@ class TimeSlotForm(forms.Form): for n in range(-self.meeting.days, self.meeting.days): date = start + datetime.timedelta(days=n) choices.append((n, date.strftime("%a %b %d"))) + # make sure the choices include the initial day + if self.initial and 'day' in self.initial: + day = self.initial['day'] + date = start + datetime.timedelta(days=day) + datestr = date.strftime("%a %b %d") + if day < -self.meeting.days: + choices.insert(0, (day, datestr)) + elif day >= self.meeting.days: + choices.append((day, datestr)) return choices diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index c6576a0cc..bf052abe4 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -14,6 +14,7 @@ import debug # pyflakes:ignore from django.conf import settings from django.urls import reverse +from django.utils import timezone from ietf.group.models import Group, GroupEvent from ietf.meeting.factories import MeetingFactory @@ -137,7 +138,10 @@ class SecrMeetingTestCase(TestCase): "Edit Meeting" meeting = make_meeting_test_data() url = reverse('ietf.secr.meetings.views.edit_meeting',kwargs={'meeting_id':meeting.number}) - post_data = dict(number=meeting.number,date='2014-07-20',city='Toronto', + post_data = dict(number=meeting.number, + date='2014-07-20', + city='Toronto', + time_zone='America/Toronto', days=7, idsubmit_cutoff_day_offset_00=13, idsubmit_cutoff_day_offset_01=20, @@ -212,8 +216,8 @@ class SecrMeetingTestCase(TestCase): self.assertEqual(q('#id_notification_list').html(),'ames, mars') # test that only changes since last notification show up - now = datetime.datetime.now() - then = datetime.datetime.now()+datetime.timedelta(hours=1) + now = timezone.now() + then = timezone.now()+datetime.timedelta(hours=1) person = Person.objects.get(name="(System)") GroupEvent.objects.create(group=mars_group,time=now,type='sent_notification', by=person,desc='sent scheduled notification for %s' % meeting) @@ -285,7 +289,7 @@ class SecrMeetingTestCase(TestCase): url = reverse('ietf.secr.meetings.views.times_delete',kwargs={ 'meeting_id':meeting.number, 'schedule_name':meeting.schedule.name, - 'time':qs.first().time.strftime("%Y:%m:%d:%H:%M") + 'time':qs.first().time.astimezone(meeting.tz()).strftime("%Y:%m:%d:%H:%M") }) redirect_url = reverse('ietf.secr.meetings.views.times',kwargs={ 'meeting_id':meeting.number, @@ -305,7 +309,7 @@ class SecrMeetingTestCase(TestCase): url = reverse('ietf.secr.meetings.views.times_edit',kwargs={ 'meeting_id':72, 'schedule_name':'test-schedule', - 'time':timeslot.time.strftime("%Y:%m:%d:%H:%M") + 'time':timeslot.time.astimezone(meeting.tz()).strftime("%Y:%m:%d:%H:%M") }) self.client.login(username="secretary", password="secretary+password") response = self.client.post(url, { @@ -371,7 +375,7 @@ class SecrMeetingTestCase(TestCase): timeslot = session.official_timeslotassignment().timeslot url = reverse('ietf.secr.meetings.views.misc_session_edit',kwargs={'meeting_id':72,'schedule_name':meeting.schedule.name,'slot_id':timeslot.pk}) redirect_url = reverse('ietf.secr.meetings.views.misc_sessions',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) - new_time = timeslot.time + datetime.timedelta(days=1) + new_time = (timeslot.time + datetime.timedelta(days=1)).astimezone(meeting.tz()) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -389,7 +393,7 @@ class SecrMeetingTestCase(TestCase): }) self.assertRedirects(response, redirect_url) timeslot = session.official_timeslotassignment().timeslot - self.assertEqual(timeslot.time,new_time) + self.assertEqual(timeslot.time, new_time) def test_meetings_misc_session_delete(self): meeting = make_meeting_test_data() diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 7d6a227dc..bd0c8efa1 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -11,6 +11,7 @@ from django.db.models import IntegerField from django.db.models.functions import Cast from django.forms.models import inlineformset_factory from django.shortcuts import render, get_object_or_404, redirect +from django.utils import timezone from django.utils.text import slugify import debug # pyflakes:ignore @@ -30,6 +31,7 @@ from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, from ietf.secr.sreq.views import get_initial_session from ietf.secr.utils.meeting import get_session, get_timeslot from ietf.mailtrigger.utils import gather_address_lists +from ietf.utils.timezone import date_today, make_aware # prep for agenda changes @@ -62,15 +64,23 @@ def build_timeslots(meeting,room=None): else: source_meeting = get_last_meeting(meeting) - delta = meeting.date - source_meeting.date timeslots = [] - time_seen = set() + time_seen = set() # time of source_meeting timeslot for t in source_meeting.timeslot_set.filter(type='regular'): if not t.time in time_seen: time_seen.add(t.time) timeslots.append(t) for t in timeslots: - new_time = t.time + delta + # Create new timeslot at the same wall clock time on the same day relative to meeting start + day_offset = t.local_start_time().date() - source_meeting.date + new_date = meeting.date + day_offset + new_time = make_aware( + datetime.datetime.combine( + new_date, + t.local_start_time().time(), + ), + meeting.tz(), + ) for room in rooms: TimeSlot.objects.create(type_id='regular', meeting=meeting, @@ -121,7 +131,7 @@ def send_notifications(meeting, groups, person): Send session scheduled email notifications for each group in groups. Person is the user who initiated this action, request.uesr.get_profile(). ''' - now = datetime.datetime.now() + now = timezone.now() for group in groups: sessions = group.session_set.filter(meeting=meeting) addrs = gather_address_lists('session_scheduled',group=group,session=sessions[0]) @@ -304,7 +314,7 @@ def blue_sheet_redirect(request): This is the generic blue sheet URL. It gets the next IETF meeting and redirects to the meeting specific URL. ''' - today = datetime.date.today() + today = date_today() qs = Meeting.objects.filter(date__gt=today,type='ietf').order_by('date') if qs: meeting = qs[0] @@ -341,7 +351,6 @@ def edit_meeting(request, meeting_id): else: form = MeetingModelForm(instance=meeting) - return render(request, 'meetings/edit_meeting.html', { 'meeting': meeting, 'form' : form, }, @@ -557,7 +566,7 @@ def misc_session_edit(request, meeting_id, schedule_name, slot_id): 'name':session.name, 'short':session.short, 'day':delta.days, - 'time':slot.time.strftime('%H:%M'), + 'time':slot.time.astimezone(meeting.tz()).strftime('%H:%M'), 'duration':duration_string(slot.duration), 'show_location':slot.show_location, 'purpose': session.purpose, @@ -798,7 +807,8 @@ def get_timeslot_time(form, meeting): day = form.cleaned_data['day'] date = meeting.date + datetime.timedelta(days=int(day)) - return datetime.datetime(date.year,date.month,date.day,time.hour,time.minute) + return make_aware(datetime.datetime(date.year,date.month,date.day,time.hour,time.minute), meeting.tz()) + @role_required('Secretariat') def times_edit(request, meeting_id, schedule_name, time): @@ -809,15 +819,22 @@ def times_edit(request, meeting_id, schedule_name, time): schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name) parts = [ int(x) for x in time.split(':') ] - dtime = datetime.datetime(*parts) + dtime = make_aware(datetime.datetime(*parts), meeting.tz()) timeslots = TimeSlot.objects.filter(meeting=meeting,time=dtime) + day = (dtime.date() - meeting.date).days + initial = {'day': day, + 'time': dtime.strftime('%H:%M'), + 'duration': timeslots.first().duration, + 'name': timeslots.first().name} if request.method == 'POST': button_text = request.POST.get('submit', '') if button_text == 'Cancel': return redirect('ietf.secr.meetings.views.times', meeting_id=meeting_id,schedule_name=schedule_name) - form = TimeSlotForm(request.POST, meeting=meeting) + # Pass "initial" even for a POST so the choices initialize correctly if day is outside + # the standard set of options. See TimeSlotForm.get_day_choices(). + form = TimeSlotForm(request.POST, initial=initial, meeting=meeting) if form.is_valid(): day = form.cleaned_data['day'] time = get_timeslot_time(form, meeting) @@ -836,13 +853,6 @@ def times_edit(request, meeting_id, schedule_name, time): else: # we need to pass the session to the form in order to disallow changing # of group after materials have been uploaded - day = dtime.strftime('%w') - if day == 6: - day = -1 - initial = {'day':day, - 'time':dtime.strftime('%H:%M'), - 'duration':timeslots.first().duration, - 'name':timeslots.first().name} form = TimeSlotForm(initial=initial, meeting=meeting) return render(request, 'meetings/times_edit.html', { @@ -860,7 +870,7 @@ def times_delete(request, meeting_id, schedule_name, time): meeting = get_object_or_404(Meeting, number=meeting_id) parts = [ int(x) for x in time.split(':') ] - dtime = datetime.datetime(*parts) + dtime = make_aware(datetime.datetime(*parts), meeting.tz()) status = SessionStatusName.objects.get(slug='schedw') if request.method == 'POST' and request.POST['post'] == 'yes': diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 505bcdcc7..3961f907f 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -9,6 +9,7 @@ This module contains all the functions for generating static proceedings pages ''' import datetime import os +import pytz import re import subprocess from urllib.parse import urlencode @@ -24,6 +25,7 @@ from ietf.meeting.models import Meeting, SessionPresentation, TimeSlot, SchedTim from ietf.person.models import Person from ietf.utils.log import log from ietf.utils.mail import send_mail +from ietf.utils.timezone import make_aware AUDIO_FILE_RE = re.compile(r'ietf(?P<number>[\d]+)-(?P<room>.*)-(?P<time>[\d]{8}-[\d]{4})') VIDEO_TITLE_RE = re.compile(r'IETF(?P<number>[\d]+)-(?P<name>.*)-(?P<date>\d{8})-(?P<time>\d{4})') @@ -32,7 +34,7 @@ VIDEO_TITLE_RE = re.compile(r'IETF(?P<number>[\d]+)-(?P<name>.*)-(?P<date>\d{8}) def _get_session(number,name,date,time): '''Lookup session using data from video title''' meeting = Meeting.objects.get(number=number) - timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M') + timeslot_time = make_aware(datetime.datetime.strptime(date + time,'%Y%m%d%H%M'), meeting.tz()) try: assignment = SchedTimeSessAssignment.objects.get( schedule__in = [meeting.schedule, meeting.schedule.base], @@ -102,7 +104,7 @@ def get_timeslot_for_filename(filename): try: meeting = Meeting.objects.get(number=match.groupdict()['number']) room_mapping = {normalize_room_name(room.name): room.name for room in meeting.room_set.all()} - time = datetime.datetime.strptime(match.groupdict()['time'],'%Y%m%d-%H%M') + time = make_aware(datetime.datetime.strptime(match.groupdict()['time'],'%Y%m%d-%H%M'), meeting.tz()) slots = TimeSlot.objects.filter( meeting=meeting, location__name=room_mapping[match.groupdict()['room']], @@ -201,17 +203,22 @@ def send_audio_import_warning(unmatched_files): # End Recording Functions # ------------------------------------------------- -def get_progress_stats(sdate,edate): +def get_progress_stats(sdate, edate): ''' This function takes a date range and produces a dictionary of statistics / objects for use in a progress report. Generally the end date will be the date of the last meeting and the start date will be the date of the meeting before that. + + Data between midnight UTC on the specified dates are included in the stats. ''' + sdatetime = pytz.utc.localize(datetime.datetime.combine(sdate, datetime.time())) + edatetime = pytz.utc.localize(datetime.datetime.combine(edate, datetime.time())) + data = {} data['sdate'] = sdate data['edate'] = edate - events = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lt=edate) + events = DocEvent.objects.filter(doc__type='draft', time__gte=sdatetime, time__lt=edatetime) data['actions_count'] = events.filter(type='iesg_approved').count() data['last_calls_count'] = events.filter(type='sent_last_call').count() @@ -226,7 +233,7 @@ def get_progress_stats(sdate,edate): data['updated_drafts_count'] = len(set([ e.doc_id for e in update_events ])) # Calculate Final Four Weeks stats (ffw) - ffwdate = edate - datetime.timedelta(days=28) + ffwdate = edatetime - datetime.timedelta(days=28) ffw_new_count = events.filter(time__gte=ffwdate,newrevisiondocevent__rev='00').count() try: ffw_new_percent = format(ffw_new_count / float(data['new_drafts_count']),'.0%') @@ -257,14 +264,14 @@ def get_progress_stats(sdate,edate): data['new_groups'] = Group.objects.filter( type='wg', groupevent__changestategroupevent__state='active', - groupevent__time__gte=sdate, - groupevent__time__lt=edate) + groupevent__time__gte=sdatetime, + groupevent__time__lt=edatetime) data['concluded_groups'] = Group.objects.filter( type='wg', groupevent__changestategroupevent__state='conclude', - groupevent__time__gte=sdate, - groupevent__time__lt=edate) + groupevent__time__gte=sdatetime, + groupevent__time__lt=edatetime) return data diff --git a/ietf/secr/proceedings/reports.py b/ietf/secr/proceedings/reports.py index 115e4facd..fd360774b 100644 --- a/ietf/secr/proceedings/reports.py +++ b/ietf/secr/proceedings/reports.py @@ -1,20 +1,23 @@ import datetime from django.template.loader import render_to_string +from django.utils import timezone from ietf.meeting.models import Meeting from ietf.doc.models import DocEvent, Document from ietf.secr.proceedings.proc_utils import get_progress_stats +from ietf.utils.timezone import datetime_from_date + def report_id_activity(start,end): # get previous meeting - meeting = Meeting.objects.filter(date__lt=datetime.datetime.now(),type='ietf').order_by('-date')[0] + meeting = Meeting.objects.filter(date__lt=timezone.now(),type='ietf').order_by('-date')[0] syear,smonth,sday = start.split('-') eyear,emonth,eday = end.split('-') - sdate = datetime.datetime(int(syear),int(smonth),int(sday)) - edate = datetime.datetime(int(eyear),int(emonth),int(eday)) - + sdate = datetime_from_date(datetime.date(int(syear),int(smonth),int(sday)), meeting.tz()) + edate = datetime_from_date(datetime.date(int(eyear),int(emonth),int(eday)), meeting.tz()) + #queryset = Document.objects.filter(type='draft').annotate(start_date=Min('docevent__time')) new_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision', docevent__newrevisiondocevent__rev='00', @@ -44,7 +47,7 @@ def report_id_activity(start,end): approved = events.filter(type='iesg_approved').count() # get 4 weeks - monday = Meeting.get_current_meeting().get_ietf_monday() + monday = datetime_from_date(Meeting.get_current_meeting().get_ietf_monday(), meeting.tz()) cutoff = monday + datetime.timedelta(days=3) ff1_date = cutoff - datetime.timedelta(days=28) #ff2_date = cutoff - datetime.timedelta(days=21) diff --git a/ietf/secr/proceedings/templatetags/ams_filters.py b/ietf/secr/proceedings/templatetags/ams_filters.py index ebf166e3c..d5c0837a2 100644 --- a/ietf/secr/proceedings/templatetags/ams_filters.py +++ b/ietf/secr/proceedings/templatetags/ams_filters.py @@ -38,20 +38,6 @@ def display_duration(value): x=int(value) return "%d Hours %d Minutes %d Seconds"%(x//3600,(x%3600)//60,x%60) -@register.filter -def get_published_date(doc): - ''' - Returns the published date for a RFC Document - ''' - event = doc.latest_event(type='published_rfc') - if event: - return event.time - event = doc.latest_event(type='new_revision') - if event: - return event.time - else: - return None - @register.filter def is_ppt(value): ''' diff --git a/ietf/secr/proceedings/tests.py b/ietf/secr/proceedings/tests.py index 9d3f3d27b..22ede4c8c 100644 --- a/ietf/secr/proceedings/tests.py +++ b/ietf/secr/proceedings/tests.py @@ -50,8 +50,9 @@ class VideoRecordingTestCase(TestCase): meeting = session.meeting number = meeting.number name = session.group.acronym - date = session.official_timeslotassignment().timeslot.time.strftime('%Y%m%d') - time = session.official_timeslotassignment().timeslot.time.strftime('%H%M') + ts_time = session.official_timeslotassignment().timeslot.local_start_time() + date = ts_time.strftime('%Y%m%d') + time = ts_time.strftime('%H%M') self.assertEqual(_get_session(number,name,date,time),session) def test_get_urls_from_json(self): @@ -113,7 +114,7 @@ class RecordingTestCase(TestCase): return "{prefix}-{room}-{date}.mp3".format( prefix=timeslot.meeting.type.slug + timeslot.meeting.number, room=normalize_room_name(timeslot.location.name), - date=timeslot.time.strftime('%Y%m%d-%H%M')) + date=timeslot.local_start_time().strftime('%Y%m%d-%H%M')) def test_import_audio_files_shared_timeslot(self): meeting = MeetingFactory(type_id='ietf',number='72') diff --git a/ietf/secr/proceedings/tests_reports.py b/ietf/secr/proceedings/tests_reports.py index 6039cfced..034dbe5fa 100644 --- a/ietf/secr/proceedings/tests_reports.py +++ b/ietf/secr/proceedings/tests_reports.py @@ -1,6 +1,8 @@ import datetime import debug # pyflakes:ignore +from django.utils import timezone + from ietf.doc.factories import DocumentFactory,NewRevisionDocEventFactory from ietf.secr.proceedings.reports import report_id_activity, report_progress_report from ietf.utils.test_utils import TestCase @@ -10,7 +12,7 @@ class ReportsTestCase(TestCase): def test_report_id_activity(self): - today = datetime.datetime.today() + today = timezone.now() yesterday = today - datetime.timedelta(days=1) last_quarter = today - datetime.timedelta(days=3*30) next_week = today+datetime.timedelta(days=7) @@ -24,7 +26,7 @@ class ReportsTestCase(TestCase): self.assertTrue('IETF Activity since last IETF Meeting' in result) def test_report_progress_report(self): - today = datetime.datetime.today() + today = timezone.now() last_quarter = today - datetime.timedelta(days=3*30) next_week = today+datetime.timedelta(days=7) diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py index b08375176..e88a20196 100644 --- a/ietf/secr/proceedings/views.py +++ b/ietf/secr/proceedings/views.py @@ -27,6 +27,7 @@ from ietf.meeting.utils import add_event_info_to_session_qs from ietf.secr.proceedings.forms import RecordingForm, RecordingEditForm from ietf.secr.proceedings.proc_utils import (create_recording) +from ietf.utils.timezone import date_today # ------------------------------------------------- # Globals @@ -154,7 +155,7 @@ def main(request): meetings = Meeting.objects.filter(type='ietf').order_by('-number') else: # select meetings still within the cutoff period - today = datetime.date.today() + today = date_today() meetings = [m for m in Meeting.objects.filter(type='ietf').order_by('-number') if m.get_submission_correction_date()>=today] groups = get_my_groups(request.user) @@ -165,7 +166,7 @@ def main(request): m.group = m.session_set.first().group # we today's date to see if we're past the submissio cutoff - today = datetime.date.today() + today = date_today() return render(request, 'proceedings/main.html',{ 'meetings': meetings, @@ -304,7 +305,7 @@ def select(request, meeting_num): # get the time proceedings were generated path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html') if os.path.exists(path): - last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path)) + last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path), datetime.timezone.utc) else: last_run = None diff --git a/ietf/secr/sreq/templatetags/ams_filters.py b/ietf/secr/sreq/templatetags/ams_filters.py index 125acdeda..3ef872232 100644 --- a/ietf/secr/sreq/templatetags/ams_filters.py +++ b/ietf/secr/sreq/templatetags/ams_filters.py @@ -42,20 +42,6 @@ def display_duration(value): else: return "%d Hours %d Minutes %d Seconds"%(value//3600,(value%3600)//60,value%60) -@register.filter -def get_published_date(doc): - ''' - Returns the published date for a RFC Document - ''' - event = doc.latest_event(type='published_rfc') - if event: - return event.time - event = doc.latest_event(type='new_revision') - if event: - return event.time - else: - return None - @register.filter def is_ppt(value): ''' diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index fabbc0807..7f6707b4d 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -16,6 +16,8 @@ from ietf.name.models import ConstraintName, TimerangeName from ietf.person.models import Person from ietf.secr.sreq.forms import SessionForm from ietf.utils.mail import outbox, empty_outbox, get_payload_text +from ietf.utils.timezone import date_today + from pyquery import PyQuery @@ -23,7 +25,7 @@ SECR_USER='secretary' class SreqUrlTests(TestCase): def test_urls(self): - MeetingFactory(type_id='ietf',date=datetime.date.today()) + MeetingFactory(type_id='ietf',date=date_today()) self.client.login(username="secretary", password="secretary+password") @@ -39,7 +41,7 @@ class SreqUrlTests(TestCase): class SessionRequestTestCase(TestCase): def test_main(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) SessionFactory.create_batch(2, meeting=meeting, status_id='sched') SessionFactory.create_batch(2, meeting=meeting, status_id='disappr') # An additional unscheduled group comes from make_immutable_base_data @@ -53,7 +55,7 @@ class SessionRequestTestCase(TestCase): self.assertEqual(len(unsched), 11) def test_approve(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) ad = Person.objects.get(user__username='ad') area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group mars = GroupFactory(parent=area, acronym='mars') @@ -66,7 +68,7 @@ class SessionRequestTestCase(TestCase): self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'appr') def test_cancel(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) ad = Person.objects.get(user__username='ad') area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group session = SessionFactory(meeting=meeting, group__parent=area, group__acronym='mars', status_id='sched') @@ -77,7 +79,7 @@ class SessionRequestTestCase(TestCase): self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted') def test_edit(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group group2 = GroupFactory() group3 = GroupFactory() @@ -242,7 +244,7 @@ class SessionRequestTestCase(TestCase): def test_edit_constraint_bethere(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group session = SessionFactory(meeting=meeting, group=mars, status_id='sched') Constraint.objects.create( @@ -309,7 +311,7 @@ class SessionRequestTestCase(TestCase): def test_edit_inactive_conflicts(self): """Inactive conflicts should be displayed and removable""" - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), group_conflicts=['chair_conflict']) + meeting = MeetingFactory(type_id='ietf', date=date_today(), group_conflicts=['chair_conflict']) mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group session = SessionFactory(meeting=meeting, group=mars, status_id='sched') other_group = GroupFactory() @@ -367,7 +369,7 @@ class SessionRequestTestCase(TestCase): self.assertEqual(len(mars.constraint_source_set.filter(name_id='conflict')), 0) def test_tool_status(self): - MeetingFactory(type_id='ietf', date=datetime.date.today()) + MeetingFactory(type_id='ietf', date=date_today()) url = reverse('ietf.secr.sreq.views.tool_status') self.client.login(username="secretary", password="secretary+password") r = self.client.get(url) @@ -381,7 +383,7 @@ class SessionRequestTestCase(TestCase): Relies on SessionForm representing constraint values with element IDs like id_constraint_<ConstraintName slug> """ - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars') url = reverse('ietf.secr.sreq.views.new', kwargs=dict(acronym='mars')) self.client.login(username="marschairman", password="marschairman+password") @@ -404,7 +406,7 @@ class SessionRequestTestCase(TestCase): def test_edit_req_constraint_types(self): """Editing a request constraint should show the expected constraints""" - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) SessionFactory(group__acronym='mars', status_id='schedw', meeting=meeting, @@ -438,7 +440,7 @@ class SubmitRequestCase(TestCase): MeetingFactory.reset_sequence(0) def test_submit_request(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) ad = Person.objects.get(user__username='ad') area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group group = GroupFactory(parent=area) @@ -506,7 +508,7 @@ class SubmitRequestCase(TestCase): self.assertEqual(list(session.joint_with_groups.all()), [group3, group4]) def test_submit_request_invalid(self): - MeetingFactory(type_id='ietf', date=datetime.date.today()) + MeetingFactory(type_id='ietf', date=date_today()) ad = Person.objects.get(user__username='ad') area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group group = GroupFactory(parent=area) @@ -542,8 +544,8 @@ class SubmitRequestCase(TestCase): self.assertContains(r, 'Must provide data for all sessions') def test_submit_request_check_constraints(self): - m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100)) - MeetingFactory(type_id='ietf', date=datetime.date.today(), + m1 = MeetingFactory(type_id='ietf', date=date_today() - datetime.timedelta(days=100)) + MeetingFactory(type_id='ietf', date=date_today(), group_conflicts=['chair_conflict', 'conflic2', 'conflic3']) ad = Person.objects.get(user__username='ad') area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group @@ -604,7 +606,7 @@ class SubmitRequestCase(TestCase): self.assertContains(r, "Cannot declare a conflict with the same group") def test_request_notification(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) ad = Person.objects.get(user__username='ad') area = GroupFactory(type_id='area') RoleFactory(name_id='ad', person=ad, group=area) @@ -699,7 +701,7 @@ class SubmitRequestCase(TestCase): self.assertNotIn('The third session requires your approval', notification_payload) def test_request_notification_third_session(self): - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) + meeting = MeetingFactory(type_id='ietf', date=date_today()) ad = Person.objects.get(user__username='ad') area = GroupFactory(type_id='area') RoleFactory(name_id='ad', person=ad, group=area) @@ -807,7 +809,7 @@ class SubmitRequestCase(TestCase): class LockAppTestCase(TestCase): def setUp(self): super().setUp() - self.meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(),session_request_lock_message='locked') + self.meeting = MeetingFactory(type_id='ietf', date=date_today(),session_request_lock_message='locked') self.group = GroupFactory(acronym='mars') RoleFactory(name_id='chair', group=self.group, person__user__username='marschairman') SessionFactory(group=self.group,meeting=self.meeting) @@ -853,7 +855,7 @@ class LockAppTestCase(TestCase): class NotMeetingCase(TestCase): def test_not_meeting(self): - MeetingFactory(type_id='ietf',date=datetime.date.today()) + MeetingFactory(type_id='ietf',date=date_today()) group = GroupFactory(acronym='mars') url = reverse('ietf.secr.sreq.views.no_session',kwargs={'acronym':group.acronym}) self.client.login(username="secretary", password="secretary+password") diff --git a/ietf/secr/telechat/tests.py b/ietf/secr/telechat/tests.py index f664745d7..e4661b767 100644 --- a/ietf/secr/telechat/tests.py +++ b/ietf/secr/telechat/tests.py @@ -14,6 +14,7 @@ from ietf.doc.factories import (WgDraftFactory, IndividualRfcFactory, CharterFac from ietf.doc.models import BallotDocEvent, BallotType, BallotPositionDocEvent, State, Document from ietf.doc.utils import update_telechat, create_ballot_if_not_open from ietf.utils.test_utils import TestCase +from ietf.utils.timezone import date_today, datetime_today from ietf.iesg.models import TelechatDate from ietf.person.models import Person from ietf.person.factories import PersonFactory @@ -22,7 +23,7 @@ from ietf.secr.telechat.views import get_next_telechat_date SECR_USER='secretary' def augment_data(): - TelechatDate.objects.create(date=datetime.datetime.today()) + TelechatDate.objects.create(date=date_today()) class SecrTelechatTestCase(TestCase): def test_main(self): @@ -119,7 +120,7 @@ class SecrTelechatTestCase(TestCase): def test_doc_detail_charter(self): by=Person.objects.get(name="(System)") charter = CharterFactory(states=[('charter','intrev')]) - last_week = datetime.date.today()-datetime.timedelta(days=7) + last_week = datetime_today()-datetime.timedelta(days=7) BallotDocEvent.objects.create(type='created_ballot',by=by,doc=charter, rev=charter.rev, ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'), time=last_week) @@ -138,7 +139,7 @@ class SecrTelechatTestCase(TestCase): self.assertEqual(q("#telechat-positions-table").find("th:contains('No Record')").length,1) def test_bash(self): - today = datetime.datetime.today() + today = date_today() TelechatDate.objects.create(date=today) url = reverse('ietf.secr.telechat.views.bash',kwargs={'date':today.strftime('%Y-%m-%d')}) self.client.login(username="secretary", password="secretary+password") @@ -148,7 +149,7 @@ class SecrTelechatTestCase(TestCase): def test_doc_detail_post_update_ballot(self): by=Person.objects.get(name="(System)") charter = CharterFactory(states=[('charter','intrev')]) - last_week = datetime.date.today()-datetime.timedelta(days=7) + last_week = datetime_today()-datetime.timedelta(days=7) BallotDocEvent.objects.create(type='created_ballot',by=by,doc=charter, rev=charter.rev, ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'), time=last_week) @@ -186,7 +187,7 @@ class SecrTelechatTestCase(TestCase): def test_doc_detail_post_update_state(self): by=Person.objects.get(name="(System)") charter = CharterFactory(states=[('charter','intrev')]) - last_week = datetime.date.today()-datetime.timedelta(days=7) + last_week = datetime_today()-datetime.timedelta(days=7) BallotDocEvent.objects.create(type='created_ballot',by=by,doc=charter, rev=charter.rev, ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'), time=last_week) @@ -214,7 +215,7 @@ class SecrTelechatTestCase(TestCase): ad=Person.objects.get(user__username='ad'), authors=PersonFactory.create_batch(3), ) - last_week = datetime.date.today()-datetime.timedelta(days=7) + last_week = datetime_today()-datetime.timedelta(days=7) BallotDocEvent.objects.create(type='created_ballot',by=by,doc=draft, rev=draft.rev, ballot_type=BallotType.objects.get(doc_type=draft.type,slug='approve'), time=last_week) diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py index b3f064fa1..4dd4fba4e 100644 --- a/ietf/secr/telechat/views.py +++ b/ietf/secr/telechat/views.py @@ -20,6 +20,8 @@ from ietf.iesg.models import TelechatDate, TelechatAgendaItem, Telechat from ietf.iesg.agenda import agenda_data, get_doc_section from ietf.ietfauth.utils import role_required from ietf.secr.telechat.forms import BallotForm, ChangeStateForm, DateSelectForm, TELECHAT_TAGS +from ietf.utils.timezone import date_today + ''' EXPECTED CHANGES: @@ -80,14 +82,14 @@ def get_last_telechat_date(): This function returns the date of the last telechat Tried TelechatDocEvent.objects.latest but that will return today's telechat ''' - return TelechatDate.objects.filter(date__lt=datetime.date.today()).order_by('-date')[0].date + return TelechatDate.objects.filter(date__lt=date_today()).order_by('-date')[0].date #return '2011-11-01' # uncomment for testing def get_next_telechat_date(): ''' This function returns the date of the next telechat ''' - return TelechatDate.objects.filter(date__gte=datetime.date.today()).order_by('date')[0].date + return TelechatDate.objects.filter(date__gte=date_today()).order_by('date')[0].date def get_section_header(doc, agenda): ''' diff --git a/ietf/secr/templates/meetings/misc_sessions.html b/ietf/secr/templates/meetings/misc_sessions.html index 9f9eb0f7a..2ffa1ef9e 100644 --- a/ietf/secr/templates/meetings/misc_sessions.html +++ b/ietf/secr/templates/meetings/misc_sessions.html @@ -1,7 +1,7 @@ {% extends "meetings/base_rooms_times.html" %} -{% load agenda_custom_tags %} +{% load agenda_custom_tags tz %} {% block subsection %} - +{% timezone meeting.time_zone %} <div class="module"> <h2>TimeSlots</h2> @@ -73,7 +73,7 @@ </div> <!-- module --> - +{% endtimezone %} {% endblock %} {% block extrahead %} diff --git a/ietf/secr/templates/meetings/regular_session_edit.html b/ietf/secr/templates/meetings/regular_session_edit.html index 479cef8af..0bf6453f5 100644 --- a/ietf/secr/templates/meetings/regular_session_edit.html +++ b/ietf/secr/templates/meetings/regular_session_edit.html @@ -1,5 +1,5 @@ {% extends "base_site.html" %} -{% load staticfiles %} +{% load staticfiles tz %} {% block title %}Meetings{% endblock %} @@ -19,7 +19,7 @@ <form id="meetings-schedule-form" method="post">{% csrf_token %} <div class="inline-related{% if forloop.last %} last-related{% endif %}"> - <table class="full-width amstable"> + <table class="full-width amstable">{% timezone meeting.time_zone %} <tbody> <tr> <th scope="row">Day:</th> @@ -40,7 +40,7 @@ <!-- [html-validate-disable-block element-required-attributes -- FIXME: as_table renders without scope] --> {{ form.as_table }} </tbody> - </table> + {% endtimezone %}</table> </div> <!-- inline-related --> <div class="button-group"> diff --git a/ietf/secr/templates/meetings/session_schedule_notification.txt b/ietf/secr/templates/meetings/session_schedule_notification.txt index 95cbf3535..5ad129f70 100644 --- a/ietf/secr/templates/meetings/session_schedule_notification.txt +++ b/ietf/secr/templates/meetings/session_schedule_notification.txt @@ -1,4 +1,4 @@ -Dear {{ to_name }}, +{% load tz %}{% timezone meeting.time_zone %}Dear {{ to_name }}, The session(s) that you have requested have been scheduled. Below is the scheduled session information followed by @@ -16,4 +16,4 @@ iCalendar: {{ baseurl }}{% url "ietf.meeting.views.agenda_ical" num=meeting.numb Request Information: -{% include "includes/session_info.txt" %} +{% include "includes/session_info.txt" %}{% endtimezone %} diff --git a/ietf/secr/templates/meetings/sessions.html b/ietf/secr/templates/meetings/sessions.html index 03b4d8489..e774d83ca 100644 --- a/ietf/secr/templates/meetings/sessions.html +++ b/ietf/secr/templates/meetings/sessions.html @@ -1,5 +1,5 @@ {% extends "meetings/base_rooms_times.html" %} -{% load django_bootstrap5 %} +{% load django_bootstrap5 tz %} {% block subsection %} @@ -18,7 +18,7 @@ <th scope="col"></th> </tr> </thead> - <tbody> + <tbody>{% timezone meeting.time_zone %} {% for session in sessions %} <tr> <td>{{ session.group.acronym }}</td> @@ -46,7 +46,7 @@ </td> </tr> {% endfor %} - </tbody> + {% endtimezone %}</tbody> </table> </div> <!-- module --> diff --git a/ietf/secr/templates/meetings/times.html b/ietf/secr/templates/meetings/times.html index 0ec175f03..559e2fd77 100644 --- a/ietf/secr/templates/meetings/times.html +++ b/ietf/secr/templates/meetings/times.html @@ -1,5 +1,5 @@ {% extends "meetings/base_rooms_times.html" %} - +{% load tz %} {% block subsection %} <div class="module"> @@ -16,7 +16,7 @@ <th scope="col"></th> </tr> </thead> - <tbody> + <tbody>{% timezone meeting.time_zone %} {% for item in times %} <tr class="{% cycle 'row1' 'row2' %}"> <td>{{ item.time|date:"D M d" }}</td> @@ -26,7 +26,7 @@ <td><a href="{% url "ietf.secr.meetings.views.times_delete" meeting_id=meeting.number schedule_name=schedule.name time=item.time|date:"Y:m:d:H:i" %}">Delete</a></td> </tr> {% endfor %} - </tbody> + {% endtimezone %}</tbody> </table> {% else %} <h3>No timeslots exist for this meeting. Add rooms with the "duplicate timeslots" option enabled to copy timeslots from the last meeting.</h3> diff --git a/ietf/secr/templates/proceedings/recording.html b/ietf/secr/templates/proceedings/recording.html index 906218f97..964e181ea 100755 --- a/ietf/secr/templates/proceedings/recording.html +++ b/ietf/secr/templates/proceedings/recording.html @@ -1,5 +1,5 @@ {% extends "base_site.html" %} -{% load staticfiles %} +{% load staticfiles tz %} {% block title %}Proceedings{% endblock %} @@ -59,7 +59,7 @@ </tr> </thead> {% if sessions %} - <tbody> + <tbody>{% timezone meeting.time_zone %} {% for session in sessions %} {% if session.recordings %} @@ -84,7 +84,7 @@ {% endfor %} - </tbody> + {% endtimezone %}</tbody> {% endif %} </table> </div> <!-- inline-group --> diff --git a/ietf/settings.py b/ietf/settings.py index 5e4912d85..aa35aa8e3 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -1041,8 +1041,6 @@ CHAT_URL_PATTERN = 'https://zulip.ietf.org/#narrow/stream/{chat_room_name}' # If we need to revert to xmpp # CHAT_ARCHIVE_URL_PATTERN = 'https://www.ietf.org/jabber/logs/{chat_room_name}?C=M;O=D' -PRODUCTION_TIMEZONE = "America/Los_Angeles" - PYFLAKES_DEFAULT_ARGS= ["ietf", ] VULTURE_DEFAULT_ARGS= ["ietf", ] @@ -1189,8 +1187,6 @@ CELERY_TIMEZONE = 'UTC' CELERY_BROKER_URL = 'amqp://mq/' CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' CELERY_BEAT_SYNC_EVERY = 1 # update DB after every event -assert not USE_TZ, 'Drop DJANGO_CELERY_BEAT_TZ_AWARE setting once USE_TZ is True!' -DJANGO_CELERY_BEAT_TZ_AWARE = False # Meetecho API setup: Uncomment this and provide real credentials to enable # Meetecho conference creation for interim session requests diff --git a/ietf/stats/management/commands/fetch_meeting_attendance.py b/ietf/stats/management/commands/fetch_meeting_attendance.py index 5078c7cee..7f936531d 100644 --- a/ietf/stats/management/commands/fetch_meeting_attendance.py +++ b/ietf/stats/management/commands/fetch_meeting_attendance.py @@ -1,10 +1,10 @@ # Copyright The IETF Trust 2017-2019, All Rights Reserved # Copyright 2016 IETF Trust -import datetime import syslog from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone import debug # pyflakes:ignore @@ -32,7 +32,7 @@ class Command(BaseCommand): elif options['all']: meetings = Meeting.objects.filter(type="ietf").order_by("date") elif options['latest']: - meetings = Meeting.objects.filter(type="ietf", date__lte=datetime.datetime.today()).order_by("-date")[:options['latest']] + meetings = Meeting.objects.filter(type="ietf", date__lte=timezone.now()).order_by("-date")[:options['latest']] else: raise CommandError("Please use one of --meeting, --all or --latest") diff --git a/ietf/stats/tests.py b/ietf/stats/tests.py index 32a0dfea8..dae352752 100644 --- a/ietf/stats/tests.py +++ b/ietf/stats/tests.py @@ -13,6 +13,7 @@ from requests import Response import debug # pyflakes:ignore from django.urls import reverse as urlreverse +from django.utils import timezone from ietf.utils.test_utils import login_testing_unauthorized, TestCase import ietf.stats.views @@ -29,6 +30,7 @@ from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, from ietf.stats.models import MeetingRegistration, CountryAlias from ietf.stats.factories import MeetingRegistrationFactory from ietf.stats.utils import get_meeting_registration_data +from ietf.utils.timezone import date_today class StatisticsTests(TestCase): @@ -64,7 +66,7 @@ class StatisticsTests(TestCase): Document.objects.filter(pk=draft.pk).update(words=4000) # move it back so it shows up in the yearly summaries NewRevisionDocEvent.objects.filter(doc=draft, rev=draft.rev).update( - time=datetime.datetime.now() - datetime.timedelta(days=500)) + time=timezone.now() - datetime.timedelta(days=500)) referencing_draft = Document.objects.create( name="draft-ietf-mars-referencing", @@ -89,7 +91,7 @@ class StatisticsTests(TestCase): doc=referencing_draft, desc="New revision available", rev=referencing_draft.rev, - time=datetime.datetime.now() - datetime.timedelta(days=1000) + time=timezone.now() - datetime.timedelta(days=1000) ) @@ -122,7 +124,7 @@ class StatisticsTests(TestCase): def test_meeting_stats(self): # create some data for the statistics - meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), number="96") + meeting = MeetingFactory(type_id='ietf', date=date_today(), number="96") MeetingRegistrationFactory(first_name='John', last_name='Smith', country_code='US', email="john.smith@example.us", meeting=meeting, attended=True) CountryAlias.objects.get_or_create(alias="US", country=CountryName.objects.get(slug="US")) p = MeetingRegistrationFactory(first_name='Jaume', last_name='Guillaume', country_code='FR', email="jaume.guillaume@example.fr", meeting=meeting, attended=False).person @@ -198,7 +200,7 @@ class StatisticsTests(TestCase): self.assertTrue(q('.review-stats td:contains("1")')) # check stacked chart - expected_date = datetime.date.today().replace(day=1) + expected_date = date_today().replace(day=1) expected_js_timestamp = calendar.timegm(expected_date.timetuple()) * 1000 url = urlreverse(ietf.stats.views.review_stats, kwargs={ "stats_type": "time" }) url += "?team={}".format(review_req.team.acronym) diff --git a/ietf/stats/views.py b/ietf/stats/views.py index 1667387bc..09a25b47b 100644 --- a/ietf/stats/views.py +++ b/ietf/stats/views.py @@ -18,6 +18,7 @@ from django.db.models import Count, Q from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse as urlreverse +from django.utils import timezone from django.utils.safestring import mark_safe from django.utils.text import slugify @@ -39,6 +40,8 @@ from ietf.stats.models import MeetingRegistration, CountryAlias from ietf.stats.utils import get_aliased_affiliations, get_aliased_countries, compute_hirsch_index from ietf.ietfauth.utils import has_role from ietf.utils.response import permission_denied +from ietf.utils.timezone import date_today, DEADLINE_TZINFO, RPC_TZINFO + def stats_index(request): return render(request, "stats/index.html") @@ -196,7 +199,7 @@ def document_stats(request, stats_type=None): if "y" in time_choice: try: y = int(time_choice.rstrip("y")) - from_time = datetime.datetime.today() - dateutil.relativedelta.relativedelta(years=y) + from_time = timezone.now() - dateutil.relativedelta.relativedelta(years=y) except ValueError: pass @@ -622,8 +625,9 @@ def document_stats(request, stats_type=None): type__in=["published_rfc", "new_revision"], ).values_list("doc", "time").order_by("doc") - for doc, time in docevent_qs.iterator(): - doc_years[doc].add(time.year) + for doc_id, time in docevent_qs.iterator(): + # RPC_TZINFO is used to match the timezone handling in Document.pub_date() + doc_years[doc_id].add(time.astimezone(RPC_TZINFO).year) person_qs = Person.objects.filter(person_filters) @@ -637,7 +641,7 @@ def document_stats(request, stats_type=None): template_name = "yearly" years_from = from_time.year if from_time else 1 - years_to = datetime.date.today().year - 1 + years_to = timezone.now().year - 1 if stats_type == "yearly/affiliation": @@ -905,7 +909,7 @@ def meeting_stats(request, num=None, stats_type=None): continents = {} - meetings = Meeting.objects.filter(type='ietf', date__lte=datetime.date.today()).order_by('number') + meetings = Meeting.objects.filter(type='ietf', date__lte=date_today()).order_by('number') for m in meetings: country = CountryName.objects.get(slug=m.country) continents[country.continent.name] = country.continent.name @@ -1070,12 +1074,12 @@ def review_stats(request, stats_type=None, acronym=None): except ValueError: return None - today = datetime.date.today() + today = date_today(DEADLINE_TZINFO) from_date = parse_date(request.GET.get("from")) or today - dateutil.relativedelta.relativedelta(years=1) to_date = parse_date(request.GET.get("to")) or today - from_time = datetime.datetime.combine(from_date, datetime.time.min) - to_time = datetime.datetime.combine(to_date, datetime.time.max) + from_time = datetime.datetime.combine(from_date, datetime.time.min, tzinfo=DEADLINE_TZINFO) + to_time = datetime.datetime.combine(to_date, datetime.time.max, tzinfo=DEADLINE_TZINFO) # teams/reviewers teams = list(Group.objects.exclude(reviewrequest=None).distinct().order_by("name")) diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 34f568d2e..4715f9bb5 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -7,7 +7,6 @@ import os import re import datetime import email -import pytz import sys import tempfile import xml2rfc @@ -21,6 +20,7 @@ from django import forms from django.conf import settings from django.utils.html import mark_safe, format_html # type:ignore from django.urls import reverse as urlreverse +from django.utils import timezone from django.utils.encoding import force_str import debug # pyflakes:ignore @@ -42,6 +42,7 @@ from ietf.submit.parsers.xml_parser import XMLParser from ietf.utils import log from ietf.utils.draft import PlaintextDraft from ietf.utils.text import normalize_text +from ietf.utils.timezone import date_today from ietf.utils.xmldraft import XMLDraft, XMLParseError @@ -79,7 +80,7 @@ class SubmissionBaseUploadForm(forms.Form): self.base_formats = None # None will raise an exception in clean() if this isn't changed in a subclass def set_cutoff_warnings(self): - now = datetime.datetime.now(pytz.utc) + now = timezone.now() meeting = Meeting.get_current_meeting() if not meeting: return @@ -155,7 +156,7 @@ class SubmissionBaseUploadForm(forms.Form): raise forms.ValidationError('The submission tool is currently shut down') # check general submission rate thresholds before doing any more work - today = datetime.date.today() + today = date_today() self.check_submissions_thresholds( "for the same submitter", dict(remote_ip=self.remote_ip, submission_date=today), @@ -572,7 +573,7 @@ class DeprecatedSubmissionBaseUploadForm(SubmissionBaseUploadForm): raise forms.ValidationError(mark_safe(self.cutoff_warning)) # check thresholds - today = datetime.date.today() + today = date_today() self.check_submissions_thresholds( "for the draft %s" % self.filename, diff --git a/ietf/submit/mail.py b/ietf/submit/mail.py index 16d1f0973..81d2233af 100644 --- a/ietf/submit/mail.py +++ b/ietf/submit/mail.py @@ -4,7 +4,6 @@ import re import email -import datetime import base64 import os import pyzmail @@ -28,6 +27,8 @@ from ietf.utils.accesstoken import generate_access_token from ietf.mailtrigger.utils import gather_address_lists, get_base_submission_message_address from ietf.submit.models import SubmissionEmailEvent, Submission from ietf.submit.checkers import DraftIdnitsChecker +from ietf.utils.timezone import date_today + def send_submission_confirmation(request, submission, chair_notice=False): subject = 'Confirm submission of I-D %s' % submission.name @@ -299,7 +300,7 @@ def add_submission_email(request, remote_ip, name, rev, submission_pk, message, rev=rev, title=name, note="", - submission_date=datetime.date.today(), + submission_date=date_today(), replaces="", ) from ietf.submit.utils import create_submission_event, docevent_from_submission diff --git a/ietf/submit/migrations/0011_use_timezone_now_for_submit_models.py b/ietf/submit/migrations/0011_use_timezone_now_for_submit_models.py new file mode 100644 index 000000000..69d40bf8a --- /dev/null +++ b/ietf/submit/migrations/0011_use_timezone_now_for_submit_models.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.28 on 2022-07-12 11:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('submit', '0010_create_cancel_stale_submissions_task'), + ] + + operations = [ + migrations.AlterField( + model_name='preapproval', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='submissioncheck', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='submissionevent', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/ietf/submit/migrations/0012_use_date_today_helper.py b/ietf/submit/migrations/0012_use_date_today_helper.py new file mode 100644 index 000000000..3a02552f6 --- /dev/null +++ b/ietf/submit/migrations/0012_use_date_today_helper.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.28 on 2022-10-18 15:43 + +from django.db import migrations, models +import ietf.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('submit', '0011_use_timezone_now_for_submit_models'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='submission_date', + field=models.DateField(default=ietf.utils.timezone.date_today), + ), + ] diff --git a/ietf/submit/models.py b/ietf/submit/models.py index 9f0e2fa33..0f9a86cd2 100644 --- a/ietf/submit/models.py +++ b/ietf/submit/models.py @@ -2,11 +2,11 @@ # -*- coding: utf-8 -*- -import datetime import email import jsonfield from django.db import models +from django.utils import timezone import debug # pyflakes:ignore @@ -18,6 +18,7 @@ from ietf.name.models import DraftSubmissionStateName, FormalLanguageName from ietf.utils.accesstoken import generate_random_key, generate_access_token from ietf.utils.text import parse_unicode from ietf.utils.models import ForeignKey +from ietf.utils.timezone import date_today def parse_email_line(line): @@ -53,7 +54,7 @@ class Submission(models.Model): file_types = models.CharField(max_length=50, blank=True) file_size = models.IntegerField(null=True, blank=True) document_date = models.DateField(null=True, blank=True) - submission_date = models.DateField(default=datetime.date.today) + submission_date = models.DateField(default=date_today) xml_version = models.CharField(null=True, max_length=4, default=None) submitter = models.CharField(max_length=255, blank=True, help_text="Name and email of submitter, e.g. \"John Doe <john@example.org>\".") @@ -120,7 +121,7 @@ class Submission(models.Model): class SubmissionCheck(models.Model): - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) submission = ForeignKey(Submission, related_name='checks') checker = models.CharField(max_length=256, blank=True) passed = models.BooleanField(null=True, default=False) @@ -139,7 +140,7 @@ class SubmissionCheck(models.Model): class SubmissionEvent(models.Model): submission = ForeignKey(Submission) - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) by = ForeignKey(Person, null=True, blank=True) desc = models.TextField() @@ -157,7 +158,7 @@ class Preapproval(models.Model): """Pre-approved draft submission name.""" name = models.CharField(max_length=255, db_index=True) by = ForeignKey(Person) - time = models.DateTimeField(default=datetime.datetime.now) + time = models.DateTimeField(default=timezone.now) def __str__(self): return self.name diff --git a/ietf/submit/parsers/base.py b/ietf/submit/parsers/base.py index 5c5d0d6fc..679798cc4 100644 --- a/ietf/submit/parsers/base.py +++ b/ietf/submit/parsers/base.py @@ -3,7 +3,6 @@ import re -import datetime import debug # pyflakes:ignore from typing import List, Optional # pyflakes:ignore @@ -12,6 +11,8 @@ from django.conf import settings from django.template.defaultfilters import filesizeformat from ietf.utils.mime import get_mime_type +from ietf.utils.timezone import date_today + class MetaData(object): rev = None @@ -60,7 +61,7 @@ class FileParser(object): self.parse_max_size(); self.parse_filename_extension() self.parse_file_type() - self.parsed_info.metadata.submission_date = datetime.date.today() + self.parsed_info.metadata.submission_date = date_today() return self.parsed_info def parse_invalid_chars_in_filename(self): diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 70baff432..9f6087d9b 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -52,6 +52,7 @@ from ietf.utils.accesstoken import generate_access_token from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.models import VersionInfo from ietf.utils.test_utils import login_testing_unauthorized, TestCase +from ietf.utils.timezone import date_today from ietf.utils.draft import PlaintextDraft @@ -91,7 +92,7 @@ class BaseSubmitTestCase(TestCase): return settings.INTERNET_DRAFT_ARCHIVE_DIR def submission_file(name_in_doc, name_in_post, group, templatename, author=None, email=None, title=None, year=None, ascii=True): - _today = datetime.date.today() + _today = date_today() # construct appropriate text draft f = io.open(os.path.join(settings.BASE_DIR, "submit", templatename)) template = f.read() @@ -146,7 +147,7 @@ def create_draft_submission_with_rev_mismatch(rev='01'): sub = Submission.objects.create( name=draft_name, group=None, - submission_date=datetime.date.today() - datetime.timedelta(days=1), + submission_date=date_today() - datetime.timedelta(days=1), rev=rev, state_id='posted', ) @@ -164,7 +165,7 @@ class SubmitTests(BaseSubmitTestCase): def setUp(self): super().setUp() # Submit views assume there is a "next" IETF to look for cutoff dates against - MeetingFactory(type_id='ietf', date=datetime.date.today()+datetime.timedelta(days=180)) + MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=180)) def create_and_post_submission(self, name, rev, author, group=None, formats=("txt",), base_filename=None): """Helper to create and post a submission @@ -287,7 +288,7 @@ class SubmitTests(BaseSubmitTestCase): # prepare draft to suggest replace sug_replaced_draft = Document.objects.create( name="draft-ietf-ames-sug-replaced", - time=datetime.datetime.now(), + time=timezone.now(), type_id="draft", title="Draft to be suggested to be replaced", stream_id="ietf", @@ -298,7 +299,7 @@ class SubmitTests(BaseSubmitTestCase): words=100, intended_std_level_id="ps", ad=draft.ad, - expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), + expires=timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), notify="aliens@example.mars", note="", ) @@ -357,7 +358,7 @@ class SubmitTests(BaseSubmitTestCase): self.assertTrue(os.path.exists(os.path.join(self.repository_dir, "%s-%s.txt" % (name, rev)))) self.assertEqual(draft.type_id, "draft") self.assertEqual(draft.stream_id, "ietf") - self.assertTrue(draft.expires >= datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1)) + self.assertTrue(draft.expires >= timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1)) self.assertEqual(draft.get_state("draft-stream-%s" % draft.stream_id).slug, "wg-doc") authors = draft.documentauthor_set.all() self.assertEqual(len(authors), 1) @@ -1355,7 +1356,7 @@ class SubmitTests(BaseSubmitTestCase): # edit mailbox_before = len(outbox) # FIXME If this test is started before midnight, and ends after, it will fail - document_date = datetime.date.today() - datetime.timedelta(days=-3) + document_date = date_today() - datetime.timedelta(days=-3) r = self.client.post(edit_url, { "edit-title": "some title", "edit-rev": "00", @@ -1523,7 +1524,7 @@ class SubmitTests(BaseSubmitTestCase): def test_expire_submissions(self): s = Submission.objects.create(name="draft-ietf-mars-foo", group=None, - submission_date=datetime.date.today() - datetime.timedelta(days=10), + submission_date=date_today() - datetime.timedelta(days=10), rev="00", state_id="uploaded") @@ -1557,7 +1558,7 @@ class SubmitTests(BaseSubmitTestCase): # Put today in the blackout period meeting = Meeting.get_current_meeting() - meeting.importantdate_set.create(name_id='idcutoff',date=datetime.date.today()-datetime.timedelta(days=2)) + meeting.importantdate_set.create(name_id='idcutoff',date=date_today()-datetime.timedelta(days=2)) # regular user, no access r = self.client.get(url) @@ -1576,30 +1577,30 @@ class SubmitTests(BaseSubmitTestCase): url = urlreverse('ietf.submit.views.upload_submission') meeting = Meeting.get_current_meeting() - meeting.date = datetime.date.today()+datetime.timedelta(days=7) + meeting.date = date_today()+datetime.timedelta(days=7) meeting.save() meeting.importantdate_set.filter(name_id='idcutoff').delete() - meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today()+datetime.timedelta(days=7)) + meeting.importantdate_set.create(name_id='idcutoff', date=date_today()+datetime.timedelta(days=7)) r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) self.assertEqual(len(q('input[type=file][name=txt]')), 1) meeting = Meeting.get_current_meeting() - meeting.date = datetime.date.today() + meeting.date = date_today() meeting.save() meeting.importantdate_set.filter(name_id='idcutoff').delete() - meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today()) + meeting.importantdate_set.create(name_id='idcutoff', date=date_today()) r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) self.assertEqual(len(q('input[type=file][name=txt]')), 1) meeting = Meeting.get_current_meeting() - meeting.date = datetime.date.today()-datetime.timedelta(days=1) + meeting.date = date_today()-datetime.timedelta(days=1) meeting.save() meeting.importantdate_set.filter(name_id='idcutoff').delete() - meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today()-datetime.timedelta(days=1)) + meeting.importantdate_set.create(name_id='idcutoff', date=date_today()-datetime.timedelta(days=1)) r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) @@ -1864,7 +1865,7 @@ class SubmitTests(BaseSubmitTestCase): self.index += 1 sub = Submission.objects.create(name="draft-ietf-mars-bar-%d" % self.index, group=Group.objects.get(acronym="mars"), - submission_date=datetime.date.today(), + submission_date=date_today(), authors=[dict(name=self.author.name, email=self.author.user.email, affiliation='affiliation', @@ -2136,17 +2137,17 @@ class ApprovalsTestCase(BaseSubmitTestCase): Submission.objects.create(name="draft-ietf-mars-foo", group=Group.objects.get(acronym="mars"), - submission_date=datetime.date.today(), + submission_date=date_today(), rev="00", state_id="posted") Submission.objects.create(name="draft-ietf-mars-bar", group=Group.objects.get(acronym="mars"), - submission_date=datetime.date.today(), + submission_date=date_today(), rev="00", state_id="grp-appr") Submission.objects.create(name="draft-ietf-mars-quux", group=Group.objects.get(acronym="mars"), - submission_date=datetime.date.today(), + submission_date=date_today(), rev="00", state_id="ad-appr") @@ -2295,11 +2296,11 @@ class ManualPostsTestCase(BaseSubmitTestCase): Submission.objects.create(name="draft-ietf-mars-foo", group=Group.objects.get(acronym="mars"), - submission_date=datetime.date.today(), + submission_date=date_today(), state_id="manual") Submission.objects.create(name="draft-ietf-mars-bar", group=Group.objects.get(acronym="mars"), - submission_date=datetime.date.today(), + submission_date=date_today(), rev="00", state_id="grp-appr") @@ -2320,7 +2321,7 @@ Subject: test submission via email Please submit my draft at http://test.com/mydraft.txt Thank you -""".format(datetime.datetime.now().ctime()) +""".format(timezone.now().ctime()) message = email.message_from_string(force_str(message_string)) submission, submission_email_event = ( add_submission_email(request=None, @@ -2402,7 +2403,7 @@ Content-Disposition: attachment; filename="attach.txt" QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg== --------------090908050800030909090207-- -""".format(frm, datetime.datetime.now().ctime()) +""".format(frm, timezone.now().ctime()) message = email.message_from_string(force_str(message_string)) submission, submission_email_event = ( @@ -2458,7 +2459,7 @@ Content-Disposition: attachment; filename="attachment.txt" QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg== --------------090908050800030909090207-- -""".format(datetime.datetime.now().ctime()) +""".format(timezone.now().ctime()) # Back to secretariat self.client.login(username="secretary", password="secretary+password") @@ -2597,7 +2598,7 @@ Subject: Another message About my submission Thank you -""".format(datetime.datetime.now().ctime()) +""".format(timezone.now().ctime()) r = self.client.post(add_email_url, { "name": "{}-{}".format(submission.name, submission.rev), @@ -2664,7 +2665,7 @@ Thank you From: {} Date: {} Subject: test -""".format(reply_to, to, datetime.datetime.now().ctime()) +""".format(reply_to, to, timezone.now().ctime()) result = process_response_email(message_string) self.assertIsInstance(result, Message) @@ -2885,7 +2886,7 @@ class ApiSubmissionTests(BaseSubmitTestCase): class SubmissionUploadFormTests(BaseSubmitTestCase): def test_check_submission_thresholds(self): - today = datetime.date.today() + today = date_today() yesterday = today - datetime.timedelta(days=1) (this_group, that_group) = GroupFactory.create_batch(2, type_id='wg') this_ip = '10.0.0.1' @@ -3101,7 +3102,7 @@ class AsyncSubmissionTests(BaseSubmitTestCase): """Tests of async submission-related tasks""" def test_process_uploaded_submission(self): """process_uploaded_submission should properly process a submission""" - _today = datetime.date.today() + _today = date_today() xml, author = submission_file('draft-somebody-test-00', 'draft-somebody-test-00.xml', None, 'test_submission.xml') xml_data = xml.read() xml.close() @@ -3389,7 +3390,7 @@ class ApiSubmitTests(BaseSubmitTestCase): super().setUp() # break early in case of missing configuration self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY)) - MeetingFactory(type_id='ietf', date=datetime.date.today()+datetime.timedelta(days=60)) + MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60)) def do_post_submission(self, rev, author=None, name=None, group=None, email=None, title=None, year=None): url = urlreverse('ietf.submit.views.api_submit') diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index eb6838d94..6fb0a36b6 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -21,6 +21,7 @@ from django.db import transaction from django.http import HttpRequest # pyflakes:ignore from django.utils.module_loading import import_string from django.contrib.auth.models import AnonymousUser +from django.utils import timezone import debug # pyflakes:ignore @@ -47,6 +48,7 @@ from ietf.utils.accesstoken import generate_random_key from ietf.utils.draft import PlaintextDraft from ietf.utils.mail import is_valid_email from ietf.utils.text import parse_unicode, normalize_text +from ietf.utils.timezone import date_today from ietf.utils.xmldraft import XMLDraft from ietf.person.name import unidecode_name @@ -338,7 +340,7 @@ def post_submission(request, submission, approved_doc_desc, approved_subm_desc): if stream_slug: draft.stream = StreamName.objects.get(slug=stream_slug) - draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) + draft.expires = timezone.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) log.log(f"{submission.name}: got draft details") events = [] @@ -609,7 +611,7 @@ def ensure_person_email_info_exists(name, email, docname): email.active = active email.person = person if email.time is None: - email.time = datetime.datetime.now() + email.time = timezone.now() email.origin = "author: %s" % docname email.save() @@ -725,7 +727,7 @@ def recently_approved_by_user(user, since): ) def expirable_submissions(older_than_days): - cutoff = datetime.date.today() - datetime.timedelta(days=older_than_days) + cutoff = date_today() - datetime.timedelta(days=older_than_days) return Submission.objects.exclude(state__in=("cancel", "posted")).filter(submission_date__lt=cutoff) def expire_submission(submission, by): @@ -848,7 +850,7 @@ def fill_in_submission(form, submission, authors, abstract, file_size): submission.file_size = file_size submission.file_types = ','.join(form.file_types) submission.xml_version = form.xml_version - submission.submission_date = datetime.date.today() + submission.submission_date = date_today() submission.replaces = "" if form.parsed_draft is not None: submission.pages = form.parsed_draft.get_pagecount() diff --git a/ietf/submit/views.py b/ietf/submit/views.py index bab859702..9dcb88d13 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -48,6 +48,8 @@ from ietf.utils.accesstoken import generate_access_token from ietf.utils.log import log from ietf.utils.mail import parseaddr, send_mail_message from ietf.utils.response import permission_denied +from ietf.utils.timezone import date_today + def upload_submission(request): if request.method == 'POST': @@ -161,7 +163,7 @@ def api_submission(request): submission.state = DraftSubmissionStateName.objects.get(slug="validating") submission.remote_ip = form.remote_ip submission.file_types = ','.join(form.file_types) - submission.submission_date = datetime.date.today() + submission.submission_date = date_today() submission.submitter = user.person.formatted_email() submission.replaces = form.cleaned_data['replaces'] submission.save() @@ -723,7 +725,7 @@ def approvals(request): preapprovals = preapprovals_for_user(request.user) days = 30 - recently_approved = recently_approved_by_user(request.user, datetime.date.today() - datetime.timedelta(days=days)) + recently_approved = recently_approved_by_user(request.user, date_today() - datetime.timedelta(days=days)) return render(request, 'submit/approvals.html', {'selected': 'approvals', diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py index 9d0f66435..978b49507 100644 --- a/ietf/sync/iana.py +++ b/ietf/sync/iana.py @@ -9,7 +9,10 @@ import json import re import requests +from email.utils import parsedate_to_datetime + from django.conf import settings +from django.utils import timezone from django.utils.encoding import smart_bytes, force_str from django.utils.http import urlquote @@ -21,7 +24,6 @@ from ietf.doc.utils import add_state_change_event from ietf.person.models import Person from ietf.utils.log import log from ietf.utils.mail import parseaddr, get_payload_text -from ietf.utils.timezone import local_timezone_to_utc, email_time_to_local_timezone, utc_to_local_timezone #PROTOCOLS_URL = "https://www.iana.org/protocols/" @@ -64,8 +66,8 @@ def update_rfc_log_from_protocol_page(rfc_names, rfc_must_published_later_than): def fetch_changes_json(url, start, end): - url += "?start=%s&end=%s" % (urlquote(local_timezone_to_utc(start).strftime("%Y-%m-%d %H:%M:%S")), - urlquote(local_timezone_to_utc(end).strftime("%Y-%m-%d %H:%M:%S"))) + url += "?start=%s&end=%s" % (urlquote(start.astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")), + urlquote(end.astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S"))) # HTTP basic auth username = "ietfsync" password = settings.IANA_SYNC_PASSWORD @@ -159,8 +161,7 @@ def update_history_with_changes(changes, send_email=True): for c in changes: docname = c['doc'] - timestamp = datetime.datetime.strptime(c["time"], "%Y-%m-%d %H:%M:%S") - timestamp = utc_to_local_timezone(timestamp) # timestamps are in UTC + timestamp = datetime.datetime.strptime(c["time"], "%Y-%m-%d %H:%M:%S",).replace(tzinfo=datetime.timezone.utc) if c['type'] in ("iana_state", "iana_review"): if c['type'] == "iana_state": @@ -241,9 +242,12 @@ def parse_review_email(text): doc_name = strip_version_extension(doc_name) # date - review_time = datetime.datetime.now() + review_time = timezone.now() if "Date" in msg: - review_time = email_time_to_local_timezone(msg["Date"]) + review_time = parsedate_to_datetime(msg["Date"]) + # parsedate_to_datetime() may return a naive timezone - treat as UTC + if review_time.tzinfo is None or review_time.tzinfo.utcoffset(review_time) is None: + review_time = review_time.replace(tzinfo=datetime.timezone.utc) # by by = None diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py index c2264a0d3..1da11f01e 100644 --- a/ietf/sync/rfceditor.py +++ b/ietf/sync/rfceditor.py @@ -11,6 +11,7 @@ from urllib.parse import urlencode from xml.dom import pulldom, Node from django.conf import settings +from django.utils import timezone from django.utils.encoding import smart_bytes, force_str, force_text import debug # pyflakes:ignore @@ -24,6 +25,7 @@ from ietf.name.models import StdLevelName, StreamName from ietf.person.models import Person from ietf.utils.log import log from ietf.utils.mail import send_mail_text +from ietf.utils.timezone import datetime_from_date, RPC_TZINFO #QUEUE_URL = "https://www.rfc-editor.org/queue2.xml" #INDEX_URL = "https://www.rfc-editor.org/rfc/rfc-index.xml" @@ -331,9 +333,12 @@ def parse_index(response): def update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=None): - """Given parsed data from the RFC Editor index, update the documents - in the database. Yields a list of change descriptions for each - document, if any.""" + """Given parsed data from the RFC Editor index, update the documents in the database + + Yields a list of change descriptions for each document, if any. + + The skip_older_than_date is a bare date, not a datetime. + """ errata = {} for item in errata_data: @@ -442,8 +447,16 @@ def update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=Non # unfortunately, rfc_published_date doesn't include the correct day # at the moment because the data only has month/year, so # try to deduce it - d = datetime.datetime.combine(rfc_published_date, datetime.time()) - synthesized = datetime.datetime.now() + # + # Note: This is in done PST8PDT to preserve compatibility with events created when + # USE_TZ was False. The published_rfc event was created with a timestamp whose + # server-local datetime (PST8PDT) matched the publication date from the RFC index. + # When switching to USE_TZ=True, the timestamps were migrated so they still + # matched the publication date in PST8PDT. When interpreting the event timestamp + # as a publication date, you must treat it in the PST8PDT time zone. The + # RPC_TZINFO constant in ietf.utils.timezone is defined for this purpose. + d = datetime_from_date(rfc_published_date, RPC_TZINFO) + synthesized = timezone.now().astimezone(RPC_TZINFO) if abs(d - synthesized) > datetime.timedelta(days=60): synthesized = d else: diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index 61bab795c..f245145d2 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -10,6 +10,7 @@ import quopri from django.conf import settings from django.urls import reverse as urlreverse +from django.utils import timezone import debug # pyflakes:ignore @@ -22,6 +23,7 @@ from ietf.sync import iana, rfceditor from ietf.utils.mail import outbox, empty_outbox from ietf.utils.test_utils import login_testing_unauthorized from ietf.utils.test_utils import TestCase +from ietf.utils.timezone import date_today, RPC_TZINFO class IANASyncTests(TestCase): @@ -34,11 +36,11 @@ class IANASyncTests(TestCase): self.assertEqual(len(rfc_names), 1) self.assertEqual(rfc_names[0], "rfc1234") - iana.update_rfc_log_from_protocol_page(rfc_names, datetime.datetime.now() - datetime.timedelta(days=1)) + iana.update_rfc_log_from_protocol_page(rfc_names, timezone.now() - datetime.timedelta(days=1)) self.assertEqual(DocEvent.objects.filter(doc=draft, type="rfc_in_iana_registry").count(), 1) # make sure it doesn't create duplicates - iana.update_rfc_log_from_protocol_page(rfc_names, datetime.datetime.now() - datetime.timedelta(days=1)) + iana.update_rfc_log_from_protocol_page(rfc_names, timezone.now() - datetime.timedelta(days=1)) self.assertEqual(DocEvent.objects.filter(doc=draft, type="rfc_in_iana_registry").count(), 1) def test_changes_sync(self): @@ -190,7 +192,7 @@ ICANN doc_name, review_time, by, comment = iana.parse_review_email(msg.encode('utf-8')) self.assertEqual(doc_name, draft.name) - self.assertEqual(review_time, datetime.datetime(2012, 5, 10, 5, 0, rtime)) + self.assertEqual(review_time, datetime.datetime(2012, 5, 10, 12, 0, rtime, tzinfo=datetime.timezone.utc)) self.assertEqual(by, Person.objects.get(user__username="iana")) self.assertIn("there are no IANA Actions", comment.replace("\n", "")) @@ -237,7 +239,7 @@ class RFCSyncTests(TestCase): DocAlias.objects.create(name=updated_doc.name).docs.add(updated_doc) DocAlias.objects.create(name="rfc123").docs.add(updated_doc) - today = datetime.date.today() + today = date_today() t = '''<?xml version="1.0" encoding="UTF-8"?> <rfc-index xmlns="http://www.rfc-editor.org/rfc-index" @@ -352,7 +354,7 @@ class RFCSyncTests(TestCase): self.assertEqual(events[0].type, "sync_from_rfc_editor") self.assertEqual(events[1].type, "changed_action_holders") self.assertEqual(events[2].type, "published_rfc") - self.assertEqual(events[2].time.date(), today) + self.assertEqual(events[2].time.astimezone(RPC_TZINFO).date(), today) self.assertTrue("errata" in doc.tags.all().values_list("slug", flat=True)) self.assertTrue(DocAlias.objects.filter(name="rfc1234", docs=doc)) self.assertTrue(DocAlias.objects.filter(name="bcp1", docs=doc)) diff --git a/ietf/sync/views.py b/ietf/sync/views.py index d6fc505c2..1d22c424c 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -12,6 +12,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import render +from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from ietf.doc.models import DeletedEvent, StateDocEvent, DocEvent @@ -118,12 +119,12 @@ def rfceditor_undo(request): events = [] events.extend(StateDocEvent.objects.filter( state_type="draft-rfceditor", - time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1) + time__gte=timezone.now() - datetime.timedelta(weeks=1) ).order_by("-time", "-id")) events.extend(DocEvent.objects.filter( type="sync_from_rfc_editor", - time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1) + time__gte=timezone.now() - datetime.timedelta(weeks=1) ).order_by("-time", "-id")) events.sort(key=lambda e: (e.time, e.id), reverse=True) diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index 63ee22c4f..0bb597b60 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -13,7 +13,7 @@ title="Document changes" href="/feed/document-changes/{{ name }}/"> <meta name="description" - content="{{ doc.title }} {% if doc.get_state_slug == 'rfc' and not snapshot %}(RFC {{ rfc_number }}{% if published %}, {{ published.time|date:'F Y' }}{% endif %}{% if obsoleted_by %}; obsoleted by {{ obsoleted_by|join:', ' }}{% endif %}){% else %}(Internet-Draft, {{ doc.time|date:'Y' }}){% endif %}"> + content="{{ doc.title }} {% if doc.get_state_slug == 'rfc' and not snapshot %}(RFC {{ rfc_number }}{% if published %}, {{ doc.pub_date|date:'F Y' }}{% endif %}{% if obsoleted_by %}; obsoleted by {{ obsoleted_by|join:', ' }}{% endif %}){% else %}(Internet-Draft, {{ doc.time|date:'Y' }}){% endif %}"> {% endblock %} {% block morecss %}.inline { display: inline; }{% endblock %} {% block title %} @@ -47,7 +47,7 @@ {% if doc.get_state_slug == "rfc" and not snapshot %} <span class="text-success">RFC - {{ doc.std_level }}</span> {% if published %} - ({{ published.time|date:"F Y" }}) + ({{ doc.pub_date|date:"F Y" }}) {% else %} (Publication date unknown) {% endif %} @@ -115,7 +115,7 @@ <td class="edit"></td> <td> {{ doc.time|date:"Y-m-d" }} - {% if latest_revision and latest_revision.time.date != doc.time.date %} + {% if latest_revision and latest_revision.time|date:"Y-m-d" != doc.time|date:"Y-m-d" %} <span class="text-muted">(Latest revision {{ latest_revision.time|date:"Y-m-d" }})</span> {% endif %} </td> diff --git a/ietf/templates/group/meetings-row.html b/ietf/templates/group/meetings-row.html index ff3b3af37..f445cf1e5 100644 --- a/ietf/templates/group/meetings-row.html +++ b/ietf/templates/group/meetings-row.html @@ -1,4 +1,7 @@ +{% load origin tz %} +{% origin %} {% for s in sessions %} + {% timezone s.meeting.time_zone %} <tr> <td> {% if s.meeting.type.slug == 'ietf' %} @@ -65,4 +68,5 @@ {% endif %} </td> </tr> + {% endtimezone %} {% endfor %} \ No newline at end of file diff --git a/ietf/templates/group/meetings.html b/ietf/templates/group/meetings.html index 69ec5458c..f58f8840b 100644 --- a/ietf/templates/group/meetings.html +++ b/ietf/templates/group/meetings.html @@ -1,5 +1,6 @@ {% extends "group/group_base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} +{# TODO: Add text noting that dates and weekdays are displayed in each meeting's timezone #} {% load origin %} {% block title %} Meetings diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 65889d7be..9ba473cce 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -6,7 +6,6 @@ {% load textfilters %} {% load htmlfilters agenda_custom_tags %} {% load django_vite %} - {% block title %} IETF {{ meetingData.meetingNumber }} Meeting Agenda {% endblock %} diff --git a/ietf/templates/meeting/agenda.ics b/ietf/templates/meeting/agenda.ics index 0c9e84762..778905147 100644 --- a/ietf/templates/meeting/agenda.ics +++ b/ietf/templates/meeting/agenda.ics @@ -1,4 +1,4 @@ -{% load humanize %}{% autoescape off %}{% load ietf_filters textfilters %}{% load cache %}{% cache 1800 ietf_meeting_agenda_ics schedule.meeting.number request.path request.GET %}BEGIN:VCALENDAR +{% load humanize tz %}{% autoescape off %}{% timezone schedule.meeting.tz %}{% load ietf_filters textfilters %}{% load cache %}{% cache 1800 ietf_meeting_agenda_ics schedule.meeting.number request.path request.GET %}BEGIN:VCALENDAR VERSION:2.0 METHOD:PUBLISH PRODID:-//IETF//datatracker.ietf.org ical agenda//EN @@ -8,8 +8,8 @@ SUMMARY:{% if item.session.name %}{{item.session.name|ics_esc}}{% else %}{% if n {% if item.timeslot.show_location %}LOCATION:{{item.timeslot.get_location}} {% endif %}STATUS:{{item.session.ical_status}} CLASS:PUBLIC -DTSTART{% if schedule.meeting.time_zone %};TZID={{schedule.meeting.time_zone|ics_esc}}{%endif%}:{{ item.timeslot.time|date:"Ymd" }}T{{item.timeslot.time|date:"Hi"}}00 -DTEND{% if schedule.meeting.time_zone %};TZID={{schedule.meeting.time_zone|ics_esc}}{%endif%}:{{ item.timeslot.end_time|date:"Ymd" }}T{{item.timeslot.end_time|date:"Hi"}}00 +DTSTART;TZID={{schedule.meeting.time_zone|ics_esc}}:{{ item.timeslot.time|date:"Ymd" }}T{{item.timeslot.time|date:"Hi"}}00 +DTEND;TZID={{schedule.meeting.time_zone|ics_esc}}:{{ item.timeslot.end_time|date:"Ymd" }}T{{item.timeslot.end_time|date:"Hi"}}00 DTSTAMP:{{ item.timeslot.modified|date:"Ymd" }}T{{ item.timeslot.modified|date:"His" }}Z{% if item.session.agenda %} URL:{{item.session.agenda.get_versionless_href}}{% endif %} DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %} @@ -25,4 +25,4 @@ DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %} \n{# link agenda for ietf meetings #} See in schedule: {% absurl 'agenda' num=schedule.meeting.number %}#row-{{ item.slug }}\n{% endif %} END:VEVENT -{% endif %}{% endfor %}END:VCALENDAR{% endcache %}{% endautoescape %} +{% endif %}{% endfor %}END:VCALENDAR{% endcache %}{% endtimezone %}{% endautoescape %} diff --git a/ietf/templates/meeting/approve_proposed_slides.html b/ietf/templates/meeting/approve_proposed_slides.html index 584e28081..05d969bb8 100644 --- a/ietf/templates/meeting/approve_proposed_slides.html +++ b/ietf/templates/meeting/approve_proposed_slides.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin static django_bootstrap5 %} +{% load origin static django_bootstrap5 tz %} {% block title %} Approve Slides Proposed for {{ submission.session.meeting }} : {{ submission.session.group.acronym }} {% endblock %} @@ -15,7 +15,7 @@ </h1> {% if session_number %} <h2> - Session {{ session_number }} : {{ submission.session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }} + Session {{ session_number }} : {{ submission.session.official_timeslotassignment.timeslot.time|timezone:submission.session.meeting.time_zone|date:"D M-d-Y Hi" }} </h2> {% endif %} <p class="alert alert-info my-3"> diff --git a/ietf/templates/meeting/diff_schedules.html b/ietf/templates/meeting/diff_schedules.html index c5b117d2d..50d8b8420 100644 --- a/ietf/templates/meeting/diff_schedules.html +++ b/ietf/templates/meeting/diff_schedules.html @@ -2,7 +2,7 @@ {# Copyright The IETF Trust 2020, All Rights Reserved #} {% load origin %} {% load ietf_filters %} -{% load django_bootstrap5 %} +{% load django_bootstrap5 tz %} {% block title %}Differences between Meeting Agendas for IETF {{ meeting.number }}{% endblock %} {% block content %} {% origin %} @@ -26,7 +26,7 @@ <th scope="col"></th> </tr> </thead> - <tbody> + <tbody>{% timezone meeting.time_zone %} {% for d in diffs %} <tr> <td> @@ -40,7 +40,7 @@ </td> </tr> {% endfor %} - </tbody> + {% endtimezone %}</tbody> </table> {% else %} No differences in scheduled sessions found. diff --git a/ietf/templates/meeting/important_dates_for_meeting.ics b/ietf/templates/meeting/important_dates_for_meeting.ics index 25c26eabf..bc505e687 100644 --- a/ietf/templates/meeting/important_dates_for_meeting.ics +++ b/ietf/templates/meeting/important_dates_for_meeting.ics @@ -1,9 +1,9 @@ -{% for d in meeting.important_dates %}BEGIN:VEVENT +{% load tz %}{% for d in meeting.important_dates %}BEGIN:VEVENT UID:ietf-{{ meeting.number }}-{{ d.name_id }}-{{ d.date.isoformat }} SUMMARY:IETF {{ meeting.number }}: {{ d.name.name }} CLASS:PUBLIC DTSTART{% if not d.midnight_cutoff %};VALUE=DATE{% endif %}:{{ d.date|date:"Ymd" }}{% if d.midnight_cutoff %}235900Z{% endif %} -DTSTAMP:{{ meeting.cached_updated|date:"Ymd" }}T{{ meeting.cached_updated|date:"His" }}Z +DTSTAMP:{{ meeting.cached_updated|utc|date:"Ymd" }}T{{ meeting.cached_updated|utc|date:"His" }}Z TRANSP:TRANSPARENT DESCRIPTION:{{ d.name.desc }}{% if first and d.name.slug == 'openreg' or first and d.name.slug == 'earlybird' %}\n Register here: https://www.ietf.org/how/meetings/register/{% endif %}{% if d.name.slug == 'opensched' %}\n diff --git a/ietf/templates/meeting/interim_announcement.txt b/ietf/templates/meeting/interim_announcement.txt index 074394099..f9d5394c3 100644 --- a/ietf/templates/meeting/interim_announcement.txt +++ b/ietf/templates/meeting/interim_announcement.txt @@ -1,11 +1,11 @@ -{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW. +{% load ietf_filters tz %}{% timezone meeting.tz %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW. {% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold -{% if assignments.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}. +{% if assignments.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.time | utc | date:"H:i" }} to {{ assignments.first.timeslot.end_time | utc | date:"H:i" }} UTC){% endif %}. {% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting. {% for assignment in assignments %}Session {{ forloop.counter }}: -{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %} +{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.time | utc | date:"H:i" }} to {{ assignment.timeslot.end_time | utc | date:"H:i" }} UTC){% endif %} {% endfor %}{% endif %} {% if meeting.city %}Meeting Location: {{ meeting.city }}, {{ meeting.country }} @@ -17,3 +17,4 @@ Information about remote participation: {{ meeting.session_set.first.remote_instructions }} {{ meeting.session_set.first.agenda_note }} +{% endtimezone %} \ No newline at end of file diff --git a/ietf/templates/meeting/interim_info.txt b/ietf/templates/meeting/interim_info.txt index 4bb950b7d..6180bc664 100644 --- a/ietf/templates/meeting/interim_info.txt +++ b/ietf/templates/meeting/interim_info.txt @@ -1,4 +1,4 @@ -{% load ietf_filters %} +{% load ietf_filters tz %}{% timezone meeting.time_zone %} --------------------------------------------------------- {{ group.type.verbose_name }} Name: {{ group.name|safe }} {% if group.type.slug == "wg" or group.type.slug == "directorate" or group.type.slug == "team" %}Area Name: {{ group.parent }} @@ -17,3 +17,4 @@ Remote Participation Information: {{ session.remote_instructions }} Agenda Note: {{ session.agenda_note }} {% endfor %} --------------------------------------------------------- +{% endtimezone %} \ No newline at end of file diff --git a/ietf/templates/meeting/interim_meeting_cancellation_notice.txt b/ietf/templates/meeting/interim_meeting_cancellation_notice.txt index 615342d98..d4774c168 100644 --- a/ietf/templates/meeting/interim_meeting_cancellation_notice.txt +++ b/ietf/templates/meeting/interim_meeting_cancellation_notice.txt @@ -1,8 +1,8 @@ -{% load ams_filters %} +{% load ams_filters tz %}{% timezone meeting.time_zone %} The {{ group.name }} ({{ group.acronym }}) {% if not meeting.city %}virtual {% endif %}{% if is_multi_day %}multi-day {% endif %} interim meeting for {{ meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ meeting.time_zone }} has been cancelled. {{ meeting.session_set.0.agenda_note }} - +{% endtimezone %} diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index b627fdbb8..89545d14e 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -1,12 +1,12 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} -{% load static django_bootstrap5 widget_tweaks ietf_filters person_filters textfilters %} +{% load static django_bootstrap5 widget_tweaks ietf_filters person_filters textfilters tz %} {% block title %}Interim Request Details{% endblock %} {% block pagehead %} <link rel="stylesheet" href="{% static 'ietf/css/select2.css' %}"> {% endblock %} -{% block content %} +{% block content %}{% timezone meeting.tz %} {% origin %} <h1>Interim Meeting Request Details</h1> <dl class="row my-3"> @@ -67,7 +67,7 @@ <dd class="col-sm-10"> {{ assignment.timeslot.time|date:"H:i" }} {% if meeting.time_zone != 'UTC' %} - ({{ assignment.timeslot.utc_start_time|date:"H:i" }} UTC) + ({{ assignment.timeslot.time|utc|date:"H:i" }} UTC) {% endif %} </dd> <dt class="col-sm-2"> @@ -151,7 +151,7 @@ {% endif %} {% if can_approve and status_slug == 'apprw' %}</form>{% endif %} {% endwith %} -{% endblock %} +{% endtimezone %}{% endblock %} {% block js %} <script src="{% static 'ietf/js/select2.js' %}"></script> <script src="{% static 'ietf/js/meeting-interim-request.js' %}"></script> diff --git a/ietf/templates/meeting/interim_session_buttons.html b/ietf/templates/meeting/interim_session_buttons.html index b8443851b..3378d743b 100644 --- a/ietf/templates/meeting/interim_session_buttons.html +++ b/ietf/templates/meeting/interim_session_buttons.html @@ -1,7 +1,7 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} {% load static %} -{% load textfilters %} +{% load textfilters tz %} {% origin %} {% with item=session.official_timeslotassignment acronym=session.historic_group.acronym %} <div role="group" class="btn-group btn-group-sm"> @@ -90,8 +90,8 @@ {# iCalendar item #} <a class="btn btn-outline-primary" href="{% url 'ietf.meeting.views.agenda_ical' num=meeting.number session_id=session.id %}" - aria-label="icalendar entry for {{ acronym }} session on {{ item.timeslot.utc_start_time|date:'Y-m-d H:i' }} UTC" - title="icalendar entry for {{ acronym }} session on {{ item.timeslot.utc_start_time|date:'Y-m-d H:i' }} UTC"> + aria-label="icalendar entry for {{ acronym }} session on {{ item.timeslot.time|utc|date:'Y-m-d H:i' }} UTC" + title="icalendar entry for {{ acronym }} session on {{ item.timeslot.time|utc|date:'Y-m-d H:i' }} UTC"> <i class="bi bi-calendar"></i> </a> {% else %} diff --git a/ietf/templates/meeting/interim_session_cancellation_notice.txt b/ietf/templates/meeting/interim_session_cancellation_notice.txt index 7be4c612b..b7a7321a2 100644 --- a/ietf/templates/meeting/interim_session_cancellation_notice.txt +++ b/ietf/templates/meeting/interim_session_cancellation_notice.txt @@ -1,7 +1,7 @@ -{% load ams_filters %} +{% load ams_filters tz %}{% timezone session.meeting.time_zone %} {% if session.name %}The "{{ session.name }}"{% else %}A{% endif %} session of the {{ group.name }} ({{ group.acronym }}) {% if not session.meeting.city %}virtual {% endif %}{% if is_multi_day %}multi-day {% endif %} -interim meeting has been cancelled. This session had been scheduled for {{ meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ meeting.time_zone }}. +interim meeting has been cancelled. This session had been scheduled for {{ session.meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ session.meeting.time_zone }}. {{ session.agenda_note }} - +{% endtimezone %} diff --git a/ietf/templates/meeting/propose_session_slides.html b/ietf/templates/meeting/propose_session_slides.html index 8315c2deb..caa3a49af 100644 --- a/ietf/templates/meeting/propose_session_slides.html +++ b/ietf/templates/meeting/propose_session_slides.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin static django_bootstrap5 %} +{% load origin static django_bootstrap5 tz %} {% block title %}Propose Slides for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %} {% block content %} {% origin %} @@ -13,7 +13,7 @@ </h1> {% if session_number %} <h2 class="mt-3"> - Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }} + Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }} </h2> {% endif %} <p class="alert alert-info my-3"> diff --git a/ietf/templates/meeting/session_details.html b/ietf/templates/meeting/session_details.html index 2ecb7e1d5..c1b9f31fc 100644 --- a/ietf/templates/meeting/session_details.html +++ b/ietf/templates/meeting/session_details.html @@ -14,7 +14,7 @@ <br> <small class="text-muted">{{ acronym }}</small> </h1> - {% if meeting.date >= thisweek %} + {% if meeting.start_datetime >= thisweek %} <a class="regular float-end" title="icalendar entry for {{ acronym }}@{{ meeting.number }}" aria-label="icalendar entry for {{ acronym }}@{{ meeting.number }}" diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index d481955d0..c5c51af78 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -6,7 +6,7 @@ {% if sessions|length > 1 %}Session {{ forloop.counter }} :{% endif %} {% for time in session.times %} {% if not forloop.first %},{% endif %} - {{ time|dateformat:"l Y-m-d H:i T" }} + {{ time|timezone:session.meeting.time_zone|dateformat:"l Y-m-d H:i T" }} {% if time.tzinfo.zone != "UTC" %}<span class="small">({{ time|utc|dateformat:"H:i T" }})</span>{% endif %} {% endfor %} {% if session.cancelled %} diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index f3359f4fa..4fe5faac5 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -2,7 +2,7 @@ {# Copyright The IETF Trust 2015, 2020, All Rights Reserved #} {% load origin %} {% load cache %} -{% load ietf_filters static classname %} +{% load ietf_filters static classname tz %} {% block pagehead %} <link rel="stylesheet" href="{% static "ietf/css/list.css" %}"> <link rel="stylesheet" href="{% static "ietf/js/fullcalendar.css" %}"> @@ -67,9 +67,9 @@ {% elif entry|classname == 'Session' %} {% with session=entry group=entry.group meeting=entry.meeting %} <td class="session-time" - data-start-utc="{{ session.official_timeslotassignment.timeslot.utc_start_time | date:'Y-m-d H:i' }}Z" - data-end-utc="{{ session.official_timeslotassignment.timeslot.utc_end_time | date:'Y-m-d H:i' }}Z"> - {{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i" }}-{{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i" }} + data-start-utc="{{ session.official_timeslotassignment.timeslot.time | utc | date:'Y-m-d H:i' }}Z" + data-end-utc="{{ session.official_timeslotassignment.timeslot.end_time | utc | date:'Y-m-d H:i' }}Z"> + {{ session.official_timeslotassignment.timeslot.time | utc | date:"Y-m-d H:i" }}-{{ session.official_timeslotassignment.timeslot.end_time | utc | date:"H:i" }} </td> <td> <a href="{% url 'ietf.group.views.group_home' acronym=group.acronym %}">{{ group.acronym }}</a> @@ -132,8 +132,8 @@ { group: '{% if session.group %}{{session.group.acronym}}{% endif %}{% if session.name %} - {{session.name}}{% endif %}', filter_keywords: ["{{ session.filter_keywords|join:'","' }}"], - start_moment: moment.utc('{{session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}}'), - end_moment: moment.utc('{{session.official_timeslotassignment.timeslot.utc_end_time | date:"Y-m-d H:i"}}'), + start_moment: moment.utc('{{session.official_timeslotassignment.timeslot.time | utc | date:"Y-m-d H:i"}}'), + end_moment: moment.utc('{{session.official_timeslotassignment.timeslot.end_time | utc | date:"Y-m-d H:i"}}'), url: '{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}' } {% endwith %} diff --git a/ietf/templates/meeting/upload_session_agenda.html b/ietf/templates/meeting/upload_session_agenda.html index 4bd0b8971..67a6bf79b 100644 --- a/ietf/templates/meeting/upload_session_agenda.html +++ b/ietf/templates/meeting/upload_session_agenda.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin static django_bootstrap5 %} +{% load origin static django_bootstrap5 tz %} {% block title %} {% if agenda_sp %} Revise @@ -24,7 +24,7 @@ </small> </h1> {% if session_number %} - <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2> + <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2> {% endif %} <form enctype="multipart/form-data" method="post" class="my-3"> {% csrf_token %} diff --git a/ietf/templates/meeting/upload_session_bluesheets.html b/ietf/templates/meeting/upload_session_bluesheets.html index 4a2f484be..c79934652 100644 --- a/ietf/templates/meeting/upload_session_bluesheets.html +++ b/ietf/templates/meeting/upload_session_bluesheets.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin static django_bootstrap5 %} +{% load origin static django_bootstrap5 tz %} {% block title %} {% if bluesheet_sp %} Revise @@ -24,7 +24,7 @@ </small> </h1> {% if session_number %} - <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2> + <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2> {% endif %} <form enctype="multipart/form-data" method="post" class="my-3"> {% csrf_token %} diff --git a/ietf/templates/meeting/upload_session_minutes.html b/ietf/templates/meeting/upload_session_minutes.html index 2e3f87524..fa6f7c15e 100644 --- a/ietf/templates/meeting/upload_session_minutes.html +++ b/ietf/templates/meeting/upload_session_minutes.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin static django_bootstrap5 %} +{% load origin static django_bootstrap5 tz %} {% block title %} {% if minutes_sp %} Revise @@ -24,7 +24,7 @@ </small> </h1> {% if session_number %} - <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2> + <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2> {% endif %} <form enctype="multipart/form-data" method="post" class="my-3"> {% csrf_token %} diff --git a/ietf/templates/meeting/upload_session_slides.html b/ietf/templates/meeting/upload_session_slides.html index f5cd35d96..61b2422e4 100644 --- a/ietf/templates/meeting/upload_session_slides.html +++ b/ietf/templates/meeting/upload_session_slides.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin static django_bootstrap5 %} +{% load origin static django_bootstrap5 tz %} {% block title %} {% if slides_sp %} Revise @@ -25,7 +25,7 @@ </small> </h1> {% if session_number %} - <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2> + <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2> {% endif %} {% if slides_sp %}<h3>{{ slides_sp.document.name }}</h3>{% endif %} <form class="my-3" enctype="multipart/form-data" method="post"> diff --git a/ietf/templates/person/profile.html b/ietf/templates/person/profile.html index 6732804aa..3d0b1e067 100644 --- a/ietf/templates/person/profile.html +++ b/ietf/templates/person/profile.html @@ -62,7 +62,7 @@ </tbody> </table> {% else %} - <p>{{ person.first_name }} has no active roles as of {{ today }}.</p> + <p>{{ person.first_name }} has no active roles as of {{ today|date:"Y-m-d" }}.</p> {% endif %} {% endif %} {% if person.personextresource_set.exists %} @@ -123,7 +123,7 @@ </tbody> </table> {% else %} - {{ person.first_name }} has no RFCs as of {{ today }}. + {{ person.first_name }} has no RFCs as of {{ today|date:"Y-m-d" }}. {% endif %} <h2 class="mt-5" id="drafts-{{ forloop.counter }}"> Active Drafts <small class="text-muted">({{ person.active_drafts|length }})</small> @@ -137,7 +137,7 @@ {% endfor %} </ul> {% else %} - {{ person.first_name }} has no active drafts as of {{ today }}. + {{ person.first_name }} has no active drafts as of {{ today|date:"Y-m-d" }}. {% endif %} <h2 class="mt-5"> Expired Drafts <small class="text-muted">({{ person.expired_drafts|length }})</small> @@ -156,7 +156,7 @@ </ul> (Excluding replaced drafts.) {% else %} - {{ person.first_name }} has no expired drafts as of {{ today }}. + {{ person.first_name }} has no expired drafts as of {{ today|date:"Y-m-d" }}. {% endif %} {% if person.has_drafts %} <h2 class="mt-5"> diff --git a/ietf/utils/decorators.py b/ietf/utils/decorators.py index 694e989f8..634724880 100644 --- a/ietf/utils/decorators.py +++ b/ietf/utils/decorators.py @@ -10,6 +10,7 @@ from django.conf import settings from django.contrib.auth import login from django.http import HttpResponse from django.shortcuts import render +from django.utils import timezone from django.utils.encoding import force_bytes import debug # pyflakes:ignore @@ -64,7 +65,7 @@ def require_api_key(f, request, *args, **kwargs): person = key.person last_login = person.user.last_login if not person.user.is_staff: - time_limit = (datetime.datetime.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS)) + time_limit = (timezone.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS)) if last_login == None or last_login < time_limit: return err(400, "Too long since last regular login") # Log in @@ -74,7 +75,7 @@ def require_api_key(f, request, *args, **kwargs): person.user.save() # Update stats key.count += 1 - key.latest = datetime.datetime.now() + key.latest = timezone.now() key.save() PersonApiKeyEvent.objects.create(person=person, type='apikey_login', key=key, desc="Logged in with key ID %s, endpoint %s" % (key.id, key.endpoint)) # Execute decorated function diff --git a/ietf/utils/draft.py b/ietf/utils/draft.py index 12a614701..0a379b0e9 100755 --- a/ietf/utils/draft.py +++ b/ietf/utils/draft.py @@ -50,6 +50,9 @@ import time from typing import Dict, List # pyflakes:ignore +from .timezone import date_today + + version = "0.35" program = os.path.basename(sys.argv[0]) progdir = os.path.dirname(sys.argv[0]) @@ -467,7 +470,7 @@ class PlaintextDraft(Draft): month = int(mon) else: continue - today = datetime.date.today() + today = date_today() if day==0: # if the date was given with only month and year, use # today's date if month and year is today's month and diff --git a/ietf/utils/mail.py b/ietf/utils/mail.py index 04c0ed916..3500d888e 100644 --- a/ietf/utils/mail.py +++ b/ietf/utils/mail.py @@ -3,7 +3,6 @@ import copy -import datetime #import logging import re import smtplib @@ -27,6 +26,7 @@ from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.validators import validate_email from django.template.loader import render_to_string from django.template import Context,RequestContext +from django.utils import timezone from django.utils.encoding import force_text, force_str, force_bytes import debug # pyflakes:ignore @@ -324,7 +324,7 @@ def show_that_mail_was_sent(request,leadline,msg,bcc): if request and request.user: from ietf.ietfauth.utils import has_role if has_role(request.user,['Area Director','Secretariat','IANA','RFC Editor','ISE','IAD','IRTF Chair','WG Chair','RG Chair','WG Secretary','RG Secretary']): - info = "%s at %s %s\n" % (leadline,datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),settings.TIME_ZONE) + info = "%s at %s %s\n" % (leadline,timezone.now().strftime("%Y-%m-%d %H:%M:%S"),settings.TIME_ZONE) info += "Subject: %s\n" % force_text(msg.get('Subject','[no subject]')) info += "To: %s\n" % msg.get('To','[no to]') if msg.get('Cc'): @@ -378,7 +378,7 @@ def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=F try: send_smtp(msg, bcc) if save: - message.sent = datetime.datetime.now() + message.sent = timezone.now() message.save() if settings.SERVER_MODE != 'development': show_that_mail_was_sent(request,'Email was sent',msg,bcc) @@ -505,7 +505,7 @@ def send_mail_message(request, message, extra=None): # msg = send_mail_text(request, message.to, message.frm, message.subject, # message.body, cc=message.cc, bcc=message.bcc, extra=e, save=False) - message.sent = datetime.datetime.now() + message.sent = timezone.now() message.save() return msg diff --git a/ietf/utils/management/commands/check_draft_event_revision_integrity.py b/ietf/utils/management/commands/check_draft_event_revision_integrity.py index a4c38bd1c..c8d2cbd21 100644 --- a/ietf/utils/management/commands/check_draft_event_revision_integrity.py +++ b/ietf/utils/management/commands/check_draft_event_revision_integrity.py @@ -13,6 +13,7 @@ import django django.setup() from django.core.management.base import BaseCommand #, CommandError +from django.utils import timezone import debug # pyflakes:ignore @@ -29,7 +30,7 @@ class Command(BaseCommand): """ def add_arguments(self, parser): - default_start = datetime.datetime.now() - datetime.timedelta(days=60) + default_start = timezone.now() - datetime.timedelta(days=60) parser.add_argument( '-d', '--from', type=str, default=default_start.strftime('%Y-%m-%d'), help='Limit the list to messages saved after the given date (default %(default)s).', diff --git a/ietf/utils/management/commands/fix_ambiguous_timestamps.py b/ietf/utils/management/commands/fix_ambiguous_timestamps.py index fc872f458..84a82bb30 100644 --- a/ietf/utils/management/commands/fix_ambiguous_timestamps.py +++ b/ietf/utils/management/commands/fix_ambiguous_timestamps.py @@ -10,6 +10,7 @@ from django.apps import apps from django.conf import settings from django.core.management.base import BaseCommand from django.db import models +from django.utils import timezone import debug # pyflakes:ignore @@ -53,7 +54,7 @@ class Command(BaseCommand): def handle(self, *app_labels, **options): self.verbosity = options['verbosity'] self.quiet = self.verbosity < 1 - stop = datetime.datetime.now() + stop = timezone.now() start = stop - datetime.timedelta(days=14) for name, appconf in apps.app_configs.items(): diff --git a/ietf/utils/management/commands/send_gdpr_consent_request.py b/ietf/utils/management/commands/send_gdpr_consent_request.py new file mode 100644 index 000000000..152fc83fe --- /dev/null +++ b/ietf/utils/management/commands/send_gdpr_consent_request.py @@ -0,0 +1,104 @@ +# Copyright The IETF Trust 2018-2020, All Rights Reserved +# -*- coding: utf-8 -*- + + +import datetime +import time + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone + +import debug # pyflakes:ignore + +from ietf.person.models import Person, PersonEvent +from ietf.utils.mail import send_mail + +class Command(BaseCommand): + help = (""" + Send GDPR consent request emails to persons who have not indicated consent + to having their personal information stored. Each send is logged as a + PersonEvent. + + By default email sending happens at a rate of 1 message per second; the + rate can be adjusted with the -r option. At the start of a run, an estimate + is given of how many persons to send to, and how long the run will take. + + By default, emails are not sent out if there is less than 6 days since the + previous consent request email. The interval can be adjusted with the -m + option. One effect of this is that it is possible to break of a run and + re-start it with for instance a different rate, without having duplicate + messages go out to persons that were handled in the interrupted run. + """) + + def add_arguments(self, parser): + parser.add_argument('-n', '--dry-run', action='store_true', default=False, + help="Don't send email, just list recipients") + parser.add_argument('-d', '--date', help="Date of deletion (mentioned in message)") + parser.add_argument('-m', '--minimum-interval', type=int, default=6, + help="Minimum interval between re-sending email messages, default: %(default)s days") + parser.add_argument('-r', '--rate', type=float, default=1.0, + help='Rate of sending mail, default: %(default)s/s') + parser.add_argument('-R', '--reminder', action='store_true', default=False, + help='Preface the subject with "Reminder:"') + parser.add_argument('user', nargs='*') + + + def handle(self, *args, **options): + # Don't send copies of the whole bulk mailing to the debug mailbox + if settings.SERVER_MODE == 'production': + settings.EMAIL_COPY_TO = "Email Debug Copy <outbound@ietf.org>" + # + event_type = 'gdpr_notice_email' + # Arguments + # --date + if 'date' in options and options['date'] != None: + try: + date = datetime.datetime.strptime(options['date'], "%Y-%m-%d").date() + except ValueError as e: + raise CommandError('%s' % e) + else: + date = datetime.date.today() + datetime.timedelta(days=30) + days = (date - datetime.date.today()).days + if days <= 1: + raise CommandError('date must be more than 1 day in the future') + # --rate + delay = 1.0/options['rate'] + # --minimum_interval + minimum_interval = options['minimum_interval'] + latest_previous = timezone.now() - datetime.timedelta(days=minimum_interval) + # user + self.stdout.write('Querying the database for matching person records ...') + if 'user' in options and options['user']: + persons = Person.objects.filter(user__username__in=options['user']) + else: + exclude = PersonEvent.objects.filter(time__gt=latest_previous, type=event_type) + persons = Person.objects.exclude(consent=True).exclude(personevent__in=exclude) + # Report the size of the run + runtime = persons.count() * delay + self.stdout.write('Sending to %d users; estimated time a bit more than %d:%02d hours' % (persons.count(), runtime//3600, runtime%3600//60)) + subject='Personal Information in the IETF Datatracker' + if options['reminder']: + subject = "Reminder: " + subject + for person in persons: + fields = ', '.join(person.needs_consent()) + if fields and person.email_set.exists(): + if options['dry_run']: + print(("%-32s %-32s %-32s %-32s %s" % (person.email(), person.name_from_draft or '', person.name, person.ascii, fields)).encode('utf8')) + else: + to = [ e.address for e in person.email_set.filter(active=True) ] # pyflakes:ignore + if not to: + to = [ e.address for e in person.email_set.all() ] # pyflakes:ignore + self.stdout.write("Sendimg email to %s" % to) + send_mail(None, to, "<gdprnoreply@ietf.org>", + subject=subject, + template='utils/personal_information_notice.txt', + context={ + 'date': date, 'days': days, 'fields': fields, + 'person': person, 'settings': settings, + }, + ) + PersonEvent.objects.create(person=person, type='gdpr_notice_email', + desc="Sent GDPR notice email to %s with confirmation deadline %s" % (to, date)) + time.sleep(delay) + diff --git a/ietf/utils/migrations/0002_convert_timestamps_to_utc.py b/ietf/utils/migrations/0002_convert_timestamps_to_utc.py new file mode 100644 index 000000000..320992ae9 --- /dev/null +++ b/ietf/utils/migrations/0002_convert_timestamps_to_utc.py @@ -0,0 +1,305 @@ +# Generated by Django 2.2.28 on 2022-06-21 11:44 +# +# Important: To avoid corrupting timestamps in the database, do not use this migration as a dependency for +# future migrations. Use 0003_pause_to_change_use_tz instead. +# +import datetime + +from zoneinfo import ZoneInfo + +from django.conf import settings +from django.db import migrations, connection + +# to generate the expected columns list: +# +# from django.db import connection +# from pprint import pp +# cursor = connection.cursor() +# cursor.execute(""" +# SELECT table_name, column_name +# FROM information_schema.columns +# WHERE table_schema='ietf_utf8' +# AND column_type LIKE 'datetime%' +# AND NOT table_name LIKE 'django_celery_beat_%' +# ORDER BY table_name, column_name; +# """) +# pp(cursor.fetchall()) +# +expected_datetime_columns = ( + ('auth_user', 'date_joined'), + ('auth_user', 'last_login'), + ('community_documentchangedates', 'new_version_date'), + ('community_documentchangedates', 'normal_change_date'), + ('community_documentchangedates', 'significant_change_date'), + ('django_admin_log', 'action_time'), + ('django_migrations', 'applied'), + ('django_session', 'expire_date'), + ('doc_ballotpositiondocevent', 'comment_time'), + ('doc_ballotpositiondocevent', 'discuss_time'), + ('doc_deletedevent', 'time'), + ('doc_docevent', 'time'), + ('doc_dochistory', 'expires'), + ('doc_dochistory', 'time'), + ('doc_docreminder', 'due'), + ('doc_document', 'expires'), + ('doc_document', 'time'), + ('doc_documentactionholder', 'time_added'), + ('doc_initialreviewdocevent', 'expires'), + ('doc_irsgballotdocevent', 'duedate'), + ('doc_lastcalldocevent', 'expires'), + ('group_group', 'time'), + ('group_groupevent', 'time'), + ('group_grouphistory', 'time'), + ('group_groupmilestone', 'time'), + ('group_groupmilestonehistory', 'time'), + ('ipr_iprdisclosurebase', 'time'), + ('ipr_iprevent', 'response_due'), + ('ipr_iprevent', 'time'), + ('liaisons_liaisonstatementevent', 'time'), + ('mailinglists_subscribed', 'time'), + ('mailinglists_whitelisted', 'time'), + ('meeting_floorplan', 'modified'), + ('meeting_room', 'modified'), + ('meeting_schedtimesessassignment', 'modified'), + ('meeting_schedulingevent', 'time'), + ('meeting_session', 'modified'), + ('meeting_session', 'scheduled'), + ('meeting_slidesubmission', 'time'), + ('meeting_timeslot', 'modified'), + ('meeting_timeslot', 'time'), + ('message_message', 'sent'), + ('message_message', 'time'), + ('message_sendqueue', 'send_at'), + ('message_sendqueue', 'sent_at'), + ('message_sendqueue', 'time'), + ('nomcom_feedback', 'time'), + ('nomcom_feedbacklastseen', 'time'), + ('nomcom_nomination', 'time'), + ('nomcom_nomineeposition', 'time'), + ('nomcom_topicfeedbacklastseen', 'time'), + ('oidc_provider_code', 'expires_at'), + ('oidc_provider_token', 'expires_at'), + ('oidc_provider_userconsent', 'date_given'), + ('oidc_provider_userconsent', 'expires_at'), + ('person_email', 'time'), + ('person_historicalemail', 'history_date'), + ('person_historicalemail', 'time'), + ('person_historicalperson', 'history_date'), + ('person_historicalperson', 'time'), + ('person_person', 'time'), + ('person_personalapikey', 'created'), + ('person_personalapikey', 'latest'), + ('person_personevent', 'time'), + ('request_profiler_profilingrecord', 'end_ts'), + ('request_profiler_profilingrecord', 'start_ts'), + ('review_historicalreviewassignment', 'assigned_on'), + ('review_historicalreviewassignment', 'completed_on'), + ('review_historicalreviewassignment', 'history_date'), + ('review_historicalreviewersettings', 'history_date'), + ('review_historicalreviewrequest', 'history_date'), + ('review_historicalreviewrequest', 'time'), + ('review_historicalunavailableperiod', 'history_date'), + ('review_reviewassignment', 'assigned_on'), + ('review_reviewassignment', 'completed_on'), + ('review_reviewrequest', 'time'), + ('review_reviewwish', 'time'), + ('south_migrationhistory', 'applied'), + ('submit_preapproval', 'time'), + ('submit_submissioncheck', 'time'), + ('submit_submissionevent', 'time'), + ('tastypie_apikey', 'created'), + ('utils_versioninfo', 'time'), +) + +def convert_pre1970_timestamps(apps, schema_editor): + """Convert timestamps that CONVERT_TZ cannot handle + + This could be made to do the entire conversion but some tables that require converison + do not use 'id' as their PK. Rather than reinvent the ORM, we'll let SQL do what it can + with CONVERT_TZ and clean up after. The tables that have pre-1970 timestamps both have + 'id' columns. + """ + min_timestamp = "1969-12-31 16:00:01" # minimum PST8PDT timestamp CONVERT_TZ can convert to UTC + with connection.cursor() as cursor: + # To get these values, use: + # convert_manually = [ + # (tbl, col) for (tbl, col) in expected_datetime_columns + # if cursor.execute( + # f'SELECT COUNT(*) FROM {tbl} WHERE {col} IS NOT NULL AND {col} <= %s', + # (min_timestamp,) + # ) and cursor.fetchone()[0] > 0 + # ] + convert_manually = [('doc_docevent', 'time'), ('group_groupevent', 'time')] + pst8pdt = ZoneInfo('PST8PDT') + for (tbl, col) in convert_manually: + cursor.execute(f'SELECT id, {col} FROM {tbl} WHERE {col} < %s', (min_timestamp,)) + for (id, naive_in_pst8pdt) in cursor.fetchall(): + aware_in_pst8pdt = naive_in_pst8pdt.replace(tzinfo=pst8pdt) + aware_in_utc = aware_in_pst8pdt.astimezone(datetime.timezone.utc) + naive_in_utc = aware_in_utc.replace(tzinfo=None) + cursor.execute( + f'UPDATE {tbl} SET {col}=%s WHERE id=%s', + (naive_in_utc, id) + ) + + +def forward(apps, schema_editor): + # Check that the USE_TZ has been False so far, otherwise we might be corrupting timestamps. If this test + # fails, be sure that no timestamps have been set since changing USE_TZ to True before re-running! + assert not getattr(settings, 'USE_TZ', False), 'must keep USE_TZ = False until after this migration' + + # Check that we can safely ignore celery beat columns - it defaults to UTC if CELERY_TIMEZONE is not set. + celery_timezone = getattr(settings, 'CELERY_TIMEZONE', None) + assert celery_timezone in ('UTC', None), 'update migration, celery is not using UTC' + # If the CELERY_ENABLE_UTC flag is set, abort because someone is using a strange configuration. + assert not hasattr(settings, 'CELERY_ENABLE_UTC'), 'update migration, settings.CELERY_ENABLE_UTC was not expected' + + with connection.cursor() as cursor: + # Check that we have timezones. + # If these assertions fail, the DB does not know all the necessary time zones. + # To load timezones, + # $ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql + # (on a dev system, first connect to the db image with `docker compose exec db bash`) + cursor.execute("SELECT CONVERT_TZ('2022-06-22T17:43:00', 'PST8PDT', 'UTC');") + assert not any(None in row for row in cursor.fetchall()), 'database does not recognize PST8PDT' + cursor.execute( + "SELECT CONVERT_TZ('2022-06-22T17:43:00', time_zone, 'UTC') FROM meeting_meeting WHERE time_zone != '';" + ) + assert not any(None in row for row in cursor.fetchall()), 'database does not recognize a meeting time zone' + + # Check that we have all and only the expected datetime columns to work with. + # If this fails, figure out what changed and decide how to proceed safely. + cursor.execute(""" + SELECT table_name, column_name + FROM information_schema.columns + WHERE table_schema='ietf_utf8' + AND column_type LIKE 'datetime%' + AND NOT table_name LIKE 'django_celery_beat_%' + AND NOT table_name='utils_dumpinfo' + ORDER BY table_name, column_name; + """) + assert cursor.fetchall() == expected_datetime_columns, 'unexpected or missing datetime columns in db' + + +class Migration(migrations.Migration): + dependencies = [ + ('doc', '0046_use_timezone_now_for_doc_models'), + ('group', '0059_use_timezone_now_for_group_models'), + ('meeting', '0058_meeting_time_zone_not_blank'), + ('message', '0012_use_timezone_now_for_message_models'), + ('person', '0026_use_timezone_now_for_person_models'), + ('review', '0029_use_timezone_now_for_review_models'), + ('submit', '0011_use_timezone_now_for_submit_models'), + ('utils', '0001_initial'), + ] + + # To generate the queries: + # + # min_timestamp = "1969-12-31 16:00:01" # minimum PST8PDT timestamp CONVERT_TZ can convert to UTC + # pst8pdt_columns = [e for e in expected_datetime_columns if e != ('meeting_timeslot', 'time')] + # queries = [] + # for table, column in pst8pdt_columns: + # queries.append(f"UPDATE {table} SET {column} = CONVERT_TZ({column}, 'PST8PDT', 'UTC') WHERE {column} >= '{min_timestamp}'";) + # + # queries.append(""" + # UPDATE meeting_timeslot + # JOIN meeting_meeting + # ON meeting_meeting.id = meeting_id + # SET time = CONVERT_TZ(time, time_zone, 'UTC'); + # """) + # + # print("\n".join(queries)) + # + operations = [ + migrations.RunPython(forward), + migrations.RunSQL(""" +UPDATE auth_user SET date_joined = CONVERT_TZ(date_joined, 'PST8PDT', 'UTC') WHERE date_joined >= '1969-12-31 16:00:01'; +UPDATE auth_user SET last_login = CONVERT_TZ(last_login, 'PST8PDT', 'UTC') WHERE last_login >= '1969-12-31 16:00:01'; +UPDATE community_documentchangedates SET new_version_date = CONVERT_TZ(new_version_date, 'PST8PDT', 'UTC') WHERE new_version_date >= '1969-12-31 16:00:01'; +UPDATE community_documentchangedates SET normal_change_date = CONVERT_TZ(normal_change_date, 'PST8PDT', 'UTC') WHERE normal_change_date >= '1969-12-31 16:00:01'; +UPDATE community_documentchangedates SET significant_change_date = CONVERT_TZ(significant_change_date, 'PST8PDT', 'UTC') WHERE significant_change_date >= '1969-12-31 16:00:01'; +UPDATE django_admin_log SET action_time = CONVERT_TZ(action_time, 'PST8PDT', 'UTC') WHERE action_time >= '1969-12-31 16:00:01'; +UPDATE django_migrations SET applied = CONVERT_TZ(applied, 'PST8PDT', 'UTC') WHERE applied >= '1969-12-31 16:00:01'; +UPDATE django_session SET expire_date = CONVERT_TZ(expire_date, 'PST8PDT', 'UTC') WHERE expire_date >= '1969-12-31 16:00:01'; +UPDATE doc_ballotpositiondocevent SET comment_time = CONVERT_TZ(comment_time, 'PST8PDT', 'UTC') WHERE comment_time >= '1969-12-31 16:00:01'; +UPDATE doc_ballotpositiondocevent SET discuss_time = CONVERT_TZ(discuss_time, 'PST8PDT', 'UTC') WHERE discuss_time >= '1969-12-31 16:00:01'; +UPDATE doc_deletedevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE doc_docevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE doc_dochistory SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01'; +UPDATE doc_dochistory SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE doc_docreminder SET due = CONVERT_TZ(due, 'PST8PDT', 'UTC') WHERE due >= '1969-12-31 16:00:01'; +UPDATE doc_document SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01'; +UPDATE doc_document SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE doc_documentactionholder SET time_added = CONVERT_TZ(time_added, 'PST8PDT', 'UTC') WHERE time_added >= '1969-12-31 16:00:01'; +UPDATE doc_initialreviewdocevent SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01'; +UPDATE doc_irsgballotdocevent SET duedate = CONVERT_TZ(duedate, 'PST8PDT', 'UTC') WHERE duedate >= '1969-12-31 16:00:01'; +UPDATE doc_lastcalldocevent SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01'; +UPDATE group_group SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE group_groupevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE group_grouphistory SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE group_groupmilestone SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE group_groupmilestonehistory SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE ipr_iprdisclosurebase SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE ipr_iprevent SET response_due = CONVERT_TZ(response_due, 'PST8PDT', 'UTC') WHERE response_due >= '1969-12-31 16:00:01'; +UPDATE ipr_iprevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE liaisons_liaisonstatementevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE mailinglists_subscribed SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE mailinglists_whitelisted SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE meeting_floorplan SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01'; +UPDATE meeting_room SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01'; +UPDATE meeting_schedtimesessassignment SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01'; +UPDATE meeting_schedulingevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE meeting_session SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01'; +UPDATE meeting_session SET scheduled = CONVERT_TZ(scheduled, 'PST8PDT', 'UTC') WHERE scheduled >= '1969-12-31 16:00:01'; +UPDATE meeting_slidesubmission SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE meeting_timeslot SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01'; +UPDATE message_message SET sent = CONVERT_TZ(sent, 'PST8PDT', 'UTC') WHERE sent >= '1969-12-31 16:00:01'; +UPDATE message_message SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE message_sendqueue SET send_at = CONVERT_TZ(send_at, 'PST8PDT', 'UTC') WHERE send_at >= '1969-12-31 16:00:01'; +UPDATE message_sendqueue SET sent_at = CONVERT_TZ(sent_at, 'PST8PDT', 'UTC') WHERE sent_at >= '1969-12-31 16:00:01'; +UPDATE message_sendqueue SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE nomcom_feedback SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE nomcom_feedbacklastseen SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE nomcom_nomination SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE nomcom_nomineeposition SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE nomcom_topicfeedbacklastseen SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE oidc_provider_code SET expires_at = CONVERT_TZ(expires_at, 'PST8PDT', 'UTC') WHERE expires_at >= '1969-12-31 16:00:01'; +UPDATE oidc_provider_token SET expires_at = CONVERT_TZ(expires_at, 'PST8PDT', 'UTC') WHERE expires_at >= '1969-12-31 16:00:01'; +UPDATE oidc_provider_userconsent SET date_given = CONVERT_TZ(date_given, 'PST8PDT', 'UTC') WHERE date_given >= '1969-12-31 16:00:01'; +UPDATE oidc_provider_userconsent SET expires_at = CONVERT_TZ(expires_at, 'PST8PDT', 'UTC') WHERE expires_at >= '1969-12-31 16:00:01'; +UPDATE person_email SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE person_historicalemail SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01'; +UPDATE person_historicalemail SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE person_historicalperson SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01'; +UPDATE person_historicalperson SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE person_person SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE person_personalapikey SET created = CONVERT_TZ(created, 'PST8PDT', 'UTC') WHERE created >= '1969-12-31 16:00:01'; +UPDATE person_personalapikey SET latest = CONVERT_TZ(latest, 'PST8PDT', 'UTC') WHERE latest >= '1969-12-31 16:00:01'; +UPDATE person_personevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE request_profiler_profilingrecord SET end_ts = CONVERT_TZ(end_ts, 'PST8PDT', 'UTC') WHERE end_ts >= '1969-12-31 16:00:01'; +UPDATE request_profiler_profilingrecord SET start_ts = CONVERT_TZ(start_ts, 'PST8PDT', 'UTC') WHERE start_ts >= '1969-12-31 16:00:01'; +UPDATE review_historicalreviewassignment SET assigned_on = CONVERT_TZ(assigned_on, 'PST8PDT', 'UTC') WHERE assigned_on >= '1969-12-31 16:00:01'; +UPDATE review_historicalreviewassignment SET completed_on = CONVERT_TZ(completed_on, 'PST8PDT', 'UTC') WHERE completed_on >= '1969-12-31 16:00:01'; +UPDATE review_historicalreviewassignment SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01'; +UPDATE review_historicalreviewersettings SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01'; +UPDATE review_historicalreviewrequest SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01'; +UPDATE review_historicalreviewrequest SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE review_historicalunavailableperiod SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01'; +UPDATE review_reviewassignment SET assigned_on = CONVERT_TZ(assigned_on, 'PST8PDT', 'UTC') WHERE assigned_on >= '1969-12-31 16:00:01'; +UPDATE review_reviewassignment SET completed_on = CONVERT_TZ(completed_on, 'PST8PDT', 'UTC') WHERE completed_on >= '1969-12-31 16:00:01'; +UPDATE review_reviewrequest SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE review_reviewwish SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE south_migrationhistory SET applied = CONVERT_TZ(applied, 'PST8PDT', 'UTC') WHERE applied >= '1969-12-31 16:00:01'; +UPDATE submit_preapproval SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE submit_submissioncheck SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE submit_submissionevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; +UPDATE tastypie_apikey SET created = CONVERT_TZ(created, 'PST8PDT', 'UTC') WHERE created >= '1969-12-31 16:00:01'; +UPDATE utils_versioninfo SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01'; + +UPDATE meeting_timeslot + JOIN meeting_meeting + ON meeting_meeting.id = meeting_id + SET time = CONVERT_TZ(time, time_zone, 'UTC'); +"""), + migrations.RunPython(convert_pre1970_timestamps), + ] diff --git a/ietf/utils/migrations/0003_pause_to_change_use_tz.py b/ietf/utils/migrations/0003_pause_to_change_use_tz.py new file mode 100644 index 000000000..e2719e241 --- /dev/null +++ b/ietf/utils/migrations/0003_pause_to_change_use_tz.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.28 on 2022-08-29 10:16 + +from django.conf import settings +from django.db import migrations + + +def forward(apps, schema_editor): + assert getattr(settings, 'USE_TZ', False), 'Please change USE_TZ to True before continuing.' + + +def reverse(apps, schema_editor): + assert not getattr(settings, 'USE_TZ', False), 'Please change USE_TZ to False before continuing.' + + +class Migration(migrations.Migration): + + dependencies = [ + ('utils', '0002_convert_timestamps_to_utc'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/utils/serialize.py b/ietf/utils/serialize.py index 64254300a..342d211cf 100644 --- a/ietf/utils/serialize.py +++ b/ietf/utils/serialize.py @@ -1,3 +1,5 @@ +import datetime + from django.db import models def object_as_shallow_dict(obj): @@ -14,7 +16,7 @@ def object_as_shallow_dict(obj): if isinstance(f, models.ManyToManyField): v = list(v.values_list("pk", flat=True)) elif isinstance(f, models.DateTimeField): - v = v.strftime('%Y-%m-%d %H:%M:%S') + v = v.astimezone(datetime.timezone.utc).isoformat() elif isinstance(f, models.DateField): v = v.strftime('%Y-%m-%d') diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 7a7a77af4..2c6f048a5 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -6,6 +6,7 @@ import datetime from django.conf import settings from django.contrib.auth.models import User +from django.utils import timezone from django.utils.encoding import smart_text import debug # pyflakes:ignore @@ -20,6 +21,7 @@ from ietf.person.models import Person, Email from ietf.group.utils import setup_default_community_list_for_group from ietf.review.models import (ReviewRequest, ReviewerSettings, ReviewResultName, ReviewTypeName, ReviewTeamSettings ) from ietf.person.name import unidecode_name +from ietf.utils.timezone import date_today def create_person(group, role_name, name=None, username=None, email_address=None, password=None, is_staff=False, is_superuser=False): @@ -51,7 +53,7 @@ def make_immutable_base_data(): all tests in a run.""" # telechat dates - t = datetime.date.today() + datetime.timedelta(days=1) + t = date_today() + datetime.timedelta(days=1) old = TelechatDate.objects.create(date=t - datetime.timedelta(days=14)).date # pyflakes:ignore date1 = TelechatDate.objects.create(date=t).date # pyflakes:ignore date2 = TelechatDate.objects.create(date=t + datetime.timedelta(days=14)).date # pyflakes:ignore @@ -271,14 +273,14 @@ def make_test_data(): # old draft old_draft = Document.objects.create( name="draft-foo-mars-test", - time=datetime.datetime.now() - datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), + time=timezone.now() - datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), type_id="draft", title="Optimizing Martian Network Topologies", stream_id="ietf", abstract="Techniques for achieving near-optimal Martian networks.", rev="00", pages=2, - expires=datetime.datetime.now(), + expires=timezone.now(), ) old_draft.set_state(State.objects.get(used=True, type="draft", slug="expired")) old_alias = DocAlias.objects.create(name=old_draft.name) @@ -287,7 +289,7 @@ def make_test_data(): # draft draft = Document.objects.create( name="draft-ietf-mars-test", - time=datetime.datetime.now(), + time=timezone.now(), type_id="draft", title="Optimizing Martian Network Topologies", stream_id="ietf", @@ -298,7 +300,7 @@ def make_test_data(): intended_std_level_id="ps", shepherd=email, ad=ad, - expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), + expires=timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), notify="aliens@example.mars", note="", ) @@ -364,7 +366,7 @@ def make_test_data(): ietf72 = Meeting.objects.create( number="72", type_id="ietf", - date=datetime.date.today() + datetime.timedelta(days=180), + date=date_today() + datetime.timedelta(days=180), city="New York", country="US", time_zone="US/Eastern", @@ -458,7 +460,7 @@ def make_review_data(doc): doc=doc, team=team1, type_id="early", - deadline=datetime.datetime.now() + datetime.timedelta(days=20), + deadline=timezone.now() + datetime.timedelta(days=20), state_id="accepted", requested_by=reviewer, reviewer=email, diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py index f7790df55..dd492e58a 100644 --- a/ietf/utils/test_runner.py +++ b/ietf/utils/test_runner.py @@ -40,10 +40,8 @@ import os import sys import time import json -import pytz import importlib import socket -import datetime import gzip import unittest import pathlib @@ -76,6 +74,7 @@ from django.core.management import call_command from django.urls import URLResolver # type: ignore from django.template.backends.django import DjangoTemplates from django.template.backends.django import Template # type: ignore[attr-defined] +from django.utils import timezone # from django.utils.safestring import mark_safe import debug # pyflakes:ignore @@ -571,7 +570,7 @@ class CoverageTest(unittest.TestCase): checker.stop() # Save to the .coverage file checker.save() - # Apply the configured and requested omit and include data + # Apply the configured and requested omit and include data checker.config.from_args(ignore_errors=None, omit=settings.TEST_CODE_COVERAGE_EXCLUDE_FILES, include=include, file=None) for pattern in settings.TEST_CODE_COVERAGE_EXCLUDE_LINES: @@ -744,7 +743,7 @@ class IetfTestRunner(DiscoverRunner): print(" Datatracker %s test suite, %s:" % (ietf.__version__, time.strftime("%d %B %Y %H:%M:%S %Z"))) print(" Python %s." % sys.version.replace('\n', ' ')) print(" Django %s, settings '%s'" % (django.get_version(), settings.SETTINGS_MODULE)) - + settings.TEMPLATES[0]['BACKEND'] = 'ietf.utils.test_runner.ValidatingTemplates' if self.check_coverage: if self.coverage_file.endswith('.gz'): @@ -754,19 +753,19 @@ class IetfTestRunner(DiscoverRunner): with io.open(self.coverage_file, encoding='utf-8') as file: self.coverage_master = json.load(file) self.coverage_data = { - "time": datetime.datetime.now(pytz.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "time": timezone.now().strftime("%Y-%m-%dT%H:%M:%SZ"), "template": { - "coverage": 0.0, + "coverage": 0.0, "covered": {}, "format": 1, # default format, coverage data in 'covered' are just fractions }, "url": { - "coverage": 0.0, + "coverage": 0.0, "covered": {}, "format": 4, }, "code": { - "coverage": 0.0, + "coverage": 0.0, "covered": {}, "format": 1, }, @@ -813,8 +812,8 @@ class IetfTestRunner(DiscoverRunner): for offset in range(10): try: # remember the value so ietf.utils.mail.send_smtp() will use the same - ietf.utils.mail.SMTP_ADDR['port'] = base + offset - self.smtpd_driver = SMTPTestServerDriver((ietf.utils.mail.SMTP_ADDR['ip4'],ietf.utils.mail.SMTP_ADDR['port']),None) + ietf.utils.mail.SMTP_ADDR['port'] = base + offset + self.smtpd_driver = SMTPTestServerDriver((ietf.utils.mail.SMTP_ADDR['ip4'],ietf.utils.mail.SMTP_ADDR['port']),None) self.smtpd_driver.start() print((" Running an SMTP test server on %(ip4)s:%(port)s to catch outgoing email." % ietf.utils.mail.SMTP_ADDR)) break diff --git a/ietf/utils/tests_meetecho.py b/ietf/utils/tests_meetecho.py index d40e013f8..db3d36f40 100644 --- a/ietf/utils/tests_meetecho.py +++ b/ietf/utils/tests_meetecho.py @@ -1,15 +1,16 @@ # Copyright The IETF Trust 2021, All Rights Reserved # -*- coding: utf-8 -*- import datetime +import pytz import requests import requests_mock -from pytz import timezone, utc from unittest.mock import patch from urllib.parse import urljoin from django.conf import settings from django.test import override_settings +from django.utils import timezone from ietf.utils.tests import TestCase from .meetecho import Conference, ConferenceManager, MeetechoAPI, MeetechoAPIError @@ -92,7 +93,7 @@ class APITests(TestCase): api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) api_response = api.schedule_meeting( wg_token='my-token', - start_time=utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), + start_time=pytz.utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), duration=datetime.timedelta(minutes=130), description='interim-2021-wgname-01', extrainfo='message for staff', @@ -120,11 +121,11 @@ class APITests(TestCase): ) # same time in different time zones for start_time in [ - utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), - timezone('america/halifax').localize(datetime.datetime(2021, 9, 14, 7, 0, 0)), - timezone('europe/kiev').localize(datetime.datetime(2021, 9, 14, 13, 0, 0)), - timezone('pacific/easter').localize(datetime.datetime(2021, 9, 14, 5, 0, 0)), - timezone('africa/porto-novo').localize(datetime.datetime(2021, 9, 14, 11, 0, 0)), + pytz.utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), + pytz.timezone('america/halifax').localize(datetime.datetime(2021, 9, 14, 7, 0, 0)), + pytz.timezone('europe/kiev').localize(datetime.datetime(2021, 9, 14, 13, 0, 0)), + pytz.timezone('pacific/easter').localize(datetime.datetime(2021, 9, 14, 5, 0, 0)), + pytz.timezone('africa/porto-novo').localize(datetime.datetime(2021, 9, 14, 11, 0, 0)), ]: self.assertEqual( api_response, @@ -191,7 +192,7 @@ class APITests(TestCase): '3d55bce0-535e-4ba8-bb8e-734911cf3c32': { 'room': { 'id': 18, - 'start_time': utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), + 'start_time': pytz.utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)), 'duration': datetime.timedelta(minutes=130), 'description': 'interim-2021-wgname-01', }, @@ -201,7 +202,7 @@ class APITests(TestCase): 'e68e96d4-d38f-475b-9073-ecab46ca96a5': { 'room': { 'id': 23, - 'start_time': utc.localize(datetime.datetime(2021, 9, 15, 14, 30, 0)), + 'start_time': pytz.utc.localize(datetime.datetime(2021, 9, 15, 14, 30, 0)), 'duration': datetime.timedelta(minutes=30), 'description': 'interim-2021-wgname-02', }, @@ -249,7 +250,7 @@ class APITests(TestCase): def test_time_serialization(self): """Time de/serialization should be consistent""" - time = datetime.datetime.now(utc).replace(microsecond=0) # cut off to 0 microseconds + time = timezone.now().astimezone(pytz.utc).replace(microsecond=0) # cut off to 0 microseconds api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) self.assertEqual(api._deserialize_time(api._serialize_time(time)), time) @@ -263,7 +264,7 @@ class ConferenceManagerTests(TestCase): 'session-1-uuid': { 'room': { 'id': 1, - 'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)), + 'start_time': pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)), 'duration': datetime.timedelta(minutes=45), 'description': 'some-description', }, @@ -273,7 +274,7 @@ class ConferenceManagerTests(TestCase): 'session-2-uuid': { 'room': { 'id': 2, - 'start_time': utc.localize(datetime.datetime(2022,2,5,4,5,6)), + 'start_time': pytz.utc.localize(datetime.datetime(2022,2,5,4,5,6)), 'duration': datetime.timedelta(minutes=90), 'description': 'another-description', }, @@ -290,7 +291,7 @@ class ConferenceManagerTests(TestCase): id=1, public_id='session-1-uuid', description='some-description', - start_time=utc.localize(datetime.datetime(2022, 2, 4, 1, 2, 3)), + start_time=pytz.utc.localize(datetime.datetime(2022, 2, 4, 1, 2, 3)), duration=datetime.timedelta(minutes=45), url='https://example.com/some/url', deletion_token='delete-me', @@ -300,7 +301,7 @@ class ConferenceManagerTests(TestCase): id=2, public_id='session-2-uuid', description='another-description', - start_time=utc.localize(datetime.datetime(2022, 2, 5, 4, 5, 6)), + start_time=pytz.utc.localize(datetime.datetime(2022, 2, 5, 4, 5, 6)), duration=datetime.timedelta(minutes=90), url='https://example.com/another/url', deletion_token='delete-me-too', @@ -316,7 +317,7 @@ class ConferenceManagerTests(TestCase): 'session-1-uuid': { 'room': { 'id': 1, - 'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)), + 'start_time': pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)), 'duration': datetime.timedelta(minutes=45), 'description': 'some-description', }, @@ -335,7 +336,7 @@ class ConferenceManagerTests(TestCase): id=1, public_id='session-1-uuid', description='some-description', - start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)), + start_time=pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)), duration=datetime.timedelta(minutes=45), url='https://example.com/some/url', deletion_token='delete-me', @@ -351,7 +352,7 @@ class ConferenceManagerTests(TestCase): 'session-1-uuid': { 'room': { 'id': 1, - 'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)), + 'start_time': pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)), 'duration': datetime.timedelta(minutes=45), 'description': 'some-description', }, @@ -369,7 +370,7 @@ class ConferenceManagerTests(TestCase): id=1, public_id='session-1-uuid', description='some-description', - start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)), + start_time=pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)), duration=datetime.timedelta(minutes=45), url='https://example.com/some/url', deletion_token='delete-me', diff --git a/ietf/utils/timezone.py b/ietf/utils/timezone.py index 723512efe..0aeedbeda 100644 --- a/ietf/utils/timezone.py +++ b/ietf/utils/timezone.py @@ -1,39 +1,82 @@ -import pytz -import email.utils import datetime +from typing import Union +from zoneinfo import ZoneInfo + from django.conf import settings +from django.utils import timezone -def local_timezone_to_utc(d): - """Takes a naive datetime in the local timezone and returns a - naive datetime with the corresponding UTC time.""" - local_timezone = pytz.timezone(settings.TIME_ZONE) - d = local_timezone.localize(d).astimezone(pytz.utc) +# Timezone constants - tempting to make these settings, but changing them will +# require code changes. +# +# Default time zone for deadlines / expiration dates. +DEADLINE_TZINFO = ZoneInfo('PST8PDT') - return d.replace(tzinfo=None) +# Time zone for dates from the RPC. This value is baked into the timestamps on DocEvents +# of type="published_rfc" - see Document.pub_date() and ietf.sync.refceditor.update_docs_from_rfc_index() +# for more information about how that works. +RPC_TZINFO = ZoneInfo('PST8PDT') -def utc_to_local_timezone(d): - """Takes a naive datetime UTC and returns a naive datetime in the - local time zone.""" - local_timezone = pytz.timezone(settings.TIME_ZONE) - d = local_timezone.normalize(d.replace(tzinfo=pytz.utc).astimezone(local_timezone)) +def _tzinfo(tz: Union[str, datetime.tzinfo, None]): + """Helper to convert a tz param into a tzinfo - return d.replace(tzinfo=None) + Accepts Defaults to UTC. + """ + if tz is None: + return datetime.timezone.utc + elif isinstance(tz, datetime.tzinfo): + return tz + else: + return ZoneInfo(tz) -def email_time_to_local_timezone(date_string): - """Takes a time string from an email and returns a naive datetime - in the local time zone.""" - t = email.utils.parsedate_tz(date_string) - d = datetime.datetime(*t[:6]) +def make_aware(dt, tz): + """Assign timezone to a naive datetime - if t[7] != None: - d += datetime.timedelta(seconds=t[9]) + Helper to deal with both pytz and zoneinfo type time zones. Can go away when pytz is removed. + """ + tzinfo = _tzinfo(tz) + if hasattr(tzinfo, 'localize'): + return tzinfo.localize(dt) # pytz-style + else: + return dt.replace(tzinfo=tzinfo) # zoneinfo- / datetime.timezone-style - return utc_to_local_timezone(d) -def date2datetime(date, tz=pytz.utc): - return datetime.datetime(*(date.timetuple()[:6]), tzinfo=tz) - +def datetime_from_date(date, tz=None): + """Get datetime at midnight on a given date""" + # accept either pytz or zoneinfo tzinfos until we get rid of pytz + return make_aware(datetime.datetime(date.year, date.month, date.day), _tzinfo(tz)) + + +def datetime_today(tz=None): + """Get a timezone-aware datetime representing midnight today + + By default, uses settings.TIME_ZONE + For use with datetime fields representing a date. + """ + if tz is None: + tz = settings.TIME_ZONE + return timezone.now().astimezone(_tzinfo(tz)).replace(hour=0, minute=0, second=0, microsecond=0) + + +def date_today(tz=None): + """Get the date corresponding to the current moment + + By default, uses settings.TIME_ZONE + Note that Dates are not themselves timezone aware. + """ + if tz is None: + tz = settings.TIME_ZONE + return timezone.now().astimezone(_tzinfo(tz)).date() + + +def time_now(tz=None): + """Get the "wall clock" time corresponding to the current moment + + The value returned by this data is a Time with no tzinfo attached. (Time + objects have only limited timezone support, even if tzinfo is filled in, + and may not behave correctly when daylight savings time shifts are relevant.) + """ + return timezone.now().astimezone(_tzinfo(tz)).time()