Merged in ^/trunk@17617.

- Legacy-Id: 17618
This commit is contained in:
Henrik Levkowetz 2020-04-14 17:11:51 +00:00
commit f2b883d2bb
50 changed files with 5607 additions and 292026 deletions

14
PLAN
View file

@ -7,8 +7,13 @@ Updated: $Date$
Planned work in rough order
===========================
* Transition to Django 2.x (depends on Python 3.x). Security updates to
Django 1.11 will cease around April 2020.
* Transition to Django 2.2 (via 2.0 and 2.1). This depends on Python 3.x.
Security updates to Django 1.11 will cease around April 2020.
* Transition to PostgreSQL. This will make it easier to start using
timezone-aware timestamps throughout the code, which will make it easy
to present localized times on web-pages. It will also provide additional
tools for performance analysis
* Investigate making RFCs first-class document objects to faciliate being
able to model BCPs that represent groups of RFCs properly. Then fix the rfc sync
@ -66,11 +71,6 @@ Planned work in rough order
* Add support for document shepherding reports, possibly re-using or
generalising some of the review plumbing. Check with IESG for details.
* Transition to PostgreSQL. This will make it easier to start using
timezone-aware timestamps throughout the code, which will make it easy
to present localized times on web-pages. It will also provide additional
tools for performance analysis
* Performance analysis of database table and index setup
* Refactor Document and types into Document subclasses, with conditional code

View file

@ -1,3 +1,78 @@
ietfdb (6.126.0) ietf; urgency=medium
**Groundwork for upcoming automatic scheduling assistance**
* Merged in ^/branch/dash/automatic-scheduler@17395 from sasha@dashcare.nl,
which adds groundwork for upcoming automatic scheduling assistance:
~ Added a management command to create a dummy IETF 999 meeting.
~ Added display of new constraints and joint sessions to agenda builder
interface.
~ The new timerange, time_relation and wg_adjacent constraints, along with
the joint_with_groups option, are now reflected in the special requests
field. This allows them to be taken into account while scheduling
sessions.
~ Clarified the wording in the session request form regarding conflicts
with BOFs.
~ Added support for structured entry and storage of joint sessions in
meetings:
- Also adds additional tests for the SessionForm
- Fixes a javascript error in session requests for non-WG groups,
that could cause incorrect form behaviour.
- Expands the tests added in [17289] a bit.
~ Added support for the timerange, wg_adjacent and time_relation
constraints. This adds three new constraints to the database and
relevant UIs:
- timerange: "This WG can't meet during these timeframes"
- wg_adjacent: "Schedule adjacent to another WG (directly following,
no breaks, same room)"
- time_relation: schedule the two sessions of one WG on subsequent
days or with at least one day seperation
-- Henrik Levkowetz <henrik@levkowetz.com> 09 Apr 2020 16:25:23 +0000
ietfdb (6.125.0) ietf; urgency=medium
**Various meeting-related fixes and improvements**
* Merged in [17590] from rcross@amsl.com:
Added support for variable length meetings to secr/meetings app.
* Changed the handling of some exceptions during draft submission to give
user feedback rather than server 500 responses, in order to deal better
with severely malformed drafts.
* Added a workaround for the current libmagic on OpenSUSE, which quite
easily can mischaracterise text/plain documents as text/x-Algol68. Fixes
issues #2941 and #2956.
* Added validation of the session duration in interim meeting requests, with
added values in settings.py for min and max duration.
* Clarified the standalone XML draft submission requirements, and
mentioned the xml2rfc switch to use for v3 sources.
* Changes to accept a wider range of URLs when displaying call-in links from
the Session agenda_note and remote_instructions fields.
* Changed some fields to raw_id_fields in the MessageAdmin, for improved
admin page load times.
* Added 'Remote instructions' at the top of interim sesssion pages, and
made the 'Meeting Details' button available to the group chairs, not only
secretariat.
-- Henrik Levkowetz <henrik@levkowetz.com> 08 Apr 2020 16:51:46 +0000
ietfdb (6.124.0) ietf; urgency=medium
**Enhanced 'Upcoming Meetings' page, and more**

View file

@ -1,5 +1,6 @@
# -*- conf-mode -*-
^/personal/markd/v6.120.0.dev0@17570 # Review issues to be resolved (08 Apr 2020)
^/personal/mahoney/6.121.1.dev0@17473 # Test commit
/personal/kivinen/6.94.2.dev0@16091 # Replaced by later commit
/personal/rjs/6.104.1.dev0@16809 # Local changes, not for merge

View file

@ -339,7 +339,7 @@ class FileUploadForm(forms.Form):
mime_type, encoding = validate_mime_type(file, self.mime_types)
if not hasattr(self, 'file_encoding'):
self.file_encoding = {}
self.file_encoding[file.name] = encoding.replace('charset=','') if encoding else None
self.file_encoding[file.name] = encoding or None
if self.mime_types:
if not file.content_type in settings.MEETING_VALID_UPLOAD_MIME_FOR_OBSERVED_MIME[mime_type]:
raise ValidationError('Upload Content-Type (%s) is different from the observed mime-type (%s)' % (file.content_type, mime_type))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.27 on 2020-02-11 04:47
from __future__ import unicode_literals
from django.db import migrations, models
def forward(apps, schema_editor):
ConstraintName = apps.get_model("name", "ConstraintName")
ConstraintName.objects.create(slug="timerange", desc="", penalty=100000,
name="Can't meet within timerange")
ConstraintName.objects.create(slug="time_relation", desc="", penalty=1000,
name="Preference for time between sessions")
ConstraintName.objects.create(slug="wg_adjacent", desc="", penalty=10000,
name="Request for adjacent scheduling with another WG")
def reverse(apps, schema_editor):
ConstraintName = apps.get_model("name", "ConstraintName")
ConstraintName.objects.filter(slug__in=["timerange", "time_relation", "wg_adjacent"]).delete()
class Migration(migrations.Migration):
dependencies = [
('name', '0010_timerangename'),
('meeting', '0026_cancel_107_sessions'),
]
operations = [
migrations.RemoveField(
model_name='constraint',
name='day',
),
migrations.AddField(
model_name='constraint',
name='time_relation',
field=models.CharField(blank=True, choices=[('subsequent-days', 'Schedule the sessions on subsequent days'), ('one-day-seperation', 'Leave at least one free day in between the two sessions')], max_length=200),
),
migrations.AddField(
model_name='constraint',
name='timeranges',
field=models.ManyToManyField(to='name.TimerangeName'),
),
migrations.AddField(
model_name='session',
name='joint_with_groups',
field=models.ManyToManyField(related_name='sessions_joint_in', to='group.Group'),
),
migrations.RunPython(forward, reverse),
]

View file

@ -29,7 +29,7 @@ from ietf.dbtemplate.models import DBTemplate
from ietf.doc.models import Document
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName, ImportantDateName
from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName, ImportantDateName, TimerangeName
from ietf.person.models import Person
from ietf.utils.decorators import memoize
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
@ -816,19 +816,27 @@ class SchedTimeSessAssignment(models.Model):
class Constraint(models.Model):
"""
Specifies a constraint on the scheduling.
One type (name=conflic?) of constraint is between source WG and target WG,
e.g. some kind of conflict.
Another type (name=bethere) of constraint is between source WG and
availability of a particular Person, usually an AD.
A third type (name=avoidday) of constraint is between source WG and
a particular day of the week, specified in day.
Available types are:
- conflict/conflic2/conflic3: a conflict between source and target WG/session,
with varying priority. The first is used for a chair conflict, the second for
technology overlap, third for key person conflict
- bethere: a constraint between source WG and a particular person
- timerange: can not meet during these times
- time_relation: preference for a time difference between sessions
- wg_adjacent: request for source WG to be adjacent (directly before or after,
no breaks, same room) the target WG
"""
TIME_RELATION_CHOICES = (
('subsequent-days', 'Schedule the sessions on subsequent days'),
('one-day-seperation', 'Leave at least one free day in between the two sessions'),
)
meeting = ForeignKey(Meeting)
source = ForeignKey(Group, related_name="constraint_source_set")
target = ForeignKey(Group, related_name="constraint_target_set", null=True)
person = ForeignKey(Person, null=True, blank=True)
day = models.DateTimeField(null=True, blank=True)
name = ForeignKey(ConstraintName)
time_relation = models.CharField(max_length=200, choices=TIME_RELATION_CHOICES, blank=True)
timeranges = models.ManyToManyField(TimerangeName)
active_status = None
@ -836,7 +844,14 @@ class Constraint(models.Model):
return u"%s %s target=%s person=%s" % (self.source, self.name.name.lower(), self.target, self.person)
def brief_display(self):
if self.target and self.person:
if self.name.slug == "wg_adjacent":
return "Adjacent with %s" % self.target.acronym
elif self.name.slug == "time_relation":
return self.get_time_relation_display()
elif self.name.slug == "timerange":
timeranges_str = ", ".join([t.desc for t in self.timeranges.all()])
return "Can't meet %s" % timeranges_str
elif self.target and self.person:
return "%s ; %s" % (self.target.acronym, self.person)
elif self.target and not self.person:
return "%s " % (self.target.acronym)
@ -858,6 +873,13 @@ class Constraint(models.Model):
if self.target is not None:
ct1['target_href'] = urljoin(host_scheme, self.target.json_url())
ct1['meeting_href'] = urljoin(host_scheme, self.meeting.json_url())
if self.time_relation:
ct1['time_relation'] = self.time_relation
ct1['time_relation_display'] = self.get_time_relation_display()
if self.timeranges.count():
ct1['timeranges_cant_meet'] = [t.slug for t in self.timeranges.all()]
timeranges_str = ", ".join([t.desc for t in self.timeranges.all()])
ct1['timeranges_display'] = "Can't meet %s" % timeranges_str
return ct1
@ -891,6 +913,7 @@ class Session(models.Model):
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
type = ForeignKey(TimeSlotTypeName)
group = ForeignKey(Group) # The group type historically determined the session type. BOFs also need to be added as a group. Note that not all meeting requests have a natural group to associate with.
joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in')
attendees = models.IntegerField(null=True, blank=True)
agenda_note = models.CharField(blank=True, max_length=255)
requested_duration = models.DurationField(default=datetime.timedelta(0))
@ -1015,6 +1038,9 @@ class Session(models.Model):
def is_material_submission_cutoff(self):
return datetime.date.today() > self.meeting.get_submission_correction_date()
def joint_with_groups_acronyms(self):
return [group.acronym for group in self.joint_with_groups.all()]
def __str__(self):
if self.meeting.type_id == "interim":
return self.meeting.number
@ -1109,6 +1135,7 @@ class Session(models.Model):
sess1['bof'] = str(self.group.is_bof())
sess1['agenda_note'] = self.agenda_note
sess1['attendees'] = str(self.attendees)
sess1['joint_with_groups'] = self.joint_with_groups_acronyms()
# fish out scheduling information - eventually, we should pick
# this out in the caller instead

View file

@ -10,6 +10,7 @@ from django.urls import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.name.models import TimerangeName
from ietf.group.models import Group
from ietf.meeting.models import Schedule, TimeSlot, Session, SchedTimeSessAssignment, Meeting, Constraint
from ietf.meeting.test_data import make_meeting_test_data
@ -117,10 +118,23 @@ class ApiTests(TestCase):
person=Person.objects.get(user__username="ad"),
name_id="bethere")
c_adjacent = Constraint.objects.create(meeting=meeting, source=session.group,
target=Group.objects.get(acronym="irg"),
name_id="wg_adjacent")
c_time_relation = Constraint.objects.create(meeting=meeting, source=session.group,
time_relation='subsequent-days',
name_id="time_relation")
c_timerange = Constraint.objects.create(meeting=meeting, source=session.group,
name_id="timerange")
c_timerange.timeranges.set(TimerangeName.objects.filter(slug__startswith='monday'))
r = self.client.get(urlreverse("ietf.meeting.ajax.session_constraints", kwargs=dict(num=meeting.number, sessionid=session.pk)))
self.assertEqual(r.status_code, 200)
constraints = r.json()
self.assertEqual(set([c_ames.pk, c_person.pk]), set(c["constraint_id"] for c in constraints))
expected_keys = set([c_ames.pk, c_person.pk, c_adjacent.pk, c_time_relation.pk, c_timerange.pk])
self.assertEqual(expected_keys, set(c["constraint_id"] for c in constraints))
def test_meeting_json(self):
meeting = make_meeting_test_data()

View file

@ -83,7 +83,7 @@ from ietf.utils.mail import send_mail_message, send_mail_text
from ietf.utils.pipe import pipe
from ietf.utils.pdf import pdf_pages
from ietf.utils.text import xslugify
from ietf.utils.validators import get_mime_type
from ietf.utils.mime import get_mime_type
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,)
@ -218,7 +218,7 @@ def materials_document(request, document, num=None, ext=None):
bytes = file.read()
mtype, chset = get_mime_type(bytes)
content_type = "%s; %s" % (mtype, chset)
content_type = "%s; charset=%s" % (mtype, chset)
file_ext = os.path.splitext(filename)
if len(file_ext) == 2 and file_ext[1] == '.md' and mtype == 'text/plain':

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
# Copyright The IETF Trust 2010-2020, All Rights Reserved
from django.contrib import admin
from ietf.name.models import (
@ -10,7 +10,7 @@ from ietf.name.models import (
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName)
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName)
from ietf.stats.models import CountryAlias
@ -79,5 +79,6 @@ admin.site.register(SessionStatusName, NameAdmin)
admin.site.register(StdLevelName, NameAdmin)
admin.site.register(StreamName, NameAdmin)
admin.site.register(TimeSlotTypeName, NameAdmin)
admin.site.register(TimerangeName, NameAdmin)
admin.site.register(TopicAudienceName, NameAdmin)
admin.site.register(DocUrlTagName, NameAdmin)

View file

