refactor user module and its logic for authentication logic

remotes/origin/HEAD
Max Yakovenko 8 years ago
parent bb67cd3461
commit b962c7973e
  1. 0
      accounts_ext/__init__.py
  2. 26
      accounts_ext/admin.py
  3. 7
      accounts_ext/apps.py
  4. 298
      accounts_ext/forms.py
  5. 172
      accounts_ext/models.py
  6. 0
      accounts_ext/templatetags/__init__.py
  7. 7
      accounts_ext/templatetags/accounts_ext_filters.py
  8. 3
      accounts_ext/tests.py
  9. 14
      accounts_ext/urls.py
  10. 20
      accounts_ext/utils.py
  11. 203
      accounts_ext/views.py

@ -0,0 +1,26 @@
from django.contrib import admin
from .models import Profile
# def delete_model(modeladmin, request, queryset):
# for obj in queryset:
# user_profile = obj.user.profile
# if user_profile.user_points >= obj.points:
# user_profile.user_points -= obj.points
# user_profile.save()
# obj.delete()
#
# delete_model.short_description = "Удалить как исполненный"
# class PickUpRequestAdmin(admin.ModelAdmin):
# list_display = [field.name for field in PickUpRequest._meta.fields]
# actions = [delete_model]
# class Meta:
# model = PickUpRequest
#
# class UserProfileAdmin(admin.ModelAdmin):
# list_display = [field.name for field in Profile._meta.fields]
# class Meta:
# model = Profile
# admin.site.register(Profile, UserProfileAdmin)
# admin.site.register(PickUpRequest, PickUpRequestAdmin)

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class AccountsExtConfig(AppConfig):
name = 'accounts_ext'
verbose_name = _("Authentication and Authorization")

