Undid last commit

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

View file

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

View file

@ -1,5 +1,4 @@
import re import re
from unidecode import unidecode
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@ -9,7 +8,7 @@ from django.contrib.auth.models import User
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.core.urlresolvers import reverse as urlreverse from django.core.urlresolvers import reverse as urlreverse
from django_password_strength.widgets import PasswordStrengthInput, PasswordConfirmationInput from unidecode import unidecode
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -32,8 +31,8 @@ class RegistrationForm(forms.Form):
class PasswordForm(forms.Form): class PasswordForm(forms.Form):
password = forms.CharField(widget=PasswordStrengthInput) password = forms.CharField(widget=forms.PasswordInput)
password_confirmation = forms.CharField(widget=PasswordConfirmationInput, password_confirmation = forms.CharField(widget=forms.PasswordInput,
help_text="Enter the same password as above, for verification.") help_text="Enter the same password as above, for verification.")
def clean_password_confirmation(self): def clean_password_confirmation(self):
@ -167,28 +166,3 @@ class WhitelistForm(forms.ModelForm):
exclude = ['by', 'time' ] 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,20 +3,23 @@
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth.views import login, logout from django.contrib.auth.views import login, logout
from ietf.ietfauth import views from ietf.ietfauth.views import add_account_whitelist
urlpatterns = [ urlpatterns = [
url(r'^$', views.index), url(r'^$', 'ietf.ietfauth.views.index'),
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', views.confirm_new_email), # url(r'^login/$', 'ietf.ietfauth.views.ietf_login'),
url(r'^create/$', views.create_account),
url(r'^create/confirm/(?P<auth>[^/]+)/$', views.confirm_account),
url(r'^login/$', login), url(r'^login/$', login),
url(r'^logout/$', logout), url(r'^logout/$', logout),
url(r'^password/$', views.change_password), # url(r'^loggedin/$', 'ietf.ietfauth.views.ietf_loggedin'),
url(r'^profile/$', views.profile), # url(r'^loggedout/$', 'ietf.ietfauth.views.logged_out'),
url(r'^reset/$', views.password_reset), url(r'^profile/$', 'ietf.ietfauth.views.profile'),
url(r'^reset/confirm/(?P<auth>[^/]+)/$', views.confirm_password_reset), # (r'^login/(?P<user>[a-z0-9.@]+)/(?P<passwd>.+)$', 'ietf.ietfauth.views.url_login'),
url(r'^review/$', views.review_overview), url(r'^testemail/$', 'ietf.ietfauth.views.test_email'),
url(r'^testemail/$', views.test_email), url(r'^create/$', 'ietf.ietfauth.views.create_account'),
url(r'whitelist/add/?$', views.add_account_whitelist), 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'),
] ]

View file

@ -32,27 +32,24 @@
# Copyright The IETF Trust 2007, All Rights Reserved # Copyright The IETF Trust 2007, All Rights Reserved
import importlib
from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
from collections import defaultdict from collections import defaultdict
import django.core.signing
from django import forms
from django.contrib import messages
from django.conf import settings from django.conf import settings
from django.contrib.auth import update_session_auth_hash from django.http import Http404 #, HttpResponse, HttpResponseRedirect
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 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
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.group.models import Role, Group from ietf.group.models import Role, Group
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm, ChangePasswordForm from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
from ietf.ietfauth.htpasswd import update_htpasswd_file from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.ietfauth.utils import role_required from ietf.ietfauth.utils import role_required
@ -468,46 +465,3 @@ def review_overview(request):
'review_wishes': review_wishes, 'review_wishes': review_wishes,
'review_wish_form': review_wish_form, '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,7 +32,6 @@ from ietf.ipr.utils import (get_genitive, get_ipr_summary,
iprs_from_docs, related_docs) iprs_from_docs, related_docs)
from ietf.message.models import Message from ietf.message.models import Message
from ietf.message.utils import infer_message from ietf.message.utils import infer_message
from ietf.name.models import IprLicenseTypeName
from ietf.person.models import Person from ietf.person.models import Person
from ietf.secr.utils.document import get_rfc_num, is_draft from ietf.secr.utils.document import get_rfc_num, is_draft
from ietf.utils.draft_search import normalize_draftname from ietf.utils.draft_search import normalize_draftname
@ -704,7 +703,6 @@ def get_details_tabs(ipr, selected):
('History', urlreverse('ipr_history', kwargs={ 'id': ipr.pk })) ('History', urlreverse('ipr_history', kwargs={ 'id': ipr.pk }))
]] ]]
@debug.trace
def show(request, id): def show(request, id):
"""View of individual declaration""" """View of individual declaration"""
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child() ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
@ -719,7 +717,6 @@ def show(request, id):
return render(request, "ipr/details_view.html", { return render(request, "ipr/details_view.html", {
'ipr': ipr, 'ipr': ipr,
'tabs': get_details_tabs(ipr, 'Disclosure'), '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(), 'updates_iprs': ipr.relatedipr_source_set.all(),
'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted") 'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted")
}) })