@ -5609,6 +5609,39 @@
"model": "name.constraintname",
"pk": "conflict"
},
{
"fields": {
"desc": "",
"name": "Preference for time between sessions",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "time_relation"
},
{
"fields": {
"desc": "",
"name": "Can't meet within timerange",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "timerange"
},
{
"fields": {
"desc": "",
"name": "Request for adjacent scheduling with another WG",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "wg_adjacent"
},
{
"fields": {
"desc": "",
@ -11701,6 +11734,156 @@
"model": "name.streamname",
"pk": "legacy"
},
{
"fields": {
"desc": "Friday early afternoon",
"name": "friday-afternoon-early",
"order": 13,
"used": true
},
"model": "name.timerangename",
"pk": "friday-afternoon-early"
},
{
"fields": {
"desc": "Friday late afternoon",
"name": "friday-afternoon-late",
"order": 14,
"used": true
},
"model": "name.timerangename",
"pk": "friday-afternoon-late"
},
{
"fields": {
"desc": "Friday morning",
"name": "friday-morning",
"order": 12,
"used": true
},
"model": "name.timerangename",
"pk": "friday-morning"
},
{
"fields": {
"desc": "Monday early afternoon",
"name": "monday-afternoon-early",
"order": 1,
"used": true
},
"model": "name.timerangename",
"pk": "monday-afternoon-early"
},
{
"fields": {
"desc": "Monday late afternoon",
"name": "monday-afternoon-late",
"order": 2,
"used": true
},
"model": "name.timerangename",
"pk": "monday-afternoon-late"
},
{
"fields": {
"desc": "Monday morning",
"name": "monday-morning",
"order": 0,
"used": true
},
"model": "name.timerangename",
"pk": "monday-morning"
},
{
"fields": {
"desc": "Thursday early afternoon",
"name": "thursday-afternoon-early",
"order": 10,
"used": true
},
"model": "name.timerangename",
"pk": "thursday-afternoon-early"
},
{
"fields": {
"desc": "Thursday late afternoon",
"name": "thursday-afternoon-late",
"order": 11,
"used": true
},
"model": "name.timerangename",
"pk": "thursday-afternoon-late"
},
{
"fields": {
"desc": "Thursday morning",
"name": "thursday-morning",
"order": 9,
"used": true
},
"model": "name.timerangename",
"pk": "thursday-morning"
},
{
"fields": {
"desc": "Tuesday early afternoon",
"name": "tuesday-afternoon-early",
"order": 4,
"used": true
},
"model": "name.timerangename",
"pk": "tuesday-afternoon-early"
},
{
"fields": {
"desc": "Tuesday late afternoon",
"name": "tuesday-afternoon-late",
"order": 5,
"used": true
},
"model": "name.timerangename",
"pk": "tuesday-afternoon-late"
},
{
"fields": {
"desc": "Tuesday morning",
"name": "tuesday-morning",
"order": 3,
"used": true
},
"model": "name.timerangename",
"pk": "tuesday-morning"
},
{
"fields": {
"desc": "Wednesday early afternoon",
"name": "wednesday-afternoon-early",
"order": 7,
"used": true
},
"model": "name.timerangename",
"pk": "wednesday-afternoon-early"
},
{
"fields": {
"desc": "Wednesday late afternoon",
"name": "wednesday-afternoon-late",
"order": 8,
"used": true
},
"model": "name.timerangename",
"pk": "wednesday-afternoon-late"
},
{
"fields": {
"desc": "Wednesday morning",
"name": "wednesday-morning",
"order": 6,
"used": true
},
"model": "name.timerangename",
"pk": "wednesday-morning"
},
{
"fields": {
"desc": "",

View file

@ -0,0 +1,58 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.27 on 2020-02-04 05:43
from __future__ import unicode_literals
from django.db import migrations, models
def forward(apps, schema_editor):
TimerangeName = apps.get_model('name', 'TimerangeName')
timeranges = [
('monday-morning', 'Monday morning'),
('monday-afternoon-early', 'Monday early afternoon'),
('monday-afternoon-late', 'Monday late afternoon'),
('tuesday-morning', 'Tuesday morning'),
('tuesday-afternoon-early', 'Tuesday early afternoon'),
('tuesday-afternoon-late', 'Tuesday late afternoon'),
('wednesday-morning', 'Wednesday morning'),
('wednesday-afternoon-early', 'Wednesday early afternoon'),
('wednesday-afternoon-late', 'Wednesday late afternoon'),
('thursday-morning', 'Thursday morning'),
('thursday-afternoon-early', 'Thursday early afternoon'),
('thursday-afternoon-late', 'Thursday late afternoon'),
('friday-morning', 'Friday morning'),
('friday-afternoon-early', 'Friday early afternoon'),
('friday-afternoon-late', 'Friday late afternoon'),
]
for order, (slug, desc) in enumerate(timeranges):
TimerangeName.objects.create(slug=slug, name=slug, desc=desc, used=True, order=order)
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('name', '0009_add_verified_errata_to_doctagname'),
]
operations = [
migrations.CreateModel(
name='TimerangeName',
fields=[
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
migrations.RunPython(forward, reverse),
]

View file

@ -69,8 +69,10 @@ class SessionStatusName(NameModel):
class TimeSlotTypeName(NameModel):
"""Session, Break, Registration, Other, Reserved, unavail"""
class ConstraintName(NameModel):
"""Conflict"""
"""conflict, conflic2, conflic3, bethere, timerange, time_relation, wg_adjacent"""
penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)")
class TimerangeName(NameModel):
"""(monday|tuesday|wednesday|thursday|friday)-(morning|afternoon-early|afternoon-late)"""
class LiaisonStatementPurposeName(NameModel):
"""For action, For comment, For information, In response, Other"""
class NomineePositionStateName(NameModel):

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
# Copyright The IETF Trust 2014-2020, All Rights Reserved
# Autogenerated by the makeresources management command 2015-08-27 11:01 PDT
from ietf.api import ModelResource
from ietf.api import ToOneField # pyflakes:ignore
@ -17,7 +17,7 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName)
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName)
class TimeSlotTypeNameResource(ModelResource):
class Meta:
@ -598,3 +598,20 @@ class AgendaTypeNameResource(ModelResource):
api.name.register(AgendaTypeNameResource())
class TimerangeNameResource(ModelResource):
class Meta:
queryset = TimerangeName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'timerangename'
ordering = ['slug', ]
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(TimerangeNameResource())

View file

@ -1,178 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
import datetime
import glob
import os
import debug # pyflakes:ignore
from django.conf import settings
from django.template.loader import render_to_string
from ietf.message.models import Message, SendQueue
from ietf.message.utils import send_scheduled_message_from_send_queue
from ietf.doc.models import DocumentAuthor
from ietf.person.models import Person
def announcement_from_form(data, **kwargs):
'''
This function creates a new message record. Taking as input EmailForm.data
and key word arguments used to override some of the message fields
'''
# possible overrides
by = kwargs.get('by',Person.objects.get(name='(System)'))
from_val = kwargs.get('from_val','Datatracker <internet-drafts-reply@ietf.org>')
content_type = kwargs.get('content_type','text/plain')
# from the form
subject = data['subject']
to_val = data['to']
cc_val = data['cc']
body = data['body']
message = Message.objects.create(by=by,
subject=subject,
frm=from_val,
to=to_val,
cc=cc_val,
body=body,
content_type=content_type)
# create SendQueue
send_queue = SendQueue.objects.create(by=by,message=message)
# uncomment for testing
send_scheduled_message_from_send_queue(send_queue)
return message
def get_authors(draft):
"""
Takes a draft object and returns a list of authors suitable for a tombstone document
"""
authors = []
for a in draft.documentauthor_set.all():
initial = ''
prefix, first, middle, last, suffix = a.person.name_parts()
if first:
initial = first + '. '
entry = '%s%s <%s>' % (initial,last,a.email.address)
authors.append(entry)
return authors
def get_abbr_authors(draft):
"""
Takes a draft object and returns a string of first author followed by "et al"
for use in New Revision email body.
"""
initial = ''
result = ''
authors = DocumentAuthor.objects.filter(document=draft).order_by("order")
if authors:
prefix, first, middle, last, suffix = authors[0].person.name_parts()
if first:
initial = first[0] + '. '
result = '%s%s' % (initial,last)
if len(authors) > 1:
result += ', et al'
return result
def get_last_revision(filename):
"""
This function takes a filename, in the same form it appears in the InternetDraft record,
no revision or extension (ie. draft-ietf-alto-reqs) and returns a string which is the
reivision number of the last active version of the document, the highest revision
txt document in the archive directory. If no matching file is found raise exception.
"""
files = glob.glob(os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR,filename) + '-??.txt')
if files:
sorted_files = sorted(files)
return get_revision(sorted_files[-1])
else:
raise Exception('last revision not found in archive')
def get_revision(name):
"""
Takes a draft filename and returns the revision, as a string.
"""
#return name[-6:-4]
base,ext = os.path.splitext(name)
return base[-2:]
def get_fullcc_list(draft):
"""
This function takes a draft object and returns a string of emails to use in cc field
of a standard notification. Uses an intermediate "emails" dictionary, emails are the
key, name is the value, to prevent adding duplicate emails to the list.
"""
emails = {}
# get authors
for author in draft.documentauthor_set.all():
if author.email and author.email.address not in emails:
emails[author.email.address] = '"%s"' % (author.person.name)
if draft.group.acronym != 'none':
# add chairs
for role in draft.group.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
# add AD
if draft.group.type.slug == 'wg':
emails['%s-ads@ietf.org' % draft.group.acronym] = '"%s-ads"' % (draft.group.acronym)
elif draft.group.type.slug == 'rg':
email = draft.group.parent.role_set.filter(name='chair')[0].email
emails[email.address] = '"%s"' % (email.person.name)
# add sheperd
if draft.shepherd:
emails[draft.shepherd.address] = '"%s"' % (draft.shepherd.person.name)
# use sort so we get consistently ordered lists
result_list = []
for key in sorted(emails):
if emails[key]:
result_list.append('%s <%s>' % (emails[key],key))
else:
result_list.append('<%s>' % key)
return ','.join(result_list)
def get_email_initial(draft, action=None, input=None):
"""
Takes a draft object, a string representing the email type:
(extend,resurrect,revision,update,withdraw) and
a dictonary of the action form input data (for use with update, extend).
Returns a dictionary containing initial field values for a email notification.
The dictionary consists of to, cc, subject, body.
"""
expiration_date = (datetime.date.today() + datetime.timedelta(185)).strftime('%B %d, %Y')
curr_filename = draft.name + '-' + draft.rev + '.txt'
data = {}
data['cc'] = get_fullcc_list(draft)
data['to'] = ''
data['action'] = action
if action == 'extend':
context = {'doc':curr_filename,'expire_date':input['expiration_date']}
data['subject'] = 'Extension of Expiration Date for %s' % (curr_filename)
data['body'] = render_to_string('drafts/message_extend.txt', context)
data['expiration_date'] = input['expiration_date']
elif action == 'resurrect':
last_revision = get_last_revision(draft.name)
last_filename = draft.name + '-' + last_revision + '.txt'
context = {'doc':last_filename,'expire_date':expiration_date}
data['subject'] = 'Resurrection of %s' % (last_filename)
data['body'] = render_to_string('drafts/message_resurrect.txt', context)
data['action'] = action
elif action == 'withdraw':
context = {'doc':curr_filename,'by':input['withdraw_type']}
data['subject'] = 'Withdraw of %s' % (curr_filename)
data['body'] = render_to_string('drafts/message_withdraw.txt', context)
data['action'] = action
data['withdraw_type'] = input['withdraw_type']
return data

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,56 +0,0 @@
[
{
"pk": "rcross@amsl.com",
"model": "person.email",
"fields": {
"active": true,
"person": 111252,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "fluffy@cisco.com",
"model": "person.email",
"fields": {
"active": true,
"person": 105791,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "cabo@tzi.org",
"model": "person.email",
"fields": {
"active": true,
"person": 11843,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "br@brianrosen.net",
"model": "person.email",
"fields": {
"active": true,
"person": 106987,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "gaborbajko@yahoo.com",
"model": "person.email",
"fields": {
"active": true,
"person": 108123,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "wdec@cisco.com",
"model": "person.email",
"fields": {
"active": true,
"person": 106526,
"time": "1970-01-01 23:59:59"
}
}
]

View file

@ -1,224 +0,0 @@
[
{
"pk": 4,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": null,
"list_email": "",
"acronym": "secretariat",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "ietf",
"name": "IETF Secretariat"
}
},
{
"pk": 29,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": null,
"list_email": "",
"acronym": "nomcom2011",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "ietf",
"name": "IAB/IESG Nominating Committee 2011/2012"
}
},
{
"pk": 1008,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 2,
"list_email": "",
"acronym": "gen",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "area",
"name": "General Area"
}
},
{
"pk": 1052,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 2,
"list_email": "",
"acronym": "int",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "area",
"name": "Internet Area"
}
},
{
"pk": 934,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 2,
"list_email": "",
"acronym": "app",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "area",
"name": "Applications Area"
}
},
{
"pk": 1789,
"model": "group.group",
"fields": {
"charter": "charter-ietf-core",
"unused_states": [],
"ad": 105907,
"parent": 934,
"list_email": "core@ietf.org",
"acronym": "core",
"comments": "",
"list_subscribe": "https://www.ietf.org/mailman/listinfo/core",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/core/",
"type": "wg",
"name": "Constrained RESTful Environments"
}
},
{
"pk": 1819,
"model": "group.group",
"fields": {
"charter": "charter-ietf-paws",
"unused_states": [],
"ad": 105907,
"parent": 934,
"list_email": "paws@ietf.org",
"acronym": "paws",
"comments": "",
"list_subscribe": "https://www.ietf.org/mailman/listinfo/paws",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/paws/",
"type": "wg",
"name": "Protocol to Access WS database"
}
},
{
"pk": 1693,
"model": "group.group",
"fields": {
"charter": "charter-ietf-ancp",
"unused_states": [],
"ad": 2348,
"parent": 1052,
"list_email": "ancp@ietf.org",
"acronym": "ancp",
"comments": "",
"list_subscribe": "ancp-request@ietf.org",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/ancp/",
"type": "wg",
"name": "Access Node Control Protocol"
}
},
{
"pk": 1723,
"model": "group.group",
"fields": {
"charter": "charter-ietf-6man",
"unused_states": [],
"ad": 21072,
"parent": 1052,
"list_email": "ipv6@ietf.org",
"acronym": "6man",
"comments": "",
"list_subscribe": "https://www.ietf.org/mailman/listinfo/ipv6",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/ipv6",
"type": "wg",
"name": "IPv6 Maintenance"
}
},
{
"pk": 1377,
"model": "group.group",
"fields": {
"charter": "charter-ietf-adsl",
"unused_states": [],
"ad": null,
"parent": 1052,
"list_email": "adsl@xlist.agcs.com",
"acronym": "adsl",
"comments": "",
"list_subscribe": "mgr@xlist.agcs.com",
"state": "conclude",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "",
"type": "wg",
"name": "Asymmetric Digital Subscriber Line"
}
},
{
"pk": 30,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 3,
"list_email": "",
"acronym": "asrg",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "rg",
"name": "Anti-Spam Research Group"
}
}
]

View file

@ -1,102 +0,0 @@
[
{
"pk": 79,
"model": "meeting.meeting",
"fields": {
"city": "Beijing",
"venue_name": "",
"country": "CN",
"time_zone": "Asia/Shanghai",
"reg_area": "Valley Ballroom Foyer",
"number": "79",
"break_area": "Valley Ballroom Foyer",
"date": "2010-11-07",
"type": "ietf",
"venue_addr": ""
"idsubmit_cutoff_day_offset_00": 20,
"idsubmit_cutoff_day_offset_01": 13,
"idsubmit_cutoff_time_utc": "23:59:59",
"idsubmit_cutoff_warning_days": "21 days, 0:00:00",
}
},
{
"pk": 80,
"model": "meeting.meeting",
"fields": {
"city": "Prague",
"venue_name": "",
"country": "CZ",
"time_zone": "Europe/Prague",
"reg_area": "Congress Hall Foyer",
"number": "80",
"break_area": "Congress Hall Foyer",
"date": "2011-03-27",
"type": "ietf",
"venue_addr": ""
"idsubmit_cutoff_day_offset_00": 20,
"idsubmit_cutoff_day_offset_01": 13,
"idsubmit_cutoff_time_utc": "23:59:59",
"idsubmit_cutoff_warning_days": "21 days, 0:00:00",
}
},
{
"pk": 81,
"model": "meeting.meeting",
"fields": {
"city": "Quebec",
"venue_name": "",
"country": "CA",
"time_zone": "",
"reg_area": "2000 A",
"number": "81",
"break_area": "2000 BC",
"date": "2011-07-24",
"type": "ietf",
"venue_addr": ""
"idsubmit_cutoff_day_offset_00": 20,
"idsubmit_cutoff_day_offset_01": 13,
"idsubmit_cutoff_time_utc": "23:59:59",
"idsubmit_cutoff_warning_days": "21 days, 0:00:00",
}
},
{
"pk": 82,
"model": "meeting.meeting",
"fields": {
"city": "Taipei",
"venue_name": "",
"country": "TW",
"time_zone": "",
"reg_area": "1F North Extended",
"number": "82",
"break_area": "Common Area",
"date": "2011-11-13",
"type": "ietf",
"venue_addr": ""
"idsubmit_cutoff_day_offset_00": 20,
"idsubmit_cutoff_day_offset_01": 13,
"idsubmit_cutoff_time_utc": "23:59:59",
"idsubmit_cutoff_warning_days": "21 days, 0:00:00",
}
},
{
"pk": 83,
"model": "meeting.meeting",
"fields": {
"city": "Paris",
"venue_name": "",
"country": "FR",
"time_zone": "Europe/Paris",
"reg_area": "",
"number": "83",
"break_area": "",
"date": "2012-03-25",
"type": "ietf",
"venue_addr": ""
"idsubmit_cutoff_day_offset_00": 20,
"idsubmit_cutoff_day_offset_01": 13,
"idsubmit_cutoff_time_utc": "23:59:59",
"idsubmit_cutoff_warning_days": "21 days, 0:00:00",
}
}
]

View file

@ -1,132 +0,0 @@
[
{
"pk": 111252,
"model": "person.person",
"fields": {
"name": "Ryan Cross",
"ascii_short": null,
"time": "2012-01-24 13:00:24",
"affiliation": "",
"user": 486,
"address": "",
"ascii": "Ryan Cross"
}
},
{
"pk": 105791,
"model": "person.person",
"fields": {
"name": "Cullen Jennings",
"ascii_short": null,
"time": "2012-01-24 13:00:23",
"affiliation": "Cisco Systems",
"user": 454,
"address": "",
"ascii": "Cullen Jennings"
}
},
{
"pk": 11843,
"model": "person.person",
"fields": {
"name": "Dr. Carsten Bormann",
"ascii_short": null,
"time": "2012-01-24 13:00:13",
"affiliation": "University Bremen TZI",
"user": 1128,
"address": "",
"ascii": "Dr. Carsten Bormann"
}
},
{
"pk": 106987,
"model": "person.person",
"fields": {
"name": "Brian Rosen",
"ascii_short": null,
"time": "2012-01-24 13:00:13",
"affiliation": "",
"user": 1016,
"address": "",
"ascii": "Brian Rosen"
}
},
{
"pk": 108123,
"model": "person.person",
"fields": {
"name": "Gabor Bajko",
"ascii_short": null,
"time": "2012-01-24 13:00:15",
"affiliation": "",
"user": 700,
"address": "",
"ascii": "Gabor Bajko"
}
},
{
"pk": 106526,
"model": "person.person",
"fields": {
"name": "Wojciech Dec",
"ascii_short": null,
"time": "2012-01-24 13:00:19",
"affiliation": "",
"user": 1395,
"address": "",
"ascii": "Wojciech Dec"
}
},
{
"pk": 105786,
"model": "person.person",
"fields": {
"name": "Matthew Bocci",
"ascii_short": null,
"time": "2012-01-24 13:00:16",
"affiliation": "",
"user": 483,
"address": "",
"ascii": "Matthew Bocci"
}
},
{
"pk": 2793,
"model": "person.person",
"fields": {
"name": "Robert M. Hinden",
"ascii_short": null,
"time": "2012-01-24 13:00:13",
"affiliation": "Nokia",
"user": 844,
"address": "",
"ascii": "Robert M. Hinden"
}
},
{
"pk": 106653,
"model": "person.person",
"fields": {
"name": "Brian Haberman",
"ascii_short": null,
"time": "2012-01-24 13:12:51",
"affiliation": "",
"user": null,
"address": "",
"ascii": "Brian Haberman"
}
},
{
"pk": 112453,
"model": "person.person",
"fields": {
"name": "Russel Housley",
"ascii_short": null,
"time": "2012-01-24 13:16:11",
"affiliation": "",
"user": null,
"address": "",
"ascii": "Russel Housley"
}
}
]

View file

@ -1,62 +0,0 @@
[
{
"pk": 1610,
"model": "group.role",
"fields": {
"person": 111252,
"group": 4,
"name": "secr",
"email": "rcross@amsl.com"
}
},
{
"pk": 1229,
"model": "group.role",
"fields": {
"person": 105791,
"group": 1358,
"name": "chair",
"email": "fluffy@cisco.com"
}
},
{
"pk": 1416,
"model": "group.role",
"fields": {
"person": 11843,
"group": 1774,
"name": "chair",
"email": "cabo@tzi.org"
}
},
{
"pk": 1515,
"model": "group.role",
"fields": {
"person": 106987,
"group": 1819,
"name": "chair",
"email": "br@brianrosen.net"
}
},
{
"pk": 1516,
"model": "group.role",
"fields": {
"person": 108123,
"group": 1819,
"name": "chair",
"email": "Gabor.Bajko@nokia.com"
}
},
{
"pk": 461,
"model": "group.role",
"fields": {
"person": 106526,
"group": 1693,
"name": "chair",
"email": "wdec@cisco.com"
}
}
]

View file

@ -1,164 +0,0 @@
[
{
"pk": 486,
"model": "auth.user",
"fields": {
"username": "rcross",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": true,
"is_staff": true,
"last_login": "2012-01-25 08:56:54",
"groups": [],
"user_permissions": [],
"password": "nopass",
"email": "",
"date_joined": "2010-07-27 01:32:02"
}
},
{
"pk": 454,
"model": "auth.user",
"fields": {
"username": "fluffy@cisco.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-23 17:27:39",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2010-03-10 16:04:51"
}
},
{
"pk": 1128,
"model": "auth.user",
"fields": {
"username": "cabo@tzi.org",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-10 05:07:13",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-12-20 03:37:01"
}
},
{
"pk": 1016,
"model": "auth.user",
"fields": {
"username": "br@brianrosen.net",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-11-16 17:55:41",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-11-16 17:55:41"
}
},
{
"pk": 700,
"model": "auth.user",
"fields": {
"username": "gabor.bajko@nokia.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-09-09 10:07:39",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-09-09 10:07:39"
}
},
{
"pk": 1395,
"model": "auth.user",
"fields": {
"username": "wdec@cisco.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-24 13:00:19",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2012-01-24 13:00:19"
}
},
{
"pk": 483,
"model": "auth.user",
"fields": {
"username": "matthew.bocci@alcatel.co.uk",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-13 09:12:04",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2010-07-19 07:16:42"
}
},
{
"pk": 986,
"model": "auth.user",
"fields": {
"username": "bob.hinden@nokia.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-11-14 03:19:35",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-11-14 03:08:01"
}
},
{
"pk": 1066,
"model": "auth.user",
"fields": {
"username": "brian@innovationslab.net",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-11-28 11:00:16",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-11-28 11:00:16"
}
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,242 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import re
import os
from django import forms
from ietf.doc.models import Document, State
from ietf.name.models import IntendedStdLevelName
from ietf.group.models import Group
from ietf.person.models import Person, Email
from ietf.person.fields import SearchableEmailField
from ietf.secr.groups.forms import get_person
# ---------------------------------------------
# Select Choices
# ---------------------------------------------
WITHDRAW_CHOICES = (('ietf','Withdraw by IETF'),('author','Withdraw by Author'))
# ---------------------------------------------
# Custom Fields
# ---------------------------------------------
class DocumentField(forms.FileField):
'''A validating document upload field'''
def __init__(self, unique=False, *args, **kwargs):
self.extension = kwargs.pop('extension')
self.filename = kwargs.pop('filename')
self.rev = kwargs.pop('rev')
super(DocumentField, self).__init__(*args, **kwargs)
def clean(self, data, initial=None):
file = super(DocumentField, self).clean(data,initial)
if file:
# validate general file format
m = re.search(r'.*-\d{2}\.(txt|pdf|ps|xml)', file.name)
if not m:
raise forms.ValidationError('File name must be in the form base-NN.[txt|pdf|ps|xml]')
# ensure file extension is correct
base,ext = os.path.splitext(file.name)
if ext != self.extension:
raise forms.ValidationError('Incorrect file extension: %s' % ext)
# if this isn't a brand new submission we need to do some extra validations
if self.filename:
# validate filename
if base[:-3] != self.filename:
raise forms.ValidationError("Filename: %s doesn't match Draft filename." % base[:-3])
# validate revision
next_revision = str(int(self.rev)+1).zfill(2)
if base[-2:] != next_revision:
raise forms.ValidationError("Expected revision # %s" % (next_revision))
return file
class GroupModelChoiceField(forms.ModelChoiceField):
'''
Custom ModelChoiceField sets queryset to include all active workgroups and the
individual submission group, none. Displays group acronyms as choices. Call it without the
queryset argument, for example:
group = GroupModelChoiceField(required=True)
'''
def __init__(self, *args, **kwargs):
kwargs['queryset'] = Group.objects.filter(type__in=('wg','individ'),state__in=('bof','proposed','active')).order_by('acronym')
super(GroupModelChoiceField, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
return obj.acronym
class AliasModelChoiceField(forms.ModelChoiceField):
'''
Custom ModelChoiceField, just uses Alias name in the select choices as opposed to the
more confusing alias -> doc format used by DocAlias.__unicode__
'''
def label_from_instance(self, obj):
return obj.name
# ---------------------------------------------
# Forms
# ---------------------------------------------
class AuthorForm(forms.Form):
'''
The generic javascript for populating the email list based on the name selected expects to
see an id_email field
'''
person = forms.CharField(max_length=50,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.")
email = forms.CharField(widget=forms.Select(),help_text="Select an email.")
affiliation = forms.CharField(max_length=100, required=False, help_text="Affiliation")
country = forms.CharField(max_length=255, required=False, help_text="Country")
# check for id within parenthesis to ensure name was selected from the list
def clean_person(self):
person = self.cleaned_data.get('person', '')
m = re.search(r'(\d+)', person)
if person and not m:
raise forms.ValidationError("You must select an entry from the list!")
# return person object
return get_person(person)
# check that email exists and return the Email object
def clean_email(self):
email = self.cleaned_data['email']
try:
obj = Email.objects.get(address=email)
except Email.ObjectDoesNoExist:
raise forms.ValidationError("Email address not found!")
# return email object
return obj
class EditModelForm(forms.ModelForm):
#expiration_date = forms.DateField(required=False)
state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft'),empty_label=None)
iesg_state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft-iesg'),empty_label=None)
group = GroupModelChoiceField(required=True)
review_by_rfc_editor = forms.BooleanField(required=False)
shepherd = SearchableEmailField(required=False, only_users=True)
class Meta:
model = Document
fields = ('title','group','ad','shepherd','notify','stream','review_by_rfc_editor','name','rev','pages','intended_std_level','std_level','abstract','internal_comments')
# use this method to set attrs which keeps other meta info from model.
def __init__(self, *args, **kwargs):
super(EditModelForm, self).__init__(*args, **kwargs)
self.fields['ad'].queryset = Person.objects.filter(role__name='ad').distinct()
self.fields['title'].label='Document Name'
self.fields['title'].widget=forms.Textarea()
self.fields['rev'].widget.attrs['size'] = 2
self.fields['abstract'].widget.attrs['cols'] = 72
self.initial['state'] = self.instance.get_state().pk
self.initial['iesg_state'] = self.instance.get_state('draft-iesg').pk
# setup special fields
if self.instance:
# setup replaced
self.fields['review_by_rfc_editor'].initial = bool(self.instance.tags.filter(slug='rfc-rev'))
def save(self, commit=False):
m = super(EditModelForm, self).save(commit=False)
state = self.cleaned_data['state']
iesg_state = self.cleaned_data['iesg_state']
if 'state' in self.changed_data:
m.set_state(state)
# note we're not sending notices here, is this desired
if 'iesg_state' in self.changed_data:
m.set_state(iesg_state)
if 'review_by_rfc_editor' in self.changed_data:
if self.cleaned_data.get('review_by_rfc_editor',''):
m.tags.add('rfc-rev')
else:
m.tags.remove('rfc-rev')
if 'shepherd' in self.changed_data:
email = self.cleaned_data.get('shepherd')
if email and not email.origin:
email.origin = 'shepherd: %s' % m.name
email.save()
# handle replaced by
return m
# field must contain filename of existing draft
def clean_replaced_by(self):
name = self.cleaned_data.get('replaced_by', '')
if name and not Document.objects.filter(name=name):
raise forms.ValidationError("ERROR: Draft does not exist")
return name
def clean(self):
super(EditModelForm, self).clean()
cleaned_data = self.cleaned_data
"""
expiration_date = cleaned_data.get('expiration_date','')
status = cleaned_data.get('status','')
replaced = cleaned_data.get('replaced',False)
replaced_by = cleaned_data.get('replaced_by','')
replaced_status_object = IDStatus.objects.get(status_id=5)
expired_status_object = IDStatus.objects.get(status_id=2)
# this condition seems to be valid
#if expiration_date and status != expired_status_object:
# raise forms.ValidationError('Expiration Date set but status is %s' % (status))
if status == expired_status_object and not expiration_date:
raise forms.ValidationError('Status is Expired but Expirated Date is not set')
if replaced and status != replaced_status_object:
raise forms.ValidationError('You have checked Replaced but status is %s' % (status))
if replaced and not replaced_by:
raise forms.ValidationError('You have checked Replaced but Replaced By field is empty')
"""
return cleaned_data
class EmailForm(forms.Form):
# max_lengths come from db limits, cc is not limited
action = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False)
expiration_date = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False)
withdraw_type = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False)
replaced = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False)
replaced_by = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False)
filename = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False)
to = forms.CharField(max_length=255)
cc = forms.CharField(required=False)
subject = forms.CharField(max_length=255)
body = forms.CharField(widget=forms.Textarea(), strip=False)
def __init__(self, *args, **kwargs):
if 'hidden' in kwargs:
self.hidden = kwargs.pop('hidden')
else:
self.hidden = False
super(EmailForm, self).__init__(*args, **kwargs)
if self.hidden:
for key in list(self.fields.keys()):
self.fields[key].widget = forms.HiddenInput()
class ExtendForm(forms.Form):
action = forms.CharField(max_length=255, widget=forms.HiddenInput(),initial='extend')
expiration_date = forms.DateField()
class SearchForm(forms.Form):
intended_std_level = forms.ModelChoiceField(queryset=IntendedStdLevelName.objects,label="Intended Status",required=False)
document_title = forms.CharField(max_length=80,label='Document Title',required=False)
group = forms.CharField(max_length=12,required=False)
filename = forms.CharField(max_length=80,required=False)
state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft'),required=False)
revision_date_start = forms.DateField(label='Revision Date (start)',required=False)
revision_date_end = forms.DateField(label='Revision Date (end)',required=False)
class WithdrawForm(forms.Form):
withdraw_type = forms.CharField(widget=forms.Select(choices=WITHDRAW_CHOICES),help_text='Select which type of withdraw to perform.')

