Tweaked the IPR Details page to show the possible a), b), and c) choices under section 'V' when licensing declaration to be provided later has been chosen.

- Legacy-Id: 12791
This commit is contained in:
Henrik Levkowetz 2017-02-08 18:02:03 +00:00
parent ffb029300f
commit 15628c698f
14 changed files with 341 additions and 80 deletions

View file

@ -15,7 +15,8 @@
"respond": "~1",
"select2": "~3",
"select2-bootstrap-css": "~1",
"spin.js": "~2"
"spin.js": "~2",
"zxcvbn": "~4"
},
"devDependencies": {},
"overrides": {

View file

@ -1,4 +1,5 @@
import re
from unidecode import unidecode
from django import forms
from django.conf import settings
@ -8,7 +9,7 @@ from django.contrib.auth.models import User
from django.utils.html import mark_safe
from django.core.urlresolvers import reverse as urlreverse
from unidecode import unidecode
from django_password_strength.widgets import PasswordStrengthInput, PasswordConfirmationInput
import debug # pyflakes:ignore
@ -31,8 +32,8 @@ class RegistrationForm(forms.Form):
class PasswordForm(forms.Form):
password = forms.CharField(widget=forms.PasswordInput)
password_confirmation = forms.CharField(widget=forms.PasswordInput,
password = forms.CharField(widget=PasswordStrengthInput)
password_confirmation = forms.CharField(widget=PasswordConfirmationInput,
help_text="Enter the same password as above, for verification.")
def clean_password_confirmation(self):
@ -166,3 +167,28 @@ class WhitelistForm(forms.ModelForm):
exclude = ['by', 'time' ]
from django import forms
class ChangePasswordForm(forms.Form):
current_password = forms.CharField(widget=forms.PasswordInput)
new_password = forms.CharField(widget=PasswordStrengthInput)
new_password_confirmation = forms.CharField(widget=PasswordConfirmationInput)
def __init__(self, user, data=None):
self.user = user
super(ChangePasswordForm, self).__init__(data)
def clean_current_password(self):
password = self.cleaned_data.get('current_password', None)
if not self.user.check_password(password):
raise ValidationError('Invalid password')
def clean(self):
new_password = self.cleaned_data.get('new_password', None)
conf_password = self.cleaned_data.get('new_password_confirmation', None)
if not new_password == conf_password:
raise ValidationError("The password confirmation is different than the new password")

View file

@ -3,23 +3,20 @@
from django.conf.urls import url
from django.contrib.auth.views import login, logout
from ietf.ietfauth.views import add_account_whitelist
from ietf.ietfauth import views
urlpatterns = [
url(r'^$', 'ietf.ietfauth.views.index'),
# url(r'^login/$', 'ietf.ietfauth.views.ietf_login'),
url(r'^$', views.index),
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', views.confirm_new_email),
url(r'^create/$', views.create_account),
url(r'^create/confirm/(?P<auth>[^/]+)/$', views.confirm_account),
url(r'^login/$', login),
url(r'^logout/$', logout),
# url(r'^loggedin/$', 'ietf.ietfauth.views.ietf_loggedin'),
# url(r'^loggedout/$', 'ietf.ietfauth.views.logged_out'),
url(r'^profile/$', 'ietf.ietfauth.views.profile'),
# (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'ietf.ietfauth.views.url_login'),
url(r'^testemail/$', 'ietf.ietfauth.views.test_email'),
url(r'^create/$', 'ietf.ietfauth.views.create_account'),
url(r'^create/confirm/(?P<auth>[^/]+)/$', 'ietf.ietfauth.views.confirm_account'),
url(r'^reset/$', 'ietf.ietfauth.views.password_reset'),
url(r'^reset/confirm/(?P<auth>[^/]+)/$', 'ietf.ietfauth.views.confirm_password_reset'),
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', 'ietf.ietfauth.views.confirm_new_email'),
url(r'whitelist/add/?$', add_account_whitelist),
url(r'^review/$', 'ietf.ietfauth.views.review_overview'),
url(r'^password/$', views.change_password),
url(r'^profile/$', views.profile),
url(r'^reset/$', views.password_reset),
url(r'^reset/confirm/(?P<auth>[^/]+)/$', views.confirm_password_reset),
url(r'^review/$', views.review_overview),
url(r'^testemail/$', views.test_email),
url(r'whitelist/add/?$', views.add_account_whitelist),
]

View file

@ -32,24 +32,27 @@
# Copyright The IETF Trust 2007, All Rights Reserved
import importlib
from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
from collections import defaultdict
from django.conf import settings
from django.http import Http404 #, HttpResponse, HttpResponseRedirect
from django.shortcuts import render, redirect, get_object_or_404
#from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
from django.contrib.auth.decorators import login_required
#from django.utils.http import urlquote
import django.core.signing
from django.contrib.sites.models import Site
from django.contrib.auth.models import User
from django import forms
from django.contrib import messages
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse as urlreverse
from django.http import Http404, HttpResponseRedirect #, HttpResponse,
from django.shortcuts import render, redirect, get_object_or_404
import debug # pyflakes:ignore
from ietf.group.models import Role, Group
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm, ChangePasswordForm
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.ietfauth.utils import role_required
@ -465,3 +468,46 @@ def review_overview(request):
'review_wishes': review_wishes,
'review_wish_form': review_wish_form,
})
@login_required
def change_password(request):
success = False
person = None
try:
person = request.user.person
except Person.DoesNotExist:
return render(request, 'registration/missing_person.html')
emails = Email.objects.filter(person=person, active=True).order_by('-primary','-time').first
if request.method == 'POST':
user = request.user
form = ChangePasswordForm(user, request.POST)
if form.is_valid():
new_password = form.cleaned_data["new_password"]
user.set_password(new_password)
user.save()
# password is also stored in htpasswd file
update_htpasswd_file(user.username, new_password)
# keep the session
update_session_auth_hash(request, user)
messages.success(request, "Your password was successfully changed")
return HttpResponseRedirect(urlreverse('ietf.ietfauth.views.profile'))
else:
form = ChangePasswordForm(request.user)
hlibname, hashername = settings.PASSWORD_HASHERS[0].rsplit('.',1)
hlib = importlib.import_module(hlibname)
hasher = getattr(hlib, hashername)
return render(request, 'registration/change_password.html', {
'form': form,
'success': success,
'hasher': hasher,
})