@ -0,0 +1,298 @@
import logging
from captcha.fields import CaptchaField
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Div, Submit, HTML
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model, password_validation
from django.core.exceptions import ObjectDoesNotExist
from django.forms import inlineformset_factory
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from registration.forms import RegistrationFormUniqueEmail
from django.contrib.auth.forms import (
AuthenticationForm as AuthenticationFormBase,
PasswordResetForm as PasswordResetFormBase,
PasswordChangeForm as PasswordChangeFormBase
)
from core.models import STATUS_ACTIVE
from .models import User, Profile, Company
logger = logging.getLogger(__name__)
# class PickUpPointsForm(layout.ModelForm):
# class Meta:'
# model = PickUpRequest
# fields = ('requisites',)
class RegistrationForm(RegistrationFormUniqueEmail):
email = forms.EmailField(label=_("E-mail"), widget=forms.EmailInput(attrs={'class': 'reg__text'}))
password1 = forms.CharField(
label=_('Пароль'),
strip=False,
widget=forms.PasswordInput(attrs={'class': 'reg__text-label'}),
help_text=password_validation.password_validators_help_text_html(),
)
password2 = forms.CharField(
label=_('Подтверждение пароля'),
widget=forms.PasswordInput(attrs={'class': 'reg__text-label'}),
strip=False,
help_text=_('Для потверждения пароля введите его еще раз'),
)
first_name = forms.CharField(label=_("Имя"), max_length=255, widget=forms.TextInput(attrs={'class': 'reg__text'}))
last_name = forms.CharField(label=_("Фамилия"), max_length=255,
widget=forms.TextInput(attrs={'class': 'reg__text'}))
patronymic = forms.CharField(label=_("Отчество"), max_length=255,
widget=forms.TextInput(attrs={'class': 'reg__text'}))
agreement = forms.BooleanField()
captcha = CaptchaField(required=not settings.DEBUG)
title = _('Регистрация')
def save(self, commit=True):
user = super().save(commit)
profile = Profile.objects.filter(user=user).first()
if profile:
profile.first_name = self.cleaned_data.get('first_name')
profile.last_name = self.cleaned_data.get('last_name')
profile.patronymic = self.cleaned_data.get('patronymic')
profile.save()
else:
logger.error(
msg="User {user_email} missing profile object".format({'user_email': user.email})
)
return user
class Meta:
model = User
fields = ('username',)
labels = {
'username': _('Логин'),
}
help_texts = {
'username': _('Не менее 5х символов'),
}
widgets = {
'username': forms.TextInput(attrs={'class': 'reg__text-label'}),
}
class RegistrationCompanyForm(forms.ModelForm):
is_individual = forms.BooleanField(label=_('Я физическое лицо'), required=False)
def save(self, user, commit=True):
self.instance.user = user
return super().save(commit)
class Meta:
model = Company
fields = ('company_name', 'address', 'inn', 'ogrn')
labels = {
'company_name': _('Наименование'),
'inn': _('ИНН'),
'ogrn': _('ОГРН'),
'address': _('Юридический адрес')
}
widgets = {
'company_name': forms.TextInput(attrs={'class': 'reg__text'}),
'inn': forms.TextInput(attrs={'class': 'reg__text'}),
'ogrn': forms.TextInput(attrs={'class': 'reg__text'}),
'address': forms.Textarea(attrs={'class': 'reg__text', 'rows': 5, 'cols': 40})
}
class AuthenticationForm(AuthenticationFormBase):
username = forms.CharField(max_length=255, widget=forms.TextInput(attrs={
'class': 'reg__text',
'style': 'text-align:center',
'placeholder': _('Логин')
}))
password = forms.CharField(max_length=255, widget=forms.PasswordInput(attrs={
'class': 'reg__text',
'style': 'text-align:center',
'placeholder': _('Пароль')
}))
field_template = 'bootstrap/forms/authentication.html'
title = _('Вход')
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
self.helper.layout = Layout(
Field('username', template=self.field_template),
Field('password', template=self.field_template),
Div(
HTML('<a class="header__link" href="{}">{}</a>'.format(
reverse_lazy('accounts_ext:reset_password'),
_('Забыли пароль ?'))
),
css_class="text-right"
),
Div(
Div(
Submit('submit', _('Войти'), css_class='btn-danger'),
css_class='col-lg-12 text-center'
),
css_class='row'
),
)
super().__init__(*args, **kwargs)
class PasswordResetRequestForm(PasswordResetFormBase):
field_template = 'bootstrap/forms/authentication.html'
email_slug = 'reset_password'
title = _('Сброс пароля')
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
self.helper.form_action = reverse_lazy('accounts_ext:reset_password')
self.helper.layout = Layout(
Field('email', css_class='reg__text', template=self.field_template, placeholder="example@email.com"),
Div(
Div(
Submit('submit', _('Сбросить'), css_class='btn-danger'),
css_class='col-lg-12 text-center'
),
css_class="row"
),
)
super().__init__(*args, **kwargs)
def get_users(self, email):
active_users = get_user_model()._default_manager.filter(**{
'%s__iexact' % get_user_model().get_email_field_name(): email,
'status': STATUS_ACTIVE,
})
return (u for u in active_users if u.has_usable_password())
def send_mail(self, subject_template_name, email_template_name, context, from_email, to_email,
html_email_template_name=None):
try:
# mail_template = MailTemplate.objects.filter(slug=self.email_slug).first()
kwargs = {
'subject': subject_template_name,
# 'message': get_template(html_email_template_name).templatetags.source
}
if not kwargs:
kwargs = {
'slug': self.email_slug,
'name': email_template_name,
'num_of_retries': 3,
'is_html': True
}
# MailTemplate.objects.create(**kwargs)
else:
pass
# mail_template.subject = kwargs.get('subject', mail_template.subject)
# mail_template.message = kwargs.get('message', mail_template.message)
# mail_template.save()
user = User.active.filter(email__iexact=to_email).first()
first_name = user.profile.first_name or ""
last_name = user.profile.last_name or ""
full_name = first_name + ' ' + last_name
if len(full_name.strip()) != 0:
context.update({'username': full_name})
# send_db_mail(
# self.email_slug,
# to_email,
# subject_template_name,
# context,
# from_email=from_email,
# retry=True,
# retry_delay=300,
# max_retries=3,
# send_at_date=datetime.now(),
# use_celery=celery_supported()
# )
except ObjectDoesNotExist as e:
logger.critical(e)
def clean(self):
return super().clean()
class PasswordResetForm(PasswordChangeFormBase):
field_template = 'bootstrap/field.html'
field_order = ['new_password1', 'new_password2']
title = _('Сброс пароля')
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
self.helper = FormHelper()
self.helper.form_action = self.request.path
self.helper.layout = Layout(
Field('new_password1', template=self.field_template),
Field('new_password2', template=self.field_template),
Div(
Div(
Submit('submit', _('Сброс'), css_class='btn-danger'),
css_class='col-lg-12 text-center'
),
css_class="row"
),
)
super().__init__(kwargs.pop('user'), *args, **kwargs)
self.fields['new_password1'].help_text = self.fields['new_password1'].help_text.replace('<ul>',
'<ul class="required-list">')
def is_valid(self):
valid = super().is_valid()
if not valid:
if 'old_password' in self.errors:
self.errors.pop('old_password')
valid = super().is_valid()
return valid
class ProfileForm(forms.Form):
email = forms.CharField(required=False)
field_template = 'bootstrap/field_admin.html'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_action = reverse('accounts_ext:update')
self.helper.form_class = 'formajax form-replace-block'
self.helper.layout = Layout(
Div(
Field('email', template=self.field_template),
Field('first_name', template=self.field_template),
Field('last_name', template=self.field_template),
Field('patronymic', template=self.field_template),
Field('birthday', template=self.field_template),
Field('company_name', template=self.field_template),
Field('inn', template=self.field_template),
Field('ogrn', template=self.field_template),
Field('legal_address', template=self.field_template),
css_class="setProfile"
),
Div(
Submit('submit', _('Save'), css_class='button button--bigOrange'),
css_class="settings-button"
),
)
self.initial['email'] = self.instance.user.email
self.fields['email'].widget.attrs['readonly'] = True
self.fields['birthday'].widget.attrs['class'] = 'date inputText'
def clean_email(self):
instance = getattr(self, 'instance', None)
if instance and instance.pk:
return instance.user.email
else:
return self.cleaned_data['email']
class Meta:
model = Profile
exclude = ('user', 'create_at', 'updated_at', 'status',)

