Merge branch 'main' into feat/postgres
This commit is contained in:
commit
1f990bce1d
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- Python -*-
|
||||
#
|
||||
|
||||
# This script requires that the proper virtual python environment has been
|
||||
# invoked before start
|
||||
|
||||
# Set PYTHONPATH and load environment variables for standalone script -----------------
|
||||
import os, sys
|
||||
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
sys.path = [ basedir ] + sys.path
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings"
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
# -------------------------------------------------------------------------------------
|
||||
|
||||
from ietf.secr.proceedings.reports import report_id_activity
|
||||
|
||||
print(report_id_activity(sys.argv[1], sys.argv[2]), end='')
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- Python -*-
|
||||
#
|
||||
|
||||
# This script requires that the proper virtual python environment has been
|
||||
# invoked before start
|
||||
|
||||
# Set PYTHONPATH and load environment variables for standalone script -----------------
|
||||
import os, sys
|
||||
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
sys.path = [ basedir ] + sys.path
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings"
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
# -------------------------------------------------------------------------------------
|
||||
|
||||
from ietf.secr.proceedings.reports import report_progress_report
|
||||
|
||||
# handle unicode characters before attempting to print
|
||||
output = report_progress_report(sys.argv[1], sys.argv[2])
|
||||
output = output.replace(chr(160),' ') # replace NO-BREAK SPACE with space
|
||||
print(output, end='')
|
|
@ -96,6 +96,12 @@ class IESGTests(TestCase):
|
|||
ads = Role.objects.filter(group__type='area', group__state='active', name_id='ad')
|
||||
self.assertEqual(len(q('.photo')), ads.count())
|
||||
|
||||
def test_ietf_activity(self):
|
||||
url = urlreverse("ietf.iesg.views.ietf_activity")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class IESGAgendaTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
@ -542,4 +548,4 @@ class RescheduleOnAgendaTests(TestCase):
|
|||
self.assertTrue(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat"))
|
||||
self.assertEqual(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date, d)
|
||||
self.assertTrue(not draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").returning_item)
|
||||
self.assertEqual(draft.docevent_set.count(), events_before + 1)
|
||||
self.assertEqual(draft.docevent_set.count(), events_before + 1)
|
||||
|
|
|
@ -55,6 +55,7 @@ urlpatterns = [
|
|||
url(r'^past/documents/$', views.past_documents),
|
||||
url(r'^agenda/telechat-(?:%(date)s-)?docs.tgz' % settings.URL_REGEXPS, views.telechat_docs_tarfile),
|
||||
url(r'^discusses/$', views.discusses),
|
||||
url(r'^ietf-activity/$', views.ietf_activity),
|
||||
url(r'^milestones/$', views.milestones_needing_review),
|
||||
url(r'^photos/$', views.photos),
|
||||
]
|
||||
]
|
||||
|
|
|
@ -41,6 +41,7 @@ import json
|
|||
import os
|
||||
import tarfile
|
||||
import time
|
||||
from dateutil import relativedelta
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -62,6 +63,7 @@ from ietf.iesg.models import TelechatDate
|
|||
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.secr.proceedings.proc_utils import get_activity_stats
|
||||
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
|
||||
|
||||
|
@ -528,4 +530,34 @@ def photos(request):
|
|||
role.last_initial = role.person.last_name()[0]
|
||||
return render(request, 'iesg/photos.html', {'group_type': 'IESG', 'role': '', 'roles': roles })
|
||||
|
||||
|
||||
def month_choices():
|
||||
choices = [(str(n).zfill(2), str(n).zfill(2)) for n in range(1, 13)]
|
||||
return choices
|
||||
|
||||
def year_choices():
|
||||
this_year = date_today().year
|
||||
choices = [(str(n), str(n)) for n in range(this_year, 2009, -1)]
|
||||
return choices
|
||||
|
||||
class ActivityForm(forms.Form):
|
||||
month = forms.ChoiceField(choices=month_choices, help_text='Month', required=True)
|
||||
year = forms.ChoiceField(choices=year_choices, help_text='Year', required=True)
|
||||
|
||||
def ietf_activity(request):
|
||||
# default date range for last month
|
||||
today = date_today()
|
||||
edate = today.replace(day=1)
|
||||
sdate = (edate - datetime.timedelta(days=1)).replace(day=1)
|
||||
if request.method == 'GET':
|
||||
form = ActivityForm(request.GET)
|
||||
if form.is_valid():
|
||||
month = form.cleaned_data['month']
|
||||
year = form.cleaned_data['year']
|
||||
sdate = datetime.date(int(year), int(month), 1)
|
||||
edate = sdate + relativedelta.relativedelta(months=1)
|
||||
|
||||
# always pass back an unbound form to avoid annoying is-valid styling
|
||||
form = ActivityForm(initial={'month': str(sdate.month).zfill(2), 'year': sdate.year})
|
||||
context = get_activity_stats(sdate, edate)
|
||||
context['form'] = form
|
||||
return render(request, "iesg/ietf_activity_report.html", context)
|
||||
|
|
|
@ -39,7 +39,7 @@ from ietf.ietfauth.utils import has_role
|
|||
from ietf.mailinglists.models import Subscribed
|
||||
from ietf.meeting.factories import MeetingFactory
|
||||
from ietf.nomcom.factories import NomComFactory
|
||||
from ietf.person.factories import PersonFactory, EmailFactory, UserFactory
|
||||
from ietf.person.factories import PersonFactory, EmailFactory, UserFactory, PersonalApiKeyFactory
|
||||
from ietf.person.models import Person, Email, PersonalApiKey
|
||||
from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
|
||||
from ietf.review.models import ReviewWish, UnavailablePeriod
|
||||
|
@ -738,8 +738,20 @@ class IetfAuthTests(TestCase):
|
|||
url = urlreverse('ietf.ietfauth.views.apikey_disable')
|
||||
r = self.client.get(url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, 'Disable a personal API key')
|
||||
self.assertContains(r, 'Key')
|
||||
|
||||
# Try to delete something that doesn't exist
|
||||
r = self.client.post(url, {'hash': key.hash()+'bad'})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r,"Key validation failed; key not disabled")
|
||||
|
||||
# Try to delete someone else's key
|
||||
otherkey = PersonalApiKeyFactory()
|
||||
r = self.client.post(url, {'hash': otherkey.hash()})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r,"Key validation failed; key not disabled")
|
||||
|
||||
# Delete a key
|
||||
r = self.client.post(url, {'hash': key.hash()})
|
||||
|
|
|
@ -781,7 +781,7 @@ def apikey_disable(request):
|
|||
#
|
||||
class KeyDeleteForm(forms.Form):
|
||||
hash = forms.ChoiceField(label='Key', choices=choices)
|
||||
def clean_key(self):
|
||||
def clean_hash(self):
|
||||
hash = force_bytes(self.cleaned_data['hash'])
|
||||
key = PersonalApiKey.validate_key(hash)
|
||||
if key and key.person == request.user.person:
|
||||
|
@ -792,7 +792,7 @@ def apikey_disable(request):
|
|||
if request.method == 'POST':
|
||||
form = KeyDeleteForm(request.POST)
|
||||
if form.is_valid():
|
||||
hash = force_bytes(form.data['hash'])
|
||||
hash = force_bytes(form.cleaned_data['hash'])
|
||||
key = PersonalApiKey.validate_key(hash)
|
||||
key.valid = False
|
||||
key.save()
|
||||
|
|
|
@ -36,12 +36,14 @@ from ietf.utils.pipe import pipe
|
|||
from ietf.utils.text import xslugify
|
||||
|
||||
|
||||
def get_meeting(num=None,type_in=['ietf',],days=28):
|
||||
def get_meeting(num=None, type_in=('ietf',), days=28):
|
||||
meetings = Meeting.objects
|
||||
if type_in:
|
||||
if type_in is not None:
|
||||
meetings = meetings.filter(type__in=type_in)
|
||||
if num == None:
|
||||
meetings = meetings.filter(date__gte=timezone.now()-datetime.timedelta(days=days)).order_by('date')
|
||||
if num is None:
|
||||
meetings = meetings.filter(
|
||||
date__gte=timezone.now() - datetime.timedelta(days=days)
|
||||
).order_by('date')
|
||||
else:
|
||||
meetings = meetings.filter(number=num)
|
||||
if meetings.exists():
|
||||
|
|
|
@ -602,10 +602,6 @@ class TimeSlot(models.Model):
|
|||
self._session_cache = self.sessions.filter(timeslotassignments__schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting else None]).first()
|
||||
return self._session_cache
|
||||
|
||||
@property
|
||||
def time_desc(self):
|
||||
return "%s-%s" % (self.time.strftime("%H%M"), (self.time + self.duration).strftime("%H%M"))
|
||||
|
||||
def meeting_date(self):
|
||||
return self.time.date()
|
||||
|
||||
|
|
|
@ -228,9 +228,6 @@ class MeetingTests(BaseMeetingTestCase):
|
|||
|
||||
registration_text = "Registration"
|
||||
|
||||
# utc
|
||||
time_interval = r"%s<span.*/span>-%s" % (slot.utc_start_time().strftime("%H:%M").lstrip("0"), (slot.utc_start_time() + slot.duration).strftime("%H:%M").lstrip("0"))
|
||||
|
||||
# Extremely rudementary test of agenda-neue - to be replaced with back-end tests as the front-end tests are developed.
|
||||
r = self.client.get(urlreverse("agenda", kwargs=dict(num=meeting.number,utc='-utc')))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
@ -279,23 +276,32 @@ class MeetingTests(BaseMeetingTestCase):
|
|||
}
|
||||
)
|
||||
|
||||
# plain
|
||||
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
|
||||
time_interval = "%s-%s" % (slot.time.strftime("%H%M").lstrip("0"), (slot.time + slot.duration).strftime("%H%M").lstrip("0"))
|
||||
|
||||
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".txt")))
|
||||
self.assertContains(r, session.group.acronym)
|
||||
self.assertContains(r, session.group.name)
|
||||
self.assertContains(r, session.group.parent.acronym.upper())
|
||||
self.assertContains(r, slot.location.name)
|
||||
self.assertContains(r, "{}-{}".format(
|
||||
slot.time.astimezone(meeting.tz()).strftime("%H%M"),
|
||||
(slot.time + slot.duration).astimezone(meeting.tz()).strftime("%H%M"),
|
||||
))
|
||||
self.assertContains(r, f"shown in the {meeting.tz()} time zone")
|
||||
|
||||
self.assertContains(r, time_interval)
|
||||
# text, UTC
|
||||
r = self.client.get(urlreverse(
|
||||
"ietf.meeting.views.agenda_plain",
|
||||
kwargs=dict(num=meeting.number, ext=".txt", utc="-utc"),
|
||||
))
|
||||
self.assertContains(r, session.group.acronym)
|
||||
self.assertContains(r, session.group.name)
|
||||
self.assertContains(r, session.group.parent.acronym.upper())
|
||||
self.assertContains(r, slot.location.name)
|
||||
self.assertContains(r, "{}-{}".format(
|
||||
slot.time.astimezone(datetime.timezone.utc).strftime("%H%M"),
|
||||
(slot.time + slot.duration).astimezone(datetime.timezone.utc).strftime("%H%M"),
|
||||
))
|
||||
self.assertContains(r, "shown in UTC")
|
||||
|
||||
# future meeting, no agenda
|
||||
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=future_meeting.number, ext=".txt")))
|
||||
|
@ -339,6 +345,39 @@ class MeetingTests(BaseMeetingTestCase):
|
|||
r = self.client.get(urlreverse('floor-plan', kwargs=dict(num=meeting.number)))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_agenda_ical_next_meeting_type(self):
|
||||
# start with no upcoming IETF meetings, just an interim
|
||||
MeetingFactory(
|
||||
type_id="interim", date=date_today() + datetime.timedelta(days=15)
|
||||
)
|
||||
r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs={}))
|
||||
self.assertEqual(
|
||||
r.status_code, 404, "Should not return an interim meeting as next meeting"
|
||||
)
|
||||
# create an IETF meeting after the interim - it should be found as "next"
|
||||
ietf_meeting = MeetingFactory(
|
||||
type_id="ietf", date=date_today() + datetime.timedelta(days=30)
|
||||
)
|
||||
SessionFactory(meeting=ietf_meeting, name="Session at IETF meeting")
|
||||
r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs={}))
|
||||
self.assertContains(r, "Session at IETF meeting", status_code=200)
|
||||
|
||||
def test_agenda_json_next_meeting_type(self):
|
||||
# start with no upcoming IETF meetings, just an interim
|
||||
MeetingFactory(
|
||||
type_id="interim", date=date_today() + datetime.timedelta(days=15)
|
||||
)
|
||||
r = self.client.get(urlreverse("ietf.meeting.views.agenda_json", kwargs={}))
|
||||
self.assertEqual(
|
||||
r.status_code, 404, "Should not return an interim meeting as next meeting"
|
||||
)
|
||||
# create an IETF meeting after the interim - it should be found as "next"
|
||||
ietf_meeting = MeetingFactory(
|
||||
type_id="ietf", date=date_today() + datetime.timedelta(days=30)
|
||||
)
|
||||
SessionFactory(meeting=ietf_meeting, name="Session at IETF meeting")
|
||||
r = self.client.get(urlreverse("ietf.meeting.views.agenda_json", kwargs={}))
|
||||
self.assertContains(r, "Session at IETF meeting", status_code=200)
|
||||
|
||||
@override_settings(PROCEEDINGS_V1_BASE_URL='https://example.com/{meeting.number}')
|
||||
def test_agenda_redirects_for_old_meetings(self):
|
||||
|
@ -7535,7 +7574,7 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
)
|
||||
self.assertNotEqual(
|
||||
pq('a[href="{}"]'.format(
|
||||
urlreverse('ietf.meeting.views.proceedings_progress_report', kwargs=dict(num=meeting.number)))
|
||||
urlreverse('ietf.meeting.views.proceedings_activity_report', kwargs=dict(num=meeting.number)))
|
||||
),
|
||||
[],
|
||||
'Should have a link to activity report',
|
||||
|
@ -7701,14 +7740,14 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
response = self.client.get(url)
|
||||
self.assertContains(response, 'The Internet Engineering Task Force')
|
||||
|
||||
def test_proceedings_progress_report(self):
|
||||
def test_proceedings_activity_report(self):
|
||||
make_meeting_test_data()
|
||||
MeetingFactory(type_id='ietf', date=datetime.date(2016,4,3), number="96")
|
||||
MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
|
||||
|
||||
url = urlreverse('ietf.meeting.views.proceedings_progress_report',kwargs={'num':97})
|
||||
url = urlreverse('ietf.meeting.views.proceedings_activity_report',kwargs={'num':97})
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'Progress Report')
|
||||
self.assertContains(response, 'Activity Report')
|
||||
|
||||
def test_feed(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
|
|
@ -69,7 +69,7 @@ type_interim_patterns = [
|
|||
|
||||
type_ietf_only_patterns_id_optional = [
|
||||
url(r'^agenda(?P<utc>-utc)?(?P<ext>\.html)?/?$', views.agenda, name='agenda'),
|
||||
url(r'^agenda(?P<ext>\.txt)$', views.agenda_plain),
|
||||
url(r'^agenda(?P<utc>-utc)?(?P<ext>\.txt)$', views.agenda_plain),
|
||||
url(r'^agenda(?P<ext>\.csv)$', views.agenda_plain),
|
||||
url(r'^agenda/edit$',
|
||||
RedirectView.as_view(pattern_name='ietf.meeting.views.edit_meeting_schedule', permanent=True),
|
||||
|
@ -92,7 +92,7 @@ type_ietf_only_patterns_id_optional = [
|
|||
url(r'^proceedings/acknowledgements/$', views.proceedings_acknowledgements),
|
||||
url(r'^proceedings/attendees/$', views.proceedings_attendees),
|
||||
url(r'^proceedings/overview/$', views.proceedings_overview),
|
||||
url(r'^proceedings/progress-report/$', views.proceedings_progress_report),
|
||||
url(r'^proceedings/activity-report/$', views.proceedings_activity_report),
|
||||
url(r'^proceedings/materials/$', views_proceedings.material_details),
|
||||
url(r'^proceedings/materials/(?P<material_type>[a-z_]+)/$', views_proceedings.edit_material),
|
||||
url(r'^proceedings/materials/(?P<material_type>[a-z_]+)/new/$', views_proceedings.upload_material),
|
||||
|
|
|
@ -84,7 +84,7 @@ from ietf.meeting.utils import preprocess_meeting_important_dates
|
|||
from ietf.meeting.utils import new_doc_for_session, write_doc_for_session
|
||||
from ietf.message.utils import infer_message
|
||||
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
|
||||
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
|
||||
from ietf.secr.proceedings.proc_utils import (get_activity_stats, post_process, import_audio_files,
|
||||
create_recording)
|
||||
from ietf.utils import markdown
|
||||
from ietf.utils.decorators import require_api_key
|
||||
|
@ -1521,7 +1521,7 @@ def get_assignments_for_agenda(schedule):
|
|||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def agenda_plain(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
|
||||
def agenda_plain(request, num=None, name=None, base=None, ext=None, owner=None, utc=None):
|
||||
base = base if base else 'agenda'
|
||||
ext = ext if ext else '.txt'
|
||||
mimetype = {
|
||||
|
@ -1571,7 +1571,7 @@ 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())
|
||||
|
||||
display_timezone = 'UTC' if utc else meeting.time_zone
|
||||
display_timezone = meeting.time_zone if utc is None else 'UTC'
|
||||
with timezone.override(display_timezone):
|
||||
rendered_page = render(
|
||||
request,
|
||||
|
@ -1805,18 +1805,18 @@ def agenda_extract_slide (item):
|
|||
}
|
||||
|
||||
def agenda_csv(schedule, filtered_assignments):
|
||||
response = HttpResponse(content_type="text/csv; charset=%s"%settings.DEFAULT_CHARSET)
|
||||
encoding = 'utf-8'
|
||||
response = HttpResponse(content_type=f"text/csv; charset={encoding}")
|
||||
writer = csv.writer(response, delimiter=str(','), quoting=csv.QUOTE_ALL)
|
||||
|
||||
headings = ["Date", "Start", "End", "Session", "Room", "Area", "Acronym", "Type", "Description", "Session ID", "Agenda", "Slides"]
|
||||
|
||||
def write_row(row):
|
||||
encoded_row = [v.encode('utf-8') if isinstance(v, str) else v for v in row]
|
||||
|
||||
while len(encoded_row) < len(headings):
|
||||
encoded_row.append(None) # produce empty entries at the end as necessary
|
||||
|
||||
writer.writerow(encoded_row)
|
||||
if len(row) < len(headings):
|
||||
padding = [None] * (len(headings) - len(row)) # produce empty entries at the end as necessary
|
||||
else:
|
||||
padding = []
|
||||
writer.writerow(row + padding)
|
||||
|
||||
def agenda_field(item):
|
||||
agenda_doc = item.session.agenda()
|
||||
|
@ -2041,10 +2041,13 @@ def should_include_assignment(filter_params, assignment):
|
|||
hidden = len(set(filter_params['hide']).intersection(assignment.filter_keywords)) > 0
|
||||
return shown and not hidden
|
||||
|
||||
def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
|
||||
def agenda_ical(request, num=None, acronym=None, session_id=None):
|
||||
"""Agenda ical view
|
||||
|
||||
By default, all agenda items will be shown. A filter can be specified in
|
||||
If num is None, looks for the next IETF meeting. Otherwise, uses the requested meeting
|
||||
regardless of its type.
|
||||
|
||||
By default, all agenda items will be shown. A filter can be specified in
|
||||
the querystring. It has the format
|
||||
|
||||
?show=...&hide=...&showtypes=...&hidetypes=...
|
||||
|
@ -2059,8 +2062,13 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
|
|||
|
||||
Hiding (by wg or type) takes priority over showing.
|
||||
"""
|
||||
meeting = get_meeting(num, type_in=None)
|
||||
schedule = get_schedule(meeting, name)
|
||||
if num is None:
|
||||
meeting = get_ietf_meeting()
|
||||
if meeting is None:
|
||||
raise Http404
|
||||
else:
|
||||
meeting = get_meeting(num, type_in=None) # get requested meeting, whatever its type
|
||||
schedule = get_schedule(meeting)
|
||||
updated = meeting.updated()
|
||||
|
||||
if schedule is None and acronym is None and session_id is None:
|
||||
|
@ -2099,7 +2107,12 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
|
|||
|
||||
@cache_page(15 * 60)
|
||||
def agenda_json(request, num=None):
|
||||
meeting = get_meeting(num, type_in=['ietf','interim'])
|
||||
if num is None:
|
||||
meeting = get_ietf_meeting()
|
||||
if meeting is None:
|
||||
raise Http404
|
||||
else:
|
||||
meeting = get_meeting(num, type_in=None) # get requested meeting, whatever its type
|
||||
|
||||
sessions = []
|
||||
locations = set()
|
||||
|
@ -3803,8 +3816,8 @@ def proceedings_overview(request, num=None):
|
|||
'template': template,
|
||||
})
|
||||
|
||||
def proceedings_progress_report(request, num=None):
|
||||
'''Display Progress Report (stats since last meeting)'''
|
||||
def proceedings_activity_report(request, num=None):
|
||||
'''Display Activity Report (stats since last meeting)'''
|
||||
if not (num and num.isdigit()):
|
||||
raise Http404
|
||||
meeting = get_meeting(num)
|
||||
|
@ -3812,9 +3825,10 @@ def proceedings_progress_report(request, num=None):
|
|||
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}/progress-report.html')
|
||||
sdate = meeting.previous_meeting().date
|
||||
edate = meeting.date
|
||||
context = get_progress_stats(sdate,edate)
|
||||
context = get_activity_stats(sdate,edate)
|
||||
context['meeting'] = meeting
|
||||
return render(request, "meeting/proceedings_progress_report.html", context)
|
||||
context['is_meeting_report'] = True
|
||||
return render(request, "meeting/proceedings_activity_report.html", context)
|
||||
|
||||
class OldUploadRedirect(RedirectView):
|
||||
def get_redirect_url(self, **kwargs):
|
||||
|
|
|
@ -199,10 +199,10 @@ def send_audio_import_warning(unmatched_files):
|
|||
# End Recording Functions
|
||||
# -------------------------------------------------
|
||||
|
||||
def get_progress_stats(sdate, edate):
|
||||
def get_activity_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
|
||||
use in an activity 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.
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
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=timezone.now(),type='ietf').order_by('-date')[0]
|
||||
syear,smonth,sday = start.split('-')
|
||||
eyear,emonth,eday = end.split('-')
|
||||
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',
|
||||
docevent__time__gte=sdate,
|
||||
docevent__time__lte=edate)
|
||||
new = new_docs.count()
|
||||
updated = 0
|
||||
updated_more = 0
|
||||
for d in new_docs:
|
||||
updates = d.docevent_set.filter(type='new_revision',time__gte=sdate,time__lte=edate).count()
|
||||
if updates > 1:
|
||||
updated += 1
|
||||
if updates > 2:
|
||||
updated_more +=1
|
||||
|
||||
# calculate total documents updated, not counting new, rev=00
|
||||
result = set()
|
||||
events = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lte=edate)
|
||||
for e in events.filter(type='new_revision').exclude(newrevisiondocevent__rev='00'):
|
||||
result.add(e.doc)
|
||||
total_updated = len(result)
|
||||
|
||||
# calculate sent last call
|
||||
last_call = events.filter(type='sent_last_call').count()
|
||||
|
||||
# calculate approved
|
||||
approved = events.filter(type='iesg_approved').count()
|
||||
|
||||
# get 4 weeks
|
||||
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)
|
||||
#ff3_date = cutoff - datetime.timedelta(days=14)
|
||||
#ff4_date = cutoff - datetime.timedelta(days=7)
|
||||
|
||||
ff_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision',
|
||||
docevent__newrevisiondocevent__rev='00',
|
||||
docevent__time__gte=ff1_date,
|
||||
docevent__time__lte=cutoff)
|
||||
ff_new_count = ff_docs.count()
|
||||
ff_new_percent = format(ff_new_count / float(new),'.0%')
|
||||
|
||||
# calculate total documents updated in final four weeks, not counting new, rev=00
|
||||
result = set()
|
||||
events = DocEvent.objects.filter(doc__type='draft',time__gte=ff1_date,time__lte=cutoff)
|
||||
for e in events.filter(type='new_revision').exclude(newrevisiondocevent__rev='00'):
|
||||
result.add(e.doc)
|
||||
ff_update_count = len(result)
|
||||
ff_update_percent = format(ff_update_count / float(total_updated),'.0%')
|
||||
|
||||
#aug_docs = augment_with_start_time(new_docs)
|
||||
'''
|
||||
ff1_new = aug_docs.filter(start_date__gte=ff1_date,start_date__lt=ff2_date)
|
||||
ff2_new = aug_docs.filter(start_date__gte=ff2_date,start_date__lt=ff3_date)
|
||||
ff3_new = aug_docs.filter(start_date__gte=ff3_date,start_date__lt=ff4_date)
|
||||
ff4_new = aug_docs.filter(start_date__gte=ff4_date,start_date__lt=edate)
|
||||
ff_new_iD = ff1_new + ff2_new + ff3_new + ff4_new
|
||||
'''
|
||||
context = {'meeting':meeting,
|
||||
'new':new,
|
||||
'updated':updated,
|
||||
'updated_more':updated_more,
|
||||
'total_updated':total_updated,
|
||||
'last_call':last_call,
|
||||
'approved':approved,
|
||||
'ff_new_count':ff_new_count,
|
||||
'ff_new_percent':ff_new_percent,
|
||||
'ff_update_count':ff_update_count,
|
||||
'ff_update_percent':ff_update_percent}
|
||||
|
||||
report = render_to_string('proceedings/report_id_activity.txt', context)
|
||||
|
||||
return report
|
||||
|
||||
def report_progress_report(start_date,end_date):
|
||||
syear,smonth,sday = start_date.split('-')
|
||||
eyear,emonth,eday = end_date.split('-')
|
||||
sdate = datetime.datetime(int(syear),int(smonth),int(sday))
|
||||
edate = datetime.datetime(int(eyear),int(emonth),int(eday))
|
||||
|
||||
context = get_progress_stats(sdate,edate)
|
||||
|
||||
report = render_to_string('proceedings/report_progress_report.txt', context)
|
||||
|
||||
return report
|
|
@ -1,36 +0,0 @@
|
|||
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
|
||||
from ietf.meeting.factories import MeetingFactory
|
||||
|
||||
class ReportsTestCase(TestCase):
|
||||
|
||||
def test_report_id_activity(self):
|
||||
|
||||
today = timezone.now()
|
||||
yesterday = today - datetime.timedelta(days=1)
|
||||
last_quarter = today - datetime.timedelta(days=3*30)
|
||||
next_week = today+datetime.timedelta(days=7)
|
||||
|
||||
m1 = MeetingFactory(type_id='ietf',date=last_quarter)
|
||||
m2 = MeetingFactory(type_id='ietf',date=next_week,number=int(m1.number)+1)
|
||||
|
||||
doc = DocumentFactory(type_id='draft',time=yesterday,rev="00")
|
||||
NewRevisionDocEventFactory(doc=doc,time=today,rev="01")
|
||||
result = report_id_activity(m1.date.strftime("%Y-%m-%d"),m2.date.strftime("%Y-%m-%d"))
|
||||
self.assertTrue('IETF Activity since last IETF Meeting' in result)
|
||||
|
||||
def test_report_progress_report(self):
|
||||
today = timezone.now()
|
||||
last_quarter = today - datetime.timedelta(days=3*30)
|
||||
next_week = today+datetime.timedelta(days=7)
|
||||
|
||||
m1 = MeetingFactory(type_id='ietf',date=last_quarter)
|
||||
m2 = MeetingFactory(type_id='ietf',date=next_week,number=int(m1.number)+1)
|
||||
result = report_progress_report(m1.date.strftime('%Y-%m-%d'),m2.date.strftime('%Y-%m-%d'))
|
||||
self.assertTrue('IETF Activity since last IETF Meeting' in result)
|
|
@ -1,21 +0,0 @@
|
|||
IETF Activity since last IETF Meeting
|
||||
--------------------
|
||||
|
||||
IETF Activity since last IETF Meeting ({{ meeting.city }})
|
||||
|
||||
{{ new|stringformat:"3s" }} New I-Ds ({{ updated }} of which were updated, some ({{ updated_more }}) more than once)
|
||||
{{ total_updated|stringformat:"3s" }} I-Ds were updated (Some more than once)
|
||||
{{ last_call|stringformat:"3s" }} I-Ds Last Called
|
||||
{{ approved|stringformat:"3s" }} I-Ds approved for publication
|
||||
|
||||
In the final 4 weeks before meeting
|
||||
|
||||
{{ ff_new_count|stringformat:"3s" }} New I-Ds were received - {{ ff_new_percent }} of total newbies since last meeting
|
||||
{{ ff_update_count|stringformat:"3s" }} I-Ds were updated - {{ ff_update_percent }} of total updated since last meeting
|
||||
|
||||
Week1 0 %
|
||||
Week2 0 %
|
||||
Week3 0 %
|
||||
Week4 0 %
|
||||
|
||||
The IESG Secretary.
|
|
@ -1,48 +0,0 @@
|
|||
{% load ams_filters %}
|
||||
IETF Activity since last IETF Meeting
|
||||
{{ start_date }} to {{ end_date }}
|
||||
|
||||
1) {{ action_events.count }} IESG Protocol and Document Actions this period
|
||||
{% for event in action_events %}
|
||||
{{ event.doc.title }} ({{ event.doc.intended_std_level }})
|
||||
{% endfor %}
|
||||
|
||||
2) {{ lc_events.count }} IESG Last Calls issued to the IETF this period
|
||||
{% for event in lc_events %}
|
||||
{{ event.doc.title }}
|
||||
{{ event.doc.file_tag|safe }} ({{ event.doc.intended_std_level }})
|
||||
{% endfor %}
|
||||
|
||||
3) {{ new_groups.count }} New Working Group(s) formed this period
|
||||
{% for group in new_groups %}
|
||||
{{ group }} ({{ group.acronym }})
|
||||
{% endfor %}
|
||||
|
||||
4) {{ concluded_groups.count }} Working Group(s) concluded this period
|
||||
{% for group in concluded_groups %}
|
||||
{{ group }} ({{ group.acronym }})
|
||||
{% endfor %}
|
||||
|
||||
5) {{ new_docs|length }} new or revised Internet-Drafts this period
|
||||
|
||||
(o - Revised Internet-Draft; + - New Internet-Draft)
|
||||
|
||||
WG I-D Title <Filename>
|
||||
------- ------------------------------------------
|
||||
{% for doc in new_docs %}
|
||||
({{ doc.group.acronym|stringformat:"8s" }}) {% if doc.rev == "00" %} + {% else %} o {% endif %}{{ doc.title }}
|
||||
{{ doc.file_tag|safe }}
|
||||
{% endfor %}
|
||||
|
||||
6) {{ rfcs.count }} RFC(s) produced this period
|
||||
|
||||
S - Standard; PS - Proposed Standard; DS - Draft Standard;
|
||||
B - Best Current Practices; E - Experimental; I - Informational
|
||||
|
||||
RFC Stat WG Published Title
|
||||
------- -- ---------- ---------- -----------------------------------------
|
||||
{% for event in rfcs %}
|
||||
{{ event.doc.canonical_name|upper }} {{ event.doc.intended_std_level.name|abbr_status|stringformat:"2s" }} ({{ event.doc.group.acronym|stringformat:"8s" }}) {{ event.time|date:"M d" }} {{ event.doc.title }}
|
||||
{% endfor %}
|
||||
|
||||
{{ counts.std }} Standards Track; {{ counts.bcp }} BCP; {{ counts.exp }} Experimental; {{ counts.inf }} Informational
|
|
@ -2,6 +2,7 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load ietf_filters %}
|
||||
{% block title %}Change responsible AD for {{ doc.name }}-{{ doc.rev }}{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
@ -10,11 +11,20 @@
|
|||
<br>
|
||||
<small class="text-muted">{{ doc.name }}-{{ doc.rev }}</small>
|
||||
</h1>
|
||||
{% with approved=doc|state:"draft-rfceditor" %}
|
||||
{% if approved %}
|
||||
<div class="alert alert-warning my-3">
|
||||
It is unusual to change the responsible AD for a document that has
|
||||
been sent to the RFC Editor. Please make sure this is really what
|
||||
you want to do.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form class="mt-3" enctype="multipart/form-data" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<button type="submit" class="btn {{ approved|yesno:'btn-warning,btn-primary' }}">Submit</button>
|
||||
<a class="btn btn-secondary float-end"
|
||||
href="{% url "ietf.doc.views_doc.document_main" name=doc.name %}">Back</a>
|
||||
</form>
|
||||
{% endwith %}
|
||||
{% endblock %}
|
25
ietf/templates/iesg/ietf_activity_report.html
Normal file
25
ietf/templates/iesg/ietf_activity_report.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
{% load ams_filters ietf_filters django_bootstrap5 %}
|
||||
{% block title %}IETF Activity Report{% endblock %}
|
||||
{% block content %}
|
||||
<h1 class="mt-3">IETF Activity Report</h1>
|
||||
<h2 class="mt-3"><small class="text-muted">{{ sdate|date:"F Y" }}</small></h2>
|
||||
|
||||
<form action="." method="GET">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
{% bootstrap_field form.month show_label=True layout="inline" %}
|
||||
</div>
|
||||
<div class="col-2">
|
||||
{% bootstrap_field form.year layout="inline" %}
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h3 class="mt-3">Draft Activity</h3>
|
||||
{% include "meeting/activity_report.html" %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,16 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% load ams_filters ietf_filters cache %}
|
||||
{% block title %}IETF {{ meeting.number }} Proceedings - Progress Report{% endblock %}
|
||||
{% block content %}
|
||||
{% cache 3600 proceedings_progress_report meeting.number %}
|
||||
<h1>
|
||||
<a class="text-decoration-none text-reset"
|
||||
href="{% url 'ietf.meeting.views.proceedings' num=meeting.number %}">
|
||||
IETF {{ meeting.number }} proceedings
|
||||
</a>
|
||||
</h1>
|
||||
<h2 class="mt-3">IETF Progress Report</h2>
|
||||
<h3 class="mt-3">{{ sdate|date:"d-F-y" }} to {{ edate|date:"d-F-y" }}</h3>
|
||||
{% load ams_filters ietf_filters %}
|
||||
<ul class="progress-section">
|
||||
<li>{{ actions_count }} IESG Protocol and Document Actions this period</li>
|
||||
<li>{{ last_calls_count }} IESG Last Calls issued to the IETF this period</li>
|
||||
|
@ -18,15 +6,17 @@
|
|||
{{ new_drafts_count|stringformat:"3s" }} New I-Ds ({{ new_drafts_updated_count }} of which were updated, some ({{ new_drafts_updated_more_count }}) more than once)
|
||||
</li>
|
||||
<li>{{ updated_drafts_count|stringformat:"3s" }} I-Ds were updated (Some more than once)</li>
|
||||
<li>
|
||||
<h3 class="mt-3">In the final 4 weeks before meeting</h3>
|
||||
</li>
|
||||
<li>
|
||||
{{ ffw_new_count|stringformat:"3s" }} New I-Ds were received - {{ ffw_new_percent }} of total newbies since last meeting
|
||||
</li>
|
||||
<li>
|
||||
{{ ffw_update_count|stringformat:"3s" }} I-Ds were updated - {{ ffw_update_percent }} of total updated since last meeting
|
||||
</li>
|
||||
{% if is_meeting_report %}
|
||||
<li>
|
||||
<h3 class="mt-3">In the final 4 weeks before meeting</h3>
|
||||
</li>
|
||||
<li>
|
||||
{{ ffw_new_count|stringformat:"3s" }} New I-Ds were received - {{ ffw_new_percent }} of total newbies since last meeting
|
||||
</li>
|
||||
<li>
|
||||
{{ ffw_update_count|stringformat:"3s" }} I-Ds were updated - {{ ffw_update_percent }} of total updated since last meeting
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<h3 class="mt-3">{{ new_groups.count }} New Working Group(s) formed this period</h3>
|
||||
<ul class="progress-section">
|
||||
|
@ -68,5 +58,3 @@
|
|||
</tbody>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endcache %}
|
||||
{% endblock %}
|
|
@ -10,22 +10,23 @@
|
|||
{% filter center:72 %}Updated {{ updated|date:"Y-m-d H:i:s T" }}{% endfilter %}
|
||||
|
||||
{% filter center:72 %}IETF agendas are subject to change, up to and during the meeting.{% endfilter %}
|
||||
{% filter center:72 %}Times are shown in {% if display_timezone.lower == "utc" %}UTC{% else %}the {{ display_timezone }} time zone{% endif %}.{% endfilter %}
|
||||
{% for item in filtered_assignments %}{% ifchanged %}
|
||||
|
||||
|
||||
{{ item.timeslot.time|date:"l"|upper }}, {{ item.timeslot.time|date:"F j, Y" }}
|
||||
{% endifchanged %}{% if item.slot_type.slug == "reg" %}
|
||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.reg_area %} - {{ schedule.meeting.reg_area }}{% endif %}{% endif %}{% if item.slot_type.slug == "plenary" %}
|
||||
{{ item.timeslot.time_desc }} {{ item.session.name }} - {{ item.timeslot.location.name }}
|
||||
{{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.timeslot.name }}{% if schedule.meeting.reg_area %} - {{ schedule.meeting.reg_area }}{% endif %}{% endif %}{% if item.slot_type.slug == "plenary" %}
|
||||
{{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.session.name }} - {{ item.timeslot.location.name }}
|
||||
|
||||
{{ item.session.agenda_text.strip|indent:"3" }}
|
||||
{% endif %}{% if item.slot_type.slug == 'regular' %}{% ifchanged %}
|
||||
|
||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
|
||||
{{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.timeslot.name }}
|
||||
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.group_parent_at_the_time.acronym|upper|ljust:4 }} {{ item.session.group_at_the_time.acronym|ljust:10 }} {{ item.session.group_at_the_time.name }} {% if item.session.group_at_the_time.state_id == "bof" %}BOF{% elif item.session.group_at_the_time.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %}
|
||||
{% endif %}{% if item.slot_type.slug == "break" %}
|
||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.slot_type.slug == "other" %}
|
||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}
|
||||
{{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.slot_type.slug == "other" %}
|
||||
{{ item.timeslot.time|date:"Hi" }}-{{ item.timeslot.end_time|date:"Hi" }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}
|
||||
|
||||
====================================================================
|
||||
{% endautoescape %}
|
||||
|
|
|
@ -14,7 +14,7 @@ This renders the list of links below the title on the meeting proceedings page.
|
|||
<a href="{% url 'agenda' num=meeting.number %}">Meeting Agenda</a>
|
||||
</div>
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_progress_report' num=meeting.number %}">Activity Report</a>
|
||||
<a href="{% url 'ietf.meeting.views.proceedings_activity_report' num=meeting.number %}">Activity Report</a>
|
||||
</div>
|
||||
<div class="proceedings-row">
|
||||
<a href="{% url 'ietf.meeting.views.important_dates' num=meeting.number %}">Important Dates</a>
|
||||
|
|
18
ietf/templates/meeting/proceedings_activity_report.html
Normal file
18
ietf/templates/meeting/proceedings_activity_report.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% load ams_filters ietf_filters cache %}
|
||||
{% block title %}IETF {{ meeting.number }} Proceedings - Activity Report{% endblock %}
|
||||
{% block content %}
|
||||
{% cache 3600 proceedings_activity_report meeting.number %}
|
||||
<h1>
|
||||
<a class="text-decoration-none text-reset"
|
||||
href="{% url 'ietf.meeting.views.proceedings' num=meeting.number %}">
|
||||
IETF {{ meeting.number }} proceedings
|
||||
</a>
|
||||
</h1>
|
||||
<h2 class="mt-3">IETF Activity Report</h2>
|
||||
<h3 class="mt-3">{{ sdate|date:"d-F-y" }} to {{ edate|date:"d-F-y" }}</h3>
|
||||
|
||||
{% include "meeting/activity_report.html" %}
|
||||
|
||||
{% endcache %}
|
||||
{% endblock %}
|
Loading…
Reference in a new issue