View file

@ -1,3 +0,0 @@
# see add_id5.cfm ~400 for email To addresses
# see generateNotification.cfm

View file

@ -1,103 +0,0 @@
import datetime
from django.template.loader import render_to_string
from ietf.meeting.models import Meeting
from ietf.doc.models import DocEvent, Document
from ietf.secr.proceedings.proc_utils import get_progress_stats
def report_id_activity(start,end):
# get previous meeting
meeting = Meeting.objects.filter(date__lt=datetime.datetime.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))
#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 = Meeting.get_current_meeting().get_ietf_monday()
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('drafts/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('drafts/report_progress_report.txt', context)
return report

View file

@ -1,34 +0,0 @@
import datetime
import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory,NewRevisionDocEventFactory
from ietf.secr.drafts.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 = datetime.datetime.today()
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 = datetime.datetime.today()
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)

View file

@ -1,263 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import io
import os
import shutil
from collections import OrderedDict
from django.conf import settings
from django.urls import reverse as urlreverse
from django.utils.http import urlencode
from pyquery import PyQuery
import debug # pyflakes:ignore
from ietf.doc.expire import expire_draft
from ietf.doc.factories import WgDraftFactory
from ietf.doc.models import Document
from ietf.group.factories import RoleFactory
from ietf.meeting.factories import MeetingFactory
from ietf.person.factories import PersonFactory, EmailFactory
from ietf.person.models import Person
from ietf.submit.models import Preapproval
from ietf.utils.mail import outbox
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
from ietf.secr.drafts.email import get_email_initial
SECR_USER='secretary'
class SecrDraftsTestCase(TestCase):
def setUp(self):
self.saved_internet_draft_path = settings.INTERNET_DRAFT_PATH
self.repository_dir = self.tempdir('submit-repository')
settings.INTERNET_DRAFT_PATH = self.repository_dir
self.saved_internet_draft_archive_dir = settings.INTERNET_DRAFT_ARCHIVE_DIR
self.archive_dir = self.tempdir('submit-archive')
settings.INTERNET_DRAFT_ARCHIVE_DIR = self.archive_dir
self.saved_idsubmit_manual_staging_dir = settings.IDSUBMIT_MANUAL_STAGING_DIR
self.manual_dir = self.tempdir('submit-manual')
settings.IDSUBMIT_MANUAL_STAGING_DIR = self.manual_dir
def tearDown(self):
shutil.rmtree(self.repository_dir)
shutil.rmtree(self.archive_dir)
shutil.rmtree(self.manual_dir)
settings.INTERNET_DRAFT_PATH = self.saved_internet_draft_path
settings.INTERNET_DRAFT_ARCHIVE_DIR = self.saved_internet_draft_archive_dir
settings.IDSUBMIT_MANUAL_STAGING_DIR = self.saved_idsubmit_manual_staging_dir
def test_abstract(self):
draft = WgDraftFactory()
url = urlreverse('ietf.secr.drafts.views.abstract', kwargs={'id':draft.name})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_approvals(self):
Preapproval.objects.create(name='draft-dummy',
by=Person.objects.get(name="(System)"))
url = urlreverse('ietf.secr.drafts.views.approvals')
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertContains(response, 'draft-dummy')
def test_edit(self):
draft = WgDraftFactory(states=[('draft','active'),('draft-stream-ietf','wg-doc'),('draft-iesg','ad-eval')], shepherd=EmailFactory())
url = urlreverse('ietf.secr.drafts.views.edit', kwargs={'id':draft.name})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response = self.client.post(url,{'title':draft.title,'name':draft.name,'rev':draft.rev,'state':4,'group':draft.group.pk,'iesg_state':draft.get_state('draft-iesg').pk})
self.assertEqual(response.status_code, 302)
draft = Document.objects.get(pk=draft.pk)
self.assertEqual(draft.get_state().slug,'repl')
def test_email(self):
# can't test this directly, test via drafts actions
pass
def test_get_email_initial(self):
# Makes sure that a manual posting by the Secretariat of an I-D that is
# in the RFC Editor Queue will result in notification of the RFC Editor
draft = WgDraftFactory(authors=PersonFactory.create_batch(1),shepherd=EmailFactory())
RoleFactory(group=draft.group, name_id='chair')
data = get_email_initial(draft,action='extend',input={'expiration_date': '2050-01-01'})
self.assertTrue('Extension of Expiration Date' in data['subject'])
def test_makerfc(self):
draft = WgDraftFactory(intended_std_level_id='ps')
url = urlreverse('ietf.secr.drafts.views.edit', kwargs={'id':draft.name})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# It's not clear what this is testing. Was there supposed to be a POST here?
self.assertTrue(draft.intended_std_level)
def test_search(self):
WgDraftFactory() # Test exercises branch that requires >1 doc found
draft = WgDraftFactory()
url = urlreverse('ietf.secr.drafts.views.search')
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
post = dict(filename='draft',state=1,submit='submit')
response = self.client.post(url, post)
self.assertContains(response, draft.name)
def test_view(self):
draft = WgDraftFactory()
url = urlreverse('ietf.secr.drafts.views.view', kwargs={'id':draft.name})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_author_delete(self):
draft = WgDraftFactory(authors=PersonFactory.create_batch(2))
author = draft.documentauthor_set.first()
id = author.id
url = urlreverse('ietf.secr.drafts.views.author_delete', kwargs={'id':draft.name, 'oid':id})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
redirect_url = urlreverse('ietf.secr.drafts.views.authors', kwargs={'id':draft.name})
response = self.client.post(url, {'post':'yes'})
self.assertRedirects(response, redirect_url)
self.assertFalse(draft.documentauthor_set.filter(id=id))
def test_resurrect(self):
draft = WgDraftFactory()
path = os.path.join(self.repository_dir, draft.filename_with_rev())
with io.open(path, 'w') as file:
file.write('test')
expire_draft(draft)
email_url = urlreverse('ietf.secr.drafts.views.email', kwargs={'id':draft.name}) + "?action=resurrect"
confirm_url = urlreverse('ietf.secr.drafts.views.confirm', kwargs={'id':draft.name})
do_action_url = urlreverse('ietf.secr.drafts.views.do_action', kwargs={'id':draft.name})
view_url = urlreverse('ietf.secr.drafts.views.view', kwargs={'id':draft.name})
subject = 'Resurrection of %s' % draft.get_base_name()
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(email_url)
self.assertContains(response, '<title>Drafts - Email</title>')
q = PyQuery(response.content)
self.assertEqual(q("#id_subject").val(), subject)
post_data = {
'action': 'resurrect',
'to': 'john@example.com',
'cc': 'joe@example.com',
'subject': subject,
'body': 'draft resurrected',
'submit': 'Save'
}
response = self.client.post(confirm_url, post_data)
self.assertContains(response, '<title>Drafts - Confirm</title>')
self.assertEqual(response.context['email']['subject'], subject)
response = self.client.post(do_action_url, post_data)
self.assertRedirects(response, view_url)
draft = Document.objects.get(name=draft.name)
self.assertTrue(draft.get_state_slug('draft') == 'active')
recv = outbox[-1]
self.assertEqual(recv['Subject'], subject)
def test_extend(self):
draft = WgDraftFactory()
url = urlreverse('ietf.secr.drafts.views.extend', kwargs={'id':draft.name})
email_url = urlreverse('ietf.secr.drafts.views.email', kwargs={'id':draft.name})
confirm_url = urlreverse('ietf.secr.drafts.views.confirm', kwargs={'id':draft.name})
do_action_url = urlreverse('ietf.secr.drafts.views.do_action', kwargs={'id':draft.name})
view_url = urlreverse('ietf.secr.drafts.views.view', kwargs={'id':draft.name})
expiration = datetime.datetime.today() + datetime.timedelta(days=180)
expiration = expiration.replace(hour=0,minute=0,second=0,microsecond=0)
subject = 'Extension of Expiration Date for %s' % draft.get_base_name()
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
extend_data = {
'action': 'extend',
'expiration_date': expiration.strftime('%Y-%m-%d'),
}
post_data = {
'action': 'extend',
'expiration_date': expiration.strftime('%Y-%m-%d'),
'to': 'john@example.com',
'cc': 'joe@example.com',
'subject': subject,
'body': 'draft extended',
'submit': 'Save'
}
response = self.client.post(url, extend_data)
self.assertRedirects(response, email_url + '?' + urlencode(extend_data))
response = self.client.post(confirm_url, post_data)
self.assertContains(response, '<title>Drafts - Confirm</title>')
self.assertEqual(response.context['email']['subject'], subject)
response = self.client.post(do_action_url, post_data)
self.assertRedirects(response, view_url)
draft = Document.objects.get(name=draft.name)
self.assertTrue(draft.expires == expiration)
recv = outbox[-1]
self.assertEqual(recv['Subject'], subject)
def test_withdraw(self):
draft = WgDraftFactory()
url = urlreverse('ietf.secr.drafts.views.withdraw', kwargs={'id':draft.name})
email_url = urlreverse('ietf.secr.drafts.views.email', kwargs={'id':draft.name})
confirm_url = urlreverse('ietf.secr.drafts.views.confirm', kwargs={'id':draft.name})
do_action_url = urlreverse('ietf.secr.drafts.views.do_action', kwargs={'id':draft.name})
view_url = urlreverse('ietf.secr.drafts.views.view', kwargs={'id':draft.name})
subject = 'Withdraw of %s' % draft.get_base_name()
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
withdraw_data = OrderedDict([('action', 'withdraw'), ('withdraw_type', 'ietf')])
post_data = {
'action': 'withdraw',
'withdraw_type': 'ietf',
'to': 'john@example.com',
'cc': 'joe@example.com',
'subject': subject,
'body': 'draft resurrected',
'submit': 'Save'
}
response = self.client.post(url, withdraw_data)
self.assertRedirects(response, email_url + '?' + urlencode(withdraw_data))
response = self.client.post(confirm_url, post_data)
self.assertContains(response, '<title>Drafts - Confirm</title>')
self.assertEqual(response.context['email']['subject'], subject)
response = self.client.post(do_action_url, post_data)
self.assertRedirects(response, view_url)
draft = Document.objects.get(name=draft.name)
self.assertTrue(draft.get_state_slug('draft') == 'ietf-rm')
recv = outbox[-1]
self.assertEqual(recv['Subject'], subject)
def test_authors(self):
draft = WgDraftFactory()
person = PersonFactory()
url = urlreverse('ietf.secr.drafts.views.authors',kwargs={'id':draft.name})
login_testing_unauthorized(self, "secretary", url)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
response = self.client.post(url, {'submit':'Done'})
self.assertEqual(response.status_code,302)
response = self.client.post(url, {'person':'%s - (%s)'%(person.plain_name(),person.pk),'email':person.email_set.first().pk})
self.assertEqual(response.status_code,302)
self.assertTrue(draft.documentauthor_set.filter(person=person).exists)
def test_dates(self):
MeetingFactory(type_id='ietf',date=datetime.datetime.today()+datetime.timedelta(days=14))
url = urlreverse('ietf.secr.drafts.views.dates')
login_testing_unauthorized(self, "secretary", url)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
def test_nudge_report(self):
url = urlreverse('ietf.secr.drafts.views.nudge_report')
login_testing_unauthorized(self, "secretary", url)
response = self.client.get(url)
self.assertEqual(response.status_code,200)

