Merged in changes from trunk up to r17584.

- Legacy-Id: 17593
This commit is contained in:
Henrik Levkowetz 2020-04-07 16:02:52 +00:00
commit f10ddadc0e
129 changed files with 1805 additions and 1241 deletions

3
.gitignore vendored
View file

@ -26,6 +26,7 @@
/attic /attic
/bin /bin
/etc /etc
/env
/ghostdriver.log /ghostdriver.log
/htmlcov /htmlcov
/include /include
@ -48,3 +49,5 @@
/trunk37 /trunk37
/unix.tag /unix.tag
/tmp-nomcom-public-keys-dir /tmp-nomcom-public-keys-dir
*.pyc
__pycache__

View file

@ -87,16 +87,17 @@ for opt, value in opts:
if opt in ["-h", "--help"]: # Output this help, then exit if opt in ["-h", "--help"]: # Output this help, then exit
print( __doc__ % locals() ) print( __doc__ % locals() )
sys.exit(1) sys.exit(1)
elif opt in ["-v", "--version"]: # Output version information, then exit elif opt in ["-V", "--version"]: # Output version information, then exit
print( program, version ) print( program, version )
sys.exit(0) sys.exit(0)
elif opt in ["-V", "--verbose"]: # Output version information, then exit elif opt in ["-v", "--verbose"]: # Output version information, then exit
opt_verbose += 1 opt_verbose += 1
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
def say(s): def say(s):
sys.stderr.write("%s\n" % (s)) sys.stderr.write("%s\n" % (s))
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
def note(s): def note(s):
if opt_verbose: if opt_verbose:
@ -204,6 +205,7 @@ for line in pipe('svn propget svn:mergeinfo .').splitlines():
write_cache = True write_cache = True
mergeinfo[line] = merged mergeinfo[line] = merged
merged_revs.update(merged) merged_revs.update(merged)
note('')
if write_cache: if write_cache:
cache[repo] = mergeinfo cache[repo] = mergeinfo
@ -227,7 +229,7 @@ def get_changeset_list_from_file(repo, filename):
if line.startswith('#') or line == "": if line.startswith('#') or line == "":
continue continue
try: try:
note(" '%s'" % line) #note(" '%s'" % line)
parts = line.split() parts = line.split()
if len(parts) >1 and parts[1] == '@': if len(parts) >1 and parts[1] == '@':
branch, rev = parts[0], parts[2] branch, rev = parts[0], parts[2]
@ -253,7 +255,7 @@ def get_changeset_list_from_file(repo, filename):
def get_ready_commits(repo, tree): def get_ready_commits(repo, tree):
list = [] list = []
note("Getting ready commits from '%s'" % tree) note("Getting ready commits from '%s'" % tree)
cmd = 'svn log -v -r %s:HEAD %s/%s/' % ((head-500), repo, tree) cmd = 'svn log -v -r %s:HEAD %s/%s/' % ((head-200), repo, tree)
if opt_verbose > 1: if opt_verbose > 1:
note("Running '%s' ..." % cmd) note("Running '%s' ..." % cmd)
commit_log = pipe(cmd) commit_log = pipe(cmd)
@ -273,7 +275,7 @@ def get_ready_commits(repo, tree):
note(" %s %s: %s@%s" % (when.strftime("%Y-%m-%d %H:%MZ"), who, branch, rev)) note(" %s %s: %s@%s" % (when.strftime("%Y-%m-%d %H:%MZ"), who, branch, rev))
list += [(rev, repo, branch),] list += [(rev, repo, branch),]
elif rev in merged_revs and not branch == merged_revs[rev]: elif rev in merged_revs and not branch == merged_revs[rev]:
sys.stderr.write('Rev %s: %s != %s' % (rev, branch, merged_revs[rev])) sys.stderr.write('Rev %s: %s != %s\n' % (rev, branch, merged_revs[rev]))
else: else:
pass pass
else: else:
@ -286,10 +288,8 @@ ready += get_changeset_list_from_file(repo, '../ready-for-merge')
hold = get_changeset_list_from_file(repo, 'hold-for-merge') hold = get_changeset_list_from_file(repo, 'hold-for-merge')
hold += get_changeset_list_from_file(repo, '../hold-for-merge') hold += get_changeset_list_from_file(repo, '../hold-for-merge')
ready += get_ready_commits(repo, 'personal') ready += get_ready_commits(repo, 'personal')
ready += get_ready_commits(repo, 'branch/amsl')
ready += get_ready_commits(repo, 'branch/iola') ready += get_ready_commits(repo, 'branch/iola')
ready += get_ready_commits(repo, 'branch/dash') ready += get_ready_commits(repo, 'branch/dash')
ready += get_ready_commits(repo, 'branch/proceedings')
ready_commits = {} ready_commits = {}
all_commits = {} all_commits = {}
@ -328,7 +328,9 @@ for entry in ready:
else: else:
raise raise
# #
merge_path = os.path.join(*path.split(os.path.sep)[:4]) dirs = path.split(os.path.sep)
dirs = dirs[:dirs.index('ietf')] if 'ietf' in dirs else dirs[:4]
merge_path = os.path.join(*dirs)
if not (rev, repo, merge_path) in hold: if not (rev, repo, merge_path) in hold:
output_line = "%s %-24s ^/%s@%s" % (when.strftime("%Y-%m-%d_%H:%MZ"), who+":", merge_path, rev) output_line = "%s %-24s ^/%s@%s" % (when.strftime("%Y-%m-%d_%H:%MZ"), who+":", merge_path, rev)
all_commits[when] = (rev, repo, branch, who, merge_path) all_commits[when] = (rev, repo, branch, who, merge_path)
@ -388,10 +390,11 @@ for key in keys:
keys = list(not_passed.keys()) keys = list(not_passed.keys())
keys.sort() keys.sort()
if len(keys) > 0: if len(keys) > 0:
sys.stderr.write("Commits marked ready which haven't passed the test suite:\n") print("")
print("Commits marked ready which haven't passed the test suite:\n")
for key in keys: for key in keys:
sys.stderr.write(not_passed[key]+'\n') print(not_passed[key])
sys.stderr.write('\n') print('')
keys = list(ready_commits.keys()) keys = list(ready_commits.keys())
keys.sort() keys.sort()

View file

@ -78,12 +78,13 @@ trap 'echo "$program($LINENO): Command failed with error code $? ([$$] $0 $*)";
# Option parsing # Option parsing
# Options # Options
shortopts=hm:M:vV shortopts=hm:M:nvV
longopts=help,meeting=,message=,verbose,version longopts=help,meeting=,message=,dry-run,verbose,version
# Default values # Default values
num="" num=""
msg="" msg=""
do=""
if [ "$(uname)" = "Linux" ]; then if [ "$(uname)" = "Linux" ]; then
args=$(getopt -o "$shortopts" --long "$longopts" -n '$program' -- $SV "$@") args=$(getopt -o "$shortopts" --long "$longopts" -n '$program' -- $SV "$@")
@ -103,6 +104,7 @@ while true ; do
-h| --help) usage; exit;; # Show this help, then exit -h| --help) usage; exit;; # Show this help, then exit
-m| --meeting) num=$2; shift;; # Specify the IETF meeting number -m| --meeting) num=$2; shift;; # Specify the IETF meeting number
-M| --message) msg=$2; shift;; # Specify extra message text -M| --message) msg=$2; shift;; # Specify extra message text
-n| --dry-run) do="echo -- ==>";; # Only show what would be done
-v| --verbose) VERBOSE=1;; # Be more talkative -v| --verbose) VERBOSE=1;; # Be more talkative
-V| --version) version; exit;; # Show program version, then exit -V| --version) version; exit;; # Show program version, then exit
--) shift; break;; --) shift; break;;
@ -128,8 +130,8 @@ function mksvndir() {
who=$1 who=$1
if [ "$2" ]; then dir=$2; else dir=$who; fi if [ "$2" ]; then dir=$2; else dir=$who; fi
if ! svn info https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$dir >/dev/null 2>&1 ; then if ! svn info https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$dir >/dev/null 2>&1 ; then
echo "Creating personal directory area for IETF datatracker coding: /personal/$dir" $do echo "Creating personal directory area for IETF datatracker coding: /personal/$dir"
svn mkdir https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$dir -m "Personal SVN dir for $who, for IETF datatracker code" $do svn mkdir https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$dir -m "Personal SVN dir for $who, for IETF datatracker code"
else else
echo "Repository area personal/$dir is already in place." echo "Repository area personal/$dir is already in place."
fi fi
@ -143,7 +145,7 @@ cd $progdir
if [ "$who" ]; then if [ "$who" ]; then
mksvndir $who mksvndir $who
svn cp https://svn.tools.ietf.org/svn/tools/ietfdb/$source https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$who/$target/ -m "New branch for $target" $do svn cp https://svn.tools.ietf.org/svn/tools/ietfdb/$source https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$who/$target/ -m "New branch for $target"
echo "New branch: ^/personal/$who/$target" echo "New branch: ^/personal/$who/$target"
else else
[ "$msg" ] && msg=" [ "$msg" ] && msg="
@ -154,11 +156,21 @@ $msg
trac-admin /www/tools.ietf.org/tools/ietfdb wiki export IETF${n}SprintSignUp \ trac-admin /www/tools.ietf.org/tools/ietfdb wiki export IETF${n}SprintSignUp \
| egrep "^\|\|" | tail -n +2 | python -c ' | egrep "^\|\|" | tail -n +2 | python -c '
import sys, re import sys, re
afile = open("aliases") with open("aliases") as afile:
aliases = dict([ line.strip().split(None,1) for line in afile.read().splitlines() ]) try:
aliases = dict([ line.strip().split(None,1) for line in afile.read().splitlines() if line.strip() ])
except ValueError:
sys.stderr.write([ line.strip().split(None,1) for line in afile.read().splitlines() if line.strip() ])
raise
for line in sys.stdin: for line in sys.stdin:
blank, name, email, rest = line.strip().split("||", 3) try:
blank, name, email, rest = line.strip().split("||", 3)
email = email.strip()
except ValueError:
sys.stderr.write(line+"\n")
raise
login, dummy = re.split("[@.]", email, 1) login, dummy = re.split("[@.]", email, 1)
if email in aliases: if email in aliases:
login = aliases[email] login = aliases[email]
@ -171,9 +183,9 @@ for line in sys.stdin:
echo "$login ($name <$email>):" echo "$login ($name <$email>):"
mksvndir $login mksvndir $login
if ! svn info https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$login/$target >/dev/null 2>&1 ; then if ! svn info https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$login/$target >/dev/null 2>&1 ; then
echo " creating $target branch for $login ($name)." $do echo " creating $target branch for $login ($name)."
svn cp https://svn.tools.ietf.org/svn/tools/ietfdb/$source https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$login/$target/ -m "New IETF datatracker coding branch for $name" \ $do svn cp https://svn.tools.ietf.org/svn/tools/ietfdb/$source https://svn.tools.ietf.org/svn/tools/ietfdb/personal/$login/$target/ -m "New IETF datatracker coding branch for $name" \
&& mail "$name <$email>" -s "A new SVN branch for you for IETF datatracker coding${rev:+, based on $rev}." -b henrik@levkowetz.com <<-EOF && $do mail "$name <$email>" -s "A new SVN branch for you for IETF datatracker coding${rev:+, based on $rev}." -b henrik@levkowetz.com <<-EOF
Hi, Hi,
$msg $msg
This mail has been automatically generated by the $program script. This mail has been automatically generated by the $program script.
@ -199,7 +211,7 @@ for line in sys.stdin:
EOF EOF
else else
echo " branch personal/$login/$target already exists." $do echo " branch personal/$login/$target already exists."
fi fi
done done
fi fi

View file

@ -71,8 +71,8 @@ trap 'echo "$program($LINENO): Command failed with error code $? ([$$] $0 $*)";
# Option parsing # Option parsing
# Options # Options
shortopts=c:r:hvV shortopts=c:or:hvV
longopts=change=,revision=,help,verbose,version longopts=change=,overwrite,revision=,help,verbose,version
# Default values # Default values
@ -92,7 +92,7 @@ fi
while true ; do while true ; do
case "$1" in case "$1" in
-c| --change) CHG="$2"; shift;; # the change made by revision ARG -c| --change) CHG="$2"; shift;; # the change made by revision ARG
-r| --revision) REV="$2"; shift;; # the change made between revisions REV -o| --overwrite) OVER=1;; # overwrite any existing patch file
-h| --help) usage; exit;; # Show this help, then exit -h| --help) usage; exit;; # Show this help, then exit
-v| --verbose) VERBOSE=1;; # Be more talkative -v| --verbose) VERBOSE=1;; # Be more talkative
-V| --version) version; exit;; # Show program version, then exit -V| --version) version; exit;; # Show program version, then exit
@ -105,16 +105,19 @@ done
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# The program itself # The program itself
if [ $# -lt 2 ]; then die "Expected patch name and file list on the command line."; fi if [ "$CHG" ]; then
if [[ $1 =~ / ]]; then die "Expected a patch name, but the first argument to $program seems to be a file path: '$1'"; fi name=$(echo $(svn log -c $CHG | sed -r -e '/^---/d' -e '/^r[0-9]+/d' -e '/^$/d' -e 's/Merged in \[[0-9]+\] from [^:]+..//' ) | sed -r -e 's/(.*)/\L\1/' -e 's/[^[:alnum:]]/-/g' -e 's/-+/-/g' | cut -c 1-40)
name="$name-c$CHG"
name=$1; shift; else
if [ "$CHG" ]; then name="$name-c$CHG"; fi if [ $# -lt 2 ]; then die "Expected patch name and file list on the command line."; fi
if [[ $1 =~ / ]]; then die "Expected a patch name, but the first argument to $program seems to be a file path: '$1'"; fi
name=$1; shift;
fi
patchfile=$progdir/../../patches/$(date +%Y-%m-%d)-$name.patch patchfile=$progdir/../../patches/$(date +%Y-%m-%d)-$name.patch
if [ -e $patchfile ]; then die "Patchfile $patchfile already exists"; fi if [ -e $patchfile -a ! -n "$OVER" ]; then die "Patchfile $patchfile already exists"; fi
svn diff ${CHG:+ -c $CHG} ${REV:+ -r $REV} "$@" > $patchfile svn diff ${CHG:+ -c $CHG} ${REV:+ -r $REV} "$@" > $patchfile
less $patchfile less $patchfile
echo "" echo ""
echo "" echo ""
echo "Patch is in $patchfile." echo "Patch is in $patchfile"

View file

@ -66,6 +66,10 @@ function note() {
if [ -n "$VERBOSE" ]; then echo -e "\n$*"; fi if [ -n "$VERBOSE" ]; then echo -e "\n$*"; fi
} }
function check() {
[ "$(which $1)" ] || die "could not find the '$1' command. $2"
}
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
function version() { function version() {
echo -e "$program $version" echo -e "$program $version"
@ -119,6 +123,11 @@ while true ; do
shift shift
done done
# ----------------------------------------------------------------------
# Check some requirements
check bower "It is required to update web resources. Install with npm."
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# The program itself # The program itself
@ -212,6 +221,7 @@ if [ -z "$IGNORE_RESOURCES" ]; then
$do svn commit ietf/externals/static -m "Updated bower-managed static web assets" $do svn commit ietf/externals/static -m "Updated bower-managed static web assets"
# Get rid of bower-installed files which we don't use: # Get rid of bower-installed files which we don't use:
$do rm -rf ietf/externals/static/datatracker/ $do rm -rf ietf/externals/static/datatracker/
$do rm -rf ietf/externals/static/jquery.cookie/
$do rm -f $(svn st ietf/externals/ | grep '^\?' | awk '{print $2}') $do rm -f $(svn st ietf/externals/ | grep '^\?' | awk '{print $2}')
fi fi
@ -244,7 +254,9 @@ if [ -d ../coverage ]; then
rsync -a static/coverage/ ../coverage/$VER/ rsync -a static/coverage/ ../coverage/$VER/
fi fi
contributors=$(echo "$changes" | sed 's/\.[ \t\n]/ /'| tr -c "a-z0-9.@-" "\n" | sort | uniq | grep '@' | sed -r -e 's/^\.+//' -e 's/\.+$//' -e 's/^/-c /' || true) contributors=$(echo "$changes" | gawk '/^ \* Merged in \[[0-9]+\] from [^: ]+/ {sub(":",""); print $6;}' | sort | uniq)
note "Contributors:
$contributors"
note "Setting the current time on the release notes in the changelog file ..." note "Setting the current time on the release notes in the changelog file ..."
$do sed -r -i -e "1,/^ -- /s/([A-Za-z-]+ <[a-z0-9.-]+@[a-z0-9.-]+> ).*$/\1$(TZ=UTC date +'%d %b %Y %H:%M:%S %z')/" changelog $do sed -r -i -e "1,/^ -- /s/([A-Za-z-]+ <[a-z0-9.-]+@[a-z0-9.-]+> ).*$/\1$(TZ=UTC date +'%d %b %Y %H:%M:%S %z')/" changelog

320
changelog
View file

@ -1,3 +1,323 @@
ietfdb (6.124.0) ietf; urgency=medium
**Enhanced 'Upcoming Meetings' page, and more**
* Added links to agenda/materials pop-up, materials download, etherpad,
jabber room, and webex call-in session for interims on the upcoming
meetings page. With the earlier changes from [17555], this fixes issue
#2937.
* Changed the IPR patent number regex to permit space between country
code and serial number, and expanded on the help text for the IPR patent
number field.
* Added verification of response data to IphoneAppJsonTests
* Merged in [17564] from pusateri@bangj.com:
Added interim meetings to agenda.json API. Fixes #2946.
* Merged in [17562] from jennifer@painless-security.com:
Add tooltips with doc name to 'updates' and 'obsoletes' links. Fixes
#2866;
* Fixed a Py2/3 issue in the djangobwr's bower_install command
* Added a check for availability of 'bower' in bin/mkrelease.
* Cleaned up the contributors list in bin/mkrelease a bit.
* Merged in [17557] from fenton@bluepopcorn.net:
Provide more consistent links to people pages. Fixes #2918.
* Changed an obsolete document.href() to document.get_href(). Fixes
issue #2945.
* Tweaked the upcoming calendar and calendar entries slightly, to render
with times first and on two lines on narrow screens.
* Merged in [17555] from rjsparks@nostrum.com:
Remove the not-quite-working customization widgets from
/meeting/upcoming and /meeting/past. Simplify those views. Correct the list
of sessions on those pages when one interim has more than one session.
Fixes #2938. Partially addresses #2937.
* Prevent an exception on missing author.email.person when listing author
emails.
-- Henrik Levkowetz <henrik@levkowetz.com> 03 Apr 2020 10:04:43 +0000
ietfdb (6.123.1) ietf; urgency=medium
**Fixes for meeting-related issues**
* Merged in [17543] from rjsparks@nostrum.com:
Repaired construction of group_hierarchy used for the customisation
controls at /meeting/upcoming. Fixes #2940.
* Merged in [17542] from rjsparks@nostrum.com:
Show calendar entries on /meeting/upcoming in utc. Show end times.
Partially addresses #2936.
* Don't show agenda buttons for Meetecho recordings (after the session
concludes) if there isn't a Meetecho UrlResource. Fixes issue #2934
* Merged in [17538] from rjsparks@nostrum.com:
Allow an out-of-area AD assigned as the AD for a WG to approve interim
requests for that WG. Fixes #2930.
* Changed the object factory instances of nomcom private key and cert to
be byte objects (matching the production settings), and fixed the issue
with nomcom key handling under Py3 found by fenton@bluepopcorn.net. Did
some renaming in nomcom/tests.py to better match setup/teardown function
names to functionality.
* Merged in [17521] from housley@vigilsec.com:
Improve performance of log.assertion() and log.unreachable()
* Merged in [17505] from housley@vigilsec.com:
Improve performance of many document list pages
-- Henrik Levkowetz <henrik@levkowetz.com> 27 Mar 2020 14:27:24 +0000
ietfdb (6.123.0) ietf; urgency=medium
**IETF 107 code sprint**
This release contains datatracker bug fixes and enhancements from the
IETF-107 Code Sprint, our first virtual code sprint. It was a different
experience, and enjoyable despite not getting to sit down with a beer
together afterwards. A lot of good contributions were made; Thanks to
everyone who contributed!
* Merged in [17496] from rjsparks@nostrum.com:
Remove the rest of the log.assertions checking that iesg_state existed in
places we expected it to. Removed unnecessary imports.
* Changed the page for upcoming meetings to show the current IETF meeting
for 7 days from its start date, while interims are shown for today and
forward. Also changed the upcoming.ics calendar to show future sessions,
even if the meeting to which they belong started in the past. This
improves on [17518].
* Changed the starting point of display of upcoming meetings to be 7 days
before today, rather than today, to let meetings linger a bit in the
listing and iCalendar file after the meeting has started. Triggered by an
observation from resnick@episteme.net about IETF 107 sessions disappearing
from 'upcoming.ics' on meeting week Monday.
* Merged in [17495] from rjsparks@nostrum.com:
Removes a log.assertion() that was checking that we covered the edges when
we changed documents to always have an iesg state.
* Merged in [17494] from rjsparks@nostrum.com:
Use current email addresses when we have them when listing document
authors. Fixes #1902.
* Merged in [17493] from mahoney@nostrum.com:
Changed awkward IESG/IAB Nominating Committee names to just NomCom,
updated a ref. Fixes #2860.
* Merged in [17492] from rcross@amsl.com:
On session request form, made the Special Requests field smaller and
display 200 character limit. Fixes #2875.
* Merged in [17491] from rcross@amsl.com:
Prevent use of capital letters in group acronym. Fixes #2709.
* Merged in [17490] from rjsparks@nostrum.com:
Basic regex validation on community rule entry form. Fixes #2928.
* Merged in [17489] from rcross@amsl.com:
Removed redundant URL secr/groups/search because search page is
available here secr/groups. Resolves issue with Add link. Fixes #2708.
* Merged in [17488] from rcross@amsl.com:
Removed the drafts secretariat tool because this functionality is now
provided by the core Datatracker. Moved ID reports to proceedings tool.
Fixes #1655.
* Merged in [17487] from rjsparks@nostrum.com:
Let chairs know what to do after material submission uploads have been
cut off. Fixes #2887.
* Merged in [17486] from valery@smyslov.net:
Added docker/run modifications to support Cygwin.
* Merged in [17484] from valery@smyslov.net:
When requesting a new WG session, and retrieving information about the
previous session, look back to the previous time the group met, instead of
simply checking the previous IETF meeting and maybe not finding any
information to retrieve.
* Merged in [17483] from peter@akayla.com:
Changed things so that only WGs/RGs can be closed, per RJS. Fixes #1578.
* Merged in [17466] from rcross@amsl.com:
Added a migration to cancel 107 sessions
* Added a check to see if any files matching the submitted draft name and
revision already exists on disk in the active drafts or archived drafts
directories, and if so reject the submission. Fixes issue #2908
* Made sure to strip possible mail header field values of whitespace
before applying email.utils.unquite(). Resolution by kivinen@iki.fi,
Fixes issue #2899.
* Merged in [17480] from rjsparks@nostrum.com:
Show UTC times in interim announcements if the interim has a non-UTC
timzone. Fixes #2922.
-- Henrik Levkowetz <henrik@levkowetz.com> 24 Mar 2020 17:53:45 +0000
ietfdb (6.122.0) ietf; urgency=medium
**Added agenda webex URL support, and meeting-related tweaks and bugfixes**
* Added webex URL to agenda.ics if Room.webex_url is non-empty. Fixes issue
#2926.
* Added another check to the check_draft_event_revision_integrity management
command, and refined it somewhat.
* Added a utility function to convert objects to dictionaries (for
comparisons, for instance).
* Added a --dry-run option to bin/mkdevbranch, and added some exception
handling.
* Help tablesorter see 'Month Year' dates as dates with a hidden day digit.
Fixes issue #2921.
* Refactored and extended check_draft_event_revision_integrity a bit.
* Tweaked bin/mkpatch some for -c handling
* Merged in [17442] from rjsparks@nostrum.com: Allow area groups to request
interim meetings. Fixed #2919.
* Additional tweaks to bin/mkpatch; removing buggy -r option.
* Added automatic naming to bin/mkpatch when changeset or revision range is
given.
* Added WebEx room resource name, query method and template logic to show
WebEx room resources.
* Removed a debug statement
* Made links from agenda room names to floorplans conditional on the room
having a floor plan set.
-- Henrik Levkowetz <henrik@levkowetz.com> 20 Mar 2020 19:50:12 +0000
ietfdb (6.121.0) ietf; urgency=medium
**Tweaks for wholly virtual meeting, for IETF-107**
* Added code to show a webex call-in button on the agenda page if the session
agenda-note contains and IETF webex URL.
* Merged in [17425] from rjsparks@nostrum.com: Make required AD approval of
virtual interims configurable. Fixes #2912.
* Added a management command to check draft event revision numbers. To be
extended for other checks.
* Merged in [17419] from rjsparks@nostrum.com: Don't warn about idcutoff
when the cutoff is after the meeting starts. Fixes #2907.
* Merged in [17418] from rjsparks@nostrum.com: Correctly represent cancelled
sessions in ics files. Fixes #2905.
* Merged in [17396] from rjsparks@nostrum.com: Move charters for replaced
groups to a new replaced state. Close any outstanding ballots on them.
Fixes #2889, #2873, and #1286.
* Avoid trying to open meeting documents with empty .uploaded_filename.
* Added a progress bar for verbosity=1 of the community list index update
command.
* Merged back fixes from production
* Corrected the extent of a try/except block, moving more code inside the
block. Fixes a submission exception that should just be a document error
reported back to the user.
* Added a guard against accessing attributes of None.
-- Henrik Levkowetz <henrik@levkowetz.com> 13 Mar 2020 14:48:11 +0000
ietfdb (6.120.0) ietf; urgency=medium
**Submission API changes, Py2/3 transition fixes**
* Added the ability to use the submission API with active secondary account
email addresses. Fixes issue #2639.
* Tweaked the ReviewAssignmentAdmin, adding a raw_id_field.
* Replaced most cases of using of urlopen(), instead using the higher-level
'requests' module where it simplifies the code.
* Added a data migration to fix up incorrect external URLs to mailarchive.
* Fixed a Py2/3 issue with review.mailarchive.construct_query_url().
* Renamed a migration to conform to migration naming conventions, using
underscores instead of dashes in the name.
* Py2/3 compatibility tweaks for pyflakes.
* Changed some cases of urlopen() to use requests.get()
* Python3 is more ticklish about comparing strings to None than Py2. Fixed
an issue with this in generate_sort_key() for document searches.
* Fixed a Py2/3 issue in the pyflakes management command, and tweaked the
verbose output format.
* Merged back production changes to two scripts indirectly called by
/a/www/www6s/scripts/run-ietf-report, through
/a/www/www6s/scripts/run-report.
* Changed the release script to not pick up other email addresses than those
of contributors from the release notes.
* Tweaked the check_referential_integrity management command verbose output.
-- Henrik Levkowetz <henrik@levkowetz.com> 07 Mar 2020 22:55:58 +0000
ietfdb (6.119.1) ietf; urgency=medium
**Py2/3 fixes, Change to use the "requests" lib instead of urlopen()**
* Py2/3 compatibility tweaks for pyflakes.
* Changed some cases of urlopen() to use requests.get()
* Python3 is more ticklish about comparing strings to None than Py2.
Fixed an issue with this in generate_sort_key() for document searches.
* Fixed a Py2/3 issue in the pyflakes management command, and tweaked the
verbose output format.
* Merged back production changes to two scripts indirectly called by
/a/www/www6s/scripts/run-ietf-report, through
/a/www/www6s/scripts/run-report.
* Changed the release script to not pick up other email addresses than
those of contributors from the release notes.
-- Henrik Levkowetz <henrik@levkowetz.com> 03 Mar 2020 11:23:46 +0000
ietfdb (6.119.0) ietf; urgency=medium ietfdb (6.119.0) ietf; urgency=medium
**Improved email handling, and roundup of Py2/3 conversion issues** **Improved email handling, and roundup of Py2/3 conversion issues**

View file

@ -158,9 +158,9 @@ class Command(BaseCommand):
# Check if we need to copy the file at all. # Check if we need to copy the file at all.
if os.path.exists(dst_path): if os.path.exists(dst_path):
with open(src_path) as src: with open(src_path, 'br') as src:
src_hash = hashlib.sha1(src.read()).hexdigest() src_hash = hashlib.sha1(src.read()).hexdigest()
with open(dst_path) as dst: with open(dst_path, 'br') as dst:
dst_hash = hashlib.sha1(dst.read()).hexdigest() dst_hash = hashlib.sha1(dst.read()).hexdigest()
if src_hash == dst_hash: if src_hash == dst_hash:
#print('{0} = {1}'.format(src_path, dst_path)) #print('{0} = {1}'.format(src_path, dst_path))

View file

@ -133,6 +133,12 @@ if [ "$(uname)" = "Darwin" ]; then
CMD="open -a" CMD="open -a"
elif [ "$(uname)" = "Linux" ]; then elif [ "$(uname)" = "Linux" ]; then
echo "Running on Linux." echo "Running on Linux."
elif [[ $(uname) =~ CYGWIN.* ]]; then
echo "Running under Cygwin."
APP="Don't know how to start Docker when running under Cygwin"
CMD="echo"
MYSQLDIR=$(echo $MYSQLDIR | sed -e 's/^\/cygdrive\/\(.\)/\1:/')
WHO=$(echo $WHO | sed -e 's/^.*\\//' | tr -d \\r)
else else
die "This script does not have support for your architecture ($(uname)); sorry :-(" die "This script does not have support for your architecture ($(uname)); sorry :-("
fi fi
@ -184,7 +190,6 @@ else
fi fi
fi fi
image=$(docker ps | grep "$REPO:$TAG" | awk '{ print $1 }') image=$(docker ps | grep "$REPO:$TAG" | awk '{ print $1 }')
if [ "$image" ]; then if [ "$image" ]; then
if [ "$*" ]; then if [ "$*" ]; then

View file

@ -1,5 +1,6 @@
# -*- conf-mode -*- # -*- conf-mode -*-
^/personal/mahoney/6.121.1.dev0@17473 # Test commit
/personal/kivinen/6.94.2.dev0@16091 # Replaced by later commit /personal/kivinen/6.94.2.dev0@16091 # Replaced by later commit
/personal/rjs/6.104.1.dev0@16809 # Local changes, not for merge /personal/rjs/6.104.1.dev0@16809 # Local changes, not for merge
/personal/rjs/6.103.1.dev0@16761 # Fixed in a different manner in [16757] /personal/rjs/6.103.1.dev0@16761 # Fixed in a different manner in [16757]

View file

@ -5,13 +5,13 @@
from . import checks # pyflakes:ignore from . import checks # pyflakes:ignore
# Don't add patch number here: # Don't add patch number here:
__version__ = "6.119.1.dev0" __version__ = "6.124.1.dev0"
# set this to ".p1", ".p2", etc. after patching # set this to ".p1", ".p2", etc. after patching
__patch__ = "" __patch__ = ""
__date__ = "$Date$" __date__ = "$Date$"
__rev__ = "$Rev$ (dev) Latest release: Rev. 17365 " __rev__ = "$Rev$ (dev) Latest release: Rev. 17582 "
__id__ = "$Id$" __id__ = "$Id$"

View file

@ -1,5 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import io
import os import os
import sys import sys

View file

@ -28,7 +28,7 @@ TODO:
""" """
# boilerplate (from various other ietf/bin scripts) # boilerplate (from various other ietf/bin scripts)
import os, sys, re import io, os, sys, re
filename = os.path.abspath(__file__) filename = os.path.abspath(__file__)
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))

