Merged in ^/trunk@17617.
- Legacy-Id: 17618
This commit is contained in:
commit
f2b883d2bb
14
PLAN
14
PLAN
|
@ -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
|
||||
|
|
75
changelog
75
changelog
|
@ -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**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
4651
ietf/meeting/management/commands/create_dummy_meeting.py
Normal file
4651
ietf/meeting/management/commands/create_dummy_meeting.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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),
|
||||
]
|
|
@ -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))
|
||||
|
@ -1014,6 +1037,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":
|
||||
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
58
ietf/name/migrations/0010_timerangename.py
Normal file
58
ietf/name/migrations/0010_timerangename.py
Normal 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),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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
|
@ -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.')
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# see add_id5.cfm ~400 for email To addresses
|
||||
# see generateNotification.cfm
|
||||
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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),
|
||||
]
|
|
@ -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},
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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']
|
||||
|
@ -142,7 +162,12 @@ class SessionForm(forms.Form):
|
|||
conflict = self.cleaned_data['conflict3']
|
||||
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
|
||||
|
||||
|
|
|
@ -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,10 +94,64 @@ 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())
|
||||
url = reverse('ietf.secr.sreq.views.tool_status')
|
||||
|
@ -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)
|
||||
|
@ -132,6 +204,15 @@ class SubmitRequestCase(TestCase):
|
|||
self.assertRedirects(r, main_url)
|
||||
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())
|
||||
|
@ -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
|
|
@ -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,7 +478,30 @@ 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'])
|
||||
if 'comments' in form.changed_data:
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 /> <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 /> <br />i.e. restrictions on meeting times / days, etc.</td> (limit 200 characters)</td>
|
||||
<td>{{ form.comments.errors }}{{ form.comments }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1225,12 +1225,30 @@ Session.prototype.generate_info_table = function() {
|
|||
if(!read_only) {
|
||||
$("#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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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.
Loading…
Reference in a new issue