View file

@ -1,20 +0,0 @@
from ietf.secr.drafts import views
from ietf.utils.urls import url
urlpatterns = [
url(r'^$', views.search),
url(r'^approvals/$', views.approvals),
url(r'^dates/$', views.dates),
url(r'^nudge-report/$', views.nudge_report),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/$', views.view),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/abstract/$', views.abstract),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/authors/$', views.authors),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/author_delete/(?P<oid>\d{1,6})$', views.author_delete),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/confirm/$', views.confirm),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/do_action/$', views.do_action),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/edit/$', views.edit),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/extend/$', views.extend),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/email/$', views.email),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/withdraw/$', views.withdraw),
]

View file

@ -1,636 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import glob
import io
import os
import shutil
from dateutil.parser import parse
from collections import OrderedDict
from django.conf import settings
from django.contrib import messages
from django.db.models import Max
from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse
from django.utils.http import urlencode
from ietf.doc.models import Document, DocumentAuthor, State
from ietf.doc.models import DocEvent, NewRevisionDocEvent
from ietf.doc.utils import add_state_change_event
from ietf.ietfauth.utils import role_required
from ietf.meeting.helpers import get_meeting
from ietf.secr.drafts.email import announcement_from_form, get_email_initial
from ietf.secr.drafts.forms import AuthorForm, EditModelForm, EmailForm, ExtendForm, SearchForm, WithdrawForm
from ietf.secr.utils.document import get_rfc_num, get_start_date
from ietf.submit.models import Preapproval
from ietf.utils.log import log
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def get_action_details(draft, request):
'''
This function takes a draft object and request object and returns a list of dictionaries
with keys: label, value to be used in displaying information on the confirmation
page.
'''
result = []
data = request.POST
if data['action'] == 'revision':
m = {'label':'New Revision','value':data['revision']}
result.append(m)
if data['action'] == 'replace':
m = {'label':'Replaced By:','value':data['replaced_by']}
result.append(m)
return result
def handle_uploaded_file(f):
'''
Save uploaded draft files to temporary directory
'''
destination = io.open(os.path.join(settings.IDSUBMIT_MANUAL_STAGING_DIR, f.name), 'wb+')
for chunk in f.chunks():
destination.write(chunk)
destination.close()
def file_types_for_draft(draft):
'''Returns list of file extensions that exist for this draft'''
basename, ext = os.path.splitext(draft.get_file_name())
files = glob.glob(basename + '.*')
file_types = []
for filename in files:
base, ext = os.path.splitext(filename)
if ext:
file_types.append(ext)
return file_types
# -------------------------------------------------
# Action Button Functions
# -------------------------------------------------
'''
These functions handle the real work of the action buttons: database updates,
moving files, etc. Generally speaking the action buttons trigger a multi-page
sequence where information may be gathered using a custom form, an email
may be produced and presented to the user to edit, and only then when confirmation
is given will the action work take place. That's when these functions are called.
'''
def do_extend(draft, request):
'''
Actions:
- update revision_date
- set extension_date
'''
e = DocEvent.objects.create(
type='changed_document',
by=request.user.person,
doc=draft,
rev=draft.rev,
time=draft.time,
desc='Extended expiry',
)
draft.expires = parse(request.POST.get('expiration_date'))
draft.save_with_history([e])
# save scheduled announcement
form = EmailForm(request.POST)
announcement_from_form(form.data,by=request.user.person)
return
def do_resurrect(draft, request):
'''
Actions
- restore last archived version
- change state to Active
- reset expires
- create DocEvent
'''
# restore latest revision documents file from archive
files = glob.glob(os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR,draft.name) + '-??.*')
sorted_files = sorted(files)
latest,ext = os.path.splitext(sorted_files[-1])
files = glob.glob(os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR,latest) + '.*')
log("Resurrecting %s. Moving files:" % draft.name)
for file in files:
try:
shutil.move(file, settings.INTERNET_DRAFT_PATH)
log(" Moved file %s to %s" % (file, settings.INTERNET_DRAFT_PATH))
except shutil.Error as ex:
log(" Exception %s when attempting to move %s" % (ex, file))
# Update draft record
draft.set_state(State.objects.get(type="draft", slug="active"))
# set expires
draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
# create DocEvent
e = NewRevisionDocEvent.objects.create(type='completed_resurrect',
by=request.user.person,
doc=draft,
rev=draft.rev,
time=draft.time)
draft.save_with_history([e])
# send announcement
form = EmailForm(request.POST)
announcement_from_form(form.data,by=request.user.person)
return
def do_withdraw(draft,request):
'''
Actions
- change state to withdrawn
- TODO move file to archive
'''
withdraw_type = request.POST.get('withdraw_type')
prev_state = draft.get_state("draft")
new_state = None
if withdraw_type == 'ietf':
new_state = State.objects.get(type="draft", slug="ietf-rm")
elif withdraw_type == 'author':
new_state = State.objects.get(type="draft", slug="auth-rm")
if not new_state:
return
draft.set_state(new_state)
e = add_state_change_event(draft, request.user.person, prev_state, new_state)
if e:
draft.save_with_history([e])
# send announcement
form = EmailForm(request.POST)
announcement_from_form(form.data,by=request.user.person)
return
# -------------------------------------------------
# Standard View Functions
# -------------------------------------------------
@role_required('Secretariat')
def abstract(request, id):
'''
View Internet Draft Abstract
**Templates:**
* ``drafts/abstract.html``
**Template Variables:**
* draft
'''
draft = get_object_or_404(Document, name=id)
return render(request, 'drafts/abstract.html', {
'draft': draft},
)
@role_required('Secretariat')
def approvals(request):
'''
This view handles setting Initial Approval for drafts
'''
approved = Preapproval.objects.all().order_by('name')
form = None
return render(request, 'drafts/approvals.html', {
'form': form,
'approved': approved},
)
@role_required('Secretariat')
def author_delete(request, id, oid):
'''
This view deletes the specified author from the draft
'''
author = DocumentAuthor.objects.get(id=oid)
if request.method == 'POST' and request.POST['post'] == 'yes':
author.delete()
messages.success(request, 'The author was deleted successfully')
return redirect('ietf.secr.drafts.views.authors', id=id)
return render(request, 'confirm_delete.html', {'object': author})
@role_required('Secretariat')
def authors(request, id):
'''
Edit Internet Draft Authors
**Templates:**
* ``drafts/authors.html``
**Template Variables:**
* form, draft
'''
draft = get_object_or_404(Document, name=id)
action = request.GET.get('action')
if request.method == 'POST':
form = AuthorForm(request.POST)
button_text = request.POST.get('submit', '')
if button_text == 'Done':
if action == 'add':
return redirect('ietf.secr.drafts.views.announce', id=id)
return redirect('ietf.secr.drafts.views.view', id=id)
if form.is_valid():
person = form.cleaned_data['person']
email = form.cleaned_data['email']
affiliation = form.cleaned_data.get('affiliation') or ""
country = form.cleaned_data.get('country') or ""
authors = draft.documentauthor_set.all()
if authors:
order = list(authors.aggregate(Max('order')).values())[0] + 1
else:
order = 1
DocumentAuthor.objects.create(document=draft, person=person, email=email, affiliation=affiliation, country=country, order=order)
messages.success(request, 'Author added successfully!')
return redirect('ietf.secr.drafts.views.authors', id=id)
else:
form = AuthorForm()
return render(request, 'drafts/authors.html', {
'draft': draft,
'form': form},
)
@role_required('Secretariat')
def confirm(request, id):
draft = get_object_or_404(Document, name=id)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect('ietf.secr.drafts.views.view', id=id)
action = request.POST.get('action','')
form = EmailForm(request.POST)
if form.is_valid():
email = form.data
details = get_action_details(draft, request)
hidden_form = EmailForm(request.POST, hidden=True)
return render(request, 'drafts/confirm.html', {
'details': details,
'email': email,
'action': action,
'draft': draft,
'form': hidden_form},
)
else:
return render(request, 'drafts/email.html', {
'form': form,
'draft': draft,
'action': action},
)
@role_required('Secretariat')
def do_action(request, id):
'''
This view displays changes that will be made and calls appropriate
function if the user elects to proceed. If the user cancels then
the view page is returned.
'''
draft = get_object_or_404(Document, name=id)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect('ietf.secr.drafts.views.view', id=id)
action = request.POST.get('action')
if action == 'resurrect':
func = do_resurrect
elif action == 'extend':
func = do_extend
elif action == 'withdraw':
func = do_withdraw
func(draft,request)
messages.success(request, '%s action performed successfully!' % action)
return redirect('ietf.secr.drafts.views.view', id=id)
@role_required('Secretariat')
def dates(request):
'''
Manage ID Submission Dates
**Templates:**
* none
**Template Variables:**
* none
'''
meeting = get_meeting()
return render(request, 'drafts/dates.html', {
'meeting':meeting},
)
@role_required('Secretariat')
def edit(request, id):
'''
Since there's a lot going on in this function we are summarizing in the docstring.
Also serves as a record of requirements.
if revision number increases add document_comments and send notify-revision
if revision date changed and not the number return error
check if using restricted words (?)
send notification based on check box
revision date = now if a new status box checked add_id5.cfm
(notify_[resurrection,revision,updated,extended])
if rfcnum="" rfcnum=0
if status != 2, expired_tombstone="0"
if new revision move current txt and ps files to archive directory (add_id5.cfm)
if status > 3 create tombstone, else send revision notification (EmailIDRevision.cfm)
'''
draft = get_object_or_404(Document, name=id)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect('ietf.secr.drafts.views.view', id=id)
form = EditModelForm(request.POST, instance=draft)
if form.is_valid():
if form.changed_data:
e = DocEvent.objects.create(type='changed_document',
by=request.user.person,
doc=draft,
rev=draft.rev,
desc='Changed field(s): %s' % ','.join(form.changed_data))
# see EditModelForm.save() for detailed logic
form.save(commit=False)
draft.save_with_history([e])
messages.success(request, 'Draft modified successfully!')
return redirect('ietf.secr.drafts.views.view', id=id)
else:
#assert False, form.errors
pass
else:
form = EditModelForm(instance=draft)
return render(request, 'drafts/edit.html', {
'form': form,
'draft': draft},
)
@role_required('Secretariat')
def email(request, id):
'''
This function displays the notification message and allows the
user to make changes before continuing to confirmation page.
'''
draft = get_object_or_404(Document, name=id)
action = request.GET.get('action')
data = request.GET
# the resurrect email body references the last revision number, handle
# exception if no last revision found
# if this exception was handled closer to the source it would be easier to debug
# other problems with get_email_initial
try:
form = EmailForm(initial=get_email_initial(draft,action=action,input=data))
except Exception as e:
return render(request, 'drafts/error.html', { 'error': e},)
return render(request, 'drafts/email.html', {
'form': form,
'draft': draft,
'action': action,
})
@role_required('Secretariat')
def extend(request, id):
'''
This view handles extending the expiration date for an Internet-Draft
Prerequisites: draft must be active
Input: new date
Actions
- revision_date = today
# - call handle_comment
'''
draft = get_object_or_404(Document, name=id)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect('ietf.secr.drafts.views.view', id=id)
form = ExtendForm(request.POST)
if form.is_valid():
params = form.cleaned_data
params['action'] = 'extend'
url = reverse('ietf.secr.drafts.views.email', kwargs={'id':id})
url = url + '?' + urlencode(params)
return redirect(url)
else:
form = ExtendForm(initial={'revision_date':datetime.date.today().isoformat()})
return render(request, 'drafts/extend.html', {
'form': form,
'draft': draft},
)
@role_required('Secretariat')
def nudge_report(request):
'''
This view produces the Nudge Report, basically a list of documents that are in the IESG
process but have not had activity in some time
'''
docs = Document.objects.filter(type='draft',states__slug='active')
docs = docs.filter(states=12,tags='need-rev')
return render(request, 'drafts/report_nudge.html', {
'docs': docs},
)
@role_required('Secretariat')
def search(request):
'''
Search Internet Drafts
**Templates:**
* ``drafts/search.html``
**Template Variables:**
* form, results
'''
results = []
if request.method == 'POST':
form = SearchForm(request.POST)
if request.POST['submit'] == 'Add':
return redirect('sec.drafts.views.add')
if form.is_valid():
kwargs = {}
intended_std_level = form.cleaned_data['intended_std_level']
title = form.cleaned_data['document_title']
group = form.cleaned_data['group']
name = form.cleaned_data['filename']
state = form.cleaned_data['state']
revision_date_start = form.cleaned_data['revision_date_start']
revision_date_end = form.cleaned_data['revision_date_end']
# construct seach query
if intended_std_level:
kwargs['intended_std_level'] = intended_std_level
if title:
kwargs['title__istartswith'] = title
if state:
kwargs['states__type'] = 'draft'
kwargs['states'] = state
if name:
kwargs['name__istartswith'] = name
if group:
kwargs['group__acronym__istartswith'] = group
if revision_date_start:
kwargs['docevent__type'] = 'new_revision'
kwargs['docevent__time__gte'] = revision_date_start
if revision_date_end:
kwargs['docevent__type'] = 'new_revision'
kwargs['docevent__time__lte'] = revision_date_end
# perform query
if kwargs:
qs = Document.objects.filter(**kwargs)
else:
qs = Document.objects.all()
#results = qs.order_by('group__name')
results = qs.order_by('name')
# if there's just one result go straight to view
if len(results) == 1:
return redirect('ietf.secr.drafts.views.view', id=results[0].name)
else:
active_state = State.objects.get(type='draft',slug='active')
form = SearchForm(initial={'state':active_state.pk})
return render(request, 'drafts/search.html', {
'results': results,
'form': form},
)
@role_required('Secretariat')
def view(request, id):
'''
View Internet Draft
**Templates:**
* ``drafts/view.html``
**Template Variables:**
* draft, area, id_tracker_state
'''
draft = get_object_or_404(Document, name=id)
# TODO fix in Django 1.2
# some boolean state variables for use in the view.html template to manage display
# of action buttons. NOTE: Django 1.2 support new smart if tag in templates which
# will remove the need for these variables
state = draft.get_state_slug()
is_active = True if state == 'active' else False
is_expired = True if state == 'expired' else False
is_withdrawn = True if (state in ('auth-rm','ietf-rm')) else False
# TODO should I rewrite all these or just use proxy.InternetDraft?
# add legacy fields
draft.iesg_state = draft.get_state('draft-iesg')
draft.review_by_rfc_editor = bool(draft.tags.filter(slug='rfc-rev'))
# can't assume there will be a new_revision record
r_event = draft.latest_event(type__in=('new_revision','completed_resurrect'))
draft.revision_date = r_event.time.date() if r_event else None
draft.start_date = get_start_date(draft)
e = draft.latest_event(type__in=('expired_document', 'new_revision', "completed_resurrect"))
draft.expiration_date = e.time.date() if e and e.type == "expired_document" else None
draft.rfc_number = get_rfc_num(draft)
# check for replaced bys
qs = Document.objects.filter(relateddocument__target__docs=draft, relateddocument__relationship='replaces')
if qs:
draft.replaced_by = qs[0]
# check for DEVELOPMENT setting and pass to template
is_development = False
try:
is_development = settings.DEVELOPMENT
except AttributeError:
pass
return render(request, 'drafts/view.html', {
'is_active': is_active,
'is_expired': is_expired,
'is_withdrawn': is_withdrawn,
'is_development': is_development,
'draft': draft},
)
@role_required('Secretariat')
def withdraw(request, id):
'''
This view handles withdrawing an Internet-Draft
Prerequisites: draft must be active
Input: by IETF or Author
'''
draft = get_object_or_404(Document, name=id)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect('ietf.secr.drafts.views.view', id=id)
form = WithdrawForm(request.POST)
if form.is_valid():
params = OrderedDict([('action', 'withdraw')])
params['withdraw_type'] = form.cleaned_data['withdraw_type']
url = reverse('ietf.secr.drafts.views.email', kwargs={'id':id})
url = url + '?' + urlencode(params)
return redirect(url)
else:
form = WithdrawForm()
return render(request, 'drafts/withdraw.html', {
'draft': draft,
'form': form},
)