View file

@ -32,6 +32,7 @@ from ietf.ipr.utils import (get_genitive, get_ipr_summary,
iprs_from_docs, related_docs)
from ietf.message.models import Message
from ietf.message.utils import infer_message
from ietf.name.models import IprLicenseTypeName
from ietf.person.models import Person
from ietf.secr.utils.document import get_rfc_num, is_draft
from ietf.utils.draft_search import normalize_draftname
@ -703,6 +704,7 @@ def get_details_tabs(ipr, selected):
('History', urlreverse('ipr_history', kwargs={ 'id': ipr.pk }))
]]
@debug.trace
def show(request, id):
"""View of individual declaration"""
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
@ -717,6 +719,7 @@ def show(request, id):
return render(request, "ipr/details_view.html", {
'ipr': ipr,
'tabs': get_details_tabs(ipr, 'Disclosure'),
'choices_abc': [ i.desc for i in IprLicenseTypeName.objects.filter(slug__in=['no-license', 'royalty-free', 'reasonable', ]) ],
'updates_iprs': ipr.relatedipr_source_set.all(),
'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted")
})

View file

@ -54,6 +54,13 @@ ADMINS = (
('Ryan Cross', 'rcross@amsl.com'),
)
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher',
]
ALLOWED_HOSTS = [".ietf.org", ".ietf.org.", "209.208.19.216", "4.31.198.44", ]
@ -296,11 +303,12 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
# External apps
'bootstrap3',
'django_markup',
'django_password_strength',
'djangobwr',
'form_utils',
'tastypie',
'widget_tweaks',
'django_markup',
# IETF apps
'ietf.api',
'ietf.community',
@ -782,7 +790,6 @@ SILENCED_SYSTEM_CHECKS = [
"fields.W342", # Setting unique=True on a ForeignKey has the same effect as using a OneToOneField.
]
# Put the production SECRET_KEY in settings_local.py, and also any other
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.
from settings_local import * # pyflakes:ignore pylint: disable=wildcard-import

View file

