misc: allow email domains suggestions (#40166)

This commit is contained in:
Serghei Mihai 2020-06-11 10:58:23 +02:00
parent 162a984897
commit dc7bce7c05
6 changed files with 160 additions and 2 deletions

View File

@ -238,6 +238,11 @@ default_settings = dict(
A2_AUTH_PASSWORD_ENABLE=Setting(
default=True,
definition='Activate login/password authentication', names=('AUTH_PASSWORD',)),
A2_SUGGESTED_EMAIL_DOMAINS=Setting(
default=['gmail.com', 'msn.com', 'hotmail.com', 'hotmail.fr',
'wanadoo.fr', 'yahoo.fr', 'yahoo.com', 'laposte.fr',
'free.fr', 'orange.fr', 'numericable.fr'],
definition='List of suggested email domains'),
A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(
default=0,
definition='Failure count before logging a warning to '

View File

@ -25,7 +25,8 @@ from django.core.files import File
from authentic2 import app_settings
from authentic2.passwords import password_help_text, validate_password
from authentic2.forms.widgets import (PasswordInput, NewPasswordInput,
CheckPasswordInput, ProfileImageInput)
CheckPasswordInput, ProfileImageInput,
EmailInput)
from authentic2.validators import email_validator
import PIL.Image
@ -116,3 +117,4 @@ class ProfileImageField(FileField):
class ValidatedEmailField(EmailField):
default_validators = [email_validator]
widget = EmailInput

View File

@ -29,9 +29,10 @@ import uuid
import datetime
import django
from django import forms
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, ClearableFileInput
from django.forms.widgets import PasswordInput as BasePasswordInput
from django.forms.widgets import TextInput
from django.forms.widgets import TextInput, EmailInput as BaseEmailInput
from django.utils.formats import get_language, get_format
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@ -337,3 +338,24 @@ class DatalistTextInput(TextInput):
class PhoneNumberInput(TextInput):
input_type = 'tel'
class EmailInput(BaseEmailInput):
template_name = 'authentic2/widgets/email.html'
@property
def media(self):
if app_settings.A2_SUGGESTED_EMAIL_DOMAINS:
return forms.Media(
js = (
xstatic('jquery', 'jquery.min.js'),
'authentic2/js/email_domains_suggestions.js',
)
)
def get_context(self, *args, **kwargs):
context = super(EmailInput, self).get_context(*args, **kwargs)
if app_settings.A2_SUGGESTED_EMAIL_DOMAINS:
context['widget']['attrs']['data-suggested-domains'] = ':'.join(app_settings.A2_SUGGESTED_EMAIL_DOMAINS)
context['domains_suggested'] = True
return context

View File

@ -0,0 +1,105 @@
String.prototype.similarity = function(string) {
// adapted from https://github.com/jordanthomas/jaro-winkler (licensed as MIT)
var s1 = this, s2 = string;
var m = 0;
var i;
var j;
// Exit early if either are empty.
if (s1.length === 0 || s2.length === 0) {
return 0;
}
// Convert to upper
s1 = s1.toUpperCase();
s2 = s2.toUpperCase();
// Exit early if they're an exact match.
if (s1 === s2) {
return 1;
}
var range = (Math.floor(Math.max(s1.length, s2.length) / 2)) - 1;
var s1Matches = new Array(s1.length);
var s2Matches = new Array(s2.length);
for (i = 0; i < s1.length; i++) {
var low = (i >= range) ? i - range : 0;
var high = (i + range <= (s2.length - 1)) ? (i + range) : (s2.length - 1);
for (j = low; j <= high; j++) {
if (s1Matches[i] !== true && s2Matches[j] !== true && s1[i] === s2[j]) {
++m;
s1Matches[i] = s2Matches[j] = true;
break;
}
}
}
// Exit early if no matches were found.
if (m === 0) {
return 0;
}
// Count the transpositions.
var k = 0;
var numTrans = 0;
for (i = 0; i < s1.length; i++) {
if (s1Matches[i] === true) {
for (j = k; j < s2.length; j++) {
if (s2Matches[j] === true) {
k = j + 1;
break;
}
}
if (s1[i] !== s2[j]) {
++numTrans;
}
}
}
var weight = (m / s1.length + m / s2.length + (m - (numTrans / 2)) / m) / 3;
var l = 0;
var p = 0.1;
if (weight > 0.7) {
while (s1[l] === s2[l] && l < 4) {
++l;
}
weight = weight + l * p * (1 - weight);
}
return weight;
}
$(function() {
$('input[type=email]').on('change', function() {
var $email_input = $(this);
var suggested_domains = $(this).data('suggested-domains').split(':');
var val = $email_input.val();
var val_domain = val.split('@')[1];
var highest_ratio = 0;
var suggestion = null;
for (var i=0; i < suggested_domains.length; i++) {
var domain = suggested_domains[i];
var ratio = val_domain.similarity(domain);
if (ratio > highest_ratio) {
highest_ratio = ratio;
suggestion = domain;
}
}
if (highest_ratio > 0.80 && highest_ratio < 1) {
$domain_hint_div = $('div.field-live-hint')
$domain_hint_div.find('button.action').on('click', function() {
$email_input.val($email_input.val().replace(/@.*/, '@' + $(this).data('suggestion')));
$email_input.trigger('change');
$domain_hint_div.hide();
return false;
});
$domain_hint_div.find('button.close').on('click', function() {
$domain_hint_div.hide();
return false;
});
$domain_hint_div.find('> span').text($domain_hint_div.find('> span').data('hint-prefix') + ' @' + suggestion + ' ?');
$domain_hint_div.find('button.action').data('suggestion', suggestion);
$domain_hint_div.show();
} else if ($domain_hint_div) {
$domain_hint_div.hide();
}
});
})

View File

@ -0,0 +1,11 @@
{% load i18n %}
{% include "django/forms/widgets/email.html" %}
{% if domains_suggested %}
<div class="field-live-hint" style="display: none">
<span class="message" data-hint-prefix="{% trans "Did you want to write" %}"></span>
<button class="action">{% trans "Apply fix" %}</button>
<button class="close">
<span class="sr-only">x</span>
</button>
</div>
{% endif %}

View File

@ -666,3 +666,16 @@ def test_authentication_method(app, db, rf, hooks):
assert hooks.calls['event'][-2]['kwargs']['authentication_method'] == 'another'
assert hooks.calls['event'][-1]['kwargs']['name'] == 'login'
assert hooks.calls['event'][-1]['kwargs']['how'] == 'another'
def test_registration_with_email_suggestions(app, db, settings):
url = utils.make_url('registration_register')
response = app.get(url)
assert "email_domains_suggestions.js" in response.text
assert "field-live-hint" in response.text
settings.A2_SUGGESTED_EMAIL_DOMAINS = []
response = app.get(url)
assert "email_domains_suggestions.js" not in response.text
assert "field-live-hint" not in response.text