View file

@ -1,4 +1,5 @@
# Copyright The IETF Trust 2013-2019, All Rights Reserved
import datetime
import re
from django import forms
@ -10,13 +11,6 @@ from ietf.meeting.models import Meeting, Room, TimeSlot, Session, SchedTimeSessA
from ietf.name.models import TimeSlotTypeName
import ietf.utils.fields
DAYS_CHOICES = ((0,'Saturday'),
(1,'Sunday'),
(2,'Monday'),
(3,'Tuesday'),
(4,'Wednesday'),
(5,'Thursday'),
(6,'Friday'))
# using Django week_day lookup values (Sunday=1)
SESSION_DAYS = ((2,'Monday'),
@ -131,15 +125,18 @@ class MeetingRoomForm(forms.ModelForm):
exclude = ['resources']
class TimeSlotForm(forms.Form):
day = forms.ChoiceField(choices=DAYS_CHOICES)
day = forms.ChoiceField()
time = forms.TimeField()
duration = ietf.utils.fields.DurationField()
name = forms.CharField(help_text='Name that appears on the agenda')
def __init__(self,*args,**kwargs):
if 'meeting' in kwargs:
self.meeting = kwargs.pop('meeting')
super(TimeSlotForm, self).__init__(*args,**kwargs)
self.fields["time"].widget.attrs["placeholder"] = "HH:MM"
self.fields["duration"].widget.attrs["placeholder"] = "HH:MM"
self.fields["day"].choices = self.get_day_choices()
def clean_duration(self):
'''Limit to HH:MM format'''
@ -148,6 +145,16 @@ class TimeSlotForm(forms.Form):
raise forms.ValidationError('{} value has an invalid format. It must be in HH:MM format'.format(duration))
return self.cleaned_data['duration']
def get_day_choices(self):
'''Get day choices for form based on meeting duration'''
choices = []
start = self.meeting.date
for n in range(self.meeting.days):
date = start + datetime.timedelta(days=n)
choices.append((n, date.strftime("%a %b %d")))
return choices
class MiscSessionForm(TimeSlotForm):
short = forms.CharField(max_length=32,label='Short Name',help_text='Enter an abbreviated session name (used for material file names)',required=False)
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True).exclude(slug__in=('regular',)),empty_label=None)