View file

@ -17,7 +17,7 @@ mail lists: -ads, and -chairs
""" """
# boilerplate (from various other ietf/bin scripts) # boilerplate (from various other ietf/bin scripts)
import os, sys import io, os, sys
filename = os.path.abspath(__file__) filename = os.path.abspath(__file__)
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))

View file

@ -3,9 +3,10 @@
# This script requires that the proper virtual python environment has been # This script requires that the proper virtual python environment has been
# invoked before start # invoked before start
import os
import sys
import datetime import datetime
import os
import requests
import sys
import syslog import syslog
# boilerplate # boilerplate
@ -19,7 +20,7 @@ import django
django.setup() django.setup()
from django.conf import settings from django.conf import settings
from ietf.sync.iana import fetch_protocol_page, parse_protocol_page, update_rfc_log_from_protocol_page from ietf.sync.iana import parse_protocol_page, update_rfc_log_from_protocol_page
def chunks(l, n): def chunks(l, n):
"""Split list l up in chunks of max size n.""" """Split list l up in chunks of max size n."""
@ -30,7 +31,7 @@ syslog.syslog("Updating history log with new RFC entries from IANA protocols pag
# FIXME: this needs to be the date where this tool is first deployed # FIXME: this needs to be the date where this tool is first deployed
rfc_must_published_later_than = datetime.datetime(2012, 11, 26, 0, 0, 0) rfc_must_published_later_than = datetime.datetime(2012, 11, 26, 0, 0, 0)
text = fetch_protocol_page(settings.IANA_SYNC_PROTOCOLS_URL) text = requests.get(settings.IANA_SYNC_PROTOCOLS_URL).text
rfc_numbers = parse_protocol_page(text) rfc_numbers = parse_protocol_page(text)
for chunk in chunks(rfc_numbers, 100): for chunk in chunks(rfc_numbers, 100):
updated = update_rfc_log_from_protocol_page(chunk, rfc_must_published_later_than) updated = update_rfc_log_from_protocol_page(chunk, rfc_must_published_later_than)

View file

@ -3,21 +3,20 @@
# -*- Python -*- # -*- Python -*-
# #
# This script requires that the proper virtual python environment has been
# invoked before start
# Set PYTHONPATH and load environment variables for standalone script ----------------- # Set PYTHONPATH and load environment variables for standalone script -----------------
import os, sys import os, sys
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
sys.path = [ basedir ] + sys.path sys.path = [ basedir ] + sys.path
os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings" os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings"
virtualenv_activation = os.path.join(basedir, "env", "bin", "activate_this.py")
if os.path.exists(virtualenv_activation):
execfile(virtualenv_activation, dict(__file__=virtualenv_activation))
import django import django
django.setup() django.setup()
# ------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------
from ietf.secr.drafts.reports import report_id_activity from ietf.secr.proceedings.reports import report_id_activity
print report_id_activity(sys.argv[1], sys.argv[2]), print(report_id_activity(sys.argv[1], sys.argv[2]), end='')

View file

@ -3,24 +3,22 @@
# -*- Python -*- # -*- Python -*-
# #
# This script requires that the proper virtual python environment has been
# invoked before start
# Set PYTHONPATH and load environment variables for standalone script ----------------- # Set PYTHONPATH and load environment variables for standalone script -----------------
import os, sys import os, sys
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
sys.path = [ basedir ] + sys.path sys.path = [ basedir ] + sys.path
os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings" os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings"
virtualenv_activation = os.path.join(basedir, "env", "bin", "activate_this.py")
if os.path.exists(virtualenv_activation):
execfile(virtualenv_activation, dict(__file__=virtualenv_activation))
import django import django
django.setup() django.setup()
# ------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------
from ietf.secr.drafts.reports import report_progress_report from ietf.secr.proceedings.reports import report_progress_report
# handle unicode characters before attempting to print # handle unicode characters before attempting to print
output = report_progress_report(sys.argv[1], sys.argv[2]) output = report_progress_report(sys.argv[1], sys.argv[2])
output = output.replace(unichr(160),' ') # replace NO-BREAK SPACE with space output = output.replace(chr(160),' ') # replace NO-BREAK SPACE with space
output = output.encode('ascii','replace') print(output, end='')
print output,

View file

@ -4,15 +4,15 @@
# invoked before start # invoked before start
import datetime import datetime
import io
import json import json
import os import os
import requests
import socket import socket
import sys import sys
import syslog import syslog
import traceback import traceback
from urllib.request import urlopen
# boilerplate # boilerplate
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
sys.path = [ basedir ] + sys.path sys.path = [ basedir ] + sys.path
@ -49,11 +49,10 @@ log("Updating document metadata from RFC index from %s" % settings.RFC_EDITOR_IN
socket.setdefaulttimeout(30) socket.setdefaulttimeout(30)
rfc_index_xml = urlopen(settings.RFC_EDITOR_INDEX_URL) rfc_index_xml = requests.get(settings.RFC_EDITOR_INDEX_URL).text
index_data = ietf.sync.rfceditor.parse_index(rfc_index_xml) index_data = ietf.sync.rfceditor.parse_index(io.StringIO(rfc_index_xml))
rfc_errata_json = urlopen(settings.RFC_EDITOR_ERRATA_JSON_URL) errata_data = requests.get(settings.RFC_EDITOR_ERRATA_JSON_URL).json()
errata_data = json.load(rfc_errata_json)
if len(index_data) < ietf.sync.rfceditor.MIN_INDEX_RESULTS: if len(index_data) < ietf.sync.rfceditor.MIN_INDEX_RESULTS:
log("Not enough index entries, only %s" % len(index_data)) log("Not enough index entries, only %s" % len(index_data))

View file

@ -1,9 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
import io
import os import os
import requests
import socket import socket
import sys import sys
from urllib.request import urlopen
# boilerplate # boilerplate
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
@ -21,8 +22,8 @@ from ietf.utils.log import log
log("Updating RFC Editor queue states from %s" % settings.RFC_EDITOR_QUEUE_URL) log("Updating RFC Editor queue states from %s" % settings.RFC_EDITOR_QUEUE_URL)
socket.setdefaulttimeout(30) socket.setdefaulttimeout(30)
response = urlopen(settings.RFC_EDITOR_QUEUE_URL) response = requests.get(settings.RFC_EDITOR_QUEUE_URL).text
drafts, warnings = parse_queue(response) drafts, warnings = parse_queue(io.StringIO(response))
for w in warnings: for w in warnings:
log(u"Warning: %s" % w) log(u"Warning: %s" % w)

View file

@ -2,6 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re
from django import forms from django import forms
from django.db.models import Q from django.db.models import Q
@ -92,7 +94,12 @@ class SearchRuleForm(forms.ModelForm):
f.required = True f.required = True
def clean_text(self): def clean_text(self):
return self.cleaned_data["text"].strip().lower() # names are always lower case candidate_text = self.cleaned_data["text"].strip().lower() # names are always lower case
try:
re.compile(candidate_text)
except re.error as e:
raise forms.ValidationError(str(e))
return candidate_text
class SubscriptionForm(forms.ModelForm): class SubscriptionForm(forms.ModelForm):

View file

@ -25,7 +25,6 @@ def expirable_draft(draft):
two functions need to be kept in sync.""" two functions need to be kept in sync."""
if draft.type_id != 'draft': if draft.type_id != 'draft':
return False return False
log.assertion('draft.get_state_slug("draft-iesg")')
return bool(expirable_drafts(Document.objects.filter(pk=draft.pk))) return bool(expirable_drafts(Document.objects.filter(pk=draft.pk)))
nonexpirable_states = [] # type: List[State] nonexpirable_states = [] # type: List[State]

View file

@ -0,0 +1,37 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-05-21 14:27
from __future__ import absolute_import, print_function, unicode_literals
import re
from django.conf import settings
from django.db import migrations
def forward(apps, schema_editor):
Document = apps.get_model('doc', 'Document')
print('')
for d in Document.objects.filter(external_url__contains="/b'"):
match = re.search("^(%s/arch/msg/[^/]+/)b'([^']+)'$" % settings.MAILING_LIST_ARCHIVE_URL, d.external_url)
if match:
d.external_url = "%s%s" % (match.group(1), match.group(2))
d.save()
print('Fixed url #%s: %s' % (d.id, d.external_url))
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('doc', '0029_add_ipr_event_types'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright The IETF Trust 2020, All Rights Reserved
# Generated by Django 1.11.28 on 2020-03-03 13:54
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
Person = apps.get_model('person', 'Person')
Document = apps.get_model('doc','Document')
State = apps.get_model('doc','State')
BallotDocEvent = apps.get_model('doc','BallotDocEvent')
replaced_state = State.objects.create(type_id='charter', slug='replaced', name='Replaced', used=True, desc="This charter's group was replaced.", order = 0)
by = Person.objects.get(name='(System)')
for doc in Document.objects.filter(type_id='charter',states__type_id='charter',states__slug__in=['intrev','extrev'],group__state='replaced'):
doc.states.remove(*list(doc.states.filter(type_id='charter')))
doc.states.add(replaced_state)
ballot = BallotDocEvent.objects.filter(doc=doc, type__in=('created_ballot', 'closed_ballot')).order_by('-time', '-id').first()
if ballot and ballot.type == 'created_ballot':
e = BallotDocEvent(type="closed_ballot", doc=doc, rev=doc.rev, by=by)
e.ballot_type = ballot.ballot_type
e.desc = 'Closed "%s" ballot' % e.ballot_type.name
e.save()
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('doc', '0030_fix_bytes_mailarch_url'),
('person', '0009_auto_20190118_0725'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -332,7 +332,6 @@ class DocumentInfo(models.Model):
else: else:
return "Replaced" return "Replaced"
elif state.slug == "active": elif state.slug == "active":
log.assertion('iesg_state')
if iesg_state: if iesg_state:
if iesg_state.slug == "dead": if iesg_state.slug == "dead":
# Many drafts in the draft-iesg "Dead" state are not dead # Many drafts in the draft-iesg "Dead" state are not dead
@ -376,7 +375,14 @@ class DocumentInfo(models.Model):
return self.rfc_number() return self.rfc_number()
def author_list(self): def author_list(self):
return ", ".join(author.email_id for author in self.documentauthor_set.all() if author.email_id) best_addresses = []
for author in self.documentauthor_set.all():
if author.email:
if author.email.active or not author.email.person:
best_addresses.append(author.email.address)
else:
best_addresses.append(author.email.person.email_address())
return ", ".join(best_addresses)
def authors(self): def authors(self):
return [ a.person for a in self.documentauthor_set.all() ] return [ a.person for a in self.documentauthor_set.all() ]

View file

@ -43,7 +43,6 @@ from django.utils.safestring import mark_safe
from ietf.ietfauth.utils import user_is_person, has_role from ietf.ietfauth.utils import user_is_person, has_role
from ietf.doc.models import BallotPositionDocEvent, IESG_BALLOT_ACTIVE_STATES from ietf.doc.models import BallotPositionDocEvent, IESG_BALLOT_ACTIVE_STATES
from ietf.name.models import BallotPositionName from ietf.name.models import BallotPositionName
from ietf.utils import log
register = template.Library() register = template.Library()
@ -168,7 +167,6 @@ def state_age_colored(doc):
# Don't show anything for expired/withdrawn/replaced drafts # Don't show anything for expired/withdrawn/replaced drafts
return "" return ""
iesg_state = doc.get_state_slug('draft-iesg') iesg_state = doc.get_state_slug('draft-iesg')
log.assertion('iesg_state')
if not iesg_state: if not iesg_state:
return "" return ""

View file

@ -16,6 +16,7 @@ from django.utils.safestring import mark_safe, SafeData
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.encoding import force_str # pyflakes:ignore force_str is used in the doctests from django.utils.encoding import force_str # pyflakes:ignore force_str is used in the doctests
from django.urls import reverse as urlreverse
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -23,6 +24,7 @@ from ietf.doc.models import BallotDocEvent
from ietf.doc.models import ConsensusDocEvent from ietf.doc.models import ConsensusDocEvent
from ietf.utils.html import sanitize_fragment from ietf.utils.html import sanitize_fragment
from ietf.utils import log from ietf.utils import log
from ietf.doc.utils import prettify_std_name
from ietf.utils.text import wordwrap, fill, wrap_text_if_unwrapped from ietf.utils.text import wordwrap, fill, wrap_text_if_unwrapped
register = template.Library() register = template.Library()
@ -233,6 +235,24 @@ def urlize_ietf_docs(string, autoescape=None):
return mark_safe(string) return mark_safe(string)
urlize_ietf_docs = stringfilter(urlize_ietf_docs) urlize_ietf_docs = stringfilter(urlize_ietf_docs)
@register.filter(name='urlize_doc_list', is_safe=True, needs_autoescape=True)
def urlize_doc_list(docs, autoescape=None):
"""Convert a list of DocAliases into list of links using canonical name"""
links = []
for doc in docs:
name=doc.document.canonical_name()
title = doc.document.title
url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=name))
if autoescape:
name = escape(name)
title = escape(title)
links.append(mark_safe(
'<a href="%(url)s" title="%(title)s">%(name)s</a>' % dict(name=prettify_std_name(name),
title=title,
url=url)
))
return links
@register.filter(name='dashify') @register.filter(name='dashify')
def dashify(string): def dashify(string):
""" """

View file

@ -514,8 +514,19 @@ Man Expires September 22, 2015 [Page 3]
def test_document_draft(self): def test_document_draft(self):
draft = WgDraftFactory(name='draft-ietf-mars-test',rev='01') draft = WgDraftFactory(name='draft-ietf-mars-test',rev='01')
HolderIprDisclosureFactory(docs=[draft]) HolderIprDisclosureFactory(docs=[draft])
# Docs for testing relationships. Does not test 'possibly-replaces'. The 'replaced_by' direction
# is tested separately below.
replaced = IndividualDraftFactory() replaced = IndividualDraftFactory()
draft.relateddocument_set.create(relationship_id='replaces',source=draft,target=replaced.docalias.first()) draft.relateddocument_set.create(relationship_id='replaces',source=draft,target=replaced.docalias.first())
obsoleted = IndividualDraftFactory()
draft.relateddocument_set.create(relationship_id='obs',source=draft,target=obsoleted.docalias.first())
obsoleted_by = IndividualDraftFactory()
obsoleted_by.relateddocument_set.create(relationship_id='obs',source=obsoleted_by,target=draft.docalias.first())
updated = IndividualDraftFactory()
draft.relateddocument_set.create(relationship_id='updates',source=draft,target=updated.docalias.first())
updated_by = IndividualDraftFactory()
updated_by.relateddocument_set.create(relationship_id='updates',source=obsoleted_by,target=draft.docalias.first())
# these tests aren't testing all attributes yet, feel free to # these tests aren't testing all attributes yet, feel free to
# expand them # expand them
@ -525,24 +536,68 @@ Man Expires September 22, 2015 [Page 3]
self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Active Internet-Draft")
self.assertContains(r, "Show full document text") self.assertContains(r, "Show full document text")
self.assertNotContains(r, "Deimos street") self.assertNotContains(r, "Deimos street")
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates not included until draft is RFC
self.assertNotContains(r, obsoleted.canonical_name())
self.assertNotContains(r, obsoleted.title)
self.assertNotContains(r, obsoleted_by.canonical_name())
self.assertNotContains(r, obsoleted_by.title)
self.assertNotContains(r, updated.canonical_name())
self.assertNotContains(r, updated.title)
self.assertNotContains(r, updated_by.canonical_name())
self.assertNotContains(r, updated_by.title)
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=0") r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=0")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Active Internet-Draft")
self.assertNotContains(r, "Show full document text") self.assertNotContains(r, "Show full document text")
self.assertContains(r, "Deimos street") self.assertContains(r, "Deimos street")
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates not included until draft is RFC
self.assertNotContains(r, obsoleted.canonical_name())
self.assertNotContains(r, obsoleted.title)
self.assertNotContains(r, obsoleted_by.canonical_name())
self.assertNotContains(r, obsoleted_by.title)
self.assertNotContains(r, updated.canonical_name())
self.assertNotContains(r, updated.title)
self.assertNotContains(r, updated_by.canonical_name())
self.assertNotContains(r, updated_by.title)
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=foo") r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=foo")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Active Internet-Draft")
self.assertNotContains(r, "Show full document text") self.assertNotContains(r, "Show full document text")
self.assertContains(r, "Deimos street") self.assertContains(r, "Deimos street")
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates not included until draft is RFC
self.assertNotContains(r, obsoleted.canonical_name())
self.assertNotContains(r, obsoleted.title)
self.assertNotContains(r, obsoleted_by.canonical_name())
self.assertNotContains(r, obsoleted_by.title)
self.assertNotContains(r, updated.canonical_name())
self.assertNotContains(r, updated.title)
self.assertNotContains(r, updated_by.canonical_name())
self.assertNotContains(r, updated_by.title)
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=1") r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)) + "?include_text=1")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Active Internet-Draft")
self.assertNotContains(r, "Show full document text") self.assertNotContains(r, "Show full document text")
self.assertContains(r, "Deimos street") self.assertContains(r, "Deimos street")
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates not included until draft is RFC
self.assertNotContains(r, obsoleted.canonical_name())
self.assertNotContains(r, obsoleted.title)
self.assertNotContains(r, obsoleted_by.canonical_name())
self.assertNotContains(r, obsoleted_by.title)
self.assertNotContains(r, updated.canonical_name())
self.assertNotContains(r, updated.title)
self.assertNotContains(r, updated_by.canonical_name())
self.assertNotContains(r, updated_by.title)
self.client.cookies = SimpleCookie({str('full_draft'): str('on')}) self.client.cookies = SimpleCookie({str('full_draft'): str('on')})
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
@ -550,6 +605,17 @@ Man Expires September 22, 2015 [Page 3]
self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Active Internet-Draft")
self.assertNotContains(r, "Show full document text") self.assertNotContains(r, "Show full document text")
self.assertContains(r, "Deimos street") self.assertContains(r, "Deimos street")
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates not included until draft is RFC
self.assertNotContains(r, obsoleted.canonical_name())
self.assertNotContains(r, obsoleted.title)
self.assertNotContains(r, obsoleted_by.canonical_name())
self.assertNotContains(r, obsoleted_by.title)
self.assertNotContains(r, updated.canonical_name())
self.assertNotContains(r, updated.title)
self.assertNotContains(r, updated_by.canonical_name())
self.assertNotContains(r, updated_by.title)
self.client.cookies = SimpleCookie({str('full_draft'): str('off')}) self.client.cookies = SimpleCookie({str('full_draft'): str('off')})
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
@ -557,6 +623,17 @@ Man Expires September 22, 2015 [Page 3]
self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Active Internet-Draft")
self.assertContains(r, "Show full document text") self.assertContains(r, "Show full document text")
self.assertNotContains(r, "Deimos street") self.assertNotContains(r, "Deimos street")
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates not included until draft is RFC
self.assertNotContains(r, obsoleted.canonical_name())
self.assertNotContains(r, obsoleted.title)
self.assertNotContains(r, obsoleted_by.canonical_name())
self.assertNotContains(r, obsoleted_by.title)
self.assertNotContains(r, updated.canonical_name())
self.assertNotContains(r, updated.title)
self.assertNotContains(r, updated_by.canonical_name())
self.assertNotContains(r, updated_by.title)
self.client.cookies = SimpleCookie({str('full_draft'): str('foo')}) self.client.cookies = SimpleCookie({str('full_draft'): str('foo')})
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
@ -564,6 +641,17 @@ Man Expires September 22, 2015 [Page 3]
self.assertContains(r, "Active Internet-Draft") self.assertContains(r, "Active Internet-Draft")
self.assertContains(r, "Show full document text") self.assertContains(r, "Show full document text")
self.assertNotContains(r, "Deimos street") self.assertNotContains(r, "Deimos street")
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates not included until draft is RFC
self.assertNotContains(r, obsoleted.canonical_name())
self.assertNotContains(r, obsoleted.title)
self.assertNotContains(r, obsoleted_by.canonical_name())
self.assertNotContains(r, obsoleted_by.title)
self.assertNotContains(r, updated.canonical_name())
self.assertNotContains(r, updated.title)
self.assertNotContains(r, updated_by.canonical_name())
self.assertNotContains(r, updated_by.title)
r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=draft.name))) r = self.client.get(urlreverse("ietf.doc.views_doc.document_html", kwargs=dict(name=draft.name)))
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -602,7 +690,8 @@ Man Expires September 22, 2015 [Page 3]
r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "Replaced Internet-Draft") self.assertContains(r, "Replaced Internet-Draft")
self.assertContains(r, replacement.name) self.assertContains(r, replacement.canonical_name())
self.assertContains(r, replacement.title)
rel.delete() rel.delete()
# draft published as RFC # draft published as RFC
@ -625,6 +714,17 @@ Man Expires September 22, 2015 [Page 3]
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "RFC 123456") self.assertContains(r, "RFC 123456")
self.assertContains(r, draft.name) self.assertContains(r, draft.name)
self.assertContains(r, replaced.canonical_name())
self.assertContains(r, replaced.title)
# obs/updates included with RFC
self.assertContains(r, obsoleted.canonical_name())
self.assertContains(r, obsoleted.title)
self.assertContains(r, obsoleted_by.canonical_name())
self.assertContains(r, obsoleted_by.title)
self.assertContains(r, updated.canonical_name())
self.assertContains(r, updated.title)
self.assertContains(r, updated_by.canonical_name())
self.assertContains(r, updated_by.title)
# naked RFC - also wierd that we test a PS from the ISE # naked RFC - also wierd that we test a PS from the ISE
rfc = IndividualDraftFactory( rfc = IndividualDraftFactory(

View file

@ -619,10 +619,10 @@ class BallotWriteupsTests(TestCase):
verify_can_see(username, url) verify_can_see(username, url)
class ApproveBallotTests(TestCase): class ApproveBallotTests(TestCase):
@mock.patch('ietf.sync.rfceditor.urlopen', autospec=True) @mock.patch('ietf.sync.rfceditor.requests.post', autospec=True)
def test_approve_ballot(self, mock_urlopen): def test_approve_ballot(self, mock_urlopen):
mock_urlopen.return_value.read = lambda : b'OK' mock_urlopen.return_value.text = b'OK'
mock_urlopen.return_value.getcode = lambda :200 mock_urlopen.return_value.status_code = 200
# #
ad = Person.objects.get(name="Areað Irector") ad = Person.objects.get(name="Areað Irector")
draft = IndividualDraftFactory(ad=ad, intended_std_level_id='ps') draft = IndividualDraftFactory(ad=ad, intended_std_level_id='ps')

View file

@ -1197,10 +1197,10 @@ class SubmitToIesgTests(TestCase):
class RequestPublicationTests(TestCase): class RequestPublicationTests(TestCase):
@mock.patch('ietf.sync.rfceditor.urlopen', autospec=True) @mock.patch('ietf.sync.rfceditor.requests.post', autospec=True)
def test_request_publication(self, mock_urlopen): def test_request_publication(self, mockobj):
mock_urlopen.return_value.read = lambda : b'OK' mockobj.return_value.text = b'OK'
mock_urlopen.return_value.getcode = lambda :200 mockobj.return_value.status_code = 200
# #
draft = IndividualDraftFactory(stream_id='iab',group__acronym='iab',intended_std_level_id='inf',states=[('draft-stream-iab','approved')]) draft = IndividualDraftFactory(stream_id='iab',group__acronym='iab',intended_std_level_id='inf',states=[('draft-stream-iab','approved')])

View file

@ -15,7 +15,8 @@ from django.utils.encoding import smart_text, force_text
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.doc.models import NewRevisionDocEvent, WriteupDocEvent from ietf.doc.models import NewRevisionDocEvent, WriteupDocEvent, State, StateDocEvent
from ietf.doc.utils import close_open_ballots
from ietf.group.models import ChangeStateGroupEvent from ietf.group.models import ChangeStateGroupEvent
from ietf.name.models import GroupStateName from ietf.name.models import GroupStateName
from ietf.utils.history import find_history_active_at from ietf.utils.history import find_history_active_at
@ -244,4 +245,18 @@ def generate_issue_ballot_mail(request, doc, ballot):
) )
) )
def replace_charter_of_replaced_group(group, by):
assert group.state_id == 'replaced'
charter = group.charter
if charter:
close_open_ballots(charter, by)
replaced_state = State.objects.get(type_id='charter', slug='replaced')
charter.set_state(replaced_state)
state_change_event = StateDocEvent.objects.create(state_type_id='charter', state=replaced_state, doc=charter, rev=charter.rev, by=by, type="changed_state", desc="Charter's group has been replaced")
charter.save_with_history([state_change_event])