@ -0,0 +1,130 @@
// Taken from django-password-strength, with changes to use the bower-managed zxcvbn.js The
// bower-managed zxcvbn.js is kept up-to-date to a larger extent than the copy packaged with
// the django-password-strength component.
(function($, window, document, undefined){
window.djangoPasswordStrength = {
config: {
passwordClass: 'password_strength',
confirmationClass: 'password_confirmation'
},
init: function (config) {
var self = this;
// Setup configuration
if ($.isPlainObject(config)) {
$.extend(self.config, config);
}
self.initListeners();
},
initListeners: function() {
var self = this;
var body = $('body');
$('.' + self.config.passwordClass).on('keyup', function() {
var password_strength_bar = $(this).parent().find('.password_strength_bar');
var password_strength_info = $(this).parent().find('.password_strength_info');
if( $(this).val() ) {
var result = zxcvbn( $(this).val() );
if( result.score < 3 ) {
password_strength_bar.removeClass('progress-bar-success').addClass('progress-bar-warning');
password_strength_info.find('.label').removeClass('hidden');
} else {
password_strength_bar.removeClass('progress-bar-warning').addClass('progress-bar-success');
password_strength_info.find('.label').addClass('hidden');
}
password_strength_bar.width( ((result.score+1)/5)*100 + '%' ).attr('aria-valuenow', result.score + 1);
// henrik@levkowetz.com -- this is the only changed line:
password_strength_info.find('.password_strength_time').html(result.crack_times_display.online_no_throttling_10_per_second);
password_strength_info.removeClass('hidden');
} else {
password_strength_bar.removeClass('progress-bar-success').addClass('progress-bar-warning');
password_strength_bar.width( '0%' ).attr('aria-valuenow', 0);
password_strength_info.addClass('hidden');
}
self.match_passwords($(this));
});
var timer = null;
$('.' + self.config.confirmationClass).on('keyup', function() {
var password_field;
var confirm_with = $(this).data('confirm-with');
if( confirm_with ) {
password_field = $('#' + confirm_with);
} else {
password_field = $('.' + self.config.passwordClass);
}
if (timer !== null) clearTimeout(timer);
timer = setTimeout(function(){
self.match_passwords(password_field);
}, 400);
});
},
display_time: function(seconds) {
var minute = 60;
var hour = minute * 60;
var day = hour * 24;
var month = day * 31;
var year = month * 12;
var century = year * 100;
// Provide fake gettext for when it is not available
if( typeof gettext !== 'function' ) { gettext = function(text) { return text; }; };
if( seconds < minute ) return gettext('only an instant');
if( seconds < hour) return (1 + Math.ceil(seconds / minute)) + ' ' + gettext('minutes');
if( seconds < day) return (1 + Math.ceil(seconds / hour)) + ' ' + gettext('hours');
if( seconds < month) return (1 + Math.ceil(seconds / day)) + ' ' + gettext('days');
if( seconds < year) return (1 + Math.ceil(seconds / month)) + ' ' + gettext('months');
if( seconds < century) return (1 + Math.ceil(seconds / year)) + ' ' + gettext('years');
return gettext('centuries');
},
match_passwords: function(password_field, confirmation_fields) {
var self = this;
// Optional parameter: if no specific confirmation field is given, check all
if( confirmation_fields === undefined ) { confirmation_fields = $('.' + self.config.confirmationClass) }
if( confirmation_fields === undefined ) { return; }
var password = password_field.val();
confirmation_fields.each(function(index, confirm_field) {
var confirm_value = $(confirm_field).val();
var confirm_with = $(confirm_field).data('confirm-with');
if( confirm_with && confirm_with == password_field.attr('id')) {
if( confirm_value && password ) {
if (confirm_value === password) {
$(confirm_field).parent().find('.password_strength_info').addClass('hidden');
} else {
$(confirm_field).parent().find('.password_strength_info').removeClass('hidden');
}
} else {
$(confirm_field).parent().find('.password_strength_info').addClass('hidden');
}
}
});
// If a password field other than our own has been used, add the listener here
if( !password_field.hasClass(self.config.passwordClass) && !password_field.data('password-listener') ) {
password_field.on('keyup', function() {
self.match_passwords($(this));
});
password_field.data('password-listener', true);
}
}
};
// Call the init for backwards compatibility
djangoPasswordStrength.init();
})(jQuery, window, document);

View file

@ -16,14 +16,16 @@
{% else %}
{% if user.is_authenticated %}
<li><a rel="nofollow" href="/accounts/logout/" >Sign out</a></li>
<li><a rel="nofollow" href="/accounts/profile/">Edit profile</a></li>
<li><a rel="nofollow" href="/accounts/profile/">Account info</a></li>
{% else %}
<li><a rel="nofollow" href="/accounts/login/?next={{request.get_full_path|urlencode}}">Sign in</a></li>
<li><a rel="nofollow" href="/accounts/reset/">Password reset</a></li>
{% endif %}
{% endif %}
<li><a href="{% url "ietf.ietfauth.views.create_account" %}">{% if request.user.is_authenticated %}Manage account{% else %}New account{% endif %}</a></li>
{% if not request.user.is_authenticated %}
<li><a href="{% url "ietf.ietfauth.views.create_account" %}">New account</a></li>
{% endif %}
<li><a href="{%url "ietf.cookies.views.preferences" %}" rel="nofollow">Preferences</a></li>
{% if user|has_role:"Reviewer" %}

View file