View file

@ -740,7 +740,7 @@ def times(request, meeting_id, schedule_name):
times = sorted(slots, key=lambda a: a['time'])
if request.method == 'POST':
form = TimeSlotForm(request.POST)
form = TimeSlotForm(request.POST, meeting=meeting)
if form.is_valid():
time = get_timeslot_time(form, meeting)
duration = form.cleaned_data['duration']
@ -764,7 +764,7 @@ def times(request, meeting_id, schedule_name):
return redirect('ietf.secr.meetings.views.times', meeting_id=meeting_id,schedule_name=schedule_name)
else:
form = TimeSlotForm()
form = TimeSlotForm(meeting=meeting)
return render(request, 'meetings/times.html', {
'form': form,
@ -799,7 +799,7 @@ def times_edit(request, meeting_id, schedule_name, time):
if button_text == 'Cancel':
return redirect('ietf.secr.meetings.views.times', meeting_id=meeting_id,schedule_name=schedule_name)
form = TimeSlotForm(request.POST)
form = TimeSlotForm(request.POST, meeting=meeting)
if form.is_valid():
day = form.cleaned_data['day']
time = get_timeslot_time(form, meeting)
@ -825,7 +825,7 @@ def times_edit(request, meeting_id, schedule_name, time):
'time':dtime.strftime('%H:%M'),
'duration':timeslots.first().duration,
'name':timeslots.first().name}
form = TimeSlotForm(initial=initial)
form = TimeSlotForm(initial=initial, meeting=meeting)
return render(request, 'meetings/times_edit.html', {
'meeting': meeting,

View file

@ -6,8 +6,9 @@ from django import forms
import debug # pyflakes:ignore
from ietf.name.models import TimerangeName
from ietf.group.models import Group
from ietf.meeting.models import ResourceAssociation
from ietf.meeting.models import ResourceAssociation, Constraint
from ietf.person.fields import SearchablePersonsField
from ietf.utils.html import clean_text_field
@ -18,6 +19,8 @@ from ietf.utils.html import clean_text_field
NUM_SESSION_CHOICES = (('','--Please select'),('1','1'),('2','2'))
# LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'),('9000','2.5 hours'))
LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'))
SESSION_TIME_RELATION_CHOICES = (('', 'No preference'),) + Constraint.TIME_RELATION_CHOICES
JOINT_FOR_SESSION_CHOICES = (('1', 'First session'), ('2', 'Second session'), ('3', 'Third session'), )
# -------------------------------------------------
# Helper Functions
@ -63,10 +66,17 @@ class GroupSelectForm(forms.Form):
super(GroupSelectForm, self).__init__(*args,**kwargs)
self.fields['group'].widget.choices = choices
class NameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, name):
return name.desc
class SessionForm(forms.Form):
num_session = forms.ChoiceField(choices=NUM_SESSION_CHOICES)
length_session1 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES)
length_session2 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES,required=False)
session_time_relation = forms.ChoiceField(choices=SESSION_TIME_RELATION_CHOICES, required=False)
length_session3 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES,required=False)
attendees = forms.IntegerField()
# FIXME: it would cleaner to have these be
@ -75,13 +85,19 @@ class SessionForm(forms.Form):
conflict1 = forms.CharField(max_length=255,required=False)
conflict2 = forms.CharField(max_length=255,required=False)
conflict3 = forms.CharField(max_length=255,required=False)
joint_with_groups = forms.CharField(max_length=255,required=False)
joint_for_session = forms.ChoiceField(choices=JOINT_FOR_SESSION_CHOICES, required=False)
comments = forms.CharField(max_length=200,required=False)
wg_selector1 = forms.ChoiceField(choices=[],required=False)
wg_selector2 = forms.ChoiceField(choices=[],required=False)
wg_selector3 = forms.ChoiceField(choices=[],required=False)
wg_selector4 = forms.ChoiceField(choices=[],required=False)
third_session = forms.BooleanField(required=False)
resources = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,required=False)
bethere = SearchablePersonsField(label="Must be present", required=False)
timeranges = NameModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, required=False,
queryset=TimerangeName.objects.all())
adjacent_with_wg = forms.ChoiceField(required=False)
def __init__(self, group, *args, **kwargs):
if 'hidden' in kwargs:
@ -98,8 +114,10 @@ class SessionForm(forms.Form):
self.fields['length_session3'].widget.attrs['onClick'] = "if (check_third_session()) { this.disabled=true;}"
self.fields['comments'].widget = forms.Textarea(attrs={'rows':'3','cols':'65'})
group_acronym_choices = [('','--Select WG(s)')] + list(allowed_conflicting_groups().exclude(pk=group.pk).values_list('acronym','acronym').order_by('acronym'))
for i in range(1, 4):
other_groups = list(allowed_conflicting_groups().exclude(pk=group.pk).values_list('acronym', 'acronym').order_by('acronym'))
self.fields['adjacent_with_wg'].choices = [('', '--No preference')] + other_groups
group_acronym_choices = [('','--Select WG(s)')] + other_groups
for i in range(1, 5):
self.fields['wg_selector{}'.format(i)].choices = group_acronym_choices
# disabling handleconflictfield (which only enables or disables form elements) while we're hacking the meaning of the three constraints currently in use:
@ -109,6 +127,7 @@ class SessionForm(forms.Form):
self.fields['wg_selector1'].widget.attrs['onChange'] = "document.form_post.conflict1.value=document.form_post.conflict1.value + ' ' + this.options[this.selectedIndex].value; return 1;"
self.fields['wg_selector2'].widget.attrs['onChange'] = "document.form_post.conflict2.value=document.form_post.conflict2.value + ' ' + this.options[this.selectedIndex].value; return 1;"
self.fields['wg_selector3'].widget.attrs['onChange'] = "document.form_post.conflict3.value=document.form_post.conflict3.value + ' ' + this.options[this.selectedIndex].value; return 1;"
self.fields['wg_selector4'].widget.attrs['onChange'] = "document.form_post.joint_with_groups.value=document.form_post.joint_with_groups.value + ' ' + this.options[this.selectedIndex].value; return 1;"
# disabling check_prior_conflict javascript while we're hacking the meaning of the three constraints currently in use
#self.fields['wg_selector2'].widget.attrs['onClick'] = "return check_prior_conflict(2);"
@ -127,6 +146,7 @@ class SessionForm(forms.Form):
for key in list(self.fields.keys()):
self.fields[key].widget = forms.HiddenInput()
self.fields['resources'].widget = forms.MultipleHiddenInput()
self.fields['timeranges'].widget = forms.MultipleHiddenInput()
def clean_conflict1(self):
conflict = self.cleaned_data['conflict1']
@ -143,6 +163,11 @@ class SessionForm(forms.Form):
check_conflict(conflict, self.group)
return conflict
def clean_joint_with_groups(self):
groups = self.cleaned_data['joint_with_groups']
check_conflict(groups, self.group)
return groups
def clean_comments(self):
return clean_text_field(self.cleaned_data['comments'])
@ -166,10 +191,20 @@ class SessionForm(forms.Form):
if data.get('num_session','') == '2':
if not data['length_session2']:
raise forms.ValidationError('You must enter a length for all sessions')
else:
if data.get('session_time_relation'):
raise forms.ValidationError('Time between sessions can only be used when two '
'sessions are requested.')
if data['joint_for_session'] == '2':
raise forms.ValidationError('The second session can not be the joint session, '
'because you have not requested a second session.')
if data.get('third_session',False):
if not data['length_session2'] or not data.get('length_session3',None):
raise forms.ValidationError('You must enter a length for all sessions')
elif data['joint_for_session'] == '3':
raise forms.ValidationError('The third session can not be the joint session, '
'because you have not requested a third session.')
return data

View file