View file

@ -183,7 +183,7 @@ def prepare_document_table(request, docs, query=None, max_results=200):
else: else:
res.append(d.type_id); res.append(d.type_id);
res.append("-"); res.append("-");
res.append(d.get_state_slug()); res.append(d.get_state_slug() or '');
res.append("-"); res.append("-");
if sort_key == "title": if sort_key == "title":

View file

@ -75,7 +75,7 @@ from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions,
from ietf.review.models import ReviewAssignment from ietf.review.models import ReviewAssignment
from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs
from ietf.review.utils import no_review_from_teams_on_doc from ietf.review.utils import no_review_from_teams_on_doc
from ietf.utils import markup_txt, log from ietf.utils import markup_txt
from ietf.utils.text import maybe_split from ietf.utils.text import maybe_split
@ -402,7 +402,6 @@ def document_main(request, name, rev=None):
actions.append((label, urlreverse('ietf.doc.views_draft.request_publication', kwargs=dict(name=doc.name)))) actions.append((label, urlreverse('ietf.doc.views_draft.request_publication', kwargs=dict(name=doc.name))))
if doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("ietf",) and not snapshot: if doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("ietf",) and not snapshot:
log.assertion('iesg_state')
if iesg_state.slug == 'idexists' and can_edit: if iesg_state.slug == 'idexists' and can_edit:
actions.append(("Begin IESG Processing", urlreverse('ietf.doc.views_draft.edit_info', kwargs=dict(name=doc.name)) + "?new=1")) actions.append(("Begin IESG Processing", urlreverse('ietf.doc.views_draft.edit_info', kwargs=dict(name=doc.name)) + "?new=1"))
elif can_edit_stream_info and (iesg_state.slug in ('idexists','watching')): elif can_edit_stream_info and (iesg_state.slug in ('idexists','watching')):
@ -410,10 +409,6 @@ def document_main(request, name, rev=None):
augment_docs_and_user_with_user_info([doc], request.user) augment_docs_and_user_with_user_info([doc], request.user)
replaces = [d.name for d in doc.related_that_doc("replaces")]
replaced_by = [d.name for d in doc.related_that("replaces")]
possibly_replaces = [d.name for d in doc.related_that_doc("possibly-replaces")]
possibly_replaced_by = [d.name for d in doc.related_that("possibly-replaces")]
published = doc.latest_event(type="published_rfc") published = doc.latest_event(type="published_rfc")
started_iesg_process = doc.latest_event(type="started_iesg_process") started_iesg_process = doc.latest_event(type="started_iesg_process")
@ -457,14 +452,14 @@ def document_main(request, name, rev=None):
submission=submission, submission=submission,
resurrected_by=resurrected_by, resurrected_by=resurrected_by,
replaces=replaces, replaces=doc.related_that_doc("replaces"),
replaced_by=replaced_by, replaced_by=doc.related_that("replaces"),
possibly_replaces=possibly_replaces, possibly_replaces=doc.related_that_doc("possibly_replaces"),
possibly_replaced_by=possibly_replaced_by, possibly_replaced_by=doc.related_that("possibly_replaces"),
updates=[prettify_std_name(d.name) for d in doc.related_that_doc("updates")], updates=doc.related_that_doc("updates"),
updated_by=[prettify_std_name(d.document.canonical_name()) for d in doc.related_that("updates")], updated_by=doc.related_that("updates"),
obsoletes=[prettify_std_name(d.name) for d in doc.related_that_doc("obs")], obsoletes=doc.related_that_doc("obs"),
obsoleted_by=[prettify_std_name(d.document.canonical_name()) for d in doc.related_that("obs")], obsoleted_by=doc.related_that("obs"),
conflict_reviews=conflict_reviews, conflict_reviews=conflict_reviews,
status_changes=status_changes, status_changes=status_changes,
proposed_status_changes=proposed_status_changes, proposed_status_changes=proposed_status_changes,
@ -659,7 +654,7 @@ def document_main(request, name, rev=None):
revisions=revisions, revisions=revisions,
latest_rev=latest_rev, latest_rev=latest_rev,
snapshot=snapshot, snapshot=snapshot,
review_req=review_assignment.review_request, review_req=review_assignment.review_request if review_assignment else None,
other_reviews=other_reviews, other_reviews=other_reviews,
assignments=assignments, assignments=assignments,
)) ))

View file

@ -858,7 +858,7 @@ def complete_review(request, name, assignment_id=None, acronym=None):
list_name = mailarch.list_name_from_email(assignment.review_request.team.list_email) list_name = mailarch.list_name_from_email(assignment.review_request.team.list_email)
if list_name: if list_name:
review.external_url = mailarch.construct_message_url(list_name, email.utils.unquote(msg["Message-ID"])) review.external_url = mailarch.construct_message_url(list_name, email.utils.unquote(msg["Message-ID"].strip()))
review.save_with_history([close_event]) review.save_with_history([close_event])
if form.cleaned_data['email_ad'] or assignment.result in assignment.review_request.team.reviewteamsettings.notify_ad_when.all(): if form.cleaned_data['email_ad'] or assignment.result in assignment.review_request.team.reviewteamsettings.notify_ad_when.all():

View file

@ -352,7 +352,7 @@ def ad_dashboard_sort_key(doc):
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval') state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
return "1%d%s" % (state.order,seed) return "1%d%s" % (state.order,seed)
if doc.type.slug=='charter': if doc.type.slug=='charter' and doc.get_state_slug('charter') != 'replaced':
if doc.get_state_slug('charter') in ('notrev','infrev'): if doc.get_state_slug('charter') in ('notrev','infrev'):
return "100%s" % seed return "100%s" % seed
elif doc.get_state_slug('charter') == 'intrev': elif doc.get_state_slug('charter') == 'intrev':

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,7 @@
from django import template from django import template
import debug # pyflakes:ignore
from ietf.group.models import Group from ietf.group.models import Group
register = template.Library() register = template.Library()
@ -25,3 +27,12 @@ def active_nomcoms(user):
state__slug='active').distinct().select_related("type")) state__slug='active').distinct().select_related("type"))
return groups return groups
@register.inclusion_tag('person/person_link.html')
def role_person_link(role, **kwargs):
title = kwargs.get('title', '')
cls = kwargs.get('class', '')
name = role.person.name
plain_name = role.person.plain_name()
email = role.email.address
return {'name': name, 'plain_name': plain_name, 'email': email, 'title': title, 'class': cls}

View file