@ -0,0 +1,172 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import (
Group as GroupBase, AbstractBaseUser, BaseUserManager, PermissionsMixin
)
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.db import models, transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now as datetime_now
from registration.signals import user_activated
from core.models import AbstractStatusModel, ActualOnlyManager, STATUS_ACTIVE
# Create your models here.
class CaseInsensitiveQuerySet(models.QuerySet):
CASE_INSENSITIVE_FIELDS = ('email',)
def _filter_or_exclude(self, negate, *args, **kwargs):
for field in self.CASE_INSENSITIVE_FIELDS:
if field in kwargs:
kwargs[field + '__iexact'] = kwargs[field]
del kwargs[field]
return super()._filter_or_exclude(negate, *args, **kwargs)
class UserManager(ActualOnlyManager, BaseUserManager):
def create_superuser(self, email, password):
user = self.model(
email=email,
is_superuser=True,
status=STATUS_ACTIVE,
)
user.set_password(password)
user.save(using=self._db)
return user
def get_queryset(self):
return CaseInsensitiveQuerySet(self.model)
class User(AbstractStatusModel, PermissionsMixin, AbstractBaseUser):
username = models.CharField(_('username'),max_length=255, blank=False, null=False)
email = models.EmailField(
_('email'),
unique=True,
error_messages={
'unique': _("A user with that email already exists."),
},
)
# referral = models.ForeignKey('referral.Referral',
# verbose_name=_('Referral code'),
# on_delete=models.DO_NOTHING,
# blank=True, null=True,
# related_name='ref_code')
referral_user = models.ForeignKey('self',
verbose_name=_('Referral user'),
on_delete=models.DO_NOTHING,
blank=True, null=True,
related_name='ref_user')
confirmed_at = models.DateTimeField(_('email confirmed at'), blank=True, null=True)
USERNAME_FIELD = 'email'
objects = UserManager()
@property
def is_staff(self):
return self.is_superuser
@property
def date_joined(self):
return self.create_at
@date_joined.setter
def date_joined(self, value):
self.create_at = value
def get_short_name(self):
return self.email
class Group(GroupBase):
class Meta:
proxy = True
verbose_name = GroupBase._meta.verbose_name
verbose_name_plural = GroupBase._meta.verbose_name_plural
class Profile(AbstractStatusModel):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)
first_name = models.CharField(_('first name'), max_length=100, blank=True, null=True)
last_name = models.CharField(_('last name'), max_length=100, blank=True, null=True)
patronymic = models.CharField(_('patronymic'), max_length=100, blank=True, null=True)
birthday = models.DateField(_('birthday'), null=True, blank=True)
phone_regex = RegexValidator(regex=r'^\((+7)|8)?\d{10}$',
message="Phone number must be entered in the format: '+99999999999'. Up to 12 digits allowed.")
phone = models.CharField(validators=[phone_regex], max_length=12, blank=True, null=True)
@property
def is_valid(self):
return self.first_name and self.last_name and self.patronymic
@property
def is_complete(self):
return self.first_name and self.last_name and self.patronymic and self.birthday
class Meta:
verbose_name = _('profile')
verbose_name_plural = _('profiles')
def __str__(self):
return str(self.user)
COMPANY_STATUS_LEGAL = 25
COMPANY_STATUS_INDIVIDUAL = 50
COMPANY_STATUS_LIST = (
(COMPANY_STATUS_INDIVIDUAL, _("Legal")),
(COMPANY_STATUS_INDIVIDUAL, _("Individual"))
)
class Company(AbstractStatusModel):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)
company_name = models.CharField(max_length=255, blank=True, null=True)
address = models.TextField(blank=True,null=True)
inn = models.CharField(max_length=12, blank=True, null=True)
ogrn = models.CharField(max_length=13, blank=True, null=True)
status = models.SmallIntegerField(choices=COMPANY_STATUS_LIST, blank=True, null=True)
@property
def is_individual(self):
return self.status == COMPANY_STATUS_INDIVIDUAL
@property
def is_legal(self):
return self.status == COMPANY_STATUS_LEGAL
def __str__(self):
return self.company_name
class Meta:
verbose_name = _('company')
verbose_name_plural = _('companies')
@receiver(post_save, sender=get_user_model())
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=get_user_model())
def save_user_profile(sender, instance, **kwargs):
try:
instance.profile.save()
except ObjectDoesNotExist:
Profile.objects.create(user=instance)
@receiver(user_activated)
def save_activation_date(sender, user, request, **kwargs):
user.confirmed_at = datetime_now()
user.is_active = user.status == STATUS_ACTIVE
user.save()