View file

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

View file

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

View file

@ -200,19 +200,9 @@
specification, is as follows(select one licensing declaration option only): specification, is as follows(select one licensing declaration option only):
</p> </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"> <dl class="dl-horizontal">
<dt>Licensing</dt> <dt>Licensing</dt>
<dd>{% if ipr.licensing.slug == "provided-later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":117" }}){% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}</dd> <dd>{% if ipr.licensing.slug == "later" %}{{ ipr.licensing.desc|slice:"2:"|slice:":43" }}{% else %}{{ ipr.licensing.desc|slice:"2:" }}{% endif %}</dd>
<dt>Licensing information, comments, notes, or URL for further information</dt> <dt>Licensing information, comments, notes, or URL for further information</dt>
<dd>{{ ipr.licensing_comments|default:"(No information submitted)"|linebreaks }}</dd> <dd>{{ ipr.licensing_comments|default:"(No information submitted)"|linebreaks }}</dd>

View file

@ -3,56 +3,30 @@
{% load origin %} {% load origin %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles %}
{% block title %}Account creation{% endblock %} {% block title %}Change password{% 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 %} {% block content %}
{% origin %} {% origin %}
{% if success %} {% if success %}
<h1>Your password was successfully changed.</h1> <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>
{% else %} {% else %}
<div class="row"> <h1>Change password</h1>
<div class="col-md-2 col-sm-0"></div>
<div class="col-md-8 col-sm-12">
<h1>Change password</h1>
<form method="post"> <p>You can change the password below for your user {{ username }} below.</p>
{% csrf_token %} <form method="post">
{% bootstrap_form form %} {% csrf_token %}
{% bootstrap_form form %}
{% buttons %} {% buttons %}
<button type="submit" class="btn btn-primary">Change password</button> <button type="submit" class="btn btn-primary">Change password</button>
{% endbuttons %} {% endbuttons %}
</form> </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 %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -18,9 +18,22 @@
{% else %} {% else %}
<div class="row"> <div class="row">
<div class="col-md-2 col-sm-0"></div> <div class="col-md-6">
<div class="col-md-8 col-sm-12">
<h1>Account creation</h1> <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> <p>
<b>If you already have an account and want to use a new email address,</b> <b>If you already have an account and want to use a new email address,</b>
please go to your account profile page and please go to your account profile page and
@ -36,20 +49,7 @@
<p> <p>
<a class="btn btn-warning" href="{% url "ietf.ietfauth.views.password_reset" %}">Reset your password</a> <a class="btn btn-warning" href="{% url "ietf.ietfauth.views.password_reset" %}">Reset your password</a>
</p> </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>
<div class="col-md-2 col-sm-0"></div>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -22,13 +22,6 @@
</div> </div>
</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"> <div class="form-group">
<label class="col-sm-2 control-label">Email addresses</label> <label class="col-sm-2 control-label">Email addresses</label>
<div class="col-sm-10"> <div class="col-sm-10">

View file

@ -8,27 +8,24 @@
{% block content %} {% block content %}
{% origin %} {% origin %}
<div class="col-md-2 col-sm-0"></div> <h1>Sign in</h1>
<div class="col-md-8 col-sm-12">
<h1>Sign in</h1>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% buttons %}
<table class="col-md-12">
<tr>
<td >
<button type="submit" class="btn btn-primary">Sign in</button>
</td>
<td >
Forgot your password? <a href="{% url 'ietf.ietfauth.views.password_reset' %}">Request a reset</a>.
</td>
</tr>
</table>
{% endbuttons %}
</form>
{% buttons %}
<table class="col-md-12">
<tr>
<td >
<button type="submit" class="btn btn-primary">Sign in</button>
</td>
<td >
Forgot your password? <a href="{% url 'ietf.ietfauth.views.password_reset' %}">Request a reset</a>.
</td>
</tr>
</table>
{% endbuttons %}
</form>
</div>
<div class="col-md-2 col-sm-0"></div>
{% endblock %} {% endblock %}

View file

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