@ -12,7 +12,9 @@ from ietf.utils.test_utils import TestCase
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.meeting.models import Session, ResourceAssociation, SchedulingEvent, Constraint
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.name.models import TimerangeName
from ietf.person.models import Person
from ietf.secr.sreq.forms import SessionForm
from ietf.utils.mail import outbox, empty_outbox
from pyquery import PyQuery
@ -77,6 +79,9 @@ class SessionRequestTestCase(TestCase):
def test_edit(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
group2 = GroupFactory()
group3 = GroupFactory()
group4 = GroupFactory()
SessionFactory(meeting=meeting,group=mars,status_id='sched')
url = reverse('ietf.secr.sreq.views.edit', kwargs={'acronym':'mars'})
@ -89,9 +94,63 @@ class SessionRequestTestCase(TestCase):
'attendees':'10',
'conflict1':'',
'comments':'need lights',
'session_time_relation': 'subsequent-days',
'adjacent_with_wg': group2.acronym,
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
'joint_for_session': '2',
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'submit': 'Continue'}
r = self.client.post(url, post_data, HTTP_HOST='example.com')
self.assertRedirects(r,reverse('ietf.secr.sreq.views.view', kwargs={'acronym':'mars'}))
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
self.assertRedirects(r, redirect_url)
# Check whether updates were stored in the database
sessions = Session.objects.filter(meeting=meeting, group=mars)
self.assertEqual(len(sessions), 2)
session = sessions[0]
self.assertEqual(session.constraints().get(name='time_relation').time_relation, 'subsequent-days')
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
self.assertEqual(
list(session.constraints().get(name='timerange').timeranges.all().values('name')),
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
)
self.assertFalse(sessions[0].joint_with_groups.count())
self.assertEqual(list(sessions[1].joint_with_groups.all()), [group3, group4])
# Check whether the updated data is visible on the view page
r = self.client.get(redirect_url)
self.assertContains(r, 'Schedule the sessions on subsequent days')
self.assertContains(r, 'Thursday early afternoon, Thursday late afternoon')
self.assertContains(r, group2.acronym)
self.assertContains(r, 'Second session with: {} {}'.format(group3.acronym, group4.acronym))
# Edit again, changing the joint sessions and clearing some fields. The behaviour of
# edit is different depending on whether previous joint sessions were recorded.
post_data = {'num_session':'2',
'length_session1':'3600',
'length_session2':'3600',
'attendees':'10',
'conflict1':'',
'comments':'need lights',
'joint_with_groups': group2.acronym,
'joint_for_session': '1',
'submit': 'Continue'}
r = self.client.post(url, post_data, HTTP_HOST='example.com')
self.assertRedirects(r, redirect_url)
# Check whether updates were stored in the database
sessions = Session.objects.filter(meeting=meeting, group=mars)
self.assertEqual(len(sessions), 2)
session = sessions[0]
self.assertFalse(session.constraints().filter(name='time_relation'))
self.assertFalse(session.constraints().filter(name='wg_adjacent'))
self.assertFalse(session.constraints().filter(name='timerange'))
self.assertEqual(list(sessions[0].joint_with_groups.all()), [group2])
self.assertFalse(sessions[1].joint_with_groups.count())
# Check whether the updated data is visible on the view page
r = self.client.get(redirect_url)
self.assertContains(r, 'First session with: {}'.format(group2.acronym))
def test_tool_status(self):
MeetingFactory(type_id='ietf', date=datetime.date.today())
@ -108,6 +167,9 @@ class SubmitRequestCase(TestCase):
ad = Person.objects.get(user__username='ad')
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
group = GroupFactory(parent=area)
group2 = GroupFactory(parent=area)
group3 = GroupFactory(parent=area)
group4 = GroupFactory(parent=area)
session_count_before = Session.objects.filter(meeting=meeting, group=group).count()
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
@ -117,10 +179,20 @@ class SubmitRequestCase(TestCase):
'attendees':'10',
'conflict1':'',
'comments':'need projector',
'adjacent_with_wg': group2.acronym,
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
'joint_for_session': '1',
'submit': 'Continue'}
self.client.login(username="secretary", password="secretary+password")
r = self.client.post(url,post_data)
self.assertEqual(r.status_code, 200)
# Verify the contents of the confirm view
self.assertContains(r, 'Thursday early afternoon, Thursday late afternoon')
self.assertContains(r, group2.acronym)
self.assertContains(r, 'First session with: {} {}'.format(group3.acronym, group4.acronym))
post_data['submit'] = 'Submit'
r = self.client.post(confirm_url,post_data)
self.assertRedirects(r, main_url)
@ -133,6 +205,15 @@ class SubmitRequestCase(TestCase):
session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count()
self.assertEqual(session_count_after, session_count_before + 1)
# Verify database content
session = Session.objects.get(meeting=meeting, group=group)
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
self.assertEqual(
list(session.constraints().get(name='timerange').timeranges.all().values('name')),
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
)
self.assertEqual(list(session.joint_with_groups.all()), [group3, group4])
def test_submit_request_invalid(self):
MeetingFactory(type_id='ietf', date=datetime.date.today())
ad = Person.objects.get(user__username='ad')
@ -202,6 +283,8 @@ class SubmitRequestCase(TestCase):
area = GroupFactory(type_id='area')
RoleFactory(name_id='ad', person=ad, group=area)
group = GroupFactory(acronym='ames', parent=area)
group2 = GroupFactory(acronym='ames2', parent=area)
group3 = GroupFactory(acronym='ames2', parent=area)
RoleFactory(name_id='chair', group=group, person__user__username='ameschairman')
resource = ResourceAssociation.objects.create(name_id='project')
# Bit of a test data hack - the fixture now has no used resources to pick from
@ -211,20 +294,26 @@ class SubmitRequestCase(TestCase):
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
len_before = len(outbox)
post_data = {'num_session':'1',
post_data = {'num_session':'2',
'length_session1':'3600',
'length_session2':'3600',
'attendees':'10',
'bethere':str(ad.pk),
'conflict1':'',
'comments':'',
'resources': resource.pk,
'session_time_relation': 'subsequent-days',
'adjacent_with_wg': group2.acronym,
'joint_with_groups': group3.acronym,
'joint_for_session': '2',
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'submit': 'Continue'}
self.client.login(username="ameschairman", password="ameschairman+password")
# submit
r = self.client.post(url,post_data)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue('Confirm' in str(q("title")))
self.assertTrue('Confirm' in str(q("title")), r.context['form'].errors)
# confirm
post_data['submit'] = 'Submit'
r = self.client.post(confirm_url,post_data)
@ -232,11 +321,24 @@ class SubmitRequestCase(TestCase):
self.assertEqual(len(outbox),len_before+1)
notification = outbox[-1]
notification_payload = notification.get_payload(decode=True).decode(encoding="utf-8", errors="replace")
session = Session.objects.get(meeting=meeting,group=group)
sessions = Session.objects.filter(meeting=meeting,group=group)
self.assertEqual(len(sessions), 2)
session = sessions[0]
self.assertEqual(session.resources.count(),1)
self.assertEqual(session.people_constraints.count(),1)
self.assertEqual(session.constraints().get(name='time_relation').time_relation, 'subsequent-days')
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
self.assertEqual(
list(session.constraints().get(name='timerange').timeranges.all().values('name')),
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
)
resource = session.resources.first()
self.assertTrue(resource.desc in notification_payload)
self.assertTrue('Schedule the sessions on subsequent days' in notification_payload)
self.assertTrue(group2.acronym in notification_payload)
self.assertTrue("Can't meet: Thursday early afternoon, Thursday late" in notification_payload)
self.assertTrue('Second session joint with: {}'.format(group3.acronym) in notification_payload)
self.assertTrue(ad.ascii_name() in notification_payload)
class LockAppTestCase(TestCase):
@ -308,9 +410,133 @@ class NotMeetingCase(TestCase):
class RetrievePreviousCase(TestCase):
pass
# test error if already scheduled
# test get previous exists/doesn't exist
# test that groups scheduled and unscheduled add up to total groups
# test access by unauthorized
class SessionFormTest(TestCase):
def setUp(self):
self.group1 = GroupFactory()
self.group2 = GroupFactory()
self.group3 = GroupFactory()
self.group4 = GroupFactory()
self.group5 = GroupFactory()
self.group6 = GroupFactory()
self.valid_form_data = {
'num_session': '2',
'third_session': 'true',
'length_session1': '3600',
'length_session2': '3600',
'length_session3': '3600',
'attendees': '10',
'conflict1': self.group2.acronym,
'conflict2': self.group3.acronym,
'conflict3': self.group4.acronym,
'comments': 'need lights',
'session_time_relation': 'subsequent-days',
'adjacent_with_wg': self.group5.acronym,
'joint_with_groups': self.group6.acronym,
'joint_for_session': '3',
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'submit': 'Continue'
}
def test_valid(self):
# Test with three sessions
form = SessionForm(data=self.valid_form_data, group=self.group1)
self.assertTrue(form.is_valid())
# Test with two sessions
self.valid_form_data.update({
'length_session3': '',
'third_session': '',
'joint_for_session': '2'
})
form = SessionForm(data=self.valid_form_data, group=self.group1)
self.assertTrue(form.is_valid())
# Test with one session
self.valid_form_data.update({
'length_session2': '',
'num_session': 1,
'joint_for_session': '1',
'session_time_relation': '',
})
form = SessionForm(data=self.valid_form_data, group=self.group1)
self.assertTrue(form.is_valid())
def test_invalid_groups(self):
new_form_data = {
'conflict1': 'doesnotexist',
'conflict2': 'doesnotexist',
'conflict3': 'doesnotexist',
'adjacent_with_wg': 'doesnotexist',
'joint_with_groups': 'doesnotexist',
}
form = self._invalid_test_helper(new_form_data)
self.assertEqual(set(form.errors.keys()), set(new_form_data.keys()))
def test_invalid_group_appears_in_multiple_conflicts(self):
new_form_data = {
'conflict1': self.group2.acronym,
'conflict2': self.group2.acronym,
}
form = self._invalid_test_helper(new_form_data)
self.assertEqual(form.non_field_errors(), ['%s appears in conflicts more than once' % self.group2.acronym])
def test_invalid_conflict_with_self(self):
new_form_data = {
'conflict1': self.group1.acronym,
}
self._invalid_test_helper(new_form_data)
def test_invalid_session_time_relation(self):
form = self._invalid_test_helper({
'third_session': '',
'length_session2': '',
'num_session': 1,
'joint_for_session': '1',
})
self.assertEqual(form.non_field_errors(), ['Time between sessions can only be used when two '
'sessions are requested.'])
def test_invalid_joint_for_session(self):
form = self._invalid_test_helper({
'third_session': '',
'num_session': 2,
'joint_for_session': '3',
})
self.assertEqual(form.non_field_errors(), ['The third session can not be the joint session, '
'because you have not requested a third session.'])
form = self._invalid_test_helper({
'third_session': '',
'length_session2': '',
'num_session': 1,
'joint_for_session': '2',
'session_time_relation': '',
})
self.assertEqual(form.non_field_errors(), ['The second session can not be the joint session, '
'because you have not requested a second session.'])
def test_invalid_missing_session_length(self):
form = self._invalid_test_helper({
'length_session2': '',
'third_session': 'true',
})
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
form = self._invalid_test_helper({'length_session2': ''})
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
form = self._invalid_test_helper({'length_session3': ''})
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
def _invalid_test_helper(self, new_form_data):
form_data = dict(self.valid_form_data, **new_form_data)
form = SessionForm(data=form_data, group=self.group1)
self.assertFalse(form.is_valid())
return form

View file

@ -19,7 +19,7 @@ from ietf.meeting.models import Meeting, Session, Constraint, ResourceAssociatio
from ietf.meeting.helpers import get_meeting
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.name.models import SessionStatusName, ConstraintName
from ietf.secr.sreq.forms import SessionForm, ToolStatusForm, allowed_conflicting_groups
from ietf.secr.sreq.forms import SessionForm, ToolStatusForm, allowed_conflicting_groups, JOINT_FOR_SESSION_CHOICES
from ietf.secr.utils.decorators import check_permissions
from ietf.secr.utils.group import get_my_groups
from ietf.utils.mail import send_mail
@ -78,6 +78,19 @@ def get_initial_session(sessions, prune_conflicts=False):
initial['comments'] = sessions[0].comments
initial['resources'] = sessions[0].resources.all()
initial['bethere'] = [x.person for x in sessions[0].constraints().filter(name='bethere').select_related("person")]
wg_adjacent = conflicts.filter(name__slug='wg_adjacent')
initial['adjacent_with_wg'] = wg_adjacent[0].target.acronym if wg_adjacent else None
time_relation = conflicts.filter(name__slug='time_relation')
initial['session_time_relation'] = time_relation[0].time_relation if time_relation else None
initial['session_time_relation_display'] = time_relation[0].get_time_relation_display if time_relation else None
timeranges = conflicts.filter(name__slug='timerange')
initial['timeranges'] = timeranges[0].timeranges.all() if timeranges else []
initial['timeranges_display'] = [t.desc for t in initial['timeranges']]
for idx, session in enumerate(sessions):
if session.joint_with_groups.count():
initial['joint_with_groups'] = ' '.join(session.joint_with_groups_acronyms())
initial['joint_for_session'] = str(idx + 1)
initial['joint_for_session_display'] = dict(JOINT_FOR_SESSION_CHOICES)[initial['joint_for_session']]
return initial
def get_lock_message(meeting=None):
@ -163,7 +176,8 @@ def session_conflicts_as_string(group, meeting):
Takes a Group object and Meeting object and returns a string of other groups which have
a conflict with this one
'''
group_list = [ g.source.acronym for g in group.constraint_target_set.filter(meeting=meeting) ]
groups = group.constraint_target_set.filter(meeting=meeting, name__in=['conflict', 'conflic2', 'conflic3'])
group_list = [g.source.acronym for g in groups]
return ', '.join(group_list)
# -------------------------------------------------
@ -260,6 +274,12 @@ def confirm(request, acronym):
if 'bethere' in session_data:
person_id_list = [ id for id in form.data['bethere'].split(',') if id ]
session_data['bethere'] = Person.objects.filter(pk__in=person_id_list)
if session_data.get('session_time_relation'):
session_data['session_time_relation_display'] = dict(Constraint.TIME_RELATION_CHOICES)[session_data['session_time_relation']]
if session_data.get('joint_for_session'):
session_data['joint_for_session_display'] = dict(JOINT_FOR_SESSION_CHOICES)[session_data['joint_for_session']]
if form.cleaned_data.get('timeranges'):
session_data['timeranges_display'] = [t.desc for t in form.cleaned_data['timeranges']]
session_data['resources'] = [ ResourceAssociation.objects.get(pk=pk) for pk in request.POST.getlist('resources') ]
button_text = request.POST.get('submit', '')
@ -299,12 +319,27 @@ def confirm(request, acronym):
)
if 'resources' in form.data:
new_session.resources.set(session_data['resources'])
if int(form.data.get('joint_for_session', '-1')) == count:
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
joint = Group.objects.filter(acronym__in=groups_split)
new_session.joint_with_groups.set(joint)
session_changed(new_session)
# write constraint records
save_conflicts(group,meeting,form.data.get('conflict1',''),'conflict')
save_conflicts(group,meeting,form.data.get('conflict2',''),'conflic2')
save_conflicts(group,meeting,form.data.get('conflict3',''),'conflic3')
save_conflicts(group, meeting, form.data.get('adjacent_with_wg', ''), 'wg_adjacent')
if form.cleaned_data.get('session_time_relation'):
cn = ConstraintName.objects.get(slug='time_relation')
Constraint.objects.create(source=group, meeting=meeting, name=cn,
time_relation=form.cleaned_data['session_time_relation'])
if form.cleaned_data.get('timeranges'):
cn = ConstraintName.objects.get(slug='timerange')
constraint = Constraint.objects.create(source=group, meeting=meeting, name=cn)
constraint.timeranges.set(form.cleaned_data['timeranges'])
if 'bethere' in form.data:
bethere_cn = ConstraintName.objects.get(slug='bethere')
@ -443,6 +478,29 @@ def edit(request, acronym, num=None):
session.save()
session_changed(session)
# New sessions may have been created, refresh the sessions list
sessions = add_event_info_to_session_qs(
Session.objects.filter(group=group, meeting=meeting)).filter(
Q(current_status__isnull=True) | ~Q(
current_status__in=['canceled', 'notmeet'])).order_by('id')
if 'joint_with_groups' in form.changed_data or 'joint_for_session' in form.changed_data:
joint_with_groups_list = form.cleaned_data.get('joint_with_groups').replace(',', ' ').split()
new_joint_with_groups = Group.objects.filter(acronym__in=joint_with_groups_list)
new_joint_for_session_idx = int(form.data.get('joint_for_session', '-1')) - 1
current_joint_for_session_idx = None
current_joint_with_groups = None
for idx, session in enumerate(sessions):
if session.joint_with_groups.count():
current_joint_for_session_idx = idx
current_joint_with_groups = session.joint_with_groups.all()
if current_joint_with_groups != new_joint_with_groups or current_joint_for_session_idx != new_joint_for_session_idx:
if current_joint_for_session_idx is not None:
sessions[current_joint_for_session_idx].joint_with_groups.clear()
session_changed(sessions[current_joint_for_session_idx])
sessions[new_joint_for_session_idx].joint_with_groups.set(new_joint_with_groups)
session_changed(sessions[new_joint_for_session_idx])
if 'attendees' in form.changed_data:
sessions.update(attendees=form.cleaned_data['attendees'])
@ -457,6 +515,9 @@ def edit(request, acronym, num=None):
if 'conflict3' in form.changed_data:
Constraint.objects.filter(meeting=meeting,source=group,name='conflic3').delete()
save_conflicts(group,meeting,form.cleaned_data['conflict3'],'conflic3')
if 'adjacent_with_wg' in form.changed_data:
Constraint.objects.filter(meeting=meeting, source=group, name='wg_adjacent').delete()
save_conflicts(group, meeting, form.cleaned_data['adjacent_with_wg'], 'wg_adjacent')
if 'resources' in form.changed_data:
new_resource_ids = form.cleaned_data['resources']
@ -470,6 +531,20 @@ def edit(request, acronym, num=None):
for p in form.cleaned_data['bethere']:
Constraint.objects.create(name=bethere_cn, source=group, person=p, meeting=session.meeting)
if 'session_time_relation' in form.changed_data:
Constraint.objects.filter(meeting=meeting, source=group, name='time_relation').delete()
if form.cleaned_data['session_time_relation']:
cn = ConstraintName.objects.get(slug='time_relation')
Constraint.objects.create(source=group, meeting=meeting, name=cn,
time_relation=form.cleaned_data['session_time_relation'])
if 'timeranges' in form.changed_data:
Constraint.objects.filter(meeting=meeting, source=group, name='timerange').delete()
if form.cleaned_data['timeranges']:
cn = ConstraintName.objects.get(slug='timerange')
constraint = Constraint.objects.create(source=group, meeting=meeting, name=cn)
constraint.timeranges.set(form.cleaned_data['timeranges'])
# deprecated
# log activity
#add_session_activity(group,'Session Request was updated',meeting,user)

View file

@ -15,23 +15,34 @@ function stat_ls (val){
if (val == 0) {
document.form_post.length_session1.disabled = true;
document.form_post.length_session2.disabled = true;
document.form_post.length_session3.disabled = true;
if (document.form_post.length_session3) { document.form_post.length_session3.disabled = true; }
document.form_post.session_time_relation.disabled = true;
document.form_post.joint_for_session.disabled = true;
document.form_post.length_session1.value = 0;
document.form_post.length_session2.value = 0;
document.form_post.length_session3.value = 0;
document.form_post.session_time_relation.value = '';
document.form_post.joint_for_session.value = '';
document.form_post.third_session.checked=false;
}
if (val == 1) {
document.form_post.length_session1.disabled = false;
document.form_post.length_session2.disabled = true;
document.form_post.length_session3.disabled = true;
if (document.form_post.length_session3) { document.form_post.length_session3.disabled = true; }
document.form_post.session_time_relation.disabled = true;
document.form_post.joint_for_session.disabled = true;
document.form_post.length_session2.value = 0;
document.form_post.length_session3.value = 0;
document.form_post.session_time_relation.value = '';
document.form_post.joint_for_session.value = '1';
document.form_post.third_session.checked=false;
}
if (val == 2) {
document.form_post.length_session1.disabled = false;
document.form_post.length_session2.disabled = false;
if (document.form_post.length_session3) { document.form_post.length_session3.disabled = false; }
document.form_post.session_time_relation.disabled = false;
document.form_post.joint_for_session.disabled = false;
}
}
@ -111,6 +122,15 @@ function delete_last3 () {
document.form_post.conflict3.value = b;
document.form_post.wg_selector3.selectedIndex=0;
}
function delete_last_joint_with_groups () {
var b = document.form_post.joint_with_groups.value;
var temp = new Array();
temp = b.split(' ');
temp.pop();
b = temp.join(' ');
document.form_post.joint_with_groups.value = b;
document.form_post.wg_selector4.selectedIndex=0;
}
// Not calling check_prior_confict (see ietf/secr/sreq/forms.py definition of SessionForm)
// while we are hacking the use of the current three constraint types around. We could bring

View file

@ -3,6 +3,7 @@
Working Group Name: {{ group.name }}
Area Name: {{ group.parent }}
Session Requester: {{ login }}
{% if session.joint_with_groups %}{{ session.joint_for_session_display }} joint with: {{ session.joint_with_groups }}{% endif %}
Number of Sessions: {{ session.num_session }}
Length of Session(s): {{ session.length_session1|display_duration }}{% if session.length_session2 %}, {{ session.length_session2|display_duration }}{% endif %}{% if session.length_session3 %}, {{ session.length_session3|display_duration }}{% endif %}
@ -11,6 +12,9 @@ Conflicts to Avoid:
{% if session.conflict1 %} Chair Conflict: {{ session.conflict1 }}{% endif %}
{% if session.conflict2 %} Technology Overlap: {{ session.conflict2 }}{% endif %}
{% if session.conflict3 %} Key Participant Conflict: {{ session.conflict3 }}{% endif %}
{% if session.session_time_relation_display %} {{ session.session_time_relation_display }}{% endif %}
{% if session.adjacent_with_wg %} Adjacent with WG: {{ session.adjacent_with_wg }}{% endif %}
{% if session.timeranges_display %} Can't meet: {{ session.timeranges_display|join:", " }}{% endif %}
People who must be present:

View file

@ -8,6 +8,7 @@
<tr class="bg1"><td>Number of Sessions:<span class="required">*</span></td><td>{{ form.num_session.errors }}{{ form.num_session }}</td></tr>
<tr class="bg2"><td>Length of Session 1:<span class="required">*</span></td><td>{{ form.length_session1.errors }}{{ form.length_session1 }}</td></tr>
<tr class="bg2"><td>Length of Session 2:<span class="required">*</span></td><td>{{ form.length_session2.errors }}{{ form.length_session2 }}</td></tr>
<tr class="bg2"><td>Time between two sessions:</td><td>{{ form.session_time_relation.errors }}{{ form.session_time_relation }}</td></tr>
{% if group.type.slug == "wg" %}
<tr class="bg2"><td>Additional Session Request:</td><td>{{ form.third_session }} Check this box to request an additional session.<br>
Additional slot may be available after agenda scheduling has closed and with the approval of an Area Director.<br>
@ -49,7 +50,7 @@
</tr>
<tr>
<td colspan="2">BOF or IRTF Sessions:</td>
<td>Please enter free form requests in the Special Requests field below.</td>
<td>If the sessions can not be found in the fields above, please enter free form requests in the Special Requests field below.</td>
</tr>
</table>
</td>
@ -60,7 +61,34 @@
</td>
</tr>
<tr class="bg1">
<td valign="top">Special Requests:<br />&nbsp;<br />i.e. restrictions on meeting times / days, etc.<br /> (limit 200 characters)</td>
<td valign="top">Times during which this WG can <strong>not</strong> meet:</td>
<td>{{ form.timeranges.errors }}{{ form.timeranges }}</td>
</tr>
<tr class="bg2">
<td valign="top">
Plan session adjacent with another WG:<br />
(Immediately before or after another WG, no break in between, in the same room.)
</td>
<td>{{ form.adjacent_with_wg.errors }}{{ form.adjacent_with_wg }}</td>
</tr>
<tr class="bg1">
<td>
Joint session with:<br />
(To request one session for multiple WGs together.)
</td>
<td>{{ form.wg_selector4 }}
<input type="button" value="Delete the last entry" onClick="delete_last_joint_with_groups(); return 1;"><br>
{{ form.joint_with_groups.errors }}{{ form.joint_with_groups }}
</td>
</tr>
<tr class="bg1">
<td>
Of the sessions requested by this WG, the joint session, if applicable, is:
</td>
<td>{{ form.joint_for_session.errors }}{{ form.joint_for_session }}</td>
</tr>
<tr class="bg2">
<td valign="top">Special Requests:<br />&nbsp;<br />i.e. restrictions on meeting times / days, etc.</td> (limit 200 characters)</td>
<td>{{ form.comments.errors }}{{ form.comments }}</td>
</tr>
</table>

View file

@ -7,6 +7,7 @@
<tr class="row2"><td>Length of Session 1:</td><td>{{ session.length_session1|display_duration }}</td></tr>
{% if session.length_session2 %}
<tr class="row2"><td>Length of Session 2:</td><td>{{ session.length_session2|display_duration }}</td></tr>
<tr class="row2"><td>Time between sessions:</td><td>{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No preference{% endif %}</td></tr>
{% endif %}
{% if session.length_session3 %}
<tr class="row2"><td>Length of Session 3:</td><td>{{ session.length_session3|display_duration }}</td></tr>
@ -33,5 +34,23 @@
<tr class="row1">
<td>People who must be present:</td>
<td>{% if session.bethere %}<ul>{% for person in session.bethere %}<li>{{ person }}</li>{% endfor %}</ul>{% else %}<i>None</i>{% endif %}</td>
<tr class="row2"><td>Special Requests:</td><td>{{ session.comments }}</td></tr>
<tr class="row2">
<td>Can not meet on:</td>
<td>{% if session.timeranges_display %}{{ session.timeranges_display|join:', ' }}{% else %}No constraints{% endif %}</td>
</tr>
<tr class="row1">
<td>Adjacent with WG:</td>
<td>{{ session.adjacent_with_wg|default:'No preference' }}</td>
</tr>
<tr class="row2">
<td>Joint session:</td>
<td>
{% if session.joint_with_groups %}
{{ session.joint_for_session_display }} with: {{ session.joint_with_groups }}
{% else %}
Not a joint session
{% endif %}
</td>
</tr>
<tr class="row1"><td>Special Requests:</td><td>{{ session.comments }}</td></tr>
</table>

View file

@ -19,7 +19,7 @@
<tbody>
{% for item in times %}
<tr class="{% cycle 'row1' 'row2' %}">
<td>{{ item.time|date:"D" }}</td>
<td>{{ item.time|date:"D M d" }}</td>
<td>{{ item.time|date:"H:i" }} - {{ item.end_time|date:"H:i" }}</td>
<td>{{ item.name }}</td>
<td><a href="{% url "ietf.secr.meetings.views.times_edit" meeting_id=meeting.number schedule_name=schedule.name time=item.time|date:"Y:m:d:H:i" %}">Edit</a></td>

View file

@ -1226,11 +1226,29 @@ Session.prototype.generate_info_table = function() {
$("#info_location").html(generate_select_box()+"<button id='info_location_set'>set</button>");
}
if("comments" in this && this.comments.length > 0 && this.comments != "None") {
$("#special_requests").text(this.comments);
} else {
$("#special_requests").text("Special requests: None");
var special_requests_text = '';
if(this.joint_with_groups) {
special_requests_text += 'Joint session with ' + this.joint_with_groups.join(', ') + '. ';
}
if(this.constraints.wg_adjacent) {
for (var target_href in this.constraints.wg_adjacent) {
if (this.constraints.wg_adjacent.hasOwnProperty(target_href)) {
special_requests_text += 'Schedule adjacent with ' + this.constraints.wg_adjacent[target_href].othergroup.acronym + '. ';
}
}
}
if(this.constraints.time_relation) {
special_requests_text += this.constraints.time_relation.time_relation.time_relation_display + '. ';
}
if(this.constraints.timerange) {
special_requests_text += this.constraints.timerange.timerange.timeranges_display + '. ';
}
if("comments" in this && this.comments.length > 0 && this.comments != "None") {
special_requests_text += this.comments;
} else {
special_requests_text += "Special requests: None";
}
$("#special_requests").text(special_requests_text);
this.selectit();
@ -1721,13 +1739,17 @@ Session.prototype.add_constraint_obj = function(obj) {
obj.person = person;
});
} else {
// must be conflic*
// must be conflic*, timerange, time_relation or wg_adjacent
var ogroupname;
if(obj.source_href == this.group_href) {
obj.thisgroup = this.group;
obj.othergroup = find_group_by_href(obj.target_href, "constraint src"+obj.href);
obj.direction = 'ours';
ogroupname = obj.target_href;
if (obj.target_href) {
ogroupname = obj.target_href;
} else {
ogroupname = obj.name;
}
if(this.constraints[listname][ogroupname]) {
console.log("Found multiple instances of",this.group_href,listname,ogroupname);
}

View file

@ -3,7 +3,6 @@
import re
import magic
import datetime
import debug # pyflakes:ignore
@ -12,6 +11,8 @@ from typing import List, Optional # pyflakes:ignore
from django.conf import settings
from django.template.defaultfilters import filesizeformat
from ietf.utils.mime import get_mime_type
class MetaData(object):
rev = None
name = None
@ -82,20 +83,7 @@ class FileParser(object):
def parse_file_type(self):
self.fd.file.seek(0)
content = self.fd.file.read(64*1024)
if hasattr(magic, "open"):
m = magic.open(magic.MAGIC_MIME)
m.load()
filetype = m.buffer(content)
else:
m = magic.Magic()
m.cookie = magic.magic_open(magic.MAGIC_NONE | magic.MAGIC_MIME | magic.MAGIC_MIME_ENCODING)
magic.magic_load(m.cookie, None)
filetype = m.from_buffer(content)
if ';' in filetype and 'charset=' in filetype:
mimetype, charset = re.split('; *charset=', filetype)
else:
mimetype = re.split(';', filetype)[0]
charset = 'utf-8'
mimetype, charset = get_mime_type(content)
if not mimetype in self.mimetypes:
self.parsed_info.add_error('Expected an %s file of type "%s", found one of type "%s"' % (self.ext.upper(), '" or "'.join(self.mimetypes), mimetype))
self.parsed_info.mimetype = mimetype

View file

@ -11,6 +11,7 @@ from typing import Optional # pyflakes:ignore
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User
from django.db import DataError
from django.urls import reverse as urlreverse
from django.core.validators import ValidationError
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden, HttpResponse
@ -55,9 +56,9 @@ def upload_submission(request):
try:
fill_in_submission(form, submission, authors, abstract, file_size)
except Exception as e:
if submission:
submission.delete()
log("Exception: %s\n" % e)
if submission and submission.id:
submission.delete()
raise
apply_checkers(submission, file_name)
@ -79,6 +80,13 @@ def upload_submission(request):
form._errors["__all__"] = form.error_class(["There was a failure converting the xml file to text -- please verify that your xml file is valid. (%s)" % e.message])
if debug.debug:
raise
except DataError as e:
form = SubmissionManualUploadForm(request=request)
form._errors = {}
form._errors["__all__"] = form.error_class(["There was a failure processing your upload -- please verify that your draft passes idnits. (%s)" % e.message])
if debug.debug:
raise
else:
form = SubmissionManualUploadForm(request=request)

View file

@ -43,7 +43,7 @@
<th>Size</th>
<th>Requester</th>
<th>AD</th>
<th>Conflicts</th>
<th>Constraints</th>
<th>Special requests</th>
</tr>
</thead>
@ -62,6 +62,9 @@
<a href="{% url "ietf.secr.sreq.views.edit" num=meeting.number acronym=session.group.acronym %}">
{{session.group.acronym}}
</a>
{% if session.joint_with_groups.count %}
joint with {{ session.joint_with_groups_acronyms|join:' ' }}
{% endif %}
</th>
<td class="text-right">

View file

@ -3,6 +3,7 @@
import magic
import re
def get_mime_type(content):
# try to fixup encoding
@ -15,6 +16,12 @@ def get_mime_type(content):
m.cookie = magic.magic_open(magic.MAGIC_NONE | magic.MAGIC_MIME | magic.MAGIC_MIME_ENCODING)
magic.magic_load(m.cookie, None)
filetype = m.from_buffer(content)
return filetype.split('; ', 1)
# Work around silliness in libmagic on OpenSUSE 15.1
filetype = filetype.replace('text/x-Algol68;', 'text/plain;')
if ';' in filetype and 'charset=' in filetype:
mimetype, charset = re.split('; *charset=', filetype)
else:
mimetype = re.split(';', filetype)[0]
charset = 'utf-8'
return mimetype, charset

Binary file not shown.