@ -0,0 +1,7 @@
from django.template import Library
register = Library()
@register.filter(name='get_by_key')
def get_by_key(dict, key):
return getattr(dict,key,None)

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,14 @@
from django.urls import re_path
from . import views
urlpatterns = [
re_path(r'^login/$', views.LoginView.as_view(), name='login'),
re_path(r'^logout/$', views.LogoutView.as_view(), name='logout'),
re_path(r'^register/$', views.RegistrationView.as_view(), name='register'),
re_path(r'^password/reset/$', views.ResetPasswordView.as_view(), name='reset_password'),
re_path(r'^password/reset/done/$', views.ResetPasswordDoneView.as_view(), name='reset_password_done'),
re_path(r'^password/change/$', views.ChangePasswordForm.as_view(), name='change_password'),
re_path(r'^password/change/done/$', views.PasswordChangeViewBase.as_view(), name='change_password_done'),
re_path(r'^password/reset/complete/$', views.ResetPasswordCompleteView.as_view(), name='reset_password_complete')
]

@ -0,0 +1,20 @@
def get_ballances(user):
res = {}
if user:
for wallet in user.wallet_set.all():
info = '%.8f' % wallet.amount
if wallet.amount_blocked:
info += ' (%.8f)' % wallet.amount_blocked
res[wallet.currency.iso] = info
return res
def get_ballances_decimal(user):
res = {}
if user:
for wallet in user.wallet_set.all():
info = wallet.amount
blocked = wallet.amount_blocked
res[wallet.currency.iso] = (info, blocked)
return res