@ -200,9 +200,19 @@
specification, is as follows(select one licensing declaration option only):
</p>
{% if ipr.licensing.slug == "provided-later" %}
<div>
Possible licencing choices a), b), and c) when Licencing Declaration to be Provided Later:
<ul style="list-style: none">
{% for desc in choices_abc %}
<li>{{ desc}}</li>
{% endfor %}
</ul>
</p>
{% endif %}
<dl class="dl-horizontal">
<dt>Licensing</dt>
<dd>{% if ipr.licensing.slug == "later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":43" }}{% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}</dd>
<dd>{% if ipr.licensing.slug == "provided-later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":117" }}){% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}</dd>
<dt>Licensing information, comments, notes, or URL for further information</dt>
<dd>{{ ipr.licensing_comments|default:"(No information submitted)"|linebreaks }}</dd>

View file

@ -3,22 +3,28 @@
{% load origin %}
{% load bootstrap3 %}
{% load staticfiles %}
{% block title %}Change password{% endblock %}
{% block title %}Account creation{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript" src="{% static 'zxcvbn/zxcvbn.js' %}"></script>
<script type="text/javascript" src="{% static 'ietf/js/password_strength.js' %}"></script>
{% endblock %}
{% block content %}
{% origin %}
{% if success %}
<h1>Password change successful</h1>
<p>Your password has been updated.</p>
<a type="a" class="btn btn-primary" href="/accounts/login/" rel="nofollow">Sign in</a>
<h1>Your password was successfully changed.</h1>
{% else %}
<div class="row">
<div class="col-md-2 col-sm-0"></div>
<div class="col-md-8 col-sm-12">
<h1>Change password</h1>
<p>You can change the password below for your user {{ username }} below.</p>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
@ -28,5 +34,25 @@
{% endbuttons %}
</form>
<div class="help-block">
This password change form uses the
<a href="https://blogs.dropbox.com/tech/2012/04/zxcvbn-realistic-password-strength-estimation/">zxcvbn</a>
password strength estimator to give an indication of password strength.
The crack times given assume online attack without rate limiting,
at a rate of 10 attempts per second.
</div>
<div class="help-block">
The datatracker currently uses a <b>{{ hasher.algorithm }}</b>-based
password hasher with
<b>{% if hasher.iterations %}{{ hasher.iterations }} iterations{% else %}{{ hasher.rounds }} rounds{% endif %}</b>.
Calculating offline attack time if password hashes wouldleak is left
as an excercise for the reader.
</div>
</div>
<div class="col-md-2 col-sm-0"></div>
</div>
{% endif %}
{% endblock %}

View file

@ -18,22 +18,9 @@
{% else %}
<div class="row">
<div class="col-md-6">
<div class="col-md-2 col-sm-0"></div>
<div class="col-md-8 col-sm-12">
<h1>Account creation</h1>
<p>Please enter your email address in order to create a new datatracker account.</p>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Create account</button>
{% endbuttons %}
</form>
</div>
<div class="col-md-6">
<h1>Other options</h1>
<p>
<b>If you already have an account and want to use a new email address,</b>
please go to your account profile page and
@ -49,7 +36,20 @@
<p>
<a class="btn btn-warning" href="{% url "ietf.ietfauth.views.password_reset" %}">Reset your password</a>
</p>
<hr>
<p>Please enter your email address in order to create your datatracker account.</p>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Create account</button>
{% endbuttons %}
</form>
</div>
<div class="col-md-2 col-sm-0"></div>
</div>
{% endif %}
{% endblock %}

View file

@ -22,6 +22,13 @@
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Password</label>
<div class="col-sm-10">
<p class="form-control-static"><a href="{% url 'ietf.ietfauth.views.change_password' %}">Password change form</a></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Email addresses</label>
<div class="col-sm-10">

View file

@ -8,6 +8,8 @@
{% block content %}
{% origin %}
<div class="col-md-2 col-sm-0"></div>
<div class="col-md-8 col-sm-12">
<h1>Sign in</h1>
<form method="post">
@ -27,5 +29,6 @@
</table>
{% endbuttons %}
</form>
</div>
<div class="col-md-2 col-sm-0"></div>
{% endblock %}

View file

@ -8,9 +8,11 @@ coverage>=4.0.1,!=4.0.2
decorator>=3.4.0
defusedxml>=0.4.1 # for TastyPie when ussing xml; not a declared dependency
Django>=1.9,<1.10
django-bcrypt>=0.9.2
django-bootstrap3>=7.0
django-formtools>=1.0 # instead of django.contrib.formtools in 1.8
django-markup>=1.1
django-password-strength>=1.2.1
django-tastypie>=0.13.1
django-widget-tweaks>=1.3
docutils>=0.12
@ -43,3 +45,4 @@ Unidecode>=0.4.18
#wsgiref>=0.1.2
xml2rfc>=2.5.
xym>=0.1.2,!=0.3
zxcvbn-python>=4.4.14