@ -23,7 +23,7 @@ from django.utils.html import escape
from ietf.community.models import CommunityList from ietf.community.models import CommunityList
from ietf.community.utils import reset_name_contains_index_for_rule from ietf.community.utils import reset_name_contains_index_for_rule
from ietf.doc.factories import WgDraftFactory, CharterFactory from ietf.doc.factories import WgDraftFactory, CharterFactory, BallotDocEventFactory
from ietf.doc.models import Document, DocAlias, DocEvent, State from ietf.doc.models import Document, DocAlias, DocEvent, State
from ietf.doc.utils_charter import charter_name_for_group from ietf.doc.utils_charter import charter_name_for_group
from ietf.group.factories import (GroupFactory, RoleFactory, GroupEventFactory, from ietf.group.factories import (GroupFactory, RoleFactory, GroupEventFactory,
@ -755,6 +755,19 @@ class GroupEditTests(TestCase):
group = Group.objects.get(acronym=group.acronym) group = Group.objects.get(acronym=group.acronym)
self.assertEqual(group.state_id, "active") self.assertEqual(group.state_id, "active")
def test_replace(self):
group = GroupFactory(state_id='bof')
charter = CharterFactory(group=group, states=[('charter','intrev')])
BallotDocEventFactory(doc=charter, ballot_type__doc_type_id='draft', ballot_type__slug='r-extrev')
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit", field="state"))
self.client.login(username='secretary',password='secretary+password')
self.client.post(url, dict(state='replaced'))
group = Group.objects.get(pk=group.pk)
self.assertEqual(group.state_id, 'replaced')
self.assertEqual(group.charter.get_state_slug('charter'), 'replaced')
self.assertEqual(group.charter.active_ballot(), None)
def test_add_comment(self): def test_add_comment(self):
group = GroupFactory(acronym="mars",parent=GroupFactory(type_id='area')) group = GroupFactory(acronym="mars",parent=GroupFactory(type_id='area'))
RoleFactory(group=group,person=Person.objects.get(user__username='ad'),name_id='ad') RoleFactory(group=group,person=Person.objects.get(user__username='ad'),name_id='ad')

View file

@ -245,7 +245,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
if group.features.customize_workflow and can_manage: if group.features.customize_workflow and can_manage:
actions.append(("Customize workflow", urlreverse("ietf.group.views.customize_workflow", kwargs=kwargs))) actions.append(("Customize workflow", urlreverse("ietf.group.views.customize_workflow", kwargs=kwargs)))
if group.state_id in ("active", "dormant") and not group.type_id in ["sdo", "rfcedtyp", "isoc", ] and can_manage_group_type(request.user, group): if group.state_id in ("active", "dormant") and group.type_id in ["wg", "rg", ] and can_manage_group_type(request.user, group):
actions.append(("Request closing group", urlreverse("ietf.group.views.conclude", kwargs=kwargs))) actions.append(("Request closing group", urlreverse("ietf.group.views.conclude", kwargs=kwargs)))
d = { d = {

View file

@ -65,7 +65,7 @@ from ietf.community.utils import docs_tracked_by_community_list
from ietf.doc.models import DocTagName, State, DocAlias, RelatedDocument, Document from ietf.doc.models import DocTagName, State, DocAlias, RelatedDocument, Document
from ietf.doc.templatetags.ietf_filters import clean_whitespace from ietf.doc.templatetags.ietf_filters import clean_whitespace
from ietf.doc.utils import get_chartering_type, get_tags_for_stream_id from ietf.doc.utils import get_chartering_type, get_tags_for_stream_id
from ietf.doc.utils_charter import charter_name_for_group from ietf.doc.utils_charter import charter_name_for_group, replace_charter_of_replaced_group
from ietf.doc.utils_search import prepare_document_table from ietf.doc.utils_search import prepare_document_table
# #
from ietf.group.dot import make_dot from ietf.group.dot import make_dot
@ -1026,6 +1026,8 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
for attr, new, desc in changes: for attr, new, desc in changes:
if attr == 'state': if attr == 'state':
ChangeStateGroupEvent.objects.create(group=group, time=group.time, state=new, by=request.user.person, type="changed_state", desc=desc) ChangeStateGroupEvent.objects.create(group=group, time=group.time, state=new, by=request.user.person, type="changed_state", desc=desc)
if new.slug == 'replaced':
replace_charter_of_replaced_group(group=group, by=request.user.person)
else: else:
GroupEvent.objects.create(group=group, time=group.time, by=request.user.person, type="info_changed", desc=desc) GroupEvent.objects.create(group=group, time=group.time, by=request.user.person, type="info_changed", desc=desc)

View file

@ -20,7 +20,6 @@ from ietf.doc.models import IESG_SUBSTATE_TAGS
from ietf.doc.templatetags.ietf_filters import clean_whitespace from ietf.doc.templatetags.ietf_filters import clean_whitespace
from ietf.group.models import Group from ietf.group.models import Group
from ietf.person.models import Person, Email from ietf.person.models import Person, Email
from ietf.utils import log
def all_id_txt(): def all_id_txt():
# this returns a lot of data so try to be efficient # this returns a lot of data so try to be efficient
@ -154,7 +153,6 @@ def all_id2_txt():
# 3 # 3
if state == "active": if state == "active":
s = "I-D Exists" s = "I-D Exists"
log.assertion('iesg_state')
if iesg_state: if iesg_state:
s = iesg_state.name s = iesg_state.name
tags = d.tags.filter(slug__in=IESG_SUBSTATE_TAGS).values_list("name", flat=True) tags = d.tags.filter(slug__in=IESG_SUBSTATE_TAGS).values_list("name", flat=True)

View file

@ -111,18 +111,21 @@ class DraftForm(forms.ModelForm):
} }
help_texts = { 'sections': 'Sections' } help_texts = { 'sections': 'Sections' }
patent_number_help_text = "Enter one or more comma-separated patent publication or application numbers as two-letter country code and serial number, e.g.: US62/123456 or WO2017123456. Do not include thousands-separator commas in serial numbers. It is preferable to use individual disclosures for each patent, even if this field permits multiple patents to be listed, in order to get inventor, title, and date information below correct."
validate_patent_number = RegexValidator( validate_patent_number = RegexValidator(
regex=(r"^(" regex=(r"^("
r"([A-Z][A-Z]\d\d/\d{6}" r"([A-Z][A-Z] *\d\d/\d{6}"
r"|[A-Z][A-Z]\d{6,12}([A-Z]\d?)?" r"|[A-Z][A-Z] *\d{6,12}([A-Z]\d?)?"
r"|[A-Z][A-Z]\d{4}(\w{1,2}\d{5,7})?" r"|[A-Z][A-Z] *\d{4}(\w{1,2}\d{5,7})?"
r"|[A-Z][A-Z]\d{15}" r"|[A-Z][A-Z] *\d{15}"
r"|[A-Z][A-Z][A-Z]\d{1,5}/\d{4}" r"|[A-Z][A-Z][A-Z] *\d{1,5}/\d{4}"
r"|[A-Z][A-Z]\d{1,4}/\d{1,4}" r"|[A-Z][A-Z] *\d{1,4}/\d{1,4}"
r"|PCT/[A-Z][A-Z]\d{2}/\d{5}" # WO application, old r"|PCT/[A-Z][A-Z]*\d{2}/\d{5}" # WO application, old
r"|PCT/[A-Z][A-Z]\d{4}/\d{6}" # WO application, new r"|PCT/[A-Z][A-Z]*\d{4}/\d{6}" # WO application, new
r")[, ]*)+$"), r")[, ]*)+$"),
message="Please enter one or more patent publication or application numbers as country code and serial number, e.g.: US62/123456 or WO2017123456." ) message=patent_number_help_text)
""" """
Patent application number formats by country Patent application number formats by country
@ -206,7 +209,7 @@ class GenericDisclosureForm(forms.Form):
submitter_email = forms.EmailField(required=False) submitter_email = forms.EmailField(required=False)
#patent_info = forms.CharField(max_length=255,widget=forms.Textarea, required=False, help_text="Patent, Serial, Publication, Registration, or Application/File number(s), Date(s) granted or applied for, Country, and any additional notes.", strip=False) #patent_info = forms.CharField(max_length=255,widget=forms.Textarea, required=False, help_text="Patent, Serial, Publication, Registration, or Application/File number(s), Date(s) granted or applied for, Country, and any additional notes.", strip=False)
patent_number = forms.CharField(max_length=127, required=False, validators=[ validate_patent_number ], patent_number = forms.CharField(max_length=127, required=False, validators=[ validate_patent_number ],
help_text = "Patent publication or application number (2-letter country code followed by serial number)") help_text = patent_number_help_text)
patent_inventor = forms.CharField(max_length=63, required=False, validators=[ validate_name ], help_text="Inventor name") patent_inventor = forms.CharField(max_length=63, required=False, validators=[ validate_name ], help_text="Inventor name")
patent_title = forms.CharField(max_length=255, required=False, validators=[ validate_title ], help_text="Title of invention") patent_title = forms.CharField(max_length=255, required=False, validators=[ validate_title ], help_text="Title of invention")
patent_date = forms.DateField(required=False, help_text="Date granted or applied for") patent_date = forms.DateField(required=False, help_text="Date granted or applied for")
@ -275,7 +278,7 @@ class IprDisclosureFormBase(forms.ModelForm):
updates = SearchableIprDisclosuresField(required=False, help_text=mark_safe("If this disclosure <strong>updates</strong> other disclosures identify here which ones. Leave this field blank if this disclosure does not update any prior disclosures. Note: Updates to IPR disclosures must only be made by authorized representatives of the original submitters. Updates will automatically be forwarded to the current Patent Holder's Contact and to the Submitter of the original IPR disclosure.")) updates = SearchableIprDisclosuresField(required=False, help_text=mark_safe("If this disclosure <strong>updates</strong> other disclosures identify here which ones. Leave this field blank if this disclosure does not update any prior disclosures. Note: Updates to IPR disclosures must only be made by authorized representatives of the original submitters. Updates will automatically be forwarded to the current Patent Holder's Contact and to the Submitter of the original IPR disclosure."))
same_as_ii_above = forms.BooleanField(required=False) same_as_ii_above = forms.BooleanField(required=False)
patent_number = forms.CharField(max_length=127, required=True, validators=[ validate_patent_number ], patent_number = forms.CharField(max_length=127, required=True, validators=[ validate_patent_number ],
help_text = "Patent publication or application number (2-letter country code followed by serial number)") help_text = patent_number_help_text)
patent_inventor = forms.CharField(max_length=63, required=True, validators=[ validate_name ], help_text="Inventor name") patent_inventor = forms.CharField(max_length=63, required=True, validators=[ validate_name ], help_text="Inventor name")
patent_title = forms.CharField(max_length=255, required=True, validators=[ validate_title ], help_text="Title of invention") patent_title = forms.CharField(max_length=255, required=True, validators=[ validate_title ], help_text="Title of invention")
patent_date = forms.DateField(required=True, help_text="Date granted or applied for") patent_date = forms.DateField(required=True, help_text="Date granted or applied for")

View file

@ -36,7 +36,7 @@ class ShowAttachmentsWidget(Widget):
html += '<div class="attachedFiles form-control widget">' html += '<div class="attachedFiles form-control widget">'
if value and isinstance(value, QuerySet): if value and isinstance(value, QuerySet):
for attachment in value: for attachment in value:
html += '<a class="initialAttach" href="%s">%s</a>&nbsp' % (conditional_escape(attachment.document.href()), conditional_escape(attachment.document.title)) html += '<a class="initialAttach" href="%s">%s</a>&nbsp' % (conditional_escape(attachment.document.get_href()), conditional_escape(attachment.document.title))
html += '<a class="btn btn-default btn-xs" href="{}">Edit</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_edit_attachment", kwargs={'object_id':attachment.statement.pk,'doc_id':attachment.document.pk})) html += '<a class="btn btn-default btn-xs" href="{}">Edit</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_edit_attachment", kwargs={'object_id':attachment.statement.pk,'doc_id':attachment.document.pk}))
html += '<a class="btn btn-default btn-xs" href="{}">Delete</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_delete_attachment", kwargs={'object_id':attachment.statement.pk,'attach_id':attachment.pk})) html += '<a class="btn btn-default btn-xs" href="{}">Delete</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_delete_attachment", kwargs={'object_id':attachment.statement.pk,'attach_id':attachment.pk}))
html += '<br />' html += '<br />'

View file

@ -101,7 +101,7 @@ class InterimSessionInlineFormSet(BaseInlineFormSet):
class InterimMeetingModelForm(forms.ModelForm): class InterimMeetingModelForm(forms.ModelForm):
# TODO: Should area groups get to schedule Interims? # TODO: Should area groups get to schedule Interims?
group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False) group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg', 'ag'), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False)
in_person = forms.BooleanField(required=False) in_person = forms.BooleanField(required=False)
meeting_type = forms.ChoiceField(choices=( meeting_type = forms.ChoiceField(choices=(
("single", "Single"), ("single", "Single"),
@ -216,8 +216,8 @@ class InterimSessionModelForm(forms.ModelForm):
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
if 'group' in kwargs: if 'group' in kwargs:
self.group = kwargs.pop('group') self.group = kwargs.pop('group')
if 'is_approved_or_virtual' in kwargs: if 'requires_approval' in kwargs:
self.is_approved_or_virtual = kwargs.pop('is_approved_or_virtual') self.requires_approval = kwargs.pop('requires_approval')
super(InterimSessionModelForm, self).__init__(*args, **kwargs) super(InterimSessionModelForm, self).__init__(*args, **kwargs)
self.is_edit = bool(self.instance.pk) self.is_edit = bool(self.instance.pk)
# setup fields that aren't intrinsic to the Session object # setup fields that aren't intrinsic to the Session object
@ -238,6 +238,14 @@ class InterimSessionModelForm(forms.ModelForm):
raise forms.ValidationError('Required field') raise forms.ValidationError('Required field')
return date return date
def clean_requested_duration(self):
min_minutes = settings.INTERIM_SESSION_MINIMUM_MINUTES
max_minutes = settings.INTERIM_SESSION_MAXIMUM_MINUTES
duration = self.cleaned_data.get('requested_duration')
if not duration or duration < datetime.timedelta(minutes=min_minutes) or duration > datetime.timedelta(minutes=max_minutes):
raise forms.ValidationError('Provide a duration, %s-%smin.' % (min_minutes, max_minutes))
return duration
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""NOTE: as the baseform of an inlineformset self.save(commit=True) """NOTE: as the baseform of an inlineformset self.save(commit=True)
never gets called""" never gets called"""

View file

@ -221,7 +221,7 @@ def read_session_file(type, num, doc):
# #
# FIXME: uploaded_filename should be replaced with a function call that computes names that are fixed # FIXME: uploaded_filename should be replaced with a function call that computes names that are fixed
path = os.path.join(settings.AGENDA_PATH, "%s/%s/%s" % (num, type, doc.uploaded_filename)) path = os.path.join(settings.AGENDA_PATH, "%s/%s/%s" % (num, type, doc.uploaded_filename))
if os.path.exists(path): if doc.uploaded_filename and os.path.exists(path):
with io.open(path, 'rb') as f: with io.open(path, 'rb') as f:
return f.read(), path return f.read(), path
else: else:
@ -324,8 +324,9 @@ def can_approve_interim_request(meeting, user):
if not session: if not session:
return False return False
group = session.group group = session.group
if group.type.slug == 'wg' and group.parent.role_set.filter(name='ad', person=person): if group.type.slug == 'wg':
return True if group.parent.role_set.filter(name='ad', person=person) or group.role_set.filter(name='ad', person=person):
return True
if group.type.slug == 'rg' and group.parent.role_set.filter(name='chair', person=person): if group.type.slug == 'rg' and group.parent.role_set.filter(name='chair', person=person):
return True return True
return False return False
@ -600,7 +601,7 @@ def sessions_post_save(request, forms):
continue continue
if form.instance.pk is not None and not SchedulingEvent.objects.filter(session=form.instance).exists(): if form.instance.pk is not None and not SchedulingEvent.objects.filter(session=form.instance).exists():
if form.is_approved_or_virtual: if not form.requires_approval:
status_id = 'scheda' status_id = 'scheda'
else: else:
status_id = 'apprw' status_id = 'apprw'

View file

@ -0,0 +1,40 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-03-18 16:18
from __future__ import unicode_literals
from django.db import migrations
def cancel_sessions(apps, schema_editor):
Session = apps.get_model('meeting', 'Session')
SchedulingEvent = apps.get_model('meeting', 'SchedulingEvent')
SessionStatusName = apps.get_model('name', 'SessionStatusName')
Person = apps.get_model('person', 'Person')
excludes = ['txauth','dispatch','add','raw','masque','wpack','drip','gendispatch','privacypass', 'ript', 'secdispatch', 'webtrans']
canceled = SessionStatusName.objects.get(slug='canceled')
person = Person.objects.get(name='Ryan Cross')
sessions = Session.objects.filter(meeting__number=107,group__type__in=['wg','rg','ag']).exclude(group__acronym__in=excludes)
for session in sessions:
SchedulingEvent.objects.create(
session = session,
status = canceled,
by = person)
def reverse(apps, schema_editor):
SchedulingEvent = apps.get_model('meeting', 'SchedulingEvent')
Person = apps.get_model('person', 'Person')
person = Person.objects.get(name='Ryan Cross')
SchedulingEvent.objects.filter(meeting__number=107, by=person).delete()
class Migration(migrations.Migration):
dependencies = [
('meeting', '0025_rename_type_session_to_regular'),
]
operations = [
migrations.RunPython(cancel_sessions, reverse),
]

View file

@ -400,6 +400,9 @@ class Room(models.Model):
def video_stream_url(self): def video_stream_url(self):
urlresource = self.urlresource_set.filter(name_id__in=['meetecho', ]).first() urlresource = self.urlresource_set.filter(name_id__in=['meetecho', ]).first()
return urlresource.url if urlresource else None return urlresource.url if urlresource else None
def webex_url(self):
urlresource = self.urlresource_set.filter(name_id__in=['webex', ]).first()
return urlresource.url if urlresource else None
# #
class Meta: class Meta:
ordering = ["-id"] ordering = ["-id"]
@ -781,13 +784,15 @@ class SchedTimeSessAssignment(models.Model):
"""Return sensible id string for session, e.g. suitable for use as HTML anchor.""" """Return sensible id string for session, e.g. suitable for use as HTML anchor."""
components = [] components = []
components.append(self.schedule.meeting.number)
if not self.timeslot: if not self.timeslot:
components.append("unknown") components.append("unknown")
if not self.session or not (getattr(self.session, "historic_group") or self.session.group): if not self.session or not (getattr(self.session, "historic_group") or self.session.group):
components.append("unknown") components.append("unknown")
else: else:
components.append(self.timeslot.time.strftime("%a-%H%M")) components.append(self.timeslot.time.strftime("%Y-%m-%d-%a-%H%M"))
g = getattr(self.session, "historic_group", None) or self.session.group g = getattr(self.session, "historic_group", None) or self.session.group

View file

@ -4,6 +4,7 @@
import datetime import datetime
import io import io
import json
import os import os
import random import random
import re import re
@ -19,7 +20,7 @@ from urllib.parse import urlparse
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import Client from django.test import Client, override_settings
from django.db.models import F from django.db.models import F
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -35,11 +36,10 @@ from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignm
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
from ietf.meeting.utils import finalize, condition_slide_order from ietf.meeting.utils import finalize, condition_slide_order
from ietf.meeting.utils import add_event_info_to_session_qs from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import current_session_status
from ietf.meeting.views import session_draft_list from ietf.meeting.views import session_draft_list
from ietf.name.models import SessionStatusName, ImportantDateName from ietf.name.models import SessionStatusName, ImportantDateName
from ietf.utils.decorators import skip_coverage from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox from ietf.utils.mail import outbox, empty_outbox, get_payload
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
from ietf.utils.text import xslugify from ietf.utils.text import xslugify
@ -433,9 +433,10 @@ class MeetingTests(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertContains(response, 'test acknowledgements') self.assertContains(response, 'test acknowledgements')
@patch('urllib.request.urlopen') @patch('ietf.meeting.utils.requests.get')
def test_proceedings_attendees(self, mock_urlopen): def test_proceedings_attendees(self, mockobj):
mock_urlopen.return_value = BytesIO(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]') mockobj.return_value.text = b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]'
mockobj.return_value.json = lambda: json.loads(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]')
make_meeting_test_data() make_meeting_test_data()
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96") meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96")
finalize(meeting) finalize(meeting)
@ -602,6 +603,14 @@ class MeetingTests(TestCase):
self.assertEqual(response.status_code,200) self.assertEqual(response.status_code,200)
self.assertEqual(response.get('Content-Type'), 'text/calendar') self.assertEqual(response.get('Content-Type'), 'text/calendar')
def test_cancelled_ics(self):
session=SessionFactory(meeting__type_id='ietf',status_id='canceled')
url = urlreverse('ietf.meeting.views.ical_agenda', kwargs=dict(num=session.meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertIn('STATUS:CANCELLED',unicontent(r))
self.assertNotIn('STATUS:CONFIRMED',unicontent(r))
class ReorderSlidesTests(TestCase): class ReorderSlidesTests(TestCase):
def test_add_slides_to_session(self): def test_add_slides_to_session(self):
@ -1081,6 +1090,24 @@ class SessionDetailsTests(TestCase):
r = self.client.get(url) r = self.client.get(url)
self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')])) self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')]))
self.assertNotContains(r, 'deleted') self.assertNotContains(r, 'deleted')
def test_session_details_past_interim(self):
group = GroupFactory.create(type_id='wg',state_id='active')
chair = RoleFactory(name_id='chair',group=group)
session = SessionFactory.create(meeting__type_id='interim',group=group, meeting__date=datetime.date.today()-datetime.timedelta(days=90))
SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None)
SessionPresentationFactory.create(session=session,document__type_id='minutes')
SessionPresentationFactory.create(session=session,document__type_id='slides')
SessionPresentationFactory.create(session=session,document__type_id='agenda')
url = urlreverse('ietf.meeting.views.session_details', kwargs=dict(num=session.meeting.number, acronym=group.acronym))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn('The materials upload cutoff date for this session has passed', unicontent(r))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.client.login(username=chair.person.user.username,password=chair.person.user.username+'+password')
self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')]))
def test_add_session_drafts(self): def test_add_session_drafts(self):
group = GroupFactory.create(type_id='wg',state_id='active') group = GroupFactory.create(type_id='wg',state_id='active')
@ -1248,6 +1275,8 @@ class InterimTests(TestCase):
def test_interim_send_announcement(self): def test_interim_send_announcement(self):
make_meeting_test_data() make_meeting_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
meeting.time_zone = 'America/Los_Angeles'
meeting.save()
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number}) url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url) login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url) r = self.client.get(url)
@ -1259,6 +1288,8 @@ class InterimTests(TestCase):
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce')) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
self.assertEqual(len(outbox), len_before + 1) self.assertEqual(len(outbox), len_before + 1)
self.assertIn('WG Virtual Meeting', outbox[-1]['Subject']) self.assertIn('WG Virtual Meeting', outbox[-1]['Subject'])
self.assertIn('09:00 to 09:20 America/Los_Angeles', get_payload(outbox[-1]))
self.assertIn('(17:00 to 17:20 UTC)', get_payload(outbox[-1]))
def test_interim_approve_by_ad(self): def test_interim_approve_by_ad(self):
make_meeting_test_data() make_meeting_test_data()
@ -1287,13 +1318,14 @@ class InterimTests(TestCase):
today = datetime.date.today() today = datetime.date.today()
last_week = today - datetime.timedelta(days=7) last_week = today - datetime.timedelta(days=7)
ietf = SessionFactory(meeting__type_id='ietf',meeting__date=last_week,group__state_id='active',group__parent=GroupFactory(state_id='active')) ietf = SessionFactory(meeting__type_id='ietf',meeting__date=last_week,group__state_id='active',group__parent=GroupFactory(state_id='active'))
interim = SessionFactory(meeting__type_id='interim',meeting__date=last_week,status_id='canceled',group__state_id='active',group__parent=GroupFactory(state_id='active')) SessionFactory(meeting__type_id='interim',meeting__date=last_week,status_id='canceled',group__state_id='active',group__parent=GroupFactory(state_id='active'))
url = urlreverse('ietf.meeting.views.past') url = urlreverse('ietf.meeting.views.past')
r = self.client.get(url) r = self.client.get(url)
self.assertContains(r, 'IETF - %02d'%int(ietf.meeting.number)) self.assertContains(r, 'IETF - %02d'%int(ietf.meeting.number))
q = PyQuery(r.content) q = PyQuery(r.content)
id="-%s" % interim.group.acronym #id="-%s" % interim.group.acronym
self.assertIn('CANCELLED', q('[id*="'+id+'"]').text()) #self.assertIn('CANCELLED', q('[id*="'+id+'"]').text())
self.assertIn('CANCELLED', q('tr>td>a>span').text())
def test_upcoming(self): def test_upcoming(self):
make_meeting_test_data() make_meeting_test_data()
@ -1305,10 +1337,11 @@ class InterimTests(TestCase):
r = self.client.get(url) r = self.client.get(url)
self.assertContains(r, mars_interim.number) self.assertContains(r, mars_interim.number)
self.assertContains(r, ames_interim.number) self.assertContains(r, ames_interim.number)
self.assertContains(r, 'IETF - 72') self.assertContains(r, 'IETF 72')
# cancelled session # cancelled session
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertIn('CANCELLED', q('[id*="-ames"]').text()) # self.assertIn('CANCELLED', q('[id*="-ames"]').text())
self.assertIn('CANCELLED', q('tr>td>a>span').text())
self.check_interim_tabs(url) self.check_interim_tabs(url)
def test_upcoming_ical(self): def test_upcoming_ical(self):
@ -1362,7 +1395,7 @@ class InterimTests(TestCase):
r = self.client.get("/meeting/interim/request/") r = self.client.get("/meeting/interim/request/")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed')).count(), self.assertEqual(Group.objects.filter(type__in=('wg', 'rg', 'ag'), state__in=('active', 'proposed')).count(),
len(q("#id_group option")) - 1) # -1 for options placeholder len(q("#id_group option")) - 1) # -1 for options placeholder
self.client.logout() self.client.logout()
@ -1385,7 +1418,7 @@ class InterimTests(TestCase):
count = person.role_set.filter(name='chair',group__type__in=('wg', 'rg'), group__state__in=('active', 'proposed')).count() count = person.role_set.filter(name='chair',group__type__in=('wg', 'rg'), group__state__in=('active', 'proposed')).count()
self.assertEqual(count, len(q("#id_group option")) - 1) # -1 for options placeholder self.assertEqual(count, len(q("#id_group option")) - 1) # -1 for options placeholder
def test_interim_request_single_virtual(self): def do_interim_request_single_virtual(self):
make_meeting_test_data() make_meeting_test_data()
group = Group.objects.get(acronym='mars') group = Group.objects.get(acronym='mars')
date = datetime.date.today() + datetime.timedelta(days=30) date = datetime.date.today() + datetime.timedelta(days=30)
@ -1427,7 +1460,6 @@ class InterimTests(TestCase):
session = meeting.session_set.first() session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions) self.assertEqual(session.remote_instructions,remote_instructions)
self.assertEqual(session.agenda_note,agenda_note) self.assertEqual(session.agenda_note,agenda_note)
self.assertEqual(current_session_status(session).slug,'scheda')
timeslot = session.official_timeslotassignment().timeslot timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt) self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration) self.assertEqual(timeslot.duration,duration)
@ -1438,8 +1470,22 @@ class InterimTests(TestCase):
self.assertTrue(os.path.exists(path)) self.assertTrue(os.path.exists(path))
# check notice to secretariat # check notice to secretariat
self.assertEqual(len(outbox), length_before + 1) self.assertEqual(len(outbox), length_before + 1)
self.assertIn('interim meeting ready for announcement', outbox[-1]['Subject']) return meeting
@override_settings(VIRTUAL_INTERIMS_REQUIRE_APPROVAL = True)
def test_interim_request_single_virtual_settings_approval_required(self):
meeting = self.do_interim_request_single_virtual()
self.assertEqual(meeting.session_set.last().schedulingevent_set.last().status_id,'apprw')
self.assertIn('New Interim Meeting Request', outbox[-1]['Subject'])
self.assertIn('session-request@ietf.org', outbox[-1]['To'])
self.assertIn('aread@example.org', outbox[-1]['Cc'])
@override_settings(VIRTUAL_INTERIMS_REQUIRE_APPROVAL = False)
def test_interim_request_single_virtual_settings_approval_not_required(self):
meeting = self.do_interim_request_single_virtual()
self.assertEqual(meeting.session_set.last().schedulingevent_set.last().status_id,'scheda')
self.assertIn('iesg-secretary@ietf.org', outbox[-1]['To']) self.assertIn('iesg-secretary@ietf.org', outbox[-1]['To'])
self.assertIn('interim meeting ready for announcement', outbox[-1]['Subject'])
def test_interim_request_single_in_person(self): def test_interim_request_single_in_person(self):
make_meeting_test_data() make_meeting_test_data()
@ -1700,9 +1746,12 @@ class InterimTests(TestCase):
# related AD # related AD
user = User.objects.get(username='ad') user = User.objects.get(username='ad')
self.assertTrue(can_approve_interim_request(meeting=meeting,user=user)) self.assertTrue(can_approve_interim_request(meeting=meeting,user=user))
# other AD # AD from other area
user = User.objects.get(username='ops-ad') user = User.objects.get(username='ops-ad')
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user)) self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
# AD from other area assigned as the WG AD anyhow (cross-area AD)
user = RoleFactory(name_id='ad',group=group).person.user
self.assertTrue(can_approve_interim_request(meeting=meeting,user=user))
# WG Chair # WG Chair
user = User.objects.get(username='marschairman') user = User.objects.get(username='marschairman')
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user)) self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
@ -1836,7 +1885,7 @@ class InterimTests(TestCase):
'session_set-0-id':meeting.session_set.first().id, 'session_set-0-id':meeting.session_set.first().id,
'session_set-0-date':formset_initial['date'].strftime('%Y-%m-%d'), 'session_set-0-date':formset_initial['date'].strftime('%Y-%m-%d'),
'session_set-0-time':new_time.strftime('%H:%M'), 'session_set-0-time':new_time.strftime('%H:%M'),
'session_set-0-requested_duration':formset_initial['requested_duration'], 'session_set-0-requested_duration': '00:30',
'session_set-0-remote_instructions':formset_initial['remote_instructions'], 'session_set-0-remote_instructions':formset_initial['remote_instructions'],
#'session_set-0-agenda':formset_initial['agenda'], #'session_set-0-agenda':formset_initial['agenda'],
'session_set-0-agenda_note':formset_initial['agenda_note'], 'session_set-0-agenda_note':formset_initial['agenda_note'],
@ -2022,6 +2071,20 @@ class IphoneAppJsonTests(TestCase):
def tearDown(self): def tearDown(self):
pass pass
def test_iphone_app_json_interim(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type_id='interim').order_by('id').last()
url = urlreverse('ietf.meeting.views.json_agenda',kwargs={'num':meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code,200)
data = r.json()
self.assertIn(meeting.number, data.keys())
jsessions = [ s for s in data[meeting.number] if s['objtype'] == 'session' ]
msessions = meeting.session_set.exclude(type__in=['lead','offagenda','break','reg'])
self.assertEqual(len(jsessions), msessions.count())
for s in jsessions:
self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists())
def test_iphone_app_json(self): def test_iphone_app_json(self):
make_meeting_test_data() make_meeting_test_data()
meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last() meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last()
@ -2036,6 +2099,13 @@ class IphoneAppJsonTests(TestCase):
url = urlreverse('ietf.meeting.views.json_agenda',kwargs={'num':meeting.number}) url = urlreverse('ietf.meeting.views.json_agenda',kwargs={'num':meeting.number})
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code,200) self.assertEqual(r.status_code,200)
data = r.json()
self.assertIn(meeting.number, data.keys())
jsessions = [ s for s in data[meeting.number] if s['objtype'] == 'session' ]
msessions = meeting.session_set.exclude(type__in=['lead','offagenda','break','reg'])
self.assertEqual(len(jsessions), msessions.count())
for s in jsessions:
self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists())
class FinalizeProceedingsTests(TestCase): class FinalizeProceedingsTests(TestCase):
@patch('urllib.request.urlopen') @patch('urllib.request.urlopen')

View file

@ -66,9 +66,10 @@ type_ietf_only_patterns = [
# This is a limited subset of the list above -- many of the views above won't work for interim meetings # This is a limited subset of the list above -- many of the views above won't work for interim meetings
type_interim_patterns = [ type_interim_patterns = [
url(r'^agenda/(?P<session>[A-Za-z0-9-]+)-drafts.pdf$', views.session_draft_pdf), url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.pdf$', views.session_draft_pdf),
url(r'^agenda/(?P<session>[A-Za-z0-9-]+)-drafts.tgz$', views.session_draft_tarfile), url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.tgz$', views.session_draft_tarfile),
url(r'^materials/%(document)s((?P<ext>\.[a-z0-9]+)|/)?$' % settings.URL_REGEXPS, views.materials_document), url(r'^materials/%(document)s((?P<ext>\.[a-z0-9]+)|/)?$' % settings.URL_REGEXPS, views.materials_document),
url(r'^agenda.json$', views.json_agenda)
] ]
type_ietf_only_patterns_id_optional = [ type_ietf_only_patterns_id_optional = [

View file

@ -3,8 +3,7 @@
import datetime import datetime
import json import requests
import urllib.request
from urllib.error import HTTPError from urllib.error import HTTPError
from django.conf import settings from django.conf import settings
@ -113,7 +112,7 @@ def create_proceedings_templates(meeting):
# Get meeting attendees from registration system # Get meeting attendees from registration system
url = settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number) url = settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number)
try: try:
attendees = json.load(urllib.request.urlopen(url)) attendees = requests.get(url).json()
except (ValueError, HTTPError): except (ValueError, HTTPError):
attendees = [] attendees = []
@ -240,6 +239,26 @@ def only_sessions_that_can_meet(session_qs):
return qs return qs
# Keeping this as a note that might help when returning Customization to the /meetings/upcoming page
#def group_parents_from_sessions(sessions):
# group_parents = list()
# parents = {}
# for s in sessions:
# if s.group.parent_id not in parents:
# parent = s.group.parent
# parent.group_list = set()
# group_parents.append(parent)
# parents[s.group.parent_id] = parent
# parent.group_list.add(s.group)
#
# for p in parents.values():
# p.group_list = list(p.group_list)
# p.group_list.sort(key=lambda g: g.acronym)
#
# return group_parents
def data_for_meetings_overview(meetings, interim_status=None): def data_for_meetings_overview(meetings, interim_status=None):
"""Return filtered meetings with sessions and group hierarchy (for the """Return filtered meetings with sessions and group hierarchy (for the
interim menu).""" interim menu)."""
@ -275,30 +294,12 @@ def data_for_meetings_overview(meetings, interim_status=None):
if not m.type_id == 'interim' or not all(s.current_status in ['apprw', 'scheda', 'canceledpa'] for s in m.sessions) if not m.type_id == 'interim' or not all(s.current_status in ['apprw', 'scheda', 'canceledpa'] for s in m.sessions)
] ]
# group hierarchy
ietf_group = Group.objects.get(acronym='ietf') ietf_group = Group.objects.get(acronym='ietf')
group_hierarchy = [ietf_group]
parents = {}
for m in meetings:
if m.type_id == 'interim' and m.sessions:
for s in m.sessions:
parent = parents.get(s.group.parent_id)
if not parent:
parent = s.group.parent
parent.group_list = []
group_hierarchy.append(parent)
parent.group_list.append(s.group)
for p in parents.values():
p.group_list.sort(key=lambda g: g.acronym)
# set some useful attributes # set some useful attributes
for m in meetings: for m in meetings:
m.end = m.date + datetime.timedelta(days=m.days) m.end = m.date + datetime.timedelta(days=m.days)
m.responsible_group = (m.sessions[0].group if m.sessions else None) if m.type_id == 'interim' else ietf_group m.responsible_group = (m.sessions[0].group if m.sessions else None) if m.type_id == 'interim' else ietf_group
m.interim_meeting_cancelled = m.type_id == 'interim' and all(s.current_status == 'canceled' for s in m.sessions) m.interim_meeting_cancelled = m.type_id == 'interim' and all(s.current_status == 'canceled' for s in m.sessions)
return meetings, group_hierarchy return meetings

View file

@ -949,7 +949,7 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
for a in assignments: for a in assignments:
if a.session: if a.session:
a.session.ical_status = ical_session_status(a.session) a.session.ical_status = ical_session_status(a.session.current_status)
return render(request, "meeting/agenda.ics", { return render(request, "meeting/agenda.ics", {
"schedule": schedule, "schedule": schedule,
@ -959,7 +959,7 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
@cache_page(15 * 60) @cache_page(15 * 60)
def json_agenda(request, num=None ): def json_agenda(request, num=None ):
meeting = get_meeting(num) meeting = get_meeting(num, type_in=['ietf','interim'])
sessions = [] sessions = []
locations = set() locations = set()
@ -1156,6 +1156,7 @@ def session_details(request, num, acronym):
# we somewhat arbitrarily use the group of the last session we get from # we somewhat arbitrarily use the group of the last session we get from
# get_sessions() above when checking can_manage_session_materials() # get_sessions() above when checking can_manage_session_materials()
can_manage = can_manage_session_materials(request.user, session.group, session) can_manage = can_manage_session_materials(request.user, session.group, session)
can_view_request = can_view_interim_request(meeting, request.user)
scheduled_sessions = [s for s in sessions if s.current_status == 'sched'] scheduled_sessions = [s for s in sessions if s.current_status == 'sched']
unscheduled_sessions = [s for s in sessions if s.current_status != 'sched'] unscheduled_sessions = [s for s in sessions if s.current_status != 'sched']
@ -1173,7 +1174,9 @@ def session_details(request, num, acronym):
'pending_suggestions' : pending_suggestions, 'pending_suggestions' : pending_suggestions,
'meeting' :meeting , 'meeting' :meeting ,
'acronym' :acronym, 'acronym' :acronym,
'is_materials_manager' : session.group.has_role(request.user, session.group.features.matman_roles),
'can_manage_materials' : can_manage, 'can_manage_materials' : can_manage,
'can_view_request': can_view_request,
'thisweek': datetime.date.today()-datetime.timedelta(days=7), 'thisweek': datetime.date.today()-datetime.timedelta(days=7),
}) })
@ -1916,7 +1919,7 @@ def ajax_get_utc(request):
@role_required('Secretariat',) @role_required('Secretariat',)
def interim_announce(request): def interim_announce(request):
'''View which shows interim meeting requests awaiting announcement''' '''View which shows interim meeting requests awaiting announcement'''
meetings, _ = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='scheda') meetings = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='scheda')
menu_entries = get_interim_menu_entries(request) menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'announce' selected_menu_entry = 'announce'
@ -1979,7 +1982,7 @@ def interim_skip_announcement(request, number):
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair') @role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair')
def interim_pending(request): def interim_pending(request):
'''View which shows interim meeting requests pending approval''' '''View which shows interim meeting requests pending approval'''
meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='apprw') meetings = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='apprw')
menu_entries = get_interim_menu_entries(request) menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'pending' selected_menu_entry = 'pending'
@ -2014,6 +2017,8 @@ def interim_request(request):
is_virtual = form.is_virtual() is_virtual = form.is_virtual()
meeting_type = form.cleaned_data.get('meeting_type') meeting_type = form.cleaned_data.get('meeting_type')
requires_approval = not ( is_approved or ( is_virtual and not settings.VIRTUAL_INTERIMS_REQUIRE_APPROVAL ))
# pre create meeting # pre create meeting
if meeting_type in ('single', 'multi-day'): if meeting_type in ('single', 'multi-day'):
meeting = form.save(date=get_earliest_session_date(formset)) meeting = form.save(date=get_earliest_session_date(formset))
@ -2023,13 +2028,13 @@ def interim_request(request):
InterimSessionModelForm.__init__, InterimSessionModelForm.__init__,
user=request.user, user=request.user,
group=group, group=group,
is_approved_or_virtual=(is_approved or is_virtual)) requires_approval=requires_approval)
formset = SessionFormset(instance=meeting, data=request.POST) formset = SessionFormset(instance=meeting, data=request.POST)
formset.is_valid() formset.is_valid()
formset.save() formset.save()
sessions_post_save(request, formset) sessions_post_save(request, formset)
if not (is_approved or is_virtual): if requires_approval:
send_interim_approval_request(meetings=[meeting]) send_interim_approval_request(meetings=[meeting])
elif not has_role(request.user, 'Secretariat'): elif not has_role(request.user, 'Secretariat'):
send_interim_announcement_request(meeting=meeting) send_interim_announcement_request(meeting=meeting)
@ -2043,7 +2048,7 @@ def interim_request(request):
InterimSessionModelForm.__init__, InterimSessionModelForm.__init__,
user=request.user, user=request.user,
group=group, group=group,
is_approved_or_virtual=(is_approved or is_virtual)) requires_approval=requires_approval)
formset = SessionFormset(instance=Meeting(), data=request.POST) formset = SessionFormset(instance=Meeting(), data=request.POST)
formset.is_valid() # re-validate formset.is_valid() # re-validate
for session_form in formset.forms: for session_form in formset.forms:
@ -2060,7 +2065,7 @@ def interim_request(request):
series.append(meeting) series.append(meeting)
sessions_post_save(request, [session_form]) sessions_post_save(request, [session_form])
if not (is_approved or is_virtual): if requires_approval:
send_interim_approval_request(meetings=series) send_interim_approval_request(meetings=series)
elif not has_role(request.user, 'Secretariat'): elif not has_role(request.user, 'Secretariat'):
send_interim_announcement_request(meeting=meeting) send_interim_announcement_request(meeting=meeting)
@ -2188,7 +2193,7 @@ def interim_request_edit(request, number):
InterimSessionModelForm.__init__, InterimSessionModelForm.__init__,
user=request.user, user=request.user,
group=group, group=group,
is_approved_or_virtual=is_approved) requires_approval= not is_approved)
formset = SessionFormset(instance=meeting, data=request.POST) formset = SessionFormset(instance=meeting, data=request.POST)
@ -2219,17 +2224,33 @@ def past(request):
'''List of past meetings''' '''List of past meetings'''
today = datetime.datetime.today() today = datetime.datetime.today()
meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(date__lte=today).order_by('-date')) meetings = data_for_meetings_overview(Meeting.objects.filter(date__lte=today).order_by('-date'))
return render(request, 'meeting/past.html', { return render(request, 'meeting/past.html', {
'meetings': meetings, 'meetings': meetings,
'group_parents': group_parents}) })
def upcoming(request): def upcoming(request):
'''List of upcoming meetings''' '''List of upcoming meetings'''
today = datetime.datetime.today() today = datetime.date.today()
meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(date__gte=today).order_by('date')) # Get ietf meetings starting 7 days ago, and interim meetings starting today
ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7))
for m in ietf_meetings:
m.end = m.date+datetime.timedelta(days=m.days)
interim_sessions = add_event_info_to_session_qs(
Session.objects.filter(
meeting__type_id='interim',
timeslotassignments__schedule=F('meeting__schedule'),
timeslotassignments__timeslot__time__gte=today
)
).filter(current_status__in=('sched','canceled'))
for session in interim_sessions:
session.historic_group = session.group
entries = list(ietf_meetings)
entries.extend(list(interim_sessions))
entries.sort(key = lambda o: pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())) if isinstance(o,Meeting) else o.official_timeslotassignment().timeslot.utc_start_time())
# add menu entries # add menu entries
menu_entries = get_interim_menu_entries(request) menu_entries = get_interim_menu_entries(request)
@ -2244,23 +2265,25 @@ def upcoming(request):
reverse('ietf.meeting.views.upcoming_ical'))) reverse('ietf.meeting.views.upcoming_ical')))
return render(request, 'meeting/upcoming.html', { return render(request, 'meeting/upcoming.html', {
'meetings': meetings, 'entries': entries,
'menu_actions': actions, 'menu_actions': actions,
'menu_entries': menu_entries, 'menu_entries': menu_entries,
'selected_menu_entry': selected_menu_entry, 'selected_menu_entry': selected_menu_entry,
'group_parents': group_parents}) })
def upcoming_ical(request): def upcoming_ical(request):
'''Return Upcoming meetings in iCalendar file''' '''Return Upcoming meetings in iCalendar file'''
filters = request.GET.getlist('filters') filters = request.GET.getlist('filters')
today = datetime.datetime.today() today = datetime.date.today()
meetings, _ = data_for_meetings_overview(Meeting.objects.filter(date__gte=today).order_by('date')) # get meetings starting 7 days ago -- we'll filter out sessions in the past further down
meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).order_by('date'))
assignments = list(SchedTimeSessAssignment.objects.filter( assignments = list(SchedTimeSessAssignment.objects.filter(
schedule__meeting__schedule=F('schedule'), schedule__meeting__schedule=F('schedule'),
session__in=[s.pk for m in meetings for s in m.sessions] session__in=[s.pk for m in meetings for s in m.sessions],
timeslot__time__gte=today,
).order_by( ).order_by(
'schedule__meeting__date', 'session__type', 'timeslot__time' 'schedule__meeting__date', 'session__type', 'timeslot__time'
).select_related( ).select_related(

View file

@ -5,7 +5,7 @@ from ietf.message.models import Message, MessageAttachment, SendQueue, Announcem
class MessageAdmin(admin.ModelAdmin): class MessageAdmin(admin.ModelAdmin):
list_display = ["subject", "by", "time", "groups"] list_display = ["subject", "by", "time", "groups"]
search_fields = ["subject", "body"] search_fields = ["subject", "body"]
raw_id_fields = ["by"] raw_id_fields = ["by", "related_groups", "related_docs"]
ordering = ["-time"] ordering = ["-time"]
def groups(self, instance): def groups(self, instance):

View file

@ -2292,6 +2292,19 @@
"model": "doc.state", "model": "doc.state",
"pk": 156 "pk": 156
}, },
{
"fields": {
"desc": "This charter's group was replaced.",
"name": "Replaced",
"next_states": [],
"order": 0,
"slug": "replaced",
"type": "charter",
"used": true
},
"model": "doc.state",
"pk": 157
},
{ {
"fields": { "fields": {
"label": "State" "label": "State"
@ -9681,7 +9694,7 @@
{ {
"fields": { "fields": {
"desc": "Formation of the group (most likely a BoF or Proposed WG) was abandoned", "desc": "Formation of the group (most likely a BoF or Proposed WG) was abandoned",
"name": "Abandonded", "name": "Abandoned",
"order": 0, "order": 0,
"used": true "used": true
}, },
@ -9750,7 +9763,7 @@
}, },
{ {
"fields": { "fields": {
"desc": "Replaced by dnssd", "desc": "Replaced by a group with a different acronym",
"name": "Replaced", "name": "Replaced",
"order": 0, "order": 0,
"used": true "used": true
@ -11448,6 +11461,16 @@
"model": "name.roomresourcename", "model": "name.roomresourcename",
"pk": "u-shape" "pk": "u-shape"
}, },
{
"fields": {
"desc": "WebEx support",
"name": "WebEx session",
"order": 0,
"used": true
},
"model": "name.roomresourcename",
"pk": "webex"
},
{ {
"fields": { "fields": {
"desc": "", "desc": "",
@ -14226,7 +14249,7 @@
"fields": { "fields": {
"command": "xym", "command": "xym",
"switch": "--version", "switch": "--version",
"time": "2020-01-16T00:12:27.984", "time": "2020-02-19T00:13:43.554",
"used": true, "used": true,
"version": "xym 0.4" "version": "xym 0.4"
}, },
@ -14237,7 +14260,7 @@
"fields": { "fields": {
"command": "pyang", "command": "pyang",
"switch": "--version", "switch": "--version",
"time": "2020-01-16T00:12:29.007", "time": "2020-02-19T00:13:44.450",
"used": true, "used": true,
"version": "pyang 2.1.1" "version": "pyang 2.1.1"
}, },
@ -14248,7 +14271,7 @@
"fields": { "fields": {
"command": "yanglint", "command": "yanglint",
"switch": "--version", "switch": "--version",
"time": "2020-01-16T00:12:29.206", "time": "2020-02-19T00:13:44.597",
"used": true, "used": true,
"version": "yanglint 0.14.80" "version": "yanglint 0.14.80"
}, },
@ -14259,9 +14282,9 @@
"fields": { "fields": {
"command": "xml2rfc", "command": "xml2rfc",
"switch": "--version", "switch": "--version",
"time": "2020-01-16T00:12:30.657", "time": "2020-02-19T00:13:45.481",
"used": true, "used": true,
"version": "xml2rfc 2.37.3" "version": "xml2rfc 2.40.0"
}, },
"model": "utils.versioninfo", "model": "utils.versioninfo",
"pk": 4 "pk": 4

View file

@ -11,7 +11,7 @@ from ietf.person.factories import PersonFactory, UserFactory
import debug # pyflakes:ignore import debug # pyflakes:ignore
cert = '''-----BEGIN CERTIFICATE----- cert = b'''-----BEGIN CERTIFICATE-----
MIIDHjCCAgagAwIBAgIJAKDCCjbQboJzMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV MIIDHjCCAgagAwIBAgIJAKDCCjbQboJzMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
BAMMCE5vbUNvbTE1MB4XDTE0MDQwNDIxMTQxNFoXDTE2MDQwMzIxMTQxNFowEzER BAMMCE5vbUNvbTE1MB4XDTE0MDQwNDIxMTQxNFoXDTE2MDQwMzIxMTQxNFowEzER
MA8GA1UEAwwITm9tQ29tMTUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB MA8GA1UEAwwITm9tQ29tMTUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
@ -32,7 +32,7 @@ toX3j+FUe2UiUak3ACXdrOPSsFP0KRrFwuMnuHHXkGj/Uw==
-----END CERTIFICATE----- -----END CERTIFICATE-----
''' '''
key = '''-----BEGIN PRIVATE KEY----- key = b'''-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2QXCsAitYSOgP MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2QXCsAitYSOgP
Yor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6McJ+MCiHECdqDlH6n Yor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6McJ+MCiHECdqDlH6n
pQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv30hAD6q9wjqK/m6vR pQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv30hAD6q9wjqK/m6vR
@ -75,7 +75,7 @@ def nomcom_kwargs_for_year(year=None, *args, **kwargs):
if 'group__acronym' not in kwargs: if 'group__acronym' not in kwargs:
kwargs['group__acronym'] = 'nomcom%d'%year kwargs['group__acronym'] = 'nomcom%d'%year
if 'group__name' not in kwargs: if 'group__name' not in kwargs:
kwargs['group__name'] = 'TEST VERSION of IAB/IESG Nominating Committee %d/%d'%(year,year+1) kwargs['group__name'] = 'TEST VERSION of NomCom %d/%d'%(year,year+1)
return kwargs return kwargs

View file

@ -16,7 +16,7 @@ from django.conf import settings
from django.core.files import File from django.core.files import File
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text from django.utils.encoding import force_text, force_str
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -50,12 +50,12 @@ def get_cert_files():
client_test_cert_files = generate_cert() client_test_cert_files = generate_cert()
return client_test_cert_files return client_test_cert_files
def build_test_public_keys_dir(obj): def setup_test_public_keys_dir(obj):
obj.saved_nomcom_public_keys_dir = settings.NOMCOM_PUBLIC_KEYS_DIR obj.saved_nomcom_public_keys_dir = settings.NOMCOM_PUBLIC_KEYS_DIR
obj.nomcom_public_keys_dir = obj.tempdir('nomcom-public-keys') obj.nomcom_public_keys_dir = obj.tempdir('nomcom-public-keys')
settings.NOMCOM_PUBLIC_KEYS_DIR = obj.nomcom_public_keys_dir settings.NOMCOM_PUBLIC_KEYS_DIR = obj.nomcom_public_keys_dir
def clean_test_public_keys_dir(obj): def teardown_test_public_keys_dir(obj):
settings.NOMCOM_PUBLIC_KEYS_DIR = obj.saved_nomcom_public_keys_dir settings.NOMCOM_PUBLIC_KEYS_DIR = obj.saved_nomcom_public_keys_dir
shutil.rmtree(obj.nomcom_public_keys_dir) shutil.rmtree(obj.nomcom_public_keys_dir)
@ -68,7 +68,7 @@ class NomcomViewsTest(TestCase):
return response return response
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
nomcom_test_data() nomcom_test_data()
self.cert_file, self.privatekey_file = get_cert_files() self.cert_file, self.privatekey_file = get_cert_files()
self.year = NOMCOM_YEAR self.year = NOMCOM_YEAR
@ -97,7 +97,7 @@ class NomcomViewsTest(TestCase):
self.public_nominate_newperson_url = reverse('ietf.nomcom.views.public_nominate_newperson', kwargs={'year': self.year}) self.public_nominate_newperson_url = reverse('ietf.nomcom.views.public_nominate_newperson', kwargs={'year': self.year})
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def access_member_url(self, url): def access_member_url(self, url):
login_testing_unauthorized(self, COMMUNITY_USER, url) login_testing_unauthorized(self, COMMUNITY_USER, url)
@ -941,12 +941,12 @@ class NomineePositionStateSaveTest(TestCase):
"""Tests for the NomineePosition save override method""" """Tests for the NomineePosition save override method"""
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
nomcom_test_data() nomcom_test_data()
self.nominee = Nominee.objects.get(email__person__user__username=COMMUNITY_USER) self.nominee = Nominee.objects.get(email__person__user__username=COMMUNITY_USER)
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def test_state_autoset(self): def test_state_autoset(self):
"""Verify state is autoset correctly""" """Verify state is autoset correctly"""
@ -976,13 +976,13 @@ class NomineePositionStateSaveTest(TestCase):
class FeedbackTest(TestCase): class FeedbackTest(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
nomcom_test_data() nomcom_test_data()
self.cert_file, self.privatekey_file = get_cert_files() self.cert_file, self.privatekey_file = get_cert_files()
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def test_encrypted_comments(self): def test_encrypted_comments(self):
@ -1009,7 +1009,7 @@ class FeedbackTest(TestCase):
class ReminderTest(TestCase): class ReminderTest(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
nomcom_test_data() nomcom_test_data()
self.nomcom = get_nomcom_by_year(NOMCOM_YEAR) self.nomcom = get_nomcom_by_year(NOMCOM_YEAR)
self.cert_file, self.privatekey_file = get_cert_files() self.cert_file, self.privatekey_file = get_cert_files()
@ -1051,7 +1051,7 @@ class ReminderTest(TestCase):
feedback.nominees.add(n) feedback.nominees.add(n)
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def test_is_time_to_send(self): def test_is_time_to_send(self):
self.nomcom.reminder_interval = 4 self.nomcom.reminder_interval = 4
@ -1107,14 +1107,14 @@ class ReminderTest(TestCase):
class InactiveNomcomTests(TestCase): class InactiveNomcomTests(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude')) self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude'))
self.plain_person = PersonFactory.create() self.plain_person = PersonFactory.create()
self.chair = self.nc.group.role_set.filter(name='chair').first().person self.chair = self.nc.group.role_set.filter(name='chair').first().person
self.member = self.nc.group.role_set.filter(name='member').first().person self.member = self.nc.group.role_set.filter(name='member').first().person
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def test_feedback_closed(self): def test_feedback_closed(self):
for view in ['ietf.nomcom.views.public_feedback', 'ietf.nomcom.views.private_feedback']: for view in ['ietf.nomcom.views.public_feedback', 'ietf.nomcom.views.private_feedback']:
@ -1301,7 +1301,7 @@ class InactiveNomcomTests(TestCase):
class FeedbackLastSeenTests(TestCase): class FeedbackLastSeenTests(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
self.nc = NomComFactory.create(**nomcom_kwargs_for_year()) self.nc = NomComFactory.create(**nomcom_kwargs_for_year())
self.author = PersonFactory.create().email_set.first().address self.author = PersonFactory.create().email_set.first().address
self.member = self.nc.group.role_set.filter(name='member').first().person self.member = self.nc.group.role_set.filter(name='member').first().person
@ -1320,7 +1320,7 @@ class FeedbackLastSeenTests(TestCase):
self.second_from_now = now + datetime.timedelta(seconds=1) self.second_from_now = now + datetime.timedelta(seconds=1)
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def test_feedback_index_badges(self): def test_feedback_index_badges(self):
url = reverse('ietf.nomcom.views.view_feedback',kwargs={'year':self.nc.year()}) url = reverse('ietf.nomcom.views.view_feedback',kwargs={'year':self.nc.year()})
@ -1407,13 +1407,13 @@ class FeedbackLastSeenTests(TestCase):
class NewActiveNomComTests(TestCase): class NewActiveNomComTests(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
self.nc = NomComFactory.create(**nomcom_kwargs_for_year()) self.nc = NomComFactory.create(**nomcom_kwargs_for_year())
self.chair = self.nc.group.role_set.filter(name='chair').first().person self.chair = self.nc.group.role_set.filter(name='chair').first().person
self.saved_days_to_expire_nomination_link = settings.DAYS_TO_EXPIRE_NOMINATION_LINK self.saved_days_to_expire_nomination_link = settings.DAYS_TO_EXPIRE_NOMINATION_LINK
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
settings.DAYS_TO_EXPIRE_NOMINATION_LINK = self.saved_days_to_expire_nomination_link settings.DAYS_TO_EXPIRE_NOMINATION_LINK = self.saved_days_to_expire_nomination_link
def test_help(self): def test_help(self):
@ -1483,7 +1483,7 @@ class NewActiveNomComTests(TestCase):
login_testing_unauthorized(self,self.chair.user.username,url) login_testing_unauthorized(self,self.chair.user.username,url)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code,200) self.assertEqual(response.status_code,200)
response = self.client.post(url,{'key':key}) response = self.client.post(url,{'key': force_str(key)})
self.assertEqual(response.status_code,302) self.assertEqual(response.status_code,302)
def test_email_pasting(self): def test_email_pasting(self):
@ -1870,13 +1870,13 @@ class NoPublicKeyTests(TestCase):
class AcceptingTests(TestCase): class AcceptingTests(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
self.nc = NomComFactory(**nomcom_kwargs_for_year()) self.nc = NomComFactory(**nomcom_kwargs_for_year())
self.plain_person = PersonFactory.create() self.plain_person = PersonFactory.create()
self.member = self.nc.group.role_set.filter(name='member').first().person self.member = self.nc.group.role_set.filter(name='member').first().person
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def test_public_accepting_nominations(self): def test_public_accepting_nominations(self):
url = reverse('ietf.nomcom.views.public_nominate',kwargs={'year':self.nc.year()}) url = reverse('ietf.nomcom.views.public_nominate',kwargs={'year':self.nc.year()})
@ -1977,12 +1977,12 @@ class AcceptingTests(TestCase):
class ShowNomineeTests(TestCase): class ShowNomineeTests(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
self.nc = NomComFactory(**nomcom_kwargs_for_year()) self.nc = NomComFactory(**nomcom_kwargs_for_year())
self.plain_person = PersonFactory.create() self.plain_person = PersonFactory.create()
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def test_feedback_pictures(self): def test_feedback_pictures(self):
url = reverse('ietf.nomcom.views.public_nominate',kwargs={'year':self.nc.year()}) url = reverse('ietf.nomcom.views.public_nominate',kwargs={'year':self.nc.year()})
@ -1998,13 +1998,13 @@ class ShowNomineeTests(TestCase):
class TopicTests(TestCase): class TopicTests(TestCase):
def setUp(self): def setUp(self):
build_test_public_keys_dir(self) setup_test_public_keys_dir(self)
self.nc = NomComFactory(**nomcom_kwargs_for_year(populate_topics=False)) self.nc = NomComFactory(**nomcom_kwargs_for_year(populate_topics=False))
self.plain_person = PersonFactory.create() self.plain_person = PersonFactory.create()
self.chair = self.nc.group.role_set.filter(name='chair').first().person self.chair = self.nc.group.role_set.filter(name='chair').first().person
def tearDown(self): def tearDown(self):
clean_test_public_keys_dir(self) teardown_test_public_keys_dir(self)
def testAddEditListRemoveTopic(self): def testAddEditListRemoveTopic(self):
self.assertFalse(self.nc.topic_set.exists()) self.assertFalse(self.nc.topic_set.exists())

View file

@ -166,7 +166,7 @@ def retrieve_nomcom_private_key(request, year):
command = "%s bf -d -in /dev/stdin -k \"%s\" -a" command = "%s bf -d -in /dev/stdin -k \"%s\" -a"
code, out, error = pipe(command % (settings.OPENSSL_COMMAND, code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
settings.SECRET_KEY), private_key.encode('utf-8')) settings.SECRET_KEY), private_key)
if code != 0: if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error)) log("openssl error: %s:\n Error %s: %s" %(command, code, error))
return out return out
@ -178,7 +178,7 @@ def store_nomcom_private_key(request, year, private_key):
else: else:
command = "%s bf -e -in /dev/stdin -k \"%s\" -a" command = "%s bf -e -in /dev/stdin -k \"%s\" -a"
code, out, error = pipe(command % (settings.OPENSSL_COMMAND, code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
settings.SECRET_KEY), private_key.encode('utf-8')) settings.SECRET_KEY), private_key)
if code != 0: if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error)) log("openssl error: %s:\n Error %s: %s" %(command, code, error))
if error: if error:

View file

@ -7,15 +7,16 @@ import re
from collections import OrderedDict, Counter from collections import OrderedDict, Counter
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib import messages
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.urls import reverse from django.forms.models import modelformset_factory, inlineformset_factory
from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.forms.models import modelformset_factory, inlineformset_factory from django.urls import reverse
from django.utils.encoding import force_bytes
from ietf.dbtemplate.models import DBTemplate from ietf.dbtemplate.models import DBTemplate
@ -117,7 +118,7 @@ def private_key(request, year):
if request.method == 'POST': if request.method == 'POST':
form = PrivateKeyForm(data=request.POST) form = PrivateKeyForm(data=request.POST)
if form.is_valid(): if form.is_valid():
store_nomcom_private_key(request, year, form.cleaned_data.get('key', '')) store_nomcom_private_key(request, year, force_bytes(form.cleaned_data.get('key', '')))
return HttpResponseRedirect(back_url) return HttpResponseRedirect(back_url)
else: else:
form = PrivateKeyForm() form = PrivateKeyForm()

View file

@ -22,4 +22,22 @@ def person_by_name(name):
return None return None
alias = Alias.objects.filter(name=name).first() alias = Alias.objects.filter(name=name).first()
return alias.person if alias else None return alias.person if alias else None
@register.inclusion_tag('person/person_link.html')
def person_link(person, **kwargs):
title = kwargs.get('title', '')
cls = kwargs.get('class', '')
name = person.name
plain_name = person.plain_name()
email = person.email_address()
return {'name': name, 'plain_name': plain_name, 'email': email, 'title': title, 'class': cls}
@register.inclusion_tag('person/person_link.html')
def email_person_link(email, **kwargs):
title = kwargs.get('title', '')
cls = kwargs.get('class', '')
name = email.person.name
plain_name = email.person.plain_name()
email = email.address
return {'name': name, 'plain_name': plain_name, 'email': email, 'title': title, 'class': cls}

View file

@ -69,7 +69,7 @@ class ReviewAssignmentAdmin(simple_history.admin.SimpleHistoryAdmin):
list_display = ["review_request", "reviewer", "assigned_on", "result"] list_display = ["review_request", "reviewer", "assigned_on", "result"]
list_filter = ["result", "state"] list_filter = ["result", "state"]
ordering = ["-id"] ordering = ["-id"]
raw_id_fields = ["reviewer", "result", "review"] raw_id_fields = ["review_request", "reviewer", "result", "review"]
search_fields = ["review_request__doc__name"] search_fields = ["review_request__doc__name"]
admin.site.register(ReviewAssignment, ReviewAssignmentAdmin) admin.site.register(ReviewAssignment, ReviewAssignmentAdmin)

View file

@ -5,14 +5,14 @@
# various utilities for working with the mailarch mail archive at # various utilities for working with the mailarch mail archive at
# mailarchive.ietf.org # mailarchive.ietf.org
import base64
import contextlib import contextlib
import datetime import datetime
import tarfile
import mailbox
import tempfile
import hashlib
import base64
import email.utils import email.utils
import hashlib
import mailbox
import tarfile
import tempfile
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.request import urlopen from urllib.request import urlopen
@ -22,7 +22,7 @@ import debug # pyflakes:ignore
from pyquery import PyQuery from pyquery import PyQuery
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes, force_str
def list_name_from_email(list_email): def list_name_from_email(list_email):
if not list_email.endswith("@ietf.org"): if not list_email.endswith("@ietf.org"):
@ -37,7 +37,7 @@ def hash_list_message_id(list_name, msgid):
# and rightmost "=" signs are (optionally) stripped # and rightmost "=" signs are (optionally) stripped
sha = hashlib.sha1(force_bytes(msgid)) sha = hashlib.sha1(force_bytes(msgid))
sha.update(force_bytes(list_name)) sha.update(force_bytes(list_name))
return base64.urlsafe_b64encode(sha.digest()).rstrip(b"=") return force_str(base64.urlsafe_b64encode(sha.digest()).rstrip(b"="))
def construct_query_urls(doc, team, query=None): def construct_query_urls(doc, team, query=None):
list_name = list_name_from_email(team.list_email) list_name = list_name_from_email(team.list_email)
@ -94,8 +94,8 @@ def retrieve_messages_from_mbox(mbox_fileobj):
"splitfrom": email.utils.parseaddr(msg["From"]), "splitfrom": email.utils.parseaddr(msg["From"]),
"subject": msg["Subject"], "subject": msg["Subject"],
"content": content.replace("\r\n", "\n").replace("\r", "\n").strip("\n"), "content": content.replace("\r\n", "\n").replace("\r", "\n").strip("\n"),
"message_id": email.utils.unquote(msg["Message-ID"]), "message_id": email.utils.unquote(msg["Message-ID"].strip()),
"url": email.utils.unquote(msg["Archived-At"]), "url": email.utils.unquote(msg["Archived-At"].strip()),
"date": msg["Date"], "date": msg["Date"],
"utcdate": (utcdate.date().isoformat(), utcdate.time().isoformat()) if utcdate else ("", ""), "utcdate": (utcdate.date().isoformat(), utcdate.time().isoformat()) if utcdate else ("", ""),
}) })
@ -106,6 +106,8 @@ def retrieve_messages(query_data_url):
"""Retrieve and return selected content from mailarch.""" """Retrieve and return selected content from mailarch."""
res = [] res = []
# This has not been rewritten to use requests.get() because get() does
# not handle file URLs out of the box, which we need for tesing
with contextlib.closing(urlopen(query_data_url, timeout=15)) as fileobj: with contextlib.closing(urlopen(query_data_url, timeout=15)) as fileobj:
content_type = fileobj.info()["Content-type"] content_type = fileobj.info()["Content-type"]
if not content_type.startswith("application/x-tar"): if not content_type.startswith("application/x-tar"):

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Copyright The IETF Trust 2020, All Rights Reserved
# Generated by Django 1.11.26 on 2019-12-21 11:52
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('review', '0022_reviewer_queue_policy_and_request_assignment_next'),
]
operations = [
migrations.AlterField(
model_name='historicalreviewersettings',
name='history_change_reason',
field=models.TextField(null=True),
),
]

View file

@ -10,15 +10,15 @@ class HashTest(TestCase):
def test_hash_list_message_id(self): def test_hash_list_message_id(self):
for list, msgid, hash in ( for list, msgid, hash in (
('ietf', '156182196167.12901.11966487185176024571@ietfa.amsl.com', b'lr6RtZ4TiVMZn1fZbykhkXeKhEk'), ('ietf', '156182196167.12901.11966487185176024571@ietfa.amsl.com', 'lr6RtZ4TiVMZn1fZbykhkXeKhEk'),
('codesprints', 'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', b'N1nFHHUXiFWYtdzBgjtqzzILFHI'), ('codesprints', 'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', 'N1nFHHUXiFWYtdzBgjtqzzILFHI'),
('xml2rfc', '3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org', b'g6DN4SxJGDrlSuKsubwb6rRSePU'), ('xml2rfc', '3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org', 'g6DN4SxJGDrlSuKsubwb6rRSePU'),
(u'ietf', u'156182196167.12901.11966487185176024571@ietfa.amsl.com',b'lr6RtZ4TiVMZn1fZbykhkXeKhEk'), (u'ietf', u'156182196167.12901.11966487185176024571@ietfa.amsl.com','lr6RtZ4TiVMZn1fZbykhkXeKhEk'),
(u'codesprints', u'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', b'N1nFHHUXiFWYtdzBgjtqzzILFHI'), (u'codesprints', u'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', 'N1nFHHUXiFWYtdzBgjtqzzILFHI'),
(u'xml2rfc', u'3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org',b'g6DN4SxJGDrlSuKsubwb6rRSePU'), (u'xml2rfc', u'3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org','g6DN4SxJGDrlSuKsubwb6rRSePU'),
(b'ietf', b'156182196167.12901.11966487185176024571@ietfa.amsl.com',b'lr6RtZ4TiVMZn1fZbykhkXeKhEk'), (b'ietf', b'156182196167.12901.11966487185176024571@ietfa.amsl.com','lr6RtZ4TiVMZn1fZbykhkXeKhEk'),
(b'codesprints', b'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', b'N1nFHHUXiFWYtdzBgjtqzzILFHI'), (b'codesprints', b'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', 'N1nFHHUXiFWYtdzBgjtqzzILFHI'),
(b'xml2rfc', b'3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org',b'g6DN4SxJGDrlSuKsubwb6rRSePU'), (b'xml2rfc', b'3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org','g6DN4SxJGDrlSuKsubwb6rRSePU'),
): ):
self.assertEqual(hash, hash_list_message_id(list, msgid)) self.assertEqual(hash, hash_list_message_id(list, msgid))

View file

@ -74,6 +74,12 @@ class GroupModelForm(forms.ModelForm):
if lsgc: if lsgc:
self.fields['liaison_contacts'].initial = lsgc.contacts self.fields['liaison_contacts'].initial = lsgc.contacts
def clean_acronym(self):
acronym = self.cleaned_data['acronym']
if any(x.isupper() for x in acronym):
raise forms.ValidationError('Capital letters not allowed in group acronym')
return acronym
def clean_parent(self): def clean_parent(self):
parent = self.cleaned_data['parent'] parent = self.cleaned_data['parent']
type = self.cleaned_data['type'] type = self.cleaned_data['type']

View file

@ -82,6 +82,22 @@ class GroupsTest(TestCase):
response = self.client.post(url,post_data) response = self.client.post(url,post_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_add_group_capital_acronym(self):
area = GroupFactory(type_id='area')
url = reverse('ietf.secr.groups.views.add')
post_data = {'acronym':'TEST',
'name':'Test Group',
'type':'wg',
'status':'active',
'parent':area.id,
'awp-TOTAL_FORMS':'2',
'awp-INITIAL_FORMS':'0',
'submit':'Save'}
self.client.login(username="secretary", password="secretary+password")
response = self.client.post(url,post_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Capital letters not allowed in group acronym')
# ------- Test View -------- # # ------- Test View -------- #
def test_view(self): def test_view(self):
MeetingFactory(type_id='ietf') MeetingFactory(type_id='ietf')

View file

@ -7,7 +7,6 @@ urlpatterns = [
url(r'^$', views.search), url(r'^$', views.search),
url(r'^add/$', views.add), url(r'^add/$', views.add),
url(r'^blue-dot-report/$', views.blue_dot), url(r'^blue-dot-report/$', views.blue_dot),
url(r'^search/$', views.search),
#(r'^ajax/get_ads/$', views.get_ads), #(r'^ajax/get_ads/$', views.get_ads),
url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views.view), url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views.view),
url(r'^%(acronym)s/delete/(?P<id>\d{1,6})/$' % settings.URL_REGEXPS, views.delete_role), url(r'^%(acronym)s/delete/(?P<id>\d{1,6})/$' % settings.URL_REGEXPS, views.delete_role),

View file

@ -0,0 +1,103 @@
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('proceedings/report_id_activity.txt', context)
return report
def report_progress_report(start_date,end_date):
syear,smonth,sday = start_date.split('-')
eyear,emonth,eday = end_date.split('-')
sdate = datetime.datetime(int(syear),int(smonth),int(sday))
edate = datetime.datetime(int(eyear),int(emonth),int(eday))
context = get_progress_stats(sdate,edate)
report = render_to_string('proceedings/report_progress_report.txt', context)
return report

View file

@ -0,0 +1,34 @@
import datetime
import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory,NewRevisionDocEventFactory
from ietf.secr.proceedings.reports import report_id_activity, report_progress_report
from ietf.utils.test_utils import TestCase
from ietf.meeting.factories import MeetingFactory
class ReportsTestCase(TestCase):
def test_report_id_activity(self):
today = datetime.datetime.today()
yesterday = today - datetime.timedelta(days=1)
last_quarter = today - datetime.timedelta(days=3*30)
next_week = today+datetime.timedelta(days=7)
m1 = MeetingFactory(type_id='ietf',date=last_quarter)
m2 = MeetingFactory(type_id='ietf',date=next_week,number=int(m1.number)+1)
doc = DocumentFactory(type_id='draft',time=yesterday,rev="00")
NewRevisionDocEventFactory(doc=doc,time=today,rev="01")
result = report_id_activity(m1.date.strftime("%Y-%m-%d"),m2.date.strftime("%Y-%m-%d"))
self.assertTrue('IETF Activity since last IETF Meeting' in result)
def test_report_progress_report(self):
today = datetime.datetime.today()
last_quarter = today - datetime.timedelta(days=3*30)
next_week = today+datetime.timedelta(days=7)
m1 = MeetingFactory(type_id='ietf',date=last_quarter)
m2 = MeetingFactory(type_id='ietf',date=next_week,number=int(m1.number)+1)
result = report_progress_report(m1.date.strftime('%Y-%m-%d'),m2.date.strftime('%Y-%m-%d'))
self.assertTrue('IETF Activity since last IETF Meeting' in result)

View file

@ -96,7 +96,7 @@ class SessionForm(forms.Form):
self.fields['length_session1'].widget.attrs['onClick'] = "if (check_num_session(1)) this.disabled=true;" self.fields['length_session1'].widget.attrs['onClick'] = "if (check_num_session(1)) this.disabled=true;"
self.fields['length_session2'].widget.attrs['onClick'] = "if (check_num_session(2)) this.disabled=true;" self.fields['length_session2'].widget.attrs['onClick'] = "if (check_num_session(2)) this.disabled=true;"
self.fields['length_session3'].widget.attrs['onClick'] = "if (check_third_session()) { this.disabled=true;}" self.fields['length_session3'].widget.attrs['onClick'] = "if (check_third_session()) { this.disabled=true;}"
self.fields['comments'].widget = forms.Textarea(attrs={'rows':'6','cols':'65'}) 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')) 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): for i in range(1, 4):

View file

@ -592,10 +592,17 @@ def new(request, acronym):
# the "previous" querystring causes the form to be returned # the "previous" querystring causes the form to be returned
# pre-populated with data from last meeeting's session request # pre-populated with data from last meeeting's session request
elif request.method == 'GET' and 'previous' in request.GET: elif request.method == 'GET' and 'previous' in request.GET:
previous_meeting = Meeting.objects.get(number=str(int(meeting.number) - 1)) latest_session = add_event_info_to_session_qs(Session.objects.filter(meeting__type_id='ietf', group=group)).exclude(current_status__in=['notmeet', 'deleted', 'canceled',]).order_by('-meeting__date').first()
previous_sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=previous_meeting, group=group)).exclude(current_status__in=['notmeet', 'deleted']).order_by('id') if latest_session:
if not previous_sessions: previous_meeting = Meeting.objects.get(number=latest_session.meeting.number)
messages.warning(request, 'This group did not meet at %s' % previous_meeting) previous_sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=previous_meeting, group=group)).exclude(current_status__in=['notmeet', 'deleted']).order_by('id')
if not previous_sessions:
messages.warning(request, 'This group did not meet at %s' % previous_meeting)
return redirect('ietf.secr.sreq.views.new', acronym=acronym)
else:
messages.info(request, 'Fetched session info from %s' % previous_meeting)
else:
messages.warning(request, 'Did not find any previous meeting')
return redirect('ietf.secr.sreq.views.new', acronym=acronym) return redirect('ietf.secr.sreq.views.new', acronym=acronym)
initial = get_initial_session(previous_sessions, prune_conflicts=True) initial = get_initial_session(previous_sessions, prune_conflicts=True)

View file

@ -1,31 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Abstract{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; <a href="../">{{ draft.canonical_name }}</a>
&raquo; Abstract
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>View Abstract - {{ draft.canonical_name }}</h2>
<p>
{{ draft.abstract }}
</p>
<div class="button-group">
<ul>
<li><button onclick="window.location='../'">Back to View</button></li>
</ul>
</div> <!-- button-group -->
</div> <!-- module -->
{% endblock %}

View file

@ -1,47 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Approvals{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; Drafts Approvals
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Approvals</h2>
<form id="drafts-approvals-form" enctype="multipart/form-data" action="" method="post">{% csrf_token %}
<table class="full-width">
{{ form.as_table }}
</table>
</form>
<div class="inline-group">
<h2>Approved Drafts</h2>
<table id="drafts-approved" class="center" cellspacing="0">
<thead>
<tr>
<th>Filename</th>
<th>Approved Date</th>
<th>Approved by</th>
</tr>
</thead>
<tbody>
{% for doc in approved %}
<tr class="{% cycle 'row1' 'row2' %}">
<td>{{ doc.name }}</a></td>
<td>{{ doc.time|date:"Y-m-d" }}</td>
<td>{{ doc.by }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> <!-- inline-group -->
</div> <!-- module -->
{% endblock %}

View file

@ -1,74 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Authors{% endblock %}
{% block extrahead %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'secr/css/jquery-ui-1.11.4.custom.css' %}" />
<script type="text/javascript" src="{% static 'secr/js/jquery-ui-1.11.4.custom.min.js' %}"></script>
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; <a href="../">{{ draft.name }}</a>
&raquo; Authors
{% endblock %}
{% block content %}
<div class="module">
<h2>Authors</h2>
<table class="full-width">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Affiliation</th>
<th>Country</th>
<th>Order</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for author in draft.documentauthor_set.all %}
<tr class="{% cycle 'row1' 'row2' %}">
<td>{{ author.person }}</td>
<td>{{ author.email }}</td>
<td>{{ author.affiliation }}</td>
<td>{{ author.country.name }}</td>
<td>{{ author.order }}</td>
<td><a href="{% url 'ietf.secr.drafts.views.author_delete' id=draft.pk oid=author.id %}">Delete</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="inline-related">
<!-- <hr><br> -->
<h3>Add Author</h3>
<form id="groups-people" action="" method="post">{% csrf_token %}
{{ form.non_field_errors }}
<table class="full-width">
<tr>
<td>{{ form.person.errors }}{{ form.person }}{% if form.person.help_text %}<br>{{ form.person.help_text }}{% endif %}</td>
<td>{{ form.email.errors }}{{ form.email }}{% if form.email.help_text %}<br>{{ form.email.help_text }}{% endif %}</td>
</tr>
<tr>
<td>{{ form.affiliation.errors }}{{ form.affiliation }}{% if form.affiliation.help_text %}<br>{{ form.affiliation.help_text }}{% endif %}</td>
<td>{{ form.country.errors }}{{ form.country }}{% if form.country.help_text %}<br>{{ form.country.help_text }}{% endif %}</td>
<td><input type="submit" name="submit" value="Add" /></td>
</tr>
</table>
</div> <!-- inline-related -->
<div class="button-group">
<ul>
<li><button type="submit" name="submit" value="Done">Done</button></li>
</ul>
</div> <!-- button-group -->
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,49 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Confirm{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; <a href="../">{{ draft.name }}</a>
&raquo; Confirm
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Confirm</h2>
<form enctype="multipart/form-data" action="{% url "ietf.secr.drafts.views.do_action" id=draft.name %}" method="post">{% csrf_token %}
<table>
<tr><th>Action Selected:</th><td>{{ action }}</td></tr>
<tr><th>Draft:</th><td>{{ draft.name }}</td></tr>
{% for detail in details %}
<tr><th>{{ detail.label }}</th><td>{{ detail.value }}</td></tr>
{% endfor %}
</table>
{% if email %}
<div class="inline-group">
<h2>Scheduled Email</h2>
<table id="draft-confirm-email">
<tr><th>To:</th><td>{{ email.to }}</td></tr>
<tr><th>CC:</th><td>{{ email.cc }}</td></tr>
<tr><th>Subject:</th><td>{{ email.subject }}</td></tr>
<tr><th>Body:</th><td>{{ email.body }}</td></tr>
</table>
</div>
{% endif %}
{{ form }}
{% include "includes/buttons_save_cancel.html" %}
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,36 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Submission Dates{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; Submission Dates
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Submission Dates</h2>
<table class="full-width">
<tr><td>First Cut Off (Initial Version)</td><td>{{ meeting.get_first_cut_off }}</td></tr>
<tr><td>Second Cut Off (Update Version)</td><td>{{ meeting.get_second_cut_off }}</td></tr>
<tr><td>IETF Monday</td><td>{{ meeting.get_ietf_monday }}</td></tr>
<tr><td>All I-Ds will be processed by</td><td></td></tr>
<tr><td>Monday after IETF</td><td></td></tr>
<tr><td>Date for list of approved V-00 submissions from WG Chairs</td><td></td></tr>
</table>
<div class="button-group">
<ul>
<li><button onclick="history.go(-1);return true">Back</button></li>
</ul>
</div> <!-- button-group -->
</div> <!-- module -->
{% endblock %}

View file

@ -1,61 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Edit{% endblock %}
{% block extrahead %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'secr/css/jquery-ui-1.11.4.custom.css' %}" />
<script type="text/javascript" src="{% static 'secr/js/jquery-ui-1.11.4.custom.min.js' %}"></script>
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; <a href="../">{{ draft.name }}</a>
&raquo; Edit
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Edit</h2>
{{ form.non_field_errors }}
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
<table id="draft-edit-table" class="full-width amstable">
<tr><th>Document Name:</th><td>{{ form.title.errors }}{{ form.title }}</td></tr>
<tr><th>Group:</th><td>{{ form.group.errors }}{{ form.group }}</td></tr>
<tr><th>Area Director:</th><td>{{ form.ad.errors }}{{ form.ad }}</td></tr>
<tr><th>Shepherd:</th><td>{{ form.shepherd.errors }}{{ form.shepherd }}</td></tr>
<tr><th>Notify:</th><td>{{ form.notify.errors }}{{ form.notify }}</td></tr>
<tr><th>State:</th><td>{{ form.state.errors }}{{ form.state }}</td></tr>
<tr><th>IESG State:</th><td>{{ form.iesg_state.errors }}{{ form.iesg_state }}</td></tr>
<tr><th>Stream:</th><td>{{ form.stream.errors }}{{ form.stream }}</td></tr>
<tr><th>Under Review by RFC Editor:</th><td>{{ form.review_by_rfc_editor.errors }}{{ form.review_by_rfc_editor }}</td></tr>
<tr><th>File Name:</th><td>{{ form.name.errors }}{{ form.name }} - {{ form.rev.errors }}{{ form.rev }}</td></tr>
<tr><th>Number of Pages:</th><td>{{ form.pages.errors }}{{ form.pages }}</td></tr>
<tr><th>Abstract:</th><td>{{ form.abstract.errors }}{{ form.abstract }}</td></tr>
<tr><th>Expiration Date:</th><td>{{ draft.expires|date:"M. d, Y" }}</td></tr>
<tr><th>Intended Std Level:</th><td>{{ form.intended_std_level.errors }}{{ form.intended_std_level }}</td></tr>
<tr><th>Standardization Level:</th><td>{{ form.std_level.errors }}{{ form.std_level }}</td></tr>
<tr><th>RFC Number:</th><td>{{ draft.rfc_number }}</td></tr>
<tr><th>Comments:</th><td>{{ form.internal_comments.errors }}{{ form.internal_comments }}</td></tr>
<tr><th>Replaced by:</th><td>{{ form.replaced_by.errors }}{{ form.replaced_by }}</td></tr>
<tr><th>Last Modified Date:</th><td>{{ draft.time }}</td></tr>
</table>
<div class="button-group">
<ul>
<li><button type="submit" name="submit" value="Save">Save</button></li>
<li><button type="submit" name="submit" value="Cancel">Cancel</button></li>
</ul>
</div> <!-- button-group -->
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,30 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Email{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; <a href="../">{{ draft.name }}</a>
&raquo; Email
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Email</h2>
<form enctype="multipart/form-data" action="{% url "ietf.secr.drafts.views.confirm" id=draft.name %}" method="post">{% csrf_token %}
<table id="draft-email-table" class="full-width">
{{ form.as_table }}
</table>
{% include "includes/buttons_save_cancel.html" %}
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,23 +0,0 @@
{% extends "base_site.html" %}
{% block title %}Drafts{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
{% endblock %}
{% block content %}
<div class="module group-container">
<h2>Drafts - Error Message</h2>
An ERROR has occured:
<h3>{{ error }}</h3>
<div class="button-group">
<ul>
<li><button onClick="window.location='../../'">Continue</button></li>
</ul>
</div> <!-- button-group -->
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,32 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Extend{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; <a href="../">{{ draft.name }}</a>
&raquo; Extend
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Extend Expiry</h2>
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
<table class="full-width">
<tr><th><label>Current Expiration Date:</label></th><td>{{ draft.expires|date:"Y-m-d" }}</td></tr>
{{ form.as_table }}
</table>
{% include "includes/buttons_save_cancel.html" %}
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,81 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Make RFC{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
&raquo; <a href="../">{{ draft.name }}</a>
&raquo; Make RFC
{% endblock %}
{% block content %}
<div class="module draft-container">
<div id="draft-view-col1">
<h2>Draft - Make RFC</h2>
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
<table id="draft-makerfc-table" class="full-width">
{% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
<tr><th><label for="id_title">RFC Name</label></th><td colspan="3">{{ form.title.errors }}{{ form.title }}</td></tr>
<tr>
<th><label for="id_rfc_number">RFC Number</label></th><td>{{ form.rfc_number.errors }}{{ form.rfc_number }}</td>
<th><label for="id_txt_page_count">No. of Pages</label></th><td>{{ form.pages.errors }}{{ form.pages }}</td>
</tr>
<tr><th><label for="id_rfc_published_date">RFC Published Date</label></th><td>{{ form.rfc_published_date.errors }}{{ form.rfc_published_date }}</td></tr>
<tr>
<th><label for="id_status">RFC Std Level</label></th><td>{{ form.std_level.errors }}{{ form.std_level }}</td>
<th><label for="id_group_acronym">Group</label></th><td>{{ form.group.errors }}{{ form.group }}</td>
</tr>
{% comment %}
<tr>
<th><label for="id_proposed_date">Proposed Date</label></th><td>{{ form.proposed_date.errors }}{{ form.proposed_date }}</td>
<th><label for="id_draft_date">Draft Date</label></th><td>{{ form.draft_date.errors }}{{ form.draft_date }}</td>
</tr>
<tr>
<th><label for="id_standard_date">Standard Date</label></th><td>{{ form.standard_date.errors }}{{ form.standard_date }}</td>
<th><label for="id_historic_date">Historic Date</label></th><td>{{ form.historic_date.errors }}{{ form.historic_date }}</td>
</tr>
<tr>
<th><label for="id_fyi_number">FYI Number</label></th><td>{{ form.fyi_number.errors }}{{ form.fyi_number }}</td>
<th><label for="id_std_number">STD Number</label></th><td>{{ form.std_number.errors }}{{ form.std_number }}</td>
</tr>
{% endcomment %}
<tr><th><label for="id_comments">Comments</label></th><td colspan="3">{{ form.internal_comments.errors }}{{ form.internal_comments }}</td></tr>
</table>
</div> <!-- draft-view-col1 -->
<div id="draft-view-col2">
<div class="inline-related">
<h2>Author(s)</h2>
<table>
{% for author in draft.documentauthor_set.all %}
<tr><td><a href="{% url "ietf.secr.rolodex.views.view" id=author.person.pk %}">{{ author.person.name }}</a></td></tr>
{% endfor %}
</table>
</div> <!-- inline-related -->
<div class="inline-related add-form action-group">
<h2>Obsolete and Update Relationships</h2>
{{ obs_formset.management_form }}
{{ obs_formset.non_form_errors }}
<table id="draft-obsoletes-table" class="full-width">
{% for form in obs_formset.forms %}
{% comment %}<tr><td colspan="2">{{ form.relation.errors }}{{ form.rfc.errors }}</td></tr>{% endcomment %}
<tr>
<td>{% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
{{ form.relation.errors }}{{ form.relation }}</td><td>{{ form.rfc.errors }}{{ form.rfc }}</td>
</tr>
{% endfor %}
</table>
</div> <!-- inline-related -->
</div> <!-- draft-view-col2 -->
{% include "includes/buttons_save_cancel.html" %}
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,8 +0,0 @@
As you requested, the expiration date for
{{ doc }} has been extended. The draft
will expire on {{ expire_date }} unless it is replaced by an updated version, or the
Secretariat has been notified that the document is under official review by the
IESG or has been passed to the IRSG or RFC Editor for review and/or publication
as an RFC.
IETF Secretariat.

View file

@ -1,6 +0,0 @@
As you requested, {{ doc }} has been resurrected. The draft will expire on
{{ expire_date }} unless it is replaced by an updated version, or the Secretariat has been notified that the
document is under official review by the IESG or has been passed to the IRSG or RFC Editor for review and/or
publication as an RFC.
IETF Secretariat.

View file

@ -1,4 +0,0 @@
As you requested, {{ doc }}
has been marked as withdrawn by the {{ by }} in the IETF Internet-Drafts database.
IETF Secretariat.

View file

@ -1,26 +0,0 @@
{% extends "base_site.html" %}
{% load ams_filters %}
{% block title %}Drafts{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Drafts</a>
{% endblock %}
{% block content %}
<div class="module">
<h2>Nudge Report</h2>
<table>
{% for doc in docs %}
<tr>
<td><a href="{{ doc.get_absolute_url }}">{{ doc.name }}</a></td>
<td>{{ doc|get_published_date|date:"Y-m-d" }}</td>
<td></td>
<td>{% if doc.ad_name %}{{ doc.ad_name }}{% else %}&nbsp;{% endif %}</td>
</tr>
{% endfor %}
</table>
</div> <!-- module -->
{% endblock %}

View file

@ -1,34 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Search{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; Drafts
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Search</h2>
<form id="draft-search-form" enctype="multipart/form-data" action="" method="post">{% csrf_token %}
<table id="draft-search-table" class="full-width amstable">
{{ form.as_table }}
</table>
{% include "includes/buttons_search.html" %}
</form>
<div class="inline-group">
<h2>Search Results</h2>
{% include "includes/draft_search_results.html" %}
{% if not_found %}{{ not_found }}{% endif %}
</div> <!-- inline-group -->
</div> <!-- module -->
{% endblock %}

View file

@ -1,102 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - View{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../">Drafts</a>
&raquo; {{ draft.name }}
{% endblock %}
{% block content %}
{% comment %}{{ request.user }}{% endcomment %}
<div class="module draft-container">
<div id="draft-view-col1">
<h2>Draft - View</h2>
<table class="full-width">
<tr><td>Document Name:</td><td>{{ draft.title }}</td></tr>
<tr><td>Area:</td><td>{% if draft.group.parent %}<a href="{% url "ietf.secr.areas.views.view" name=draft.group.parent.acronym %}">{{ draft.group.parent }}{% endif %}</td></tr>
<tr><td>Group:</td><td>{% if draft.group %}<a href="{% url 'ietf.secr.groups.views.view' acronym=draft.group.acronym %}">{{ draft.group.acronym }}{% endif %}</td></tr>
<tr><td>Area Director:</td><td>{{ draft.ad }}</td></tr>
<tr><td>Shepherd:</td><td>{% if draft.shepherd %}{{ draft.shepherd.person }} &lt;{{ draft.shepherd.address }}&gt;{% else %}None{% endif %}</td></tr>
<tr><td>Notify:</td><td>{{ draft.notify }}</td></tr>
<tr><td>Document State:</td><td>{{ draft.get_state }}</td></tr>
<tr><td>IESG State:</td><td>{{ draft.iesg_state }}</td></tr>
<tr><td>Stream:</td><td>{{ draft.stream }}</td></tr>
<tr><td>Under Review by RFC Editor:</td><td>{% if draft.review_by_rfc_editor %}YES{% else %}NO{% endif %}</td></tr>
<tr><td>File Name:</td>
<td>{% if draft.expired_tombstone %}
{{ draft.filename }}
<div class=alert>This is a last active version - the tombstone was expired.</div>
{% else %}<a href="{% if is_development %}http://devr.amsl.com/ftp/{% else %}https://www.ietf.org/internet-drafts/{% endif %}{{ draft.name }}-{{ draft.rev }}.txt">{{ draft.name }}</a>
{% endif %}
</td></tr>
<tr><td>Document Aliases:</td>
<td>{% for alias in draft.docalias_set.all %}
{% if not forloop.first %}, {% endif %}
{{ alias.name }}
{% endfor %}
</td>
</tr>
<tr><td>Revision:</td><td>{{ draft.rev }}</td></tr>
<tr><td>Revision Date:</td><td>{{ draft.revision_date }}</td></tr>
<tr><td>Start Date:</td><td>{{ draft.start_date }}</td></tr>
<tr><td>Number of Pages:</td><td>{{ draft.pages }}</td></tr>
{% comment %}<tr><td>Local Path:</td><td>/ftp/internet-drafts/{{ draft.local_path|default_if_none:"" }}</td></tr>{% endcomment %}
<tr><td>Abstract:</td><td><a href="{% url 'ietf.secr.drafts.views.abstract' id=draft.name %}">Click here to view the abstract</td></tr>
<tr><td>Expiration Date:</td><td>{{ draft.expires|date:"M. d, Y" }}</td></tr>
<tr><td>Intended Status:</td><td>{{ draft.intended_std_level|default_if_none:"" }}</td></tr>
<tr><td>Standardization Level:</td><td>{{ draft.std_level|default_if_none:"" }}</td></tr>
<tr><td>RFC Number:</td><td>{{ draft.rfc_number|default_if_none:"" }}</td></tr>
<tr><td>Comments:</td><td>{{ draft.internal_comments|default_if_none:"" }}</td></tr>
<tr><td>Last Modified Date:</td><td>{{ draft.time }}</td></tr>
<tr><td>Replaced by:</td><td>{% if draft.replaced_by %}<a href="{% url "ietf.secr.drafts.views.view" id=draft.replaced_by.name %}">{{ draft.replaced_by.name }}{% endif %}</td></tr>
<tr><td>Related Documents:</td><td>{% for item in draft.relateddocument_set.all %}{% if not forloop.first %}, {% endif %}{{ item.relationship }} <a href="{% url "ietf.secr.drafts.views.view" id=item.target.document.pk %}">{{ item.target.name }}</a>{% endfor %}</td></tr>
<tr><td>Tags:</td>
<td>{% for tag in draft.tags.all %}
{% if not forloop.first %}, {% endif %}
{{ tag }}
{% endfor %}
</td>
</tr>
</table>
</div> <!-- draft-view-col1 -->
<div id="draft-view-col2">
<div class="inline-related">
<h2>Author(s)</h2>
<table class="full-width">
{% for author in draft.documentauthor_set.all %}
<tr><td><a href="{% url 'ietf.secr.rolodex.views.view' id=author.person.id %}">{{ author.person.name }}</a></td></tr>
{% endfor %}
</table>
</div> <!-- inline-related -->
<div class="inline-related action-group">
<h2>Actions</h2>
<ul>
<li><button {% if is_expired or is_withdrawn %}{% else %}disabled="disabled"{% endif %}onclick="window.location='{% url "ietf.secr.drafts.views.email" id=draft.name %}?action=resurrect'">Resurrect</button></li>
<li><button {% if is_active %}{% else %}disabled="disabled"{% endif %}onclick="window.location='extend/'">Extend Expiry</button></li>
<li><button {% if is_active %}{% else %}disabled="disabled"{% endif %}onclick="window.location='withdraw/'">Withdraw</button></li>
</ul>
</div> <!-- inline-related -->
</div> <!-- draft-view-col2 -->
<div class="button-group">
<ul>
<li><button onclick="window.location='edit/'">Edit</button></li>
<li><button onclick="window.location='authors/'">Authors</button></li>
{% comment %}
<li><button onclick="window.location='{% url "sec.ids.views.search" id=group.group_acronym.acronym_id %}'">Drafts</button></li>
<li><button onclick="window.location='{% url "sec.rfcs.views.search" id=group.group_acronym.acronym_id %}'">RFCs</button></li>
{% endcomment %}
</ul>
</div> <!-- button-group -->
</div> <!-- module -->
{% endblock %}

View file

@ -1,30 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Drafts - Withdraw{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../">Drafts</a>
&raquo; <a href="../">{{ draft.name }}</a>
&raquo; Withdraw
{% endblock %}
{% block content %}
<div class="module draft-container">
<h2>Draft - Withdraw</h2>
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
<table class="full-width">
{{ form.as_table }}
</table>
{% include "includes/buttons_save_cancel.html" %}
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,20 +0,0 @@
<table id="draft-search-results" class="center" cellspacing="0">
<thead>
<tr>
<th>Name</th>
<th>Group</th>
<th>Status</th>
<th>Intended Status</th>
</tr>
</thead>
<tbody>
{% for item in results %}
<tr class="{% cycle 'row1' 'row2' %}">
<td><a href="{% url "ietf.secr.drafts.views.view" id=item.name %}">{{ item.name }}</a></td>
<td>{{item.group.acronym}}</td>
<td>{{item.get_state}}</td>
<td>{{item.intended_std_level}}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -60,7 +60,7 @@
</td> </td>
</tr> </tr>
<tr class="bg1"> <tr class="bg1">
<td valign="top">Special Requests:<br />&nbsp;<br />i.e. restrictions on meeting times / days, etc.</td> <td valign="top">Special Requests:<br />&nbsp;<br />i.e. restrictions on meeting times / days, etc.<br /> (limit 200 characters)</td>
<td>{{ form.comments.errors }}{{ form.comments }}</td> <td>{{ form.comments.errors }}{{ form.comments }}</td>
</tr> </tr>
</table> </table>

View file

@ -16,7 +16,6 @@
</td> </td>
<td> <td>
<h3>IDs and WGs Process</h3> <h3>IDs and WGs Process</h3>
<li> <a href="{% url "ietf.secr.drafts.views.search" %}"><b>Drafts</b></a></li><br>
<li> <a href="{% url "ietf.secr.areas.views.list_areas" %}"><b>Areas</b></a></li><br> <li> <a href="{% url "ietf.secr.areas.views.list_areas" %}"><b>Areas</b></a></li><br>
<li> <a href="{% url 'ietf.secr.groups.views.search' %}"><b>Groups</b></a></li><br> <li> <a href="{% url 'ietf.secr.groups.views.search' %}"><b>Groups</b></a></li><br>
<li> <a href="{% url 'ietf.secr.rolodex.views.search' %}"><b>Rolodex</b></a></li><br> <li> <a href="{% url 'ietf.secr.rolodex.views.search' %}"><b>Rolodex</b></a></li><br>
@ -26,7 +25,6 @@
<tr valign="top"> <tr valign="top">
<td> <td>
<h3>Meetings and Proceedings</h3> <h3>Meetings and Proceedings</h3>
<li> <a href="{% url 'ietf.secr.drafts.views.dates' %}"><b>Draft Submission Dates</b></a></li><br>
<li> <a href="{% url "ietf.secr.sreq.views.main" %}"><b>Session Requests</b></a></li><br> <li> <a href="{% url "ietf.secr.sreq.views.main" %}"><b>Session Requests</b></a></li><br>
<li> <a href="{% url 'ietf.secr.proceedings.views.main' %}"><b>Meeting Materials Manager (Proceedings)</b></a></li><br> <li> <a href="{% url 'ietf.secr.proceedings.views.main' %}"><b>Meeting Materials Manager (Proceedings)</b></a></li><br>
<li> <a href="{% url "ietf.secr.meetings.views.main" %}"><b>Meeting Manager</b></a></li><br> <li> <a href="{% url "ietf.secr.meetings.views.main" %}"><b>Meeting Manager</b></a></li><br>

View file

@ -6,7 +6,6 @@ urlpatterns = [
url(r'^announcement/', include('ietf.secr.announcement.urls')), url(r'^announcement/', include('ietf.secr.announcement.urls')),
url(r'^areas/', include('ietf.secr.areas.urls')), url(r'^areas/', include('ietf.secr.areas.urls')),
url(r'^console/', include('ietf.secr.console.urls')), url(r'^console/', include('ietf.secr.console.urls')),
url(r'^drafts/', include('ietf.secr.drafts.urls')),
url(r'^groups/', include('ietf.secr.groups.urls')), url(r'^groups/', include('ietf.secr.groups.urls')),
url(r'^meetings/', include('ietf.secr.meetings.urls')), url(r'^meetings/', include('ietf.secr.meetings.urls')),
url(r'^proceedings/', include('ietf.secr.proceedings.urls')), url(r'^proceedings/', include('ietf.secr.proceedings.urls')),

View file

@ -436,7 +436,6 @@ INSTALLED_APPS = [
# IETF Secretariat apps # IETF Secretariat apps
'ietf.secr.announcement', 'ietf.secr.announcement',
'ietf.secr.areas', 'ietf.secr.areas',
'ietf.secr.drafts',
'ietf.secr.groups', 'ietf.secr.groups',
'ietf.secr.meetings', 'ietf.secr.meetings',
'ietf.secr.proceedings', 'ietf.secr.proceedings',
@ -760,6 +759,9 @@ IDSUBMIT_ANNOUNCE_LIST_EMAIL = 'i-d-announce@ietf.org'
# Interim Meeting Tool settings # Interim Meeting Tool settings
INTERIM_ANNOUNCE_FROM_EMAIL = 'IESG Secretary <iesg-secretary@ietf.org>' INTERIM_ANNOUNCE_FROM_EMAIL = 'IESG Secretary <iesg-secretary@ietf.org>'
VIRTUAL_INTERIMS_REQUIRE_APPROVAL = True
INTERIM_SESSION_MINIMUM_MINUTES = 30
INTERIM_SESSION_MAXIMUM_MINUTES = 300
# Days from meeting to day of cut off dates on submit -- cutoff_time_utc is added to this # Days from meeting to day of cut off dates on submit -- cutoff_time_utc is added to this
IDSUBMIT_DEFAULT_CUTOFF_DAY_OFFSET_00 = 13 IDSUBMIT_DEFAULT_CUTOFF_DAY_OFFSET_00 = 13

View file

@ -84,6 +84,11 @@ class SubmissionBaseUploadForm(forms.Form):
cutoff_00_str = cutoff_00.strftime("%Y-%m-%d %H:%M %Z") cutoff_00_str = cutoff_00.strftime("%Y-%m-%d %H:%M %Z")
cutoff_01_str = cutoff_01.strftime("%Y-%m-%d %H:%M %Z") cutoff_01_str = cutoff_01.strftime("%Y-%m-%d %H:%M %Z")
reopen_str = reopen.strftime("%Y-%m-%d %H:%M %Z") reopen_str = reopen.strftime("%Y-%m-%d %H:%M %Z")
# Workaround for IETF107. This would be better handled by a refactor that allowed meetings to have no cutoff period.
if cutoff_01 >= reopen:
return
if cutoff_00 == cutoff_01: if cutoff_00 == cutoff_01:
if now.date() >= (cutoff_00.date() - meeting.idsubmit_cutoff_warning_days) and now.date() < cutoff_00.date(): if now.date() >= (cutoff_00.date() - meeting.idsubmit_cutoff_warning_days) and now.date() < cutoff_00.date():
self.cutoff_warning = ( 'The last submission time for Internet-Drafts before %s is %s.<br/><br/>' % (meeting, cutoff_00_str)) self.cutoff_warning = ( 'The last submission time for Internet-Drafts before %s is %s.<br/><br/>' % (meeting, cutoff_00_str))
@ -308,22 +313,26 @@ class SubmissionBaseUploadForm(forms.Form):
txt_file.seek(0) txt_file.seek(0)
try: try:
text = bytes.decode(self.file_info['txt'].charset) text = bytes.decode(self.file_info['txt'].charset)
#
self.parsed_draft = Draft(text, txt_file.name)
if self.filename == None:
self.filename = self.parsed_draft.filename
elif self.filename != self.parsed_draft.filename:
self.add_error('txt', "Inconsistent name information: xml:%s, txt:%s" % (self.filename, self.parsed_draft.filename))
if self.revision == None:
self.revision = self.parsed_draft.revision
elif self.revision != self.parsed_draft.revision:
self.add_error('txt', "Inconsistent revision information: xml:%s, txt:%s" % (self.revision, self.parsed_draft.revision))
if self.title == None:
self.title = self.parsed_draft.get_title()
elif self.title != self.parsed_draft.get_title():
self.add_error('txt', "Inconsistent title information: xml:%s, txt:%s" % (self.title, self.parsed_draft.get_title()))
except (UnicodeDecodeError, LookupError) as e: except (UnicodeDecodeError, LookupError) as e:
self.add_error('txt', 'Failed decoding the uploaded file: "%s"' % str(e)) self.add_error('txt', 'Failed decoding the uploaded file: "%s"' % str(e))
#
self.parsed_draft = Draft(text, txt_file.name) rev_error = validate_submission_rev(self.filename, self.revision)
if self.filename == None: if rev_error:
self.filename = self.parsed_draft.filename raise forms.ValidationError(rev_error)
elif self.filename != self.parsed_draft.filename:
self.add_error('txt', "Inconsistent name information: xml:%s, txt:%s" % (self.filename, self.parsed_draft.filename))
if self.revision == None:
self.revision = self.parsed_draft.revision
elif self.revision != self.parsed_draft.revision:
self.add_error('txt', "Inconsistent revision information: xml:%s, txt:%s" % (self.revision, self.parsed_draft.revision))
if self.title == None:
self.title = self.parsed_draft.get_title()
elif self.title != self.parsed_draft.get_title():
self.add_error('txt', "Inconsistent title information: xml:%s, txt:%s" % (self.title, self.parsed_draft.get_title()))
# The following errors are likely noise if we have previous field # The following errors are likely noise if we have previous field
# errors: # errors:

View file

@ -32,7 +32,7 @@ from ietf.meeting.factories import MeetingFactory
from ietf.message.models import Message from ietf.message.models import Message
from ietf.name.models import FormalLanguageName from ietf.name.models import FormalLanguageName
from ietf.person.models import Person from ietf.person.models import Person
from ietf.person.factories import UserFactory, PersonFactory from ietf.person.factories import UserFactory, PersonFactory, EmailFactory
from ietf.submit.models import Submission, Preapproval from ietf.submit.models import Submission, Preapproval
from ietf.submit.mail import add_submission_email, process_response_email from ietf.submit.mail import add_submission_email, process_response_email
from ietf.utils.mail import outbox, empty_outbox, get_payload from ietf.utils.mail import outbox, empty_outbox, get_payload
@ -1005,6 +1005,40 @@ class SubmitTests(TestCase):
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(len(q('input[type=file][name=txt]')), 1) self.assertEqual(len(q('input[type=file][name=txt]')), 1)
def test_no_blackout_at_all(self):
url = urlreverse('ietf.submit.views.upload_submission')
meeting = Meeting.get_current_meeting()
meeting.date = datetime.date.today()+datetime.timedelta(days=7)
meeting.save()
meeting.importantdate_set.filter(name_id='idcutoff').delete()
meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today()+datetime.timedelta(days=7))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('input[type=file][name=txt]')), 1)
meeting = Meeting.get_current_meeting()
meeting.date = datetime.date.today()
meeting.save()
meeting.importantdate_set.filter(name_id='idcutoff').delete()
meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today())
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('input[type=file][name=txt]')), 1)
meeting = Meeting.get_current_meeting()
meeting.date = datetime.date.today()-datetime.timedelta(days=1)
meeting.save()
meeting.importantdate_set.filter(name_id='idcutoff').delete()
meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today()-datetime.timedelta(days=1))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('input[type=file][name=txt]')), 1)
def submit_bad_file(self, name, formats): def submit_bad_file(self, name, formats):
rev = "" rev = ""
group = None group = None
@ -1083,6 +1117,38 @@ class SubmitTests(TestCase):
self.assertIn('Expected the PS file to have extension ".ps"', m) self.assertIn('Expected the PS file to have extension ".ps"', m)
self.assertIn('Expected an PS file of type "application/postscript"', m) self.assertIn('Expected an PS file of type "application/postscript"', m)
def test_submit_file_in_archive(self):
name = "draft-authorname-testing-file-exists"
rev = '00'
formats = ['txt', 'xml']
group = None
# break early in case of missing configuration
self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
# get
url = urlreverse('ietf.submit.views.upload_submission')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
# submit
for dir in [self.repository_dir, self.archive_dir, ]:
files = {}
for format in formats:
fn = os.path.join(dir, "%s-%s.%s" % (name, rev, format))
with io.open(fn, 'w') as f:
f.write("a" * 2000)
files[format], author = submission_file(name, rev, group, format, "test_submission.%s" % format)
r = self.client.post(url, files)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
m = q('div.alert-danger').text()
self.assertIn('Unexpected files already in the archive', m)
def test_submit_nonascii_name(self): def test_submit_nonascii_name(self):
name = "draft-authorname-testing-nonascii" name = "draft-authorname-testing-nonascii"
rev = "00" rev = "00"
@ -1770,6 +1836,26 @@ class ApiSubmitTests(TestCase):
expected = "Upload of %s OK, confirmation requests sent to:\n %s" % (name, author.formatted_email().replace('\n','')) expected = "Upload of %s OK, confirmation requests sent to:\n %s" % (name, author.formatted_email().replace('\n',''))
self.assertContains(r, expected, status_code=200) self.assertContains(r, expected, status_code=200)
def test_api_submit_secondary_email_active(self):
person = PersonFactory()
email = EmailFactory(person=person)
r, author, name = self.post_submission('00', author=person, email=email.address)
for expected in [
"Upload of %s OK, confirmation requests sent to:" % (name, ),
author.formatted_email().replace('\n',''),
]:
self.assertContains(r, expected, status_code=200)
def test_api_submit_secondary_email_inactive(self):
person = PersonFactory()
prim = person.email()
prim.primary = True
prim.save()
email = EmailFactory(person=person, active=False)
r, author, name = self.post_submission('00', author=person, email=email.address)
expected = "No such user: %s" % email.address
self.assertContains(r, expected, status_code=400)
def test_api_submit_no_user(self): def test_api_submit_no_user(self):
email='nonexistant.user@example.org' email='nonexistant.user@example.org'
r, author, name = self.post_submission('00', email=email) r, author, name = self.post_submission('00', email=email)

View file

@ -5,6 +5,7 @@
import datetime import datetime
import io import io
import os import os
import pathlib
import re import re
from typing import Callable, Optional # pyflakes:ignore from typing import Callable, Optional # pyflakes:ignore
@ -139,6 +140,15 @@ def validate_submission_rev(name, rev):
if rev != expected: if rev != expected:
return 'Invalid revision (revision %02d is expected)' % expected return 'Invalid revision (revision %02d is expected)' % expected
for dirname in [settings.INTERNET_DRAFT_PATH, settings.INTERNET_DRAFT_ARCHIVE_DIR, ]:
dir = pathlib.Path(dirname)
pattern = '%s-%02d.*' % (name, rev)
existing = list(dir.glob(pattern))
if existing:
plural = '' if len(existing) == 1 else 's'
files = ', '.join([ f.name for f in existing ])
return 'Unexpected file%s already in the archive: %s' % (plural, files)
replaced_by=has_been_replaced_by(name) replaced_by=has_been_replaced_by(name)
if replaced_by: if replaced_by:
return 'This document has been replaced by %s' % ",".join(rd.name for rd in replaced_by) return 'This document has been replaced by %s' % ",".join(rd.name for rd in replaced_by)

View file

@ -27,7 +27,7 @@ from ietf.group.utils import group_features_group_filter
from ietf.ietfauth.utils import has_role, role_required from ietf.ietfauth.utils import has_role, role_required
from ietf.mailtrigger.utils import gather_address_lists from ietf.mailtrigger.utils import gather_address_lists
from ietf.message.models import Message, MessageAttachment from ietf.message.models import Message, MessageAttachment
from ietf.person.models import Person from ietf.person.models import Person, Email
from ietf.submit.forms import ( SubmissionManualUploadForm, SubmissionAutoUploadForm, AuthorForm, from ietf.submit.forms import ( SubmissionManualUploadForm, SubmissionAutoUploadForm, AuthorForm,
SubmitterForm, EditSubmissionForm, PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm ) SubmitterForm, EditSubmissionForm, PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm )
from ietf.submit.mail import ( send_full_url, send_manual_post_request, add_submission_email, get_reply_to ) from ietf.submit.mail import ( send_full_url, send_manual_post_request, add_submission_email, get_reply_to )
@ -103,10 +103,25 @@ def api_submit(request):
username = form.cleaned_data['user'] username = form.cleaned_data['user']
user = User.objects.filter(username=username) user = User.objects.filter(username=username)
if user.count() == 0: if user.count() == 0:
return err(400, "No such user: %s" % username) # See if a secondary login was being used
if user.count() > 1: email = Email.objects.filter(address=username, active=True)
# The error messages don't talk about 'email', as the field we're
# looking at is still the 'username' field.
if email.count() == 0:
return err(400, "No such user: %s" % username)
elif email.count() > 1:
return err(500, "Multiple matching accounts for %s" % username)
email = email.first()
if not hasattr(email, 'person'):
return err(400, "No person matches %s" % username)
person = email.person
if not hasattr(person, 'user'):
return err(400, "No user matches: %s" % username)
user = person.user
elif user.count() > 1:
return err(500, "Multiple matching accounts for %s" % username) return err(500, "Multiple matching accounts for %s" % username)
user = user.first() else:
user = user.first()
if not hasattr(user, 'person'): if not hasattr(user, 'person'):
return err(400, "No person with username %s" % username) return err(400, "No person with username %s" % username)
@ -130,7 +145,7 @@ def api_submit(request):
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
if not user.username.lower() in [ a['email'].lower() for a in authors ]: if not username.lower() in [ a['email'].lower() for a in authors ]:
raise ValidationError('Submitter %s is not one of the document authors' % user.username) raise ValidationError('Submitter %s is not one of the document authors' % user.username)
submission.submitter = user.person.formatted_email() submission.submitter = user.person.formatted_email()

View file

@ -7,8 +7,7 @@ import datetime
import email import email
import json import json
import re import re
import requests
from urllib.request import Request, urlopen
from django.conf import settings from django.conf import settings
from django.utils.encoding import smart_bytes, force_str from django.utils.encoding import smart_bytes, force_str
@ -21,19 +20,12 @@ from ietf.doc.models import Document, DocEvent, State, StateDocEvent, StateType
from ietf.doc.utils import add_state_change_event from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person from ietf.person.models import Person
from ietf.utils.mail import parseaddr from ietf.utils.mail import parseaddr
from ietf.utils.text import decode
from ietf.utils.timezone import local_timezone_to_utc, email_time_to_local_timezone, utc_to_local_timezone from ietf.utils.timezone import local_timezone_to_utc, email_time_to_local_timezone, utc_to_local_timezone
#PROTOCOLS_URL = "https://www.iana.org/protocols/" #PROTOCOLS_URL = "https://www.iana.org/protocols/"
#CHANGES_URL = "https://datatracker.dev.icann.org:8080/data-tracker/changes" #CHANGES_URL = "https://datatracker.dev.icann.org:8080/data-tracker/changes"
def fetch_protocol_page(url):
f = urlopen(settings.IANA_SYNC_PROTOCOLS_URL)
text = decode(f.read())
f.close()
return text
def parse_protocol_page(text): def parse_protocol_page(text):
"""Parse IANA protocols page to extract referenced RFCs (as """Parse IANA protocols page to extract referenced RFCs (as
rfcXXXX document names).""" rfcXXXX document names)."""
@ -73,14 +65,11 @@ def update_rfc_log_from_protocol_page(rfc_names, rfc_must_published_later_than):
def fetch_changes_json(url, start, end): def fetch_changes_json(url, start, end):
url += "?start=%s&end=%s" % (urlquote(local_timezone_to_utc(start).strftime("%Y-%m-%d %H:%M:%S")), url += "?start=%s&end=%s" % (urlquote(local_timezone_to_utc(start).strftime("%Y-%m-%d %H:%M:%S")),
urlquote(local_timezone_to_utc(end).strftime("%Y-%m-%d %H:%M:%S"))) urlquote(local_timezone_to_utc(end).strftime("%Y-%m-%d %H:%M:%S")))
request = Request(url)
# HTTP basic auth # HTTP basic auth
username = "ietfsync" username = "ietfsync"
password = settings.IANA_SYNC_PASSWORD password = settings.IANA_SYNC_PASSWORD
request.add_header("Authorization", "Basic %s" % force_str(base64.encodestring(smart_bytes("%s:%s" % (username, password)))).replace("\n", "")) headers = { "Authorization": "Basic %s" % force_str(base64.encodestring(smart_bytes("%s:%s" % (username, password)))).replace("\n", "") }
f = urlopen(request) text = requests.get(url, headers=headers).text
text = decode(f.read())
f.close()
return text return text
def parse_changes_json(text): def parse_changes_json(text):

View file

@ -5,13 +5,13 @@
import base64 import base64
import datetime import datetime
import re import re
import requests
from urllib.request import Request, urlopen
from urllib.parse import urlencode from urllib.parse import urlencode
from xml.dom import pulldom, Node from xml.dom import pulldom, Node
from django.conf import settings from django.conf import settings
from django.utils.encoding import smart_bytes, force_str from django.utils.encoding import smart_bytes, force_str, force_text
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -24,7 +24,6 @@ from ietf.name.models import StdLevelName, StreamName
from ietf.person.models import Person from ietf.person.models import Person
from ietf.utils.log import log from ietf.utils.log import log
from ietf.utils.mail import send_mail_text from ietf.utils.mail import send_mail_text
from ietf.utils.text import decode
#QUEUE_URL = "https://www.rfc-editor.org/queue2.xml" #QUEUE_URL = "https://www.rfc-editor.org/queue2.xml"
#INDEX_URL = "https://www.rfc-editor.org/rfc/rfc-index.xml" #INDEX_URL = "https://www.rfc-editor.org/rfc/rfc-index.xml"
@ -527,28 +526,28 @@ def post_approved_draft(url, name):
the data from the Datatracker and start processing it. Returns the data from the Datatracker and start processing it. Returns
response and error (empty string if no error).""" response and error (empty string if no error)."""
request = Request(url)
request.add_header("Content-type", "application/x-www-form-urlencoded")
request.add_header("Accept", "text/plain")
# HTTP basic auth # HTTP basic auth
username = "dtracksync" username = "dtracksync"
password = settings.RFC_EDITOR_SYNC_PASSWORD password = settings.RFC_EDITOR_SYNC_PASSWORD
request.add_header("Authorization", "Basic %s" % force_str(base64.encodestring(smart_bytes("%s:%s" % (username, password)))).replace("\n", "")) headers = {
"Content-type": "application/x-www-form-urlencoded",
"Accept": "text/plain",
"Authorization": "Basic %s" % force_str(base64.encodestring(smart_bytes("%s:%s" % (username, password)))).replace("\n", ""),
}
log("Posting RFC-Editor notifcation of approved draft '%s' to '%s'" % (name, url)) log("Posting RFC-Editor notifcation of approved draft '%s' to '%s'" % (name, url))
text = error = "" text = error = ""
try: try:
f = urlopen(request, data=smart_bytes(urlencode({ 'draft': name })), timeout=20) r = requests.post(url, headers=headers, data=smart_bytes(urlencode({ 'draft': name })), timeout=20)
text = decode(f.read())
status_code = f.getcode()
f.close()
log("RFC-Editor notification result for draft '%s': %s:'%s'" % (name, status_code, text))
if status_code != 200: log("RFC-Editor notification result for draft '%s': %s:'%s'" % (name, r.status_code, r.text))
raise RuntimeError("Status code is not 200 OK (it's %s)." % status_code)
if text != "OK": if r.status_code != 200:
raise RuntimeError("Response is not \"OK\".") raise RuntimeError("Status code is not 200 OK (it's %s)." % r.status_code)
if force_text(r.text) != "OK":
raise RuntimeError('Response is not "OK" (it\'s "%s").' % r.text)
except Exception as e: except Exception as e:
# catch everything so we don't leak exceptions, convert them # catch everything so we don't leak exceptions, convert them

View file

@ -77,7 +77,7 @@ def notify(request, org, notification):
if request.method == "POST": if request.method == "POST":
def runscript(name): def runscript(name):
python = os.path.join(settings.BASE_DIR, "env", "bin", "python") python = os.path.join(os.path.dirname(settings.BASE_DIR), "env", "bin", "python")
cmd = [python, os.path.join(SYNC_BIN_PATH, name)] cmd = [python, os.path.join(SYNC_BIN_PATH, name)]
cmdstring = " ".join(cmd) cmdstring = " ".join(cmd)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

View file

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{# Copyright The IETF Trust 2016, All Rights Reserved #} {# Copyright The IETF Trust 2016-2020, All Rights Reserved #}
{% load origin %} {% load origin %}
{% load staticfiles %} {% load staticfiles %}
{% load ietf_filters %} {% load ietf_filters %}
@ -52,10 +52,10 @@
RFC - {{ doc.std_level }} RFC - {{ doc.std_level }}
({% if published %}{{ published.time|date:"F Y" }}{% else %}publication date unknown{% endif %}{% if has_errata %}; <a href="https://www.rfc-editor.org/errata_search.php?rfc={{ rfc_number }}" rel="nofollow">Errata</a>{% else %}; No errata{% endif %}) ({% if published %}{{ published.time|date:"F Y" }}{% else %}publication date unknown{% endif %}{% if has_errata %}; <a href="https://www.rfc-editor.org/errata_search.php?rfc={{ rfc_number }}" rel="nofollow">Errata</a>{% else %}; No errata{% endif %})
{% if obsoleted_by %}<div>Obsoleted by {{ obsoleted_by|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if obsoleted_by %}<div>Obsoleted by {{ obsoleted_by|urlize_doc_list|join:", " }}</div>{% endif %}
{% if updated_by %}<div>Updated by {{ updated_by|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if updated_by %}<div>Updated by {{ updated_by|urlize_doc_list|join:", " }}</div>{% endif %}
{% if obsoletes %}<div>Obsoletes {{ obsoletes|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if obsoletes %}<div>Obsoletes {{ obsoletes|urlize_doc_list|join:", " }}</div>{% endif %}
{% if updates %}<div>Updates {{ updates|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if updates %}<div> Updates {{ updates|urlize_doc_list|join:", " }}</div>{% endif %}
{% if status_changes %}<div>Status changed by {{ status_changes|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if status_changes %}<div>Status changed by {{ status_changes|join:", "|urlize_ietf_docs }}</div>{% endif %}
{% if proposed_status_changes %}<div>Proposed status changed by {{ proposed_status_changes|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if proposed_status_changes %}<div>Proposed status changed by {{ proposed_status_changes|join:", "|urlize_ietf_docs }}</div>{% endif %}
{% if rfc_aliases %}<div>Also known as {{ rfc_aliases|join:", "|urlize_ietf_docs }}</div>{% endif %} {% if rfc_aliases %}<div>Also known as {{ rfc_aliases|join:", "|urlize_ietf_docs }}</div>{% endif %}
@ -89,7 +89,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ replaces|join:", "|urlize_ietf_docs|default:"(None)" }} {{ replaces|urlize_doc_list|join:", "|default:"(None)" }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -100,7 +100,7 @@
<th>Replaced by</th> <th>Replaced by</th>
<td class="edit"></td> <td class="edit"></td>
<td> <td>
{{ replaced_by|join:", "|urlize_ietf_docs }} {{ replaced_by|urlize_doc_list|join:", " }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -116,7 +116,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ possibly_replaces|join:", "|urlize_ietf_docs }} {{ possibly_replaces|urlize_doc_list|join:", " }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -131,7 +131,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ possibly_replaced_by|join:", "|urlize_ietf_docs }} {{ possibly_replaced_by|urlize_doc_list|join:", " }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}

View file

@ -2,6 +2,7 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load ietf_filters %} {% load ietf_filters %}
{% load ballot_icon %} {% load ballot_icon %}
{% load person_filters %}
<tr {% spaceless %} <tr {% spaceless %}
{% if color_row_positions %} {% if color_row_positions %}
@ -120,9 +121,9 @@
{% if ad_name == None or ad_name != doc.ad.plain_name %} {% if ad_name == None or ad_name != doc.ad.plain_name %}
<td class="area-director"> <td class="area-director">
{% if doc.ad %} {% if doc.ad %}
<a title="Area Director" href="mailto:{{ doc.ad.email_address|urlencode }}">{{ doc.ad }}</a><br> {% person_link doc.ad title="Area Director" %}<br>
{% endif %} {% endif %}
{% if doc.shepherd %}<a title="Shepherd" href="mailto:{{doc.shepherd}}"><small class="text-muted">{{doc.shepherd.person.name}}</small></a>{% endif %} {% if doc.shepherd %}{% email_person_link doc.shepherd title="Shepherd" class="small text-muted" %}{% endif %}
</td> </td>
{% endif %} {% endif %}

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