@ -0,0 +1,203 @@
import logging
from django.conf import settings
from django.contrib.auth.views import (
LoginView as LoginViewBase,
LogoutView,
PasswordResetView as PasswordResetViewBase,
PasswordResetDoneView as PasswordResetDoneViewBase,
PasswordResetConfirmView as PasswordResetConfirmViewBase,
PasswordResetCompleteView as PasswordResetCompleteViewBase,
PasswordChangeView as PasswordChangeViewBase,
PasswordChangeDoneView as PasswordChangeDoneViewBase
)
from django.views.generic import TemplateView
from registration.backends.default.views import (
RegistrationView as RegistrationViewBase,
ResendActivationView as ResendActivationViewBase
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import JsonResponse
from django.shortcuts import redirect, resolve_url
from django.urls import reverse, reverse_lazy
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
from core.views import ProtectedView
from .forms import (
PasswordResetRequestForm, PasswordResetForm, RegistrationCompanyForm,
AuthenticationForm,
RegistrationForm)
logger = logging.getLogger(__name__)
# Create your views here.
class LoginView(LoginViewBase):
redirect_field_name = 'next'
form_class = AuthenticationForm
redirect_authenticated_user = True
template_name = 'accounts_ext/authentication.html'
def dispatch(self, request, *args, **kwargs):
if self.request.user.is_authenticated:
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
if not is_safe_url(url=redirect_to, host=self.request.get_host()):
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check that "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return redirect(redirect_to)
return super().dispatch(request, *args, **kwargs)
def render_done(self, form, **kwargs):
"""
This method gets called when all forms passed. The method should also
re-validate all steps to prevent manipulation. If any form don't
validate, `render_revalidation_failure` should get called.
If everything is fine call `done`.
"""
final_form_list = []
# walk through the form list and try to validate the data again.
for form_key in self.get_form_list():
form_obj = self.get_form(step=form_key,
data=self.storage.get_step_data(form_key),
files=self.storage.get_step_files(
form_key))
form_obj.exclude_second_captcha_validation = True
if not (form_key in self.idempotent_dict or form_obj.is_valid()):
return self.render_revalidation_failure(form_key, form_obj,
**kwargs)
final_form_list.append(form_obj)
# render the done view and reset the wizard before returning the
# response. This is needed to prevent from rendering done with the
# same data twice.
done_response = self.done(final_form_list, **kwargs)
self.storage.reset()
return done_response
def get_user(self):
"""
Returns the user authenticated by the AuthenticationForm. Returns False
if not a valid user; see also issue #65.
"""
if not self.user_cache:
form_obj = self.get_form(step='auth',
data=self.storage.get_step_data('auth'))
form_obj.exclude_second_captcha_validation = True
self.user_cache = form_obj.is_valid() and form_obj.user_cache
return self.user_cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = AuthenticationForm.title
return context
class ResetPasswordView(PasswordResetViewBase):
form_class = PasswordResetRequestForm
template_name = 'accounts_ext/reset_password.html'
email_template_name = 'Reset password email'
html_email_template_name = 'emails/reset_password_email.html'
success_url = reverse_lazy('accounts_ext:reset_password_done')
from_email = settings.DEFAULT_FROM_EMAIL
subject_template_name = _('Сброс пароля')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = PasswordResetForm.title
return context
class ResetPasswordDoneView(PasswordResetDoneViewBase):
template_name = 'accounts_ext/reset_password_done.html'
class ResetPasswordConfirmView(PasswordResetConfirmViewBase):
form_class = PasswordResetForm
template_name = 'accounts_ext/reset_password_confirm.html'
success_url = reverse_lazy('accounts_ext:reset_password_complete')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
return kwargs
class ResetPasswordCompleteView(PasswordResetCompleteViewBase):
template_name = 'accounts_ext/reset_password_complete.html'
title = _('Password reset complete')
class RegistrationView(RegistrationViewBase):
http_method_names = ['get', 'post']
template_name = 'registration/registration.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['company_form'] = RegistrationCompanyForm(self.request.POST)
else:
context['company_form'] = RegistrationCompanyForm()
context['title'] = RegistrationForm.title
return context
def form_valid(self, form):
context = self.get_context_data()
company_form = context.get('company_form')
try:
user = form.save()
if company_form.is_valid():
company_form.save(user=user)
else:
logger.error("User {email} company has invalid data during registration")
except Exception as e:
logger.error(e)
return self.form_invalid(form=form)
return super().form_valid(form)
class RegistrationDoneView(TemplateView):
template_name = 'registration/registration_done.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _('Отправка письма активации')
return context
class ResendActivationView(ResendActivationViewBase):
template_name = 'registration/resend_activation.tml'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _('Отправка письма активации')
return context
class ChangePasswordForm(PasswordChangeViewBase):
template_name = 'accounts_ext/password_reset_change.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _('Введение нового пароля')
return context
class ChangePasswordDoneForm(PasswordChangeDoneViewBase):
template_name = 'accounts_ext/password_reset_change_done.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _('Сброс пароля')
return context
class ProfileView(ProtectedView):
template_name = 'accounts_ext/index.html'
Loading…
Cancel
Save