From ae40e7b3059368c5cc8b9b4da0da8eeee4e32bf6 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 17 Jan 2019 12:01:24 +0300 Subject: [PATCH 01/47] Add users app --- tests/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/settings.py b/tests/settings.py index db23317..751136c 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -21,6 +21,7 @@ 'django_filters', # custom 'vega_admin', + 'vega_admin.contrib.users', # tests 'tests.artist_app' ] From c0eed394473ccc3a7324b59ea617c9518999cd2e Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 17 Jan 2019 12:02:03 +0300 Subject: [PATCH 02/47] Add more info --- vega_admin/apps.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vega_admin/apps.py b/vega_admin/apps.py index 98decec..0eab47d 100644 --- a/vega_admin/apps.py +++ b/vega_admin/apps.py @@ -2,14 +2,16 @@ Apps module for django-vega-admin """ from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ class VegaAdminConfig(AppConfig): """ Apps config class """ - name = 'vega_admin' - app_label = 'vega_admin' + name = "vega_admin" + app_label = "vega_admin" + verbose_name = _("Vega Admin") def ready(self): """ From 063337580bba26a1323eb8ea7fe75c0668a985a3 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 17 Jan 2019 12:02:34 +0300 Subject: [PATCH 03/47] Prevent race conditions on apps ready --- vega_admin/forms.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vega_admin/forms.py b/vega_admin/forms.py index dcfb069..566715b 100644 --- a/vega_admin/forms.py +++ b/vega_admin/forms.py @@ -9,6 +9,8 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit +from vega_admin.settings import VEGA_LISTVIEW_SEARCH_QUERY_TXT + class ListViewSearchForm(forms.Form): """ @@ -16,7 +18,10 @@ class ListViewSearchForm(forms.Form): """ q = forms.CharField( - label=_(settings.VEGA_LISTVIEW_SEARCH_QUERY_TXT), required=False) + label=_( + getattr(settings, 'VEGA_LISTVIEW_SEARCH_QUERY_TXT', + VEGA_LISTVIEW_SEARCH_QUERY_TXT)), + required=False) def __init__(self, *args, **kwargs): self.request = kwargs.pop('request', None) From 5505b22dd875857e126accf9ca65baa5d12fe399 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 17 Jan 2019 12:03:01 +0300 Subject: [PATCH 04/47] Add TimeStampedModel mixin --- vega_admin/mixins.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vega_admin/mixins.py b/vega_admin/mixins.py index c30facc..53c6408 100644 --- a/vega_admin/mixins.py +++ b/vega_admin/mixins.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib import messages from django.core.exceptions import FieldDoesNotExist +from django.db import models from django.db.models import ProtectedError, Q from django.shortcuts import redirect from django.urls import reverse_lazy @@ -364,3 +365,16 @@ def derive_url_pattern(cls, crud_path: str, action: str): Derive the url pattern """ return f"{crud_path}/{action}//" + + +class TimeStampedModel: + """ + Adds timestamps to a model class + """ + + created = models.DateTimeField(_("Created"), auto_now_add=True) + modified = models.DateTimeField(_("Modified"), auto_now=True) + + class Meta: + """Meta class""" + abstract = True From ecc0f92982ffd51dcf667b311d39bd6986bbd501 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 17 Jan 2019 12:10:28 +0300 Subject: [PATCH 05/47] Upgrade to django 2.1.5 --- requirements.txt | 2 +- requirements/dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 987ec15..c8a6bdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-braces==1.13.0 django-crispy-forms==1.7.2 django-filter==2.0.0 django-tables2==2.0.3 -django==2.1.3 +django==2.1.5 et-xmlfile==1.0.1 # via openpyxl jdcal==1.4 # via openpyxl odfpy==1.3.6 # via tablib diff --git a/requirements/dev.txt b/requirements/dev.txt index 1be3e01..fa215cf 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -16,7 +16,7 @@ django-braces==1.13.0 django-crispy-forms==1.7.2 django-filter==2.0.0 django-tables2==2.0.3 -django==2.1.3 # via django-filter, django-tables2, model-mommy +django==2.1.5 # via django-filter, django-tables2, model-mommy et-xmlfile==1.0.1 # via openpyxl filelock==3.0.10 # via tox flake8==3.6.0 From c9ff8e354f8f21e1d2bc7d837caca02a2707a86b Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 23 Jan 2019 22:43:08 +0300 Subject: [PATCH 06/47] Add users app --- vega_admin/contrib/__init__.py | 0 vega_admin/contrib/users/__init__.py | 5 + vega_admin/contrib/users/apps.py | 11 ++ vega_admin/contrib/users/forms.py | 185 +++++++++++++++++++++++++++ vega_admin/contrib/users/models.py | 0 vega_admin/contrib/users/urls.py | 4 + vega_admin/contrib/users/views.py | 17 +++ 7 files changed, 222 insertions(+) create mode 100644 vega_admin/contrib/__init__.py create mode 100644 vega_admin/contrib/users/__init__.py create mode 100644 vega_admin/contrib/users/apps.py create mode 100644 vega_admin/contrib/users/forms.py create mode 100644 vega_admin/contrib/users/models.py create mode 100644 vega_admin/contrib/users/urls.py create mode 100644 vega_admin/contrib/users/views.py diff --git a/vega_admin/contrib/__init__.py b/vega_admin/contrib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vega_admin/contrib/users/__init__.py b/vega_admin/contrib/users/__init__.py new file mode 100644 index 0000000..b410e1c --- /dev/null +++ b/vega_admin/contrib/users/__init__.py @@ -0,0 +1,5 @@ +""" +init module for vega_admin.users +""" +# pylint: disable=invalid-name +default_app_config = 'vega_admin.contrib.users.apps.UsersConfig' # noqa diff --git a/vega_admin/contrib/users/apps.py b/vega_admin/contrib/users/apps.py new file mode 100644 index 0000000..04135d3 --- /dev/null +++ b/vega_admin/contrib/users/apps.py @@ -0,0 +1,11 @@ +"""AppConfig module for Vega Admin users app""" +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class UsersConfig(AppConfig): + """App config class""" + + name = "vega_admin.contrib.users" + app_label = "vega_admin" + verbose_name = _("Vega Admin Users") diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py new file mode 100644 index 0000000..525ef8f --- /dev/null +++ b/vega_admin/contrib/users/forms.py @@ -0,0 +1,185 @@ +"""Forms module for users""" +from django import forms +from django.contrib.auth import password_validation +from django.contrib.auth.models import User +from django.utils.translation import ugettext as _ +from django.conf import settings + +from crispy_forms.bootstrap import Field +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout + +from vega_admin.utils import get_form_actions + +try: + # pylint: disable=import-error + from allauth.account import app_settings + from allauth.account.adapter import get_adapter +except ModuleNotFoundError: + UNIQUE_EMAIL = False +else: + UNIQUE_EMAIL = app_settings.UNIQUE_EMAIL + + +class UserFormMixin: # pylint: disable=too-few-public-methods + """User forms mixin""" + + def clean_email(self): + """clean email address""" + value = self.cleaned_data['email'] + try: + value = get_adapter().clean_email(value) + except NameError: + pass + if value and UNIQUE_EMAIL: + value = self.validate_unique_email(value) + return value + + +class AddUserForm(UserFormMixin, forms.ModelForm): + """ + Form class to add users + """ + + password = forms.CharField( + label=_("Password"), + strip=True, + help_text=password_validation.password_validators_help_text_html() + ) + + class Meta: + model = User + fields = [ + 'first_name', + 'last_name', + 'username', + 'email', + 'password', + 'is_active' + ] + + def __init__(self, *args, **kwargs): + """init method for add user form""" + self.request = kwargs.pop('request', None) + self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) + super().__init__(*args, **kwargs) + self.fields['first_name'].required = True + self.fields['last_name'].required = True + self.fields['username'].required = False + self.fields['username'].help_text = _(settings.VEGA_USERNAME_HELP_TEXT) + self.fields['email'].help_text = _(settings.VEGA_OPTIONAL_TXT) + self.helper = FormHelper() + self.helper.form_tag = True + self.helper.form_method = 'post' + self.helper.render_required_fields = True + self.helper.form_show_labels = True + self.helper.html5_required = True + self.helper.include_media = False + self.helper.form_id = 'add-user-form' + self.helper.layout = Layout( + Field('first_name'), + Field('last_name'), + Field('username'), + Field('email'), + Field('password'), + Field('is_active'), + get_form_actions( + cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), + ) + + def clean_password(self): + """clean password field""" + data = self.cleaned_data['password'] + user = User() + if self.cleaned_data.get('first_name'): + user.first_name = self.cleaned_data['first_name'] + if self.cleaned_data.get('last_name'): + user.last_name = self.cleaned_data['last_name'] + if self.cleaned_data.get('email'): + user.email = self.cleaned_data['email'] + password_validation.validate_password(password=data, user=user) + return data + + def validate_unique_email(self, value): # pylint: disable=no-self-use + """validate unique email while adding users""" + try: + return get_adapter().validate_unique_email(value) + except NameError: + return value + + def clean(self): + """General clean method""" + cleaned_data = super().clean() + username = cleaned_data.get("username") + email = cleaned_data.get("email") + if not email and not username: + raise forms.ValidationError( + _(settings.VEGA_EMAIL_OR_USERNAME_REQUIRED_TXT)) + + def save(self, commit=True): + """general form save method""" + unique_username = self.cleaned_data.get("username") + if not unique_username: + try: + unique_username = get_adapter().generate_unique_username([ + self.cleaned_data.get('first_name'), + self.cleaned_data.get('last_name'), + self.cleaned_data.get('email'), + ]) + except NameError: + unique_username = self.cleaned_data.get('email') + user_data = dict(username=unique_username, + first_name=self.cleaned_data['first_name'], + last_name=self.cleaned_data['last_name'], + password=self.cleaned_data['password'], + ) + if self.cleaned_data.get('email'): + user_data['email'] = self.cleaned_data.get('email') + user = User.objects.create_user(**user_data) + return user + + +class EditUserForm(UserFormMixin, forms.ModelForm): + """Edit User form""" + + class Meta: + model = User + fields = [ + 'first_name', + 'last_name', + 'username', + 'email', + 'is_active' + ] + + def __init__(self, *args, **kwargs): + """init method for edit user form""" + self.request = kwargs.pop('request', None) + self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) + super().__init__(*args, **kwargs) + self.fields['email'].help_text = _(settings.VEGA_OPTIONAL_TXT) + self.helper = FormHelper() + self.helper.form_tag = True + self.helper.form_method = 'post' + self.helper.render_required_fields = True + self.helper.form_show_labels = True + self.helper.html5_required = True + self.helper.include_media = False + self.helper.form_id = 'edit-user-form' + self.helper.layout = Layout( + Field('first_name'), + Field('last_name'), + Field('username'), + Field('email'), + Field('is_active'), + get_form_actions( + cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), + ) + + def validate_unique_email(self, value): + """Validate unique email when editting users""" + try: + return get_adapter().validate_unique_email( + value, user=self.instance) + except NameError: + return value diff --git a/vega_admin/contrib/users/models.py b/vega_admin/contrib/users/models.py new file mode 100644 index 0000000..e69de29 diff --git a/vega_admin/contrib/users/urls.py b/vega_admin/contrib/users/urls.py new file mode 100644 index 0000000..aa272e3 --- /dev/null +++ b/vega_admin/contrib/users/urls.py @@ -0,0 +1,4 @@ +"""example URL Configuration""" +from vega_admin.contrib.users import views + +urlpatterns = views.UserCRUD().url_patterns() # pylint: disable=invalid-name diff --git a/vega_admin/contrib/users/views.py b/vega_admin/contrib/users/views.py new file mode 100644 index 0000000..b77d1e0 --- /dev/null +++ b/vega_admin/contrib/users/views.py @@ -0,0 +1,17 @@ +"""Views module for Vega Admin users app""" +from vega_admin.views import VegaCRUDView +from django.contrib.auth.models import User +from vega_admin.contrib.users.forms import AddUserForm, EditUserForm + + +class UserCRUD(VegaCRUDView): + """ + CRUD view for users + """ + + model = User + list_fields = ['id', 'username', 'email', 'first_name', 'last_name'] + create_form_class = AddUserForm + update_form_class = EditUserForm + protected_actions = None + permissions_actions = None From eb2551ce5fb22e526309a16cf178e8006428fc5a Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 23 Jan 2019 22:43:32 +0300 Subject: [PATCH 07/47] Install users app in example app --- example/example/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/example/example/settings.py b/example/example/settings.py index 617bd97..ed60412 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -38,6 +38,7 @@ 'django_tables2', 'django_filters', 'vega_admin', + 'vega_admin.contrib.users', 'artists', ] From c8f2397e4cf0e91b2ac28157bf0750630403e35f Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 23 Jan 2019 22:43:48 +0300 Subject: [PATCH 08/47] Add users app urls in example app --- example/example/urls.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/example/example/urls.py b/example/example/urls.py index d62b00f..796c83d 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,5 +1,11 @@ """example URL Configuration""" +from django.conf.urls import include +from django.urls import path + from artists import views -urlpatterns = views.ArtistCRUD().url_patterns() +\ + +urlpatterns = [ + path('', include('vega_admin.contrib.users.urls')), +] + views.ArtistCRUD().url_patterns() +\ views.SongCRUD().url_patterns() From 531b8a7111dfe3ea916629d90eeb4eb4fc327e16 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 23 Jan 2019 22:44:30 +0300 Subject: [PATCH 09/47] Add method to get form actions dynamically --- vega_admin/utils.py | 62 +++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/vega_admin/utils.py b/vega_admin/utils.py index d3c18b5..467caeb 100644 --- a/vega_admin/utils.py +++ b/vega_admin/utils.py @@ -17,6 +17,38 @@ from vega_admin.mixins import VegaFormMixin +def get_form_actions(cancel_url: str): + """ + Returns the FormActions class, for use in model forms in VegaCRUD + """ + return FormActions( + Div( + Div( + Div( + HTML( + f""" + + {_(settings.VEGA_CANCEL_TEXT)} + """ + ), + css_class="col-md-6", + ), + Div( + Submit( + "submit", + _(settings.VEGA_SUBMIT_TEXT), + css_class="btn-block vega-submit" + ), + css_class="col-md-6", + ), + css_class="col-md-12", + ), + css_class="row", + ) + ) + + def get_modelform( model: object, fields: list = None, extra_fields: list = None): """ @@ -39,6 +71,7 @@ def _constructor(self, *args, **kwargs): self.request = kwargs.pop("request", None) self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) cancel_url = self.vega_extra_kwargs.get("cancel_url", "/") + form_actions_class = get_form_actions(cancel_url=cancel_url) super(modelform_class, self).__init__(*args, **kwargs) # add crispy forms FormHelper self.helper = FormHelper() @@ -50,34 +83,7 @@ def _constructor(self, *args, **kwargs): self.helper.include_media = False self.helper.form_id = f"{self.model._meta.model_name}-form" self.helper.layout = Layout(*self.fields.keys()) - self.helper.layout.append( - FormActions( - Div( - Div( - Div( - HTML( - f""" - - {_(settings.VEGA_CANCEL_TEXT)} - """ - ), - css_class="col-md-6", - ), - Div( - Submit( - "submit", - _(settings.VEGA_SUBMIT_TEXT), - css_class="btn-block vega-submit" - ), - css_class="col-md-6", - ), - css_class="col-md-12", - ), - css_class="row", - ) - ) - ) + self.helper.layout.append(form_actions_class) if fields is None: fields = [_.name for _ in model._meta.concrete_fields] From 3c92225f78731ca016036fc6005d966a73aa6fe8 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 23 Jan 2019 22:44:53 +0300 Subject: [PATCH 10/47] Add settings for users app --- vega_admin/settings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vega_admin/settings.py b/vega_admin/settings.py index 411c625..5a297c8 100644 --- a/vega_admin/settings.py +++ b/vega_admin/settings.py @@ -47,3 +47,11 @@ # exceptions VEGA_INVALID_ACTION = "Invalid Action" + +# contrib +# users +VEGA_USERNAME_HELP_TEXT =\ + "Optional. 150 characters or fewer. Letters, digits and @/./+/-/_ only." +VEGA_OPTIONAL_TXT = "Optional." +VEGA_EMAIL_OR_USERNAME_REQUIRED_TXT =\ + "You must provide one of email or username" From 00b7bca569f2bdf935c9c1f263d7b48f6007fa80 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:18:50 +0300 Subject: [PATCH 11/47] Use template for the sidebar --- vega_admin/templates/vega_admin/badmin/base.html | 6 +----- vega_admin/templates/vega_admin/badmin/sidebar.html | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 vega_admin/templates/vega_admin/badmin/sidebar.html diff --git a/vega_admin/templates/vega_admin/badmin/base.html b/vega_admin/templates/vega_admin/badmin/base.html index 028c61c..df84f8d 100644 --- a/vega_admin/templates/vega_admin/badmin/base.html +++ b/vega_admin/templates/vega_admin/badmin/base.html @@ -39,11 +39,7 @@

Vega Admin

- + {% include "vega_admin/badmin/sidebar.html" %}
{% block messages %} diff --git a/vega_admin/templates/vega_admin/badmin/sidebar.html b/vega_admin/templates/vega_admin/badmin/sidebar.html new file mode 100644 index 0000000..8c61884 --- /dev/null +++ b/vega_admin/templates/vega_admin/badmin/sidebar.html @@ -0,0 +1,5 @@ + \ No newline at end of file From b274a29df9161a84e0db95fa3d36bc986d2a373d Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:19:28 +0300 Subject: [PATCH 12/47] Use `os.path.join(BASE_DIR, 'templates')` in TEMPLATES settings --- example/example/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/example/settings.py b/example/example/settings.py index ed60412..1b524bd 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -57,7 +57,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ From c59c7f8c030b6b0b776ae0b82a99ea0ebf70b6e1 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:19:52 +0300 Subject: [PATCH 13/47] Turn off autocomplete for password field --- vega_admin/contrib/users/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 525ef8f..02844a1 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -81,7 +81,7 @@ def __init__(self, *args, **kwargs): Field('last_name'), Field('username'), Field('email'), - Field('password'), + Field('password', autocomplete="off"), Field('is_active'), get_form_actions( cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), From 4d54e9766230e121f192d64d673e82f4784fa271 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:22:06 +0300 Subject: [PATCH 14/47] Format code better --- vega_admin/contrib/users/forms.py | 115 +++++++++++++++--------------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 02844a1..57d7d71 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -26,7 +26,7 @@ class UserFormMixin: # pylint: disable=too-few-public-methods def clean_email(self): """clean email address""" - value = self.cleaned_data['email'] + value = self.cleaned_data["email"] try: value = get_adapter().clean_email(value) except NameError: @@ -44,59 +44,59 @@ class AddUserForm(UserFormMixin, forms.ModelForm): password = forms.CharField( label=_("Password"), strip=True, - help_text=password_validation.password_validators_help_text_html() + help_text=password_validation.password_validators_help_text_html(), ) class Meta: model = User fields = [ - 'first_name', - 'last_name', - 'username', - 'email', - 'password', - 'is_active' + "first_name", + "last_name", + "username", + "email", + "password", + "is_active", ] def __init__(self, *args, **kwargs): """init method for add user form""" - self.request = kwargs.pop('request', None) + self.request = kwargs.pop("request", None) self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) super().__init__(*args, **kwargs) - self.fields['first_name'].required = True - self.fields['last_name'].required = True - self.fields['username'].required = False - self.fields['username'].help_text = _(settings.VEGA_USERNAME_HELP_TEXT) - self.fields['email'].help_text = _(settings.VEGA_OPTIONAL_TXT) + self.fields["first_name"].required = True + self.fields["last_name"].required = True + self.fields["username"].required = False + self.fields["username"].help_text = _(settings.VEGA_USERNAME_HELP_TEXT) + self.fields["email"].help_text = _(settings.VEGA_OPTIONAL_TXT) self.helper = FormHelper() self.helper.form_tag = True - self.helper.form_method = 'post' + self.helper.form_method = "post" self.helper.render_required_fields = True self.helper.form_show_labels = True self.helper.html5_required = True self.helper.include_media = False - self.helper.form_id = 'add-user-form' + self.helper.form_id = "add-user-form" self.helper.layout = Layout( - Field('first_name'), - Field('last_name'), - Field('username'), - Field('email'), - Field('password', autocomplete="off"), - Field('is_active'), + Field("first_name"), + Field("last_name"), + Field("username"), + Field("email"), + Field("password", autocomplete="off"), + Field("is_active"), get_form_actions( cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), ) def clean_password(self): """clean password field""" - data = self.cleaned_data['password'] + data = self.cleaned_data["password"] user = User() - if self.cleaned_data.get('first_name'): - user.first_name = self.cleaned_data['first_name'] - if self.cleaned_data.get('last_name'): - user.last_name = self.cleaned_data['last_name'] - if self.cleaned_data.get('email'): - user.email = self.cleaned_data['email'] + if self.cleaned_data.get("first_name"): + user.first_name = self.cleaned_data["first_name"] + if self.cleaned_data.get("last_name"): + user.last_name = self.cleaned_data["last_name"] + if self.cleaned_data.get("email"): + user.email = self.cleaned_data["email"] password_validation.validate_password(password=data, user=user) return data @@ -121,20 +121,23 @@ def save(self, commit=True): unique_username = self.cleaned_data.get("username") if not unique_username: try: - unique_username = get_adapter().generate_unique_username([ - self.cleaned_data.get('first_name'), - self.cleaned_data.get('last_name'), - self.cleaned_data.get('email'), - ]) + unique_username = get_adapter().generate_unique_username( + [ + self.cleaned_data.get("first_name"), + self.cleaned_data.get("last_name"), + self.cleaned_data.get("email"), + ] + ) except NameError: - unique_username = self.cleaned_data.get('email') - user_data = dict(username=unique_username, - first_name=self.cleaned_data['first_name'], - last_name=self.cleaned_data['last_name'], - password=self.cleaned_data['password'], - ) - if self.cleaned_data.get('email'): - user_data['email'] = self.cleaned_data.get('email') + unique_username = self.cleaned_data.get("email") + user_data = dict( + username=unique_username, + first_name=self.cleaned_data["first_name"], + last_name=self.cleaned_data["last_name"], + password=self.cleaned_data["password"], + ) + if self.cleaned_data.get("email"): + user_data["email"] = self.cleaned_data.get("email") user = User.objects.create_user(**user_data) return user @@ -145,33 +148,33 @@ class EditUserForm(UserFormMixin, forms.ModelForm): class Meta: model = User fields = [ - 'first_name', - 'last_name', - 'username', - 'email', - 'is_active' + "first_name", + "last_name", + "username", + "email", + "is_active", ] def __init__(self, *args, **kwargs): """init method for edit user form""" - self.request = kwargs.pop('request', None) + self.request = kwargs.pop("request", None) self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) super().__init__(*args, **kwargs) - self.fields['email'].help_text = _(settings.VEGA_OPTIONAL_TXT) + self.fields["email"].help_text = _(settings.VEGA_OPTIONAL_TXT) self.helper = FormHelper() self.helper.form_tag = True - self.helper.form_method = 'post' + self.helper.form_method = "post" self.helper.render_required_fields = True self.helper.form_show_labels = True self.helper.html5_required = True self.helper.include_media = False - self.helper.form_id = 'edit-user-form' + self.helper.form_id = "edit-user-form" self.helper.layout = Layout( - Field('first_name'), - Field('last_name'), - Field('username'), - Field('email'), - Field('is_active'), + Field("first_name"), + Field("last_name"), + Field("username"), + Field("email"), + Field("is_active"), get_form_actions( cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), ) From d514ef141bdc5ec1a7c5d018540dacd8d3ef3fdd Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:22:28 +0300 Subject: [PATCH 15/47] Override sidebar template --- example/templates/vega_admin/badmin/sidebar.html | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 example/templates/vega_admin/badmin/sidebar.html diff --git a/example/templates/vega_admin/badmin/sidebar.html b/example/templates/vega_admin/badmin/sidebar.html new file mode 100644 index 0000000..67ce171 --- /dev/null +++ b/example/templates/vega_admin/badmin/sidebar.html @@ -0,0 +1,16 @@ +{% load i18n %} + \ No newline at end of file From 53f26bd83152ce78909dd8afaf853619d5e063f8 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:50:38 +0300 Subject: [PATCH 16/47] Add `get_form_helper_class` + code format --- vega_admin/utils.py | 106 +++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/vega_admin/utils.py b/vega_admin/utils.py index 467caeb..51efbba 100644 --- a/vega_admin/utils.py +++ b/vega_admin/utils.py @@ -25,32 +25,61 @@ def get_form_actions(cancel_url: str): Div( Div( Div( - HTML( - f""" + HTML(f""" {_(settings.VEGA_CANCEL_TEXT)} - """ - ), + """), css_class="col-md-6", ), Div( Submit( "submit", _(settings.VEGA_SUBMIT_TEXT), - css_class="btn-block vega-submit" + css_class="btn-block vega-submit", ), css_class="col-md-6", ), css_class="col-md-12", ), css_class="row", - ) - ) + )) + + +def get_form_helper_class( # pylint: disable=too-many-arguments + form_tag: bool = True, + form_method: str = "POST", + render_required_fields: bool = True, + form_show_labels: bool = True, + html5_required: bool = True, + include_media: bool = True, +): + """ + Returns the base form helper class + + :param form_tag: include form tag? + :param form_method: form method + :param render_required_fields: render required fields? + :param form_show_labels: show form labels? + :param html5_required: HTML5 required? + :param include_media: include form media? + + :return: form helper class + """ + helper = FormHelper() + helper.form_tag = form_tag + helper.form_method = form_method + helper.render_required_fields = render_required_fields + helper.form_show_labels = form_show_labels + helper.html5_required = html5_required + helper.include_media = include_media + + return helper -def get_modelform( - model: object, fields: list = None, extra_fields: list = None): +def get_modelform(model: object, + fields: list = None, + extra_fields: list = None): """ Get the a ModelForm for the provided model @@ -74,13 +103,14 @@ def _constructor(self, *args, **kwargs): form_actions_class = get_form_actions(cancel_url=cancel_url) super(modelform_class, self).__init__(*args, **kwargs) # add crispy forms FormHelper - self.helper = FormHelper() - self.helper.form_tag = True - self.helper.form_method = "post" - self.helper.render_required_fields = True - self.helper.form_show_labels = True - self.helper.html5_required = True - self.helper.include_media = False + self.helper = get_form_helper_class( + form_tag=True, + form_method="POST", + render_required_fields=True, + form_show_labels=True, + html5_required=True, + include_media=True, + ) self.helper.form_id = f"{self.model._meta.model_name}-form" self.helper.layout = Layout(*self.fields.keys()) self.helper.layout.append(form_actions_class) @@ -94,7 +124,7 @@ def _constructor(self, *args, **kwargs): (), # inherit from object { "model": model, - "fields": fields, + "fields": fields }, ) @@ -116,8 +146,8 @@ def _constructor(self, *args, **kwargs): return modelform_class -def get_listview_form( - model: object, fields: list, include_search: bool = True): +def get_listview_form(model: object, fields: list, + include_search: bool = True): """ Get a search and filter form for use in ListViews @@ -133,7 +163,7 @@ def get_listview_form( "q", forms.CharField( label=_(settings.VEGA_LISTVIEW_SEARCH_QUERY_TXT), - required=False,) + required=False), ) if search_field: @@ -141,13 +171,13 @@ def get_listview_form( else: extra_fields = None - return get_modelform( - model=model, fields=fields, extra_fields=extra_fields) + return get_modelform(model=model, fields=fields, extra_fields=extra_fields) -def get_table( - model: object, fields: list = None, actions: list = None, - attrs: dict = None): +def get_table(model: object, + fields: list = None, + actions: list = None, + attrs: dict = None): """ Get the Table Class for the provided model @@ -160,7 +190,8 @@ def get_table( # the Meta class meta_options = { "model": model, - "empty_text": _(settings.VEGA_NOTHING_TO_SHOW)} + "empty_text": _(settings.VEGA_NOTHING_TO_SHOW) + } if fields: all_fields = [_.name for _ in model._meta.concrete_fields] @@ -185,7 +216,7 @@ def get_table( # pylint: disable=unused-argument def render_actions_fn(self, *args, **kwargs): """Render the actions column""" - record = kwargs['record'] + record = kwargs["record"] actions_links = [] for item in self.actions_list: try: @@ -203,15 +234,13 @@ def render_actions_fn(self, *args, **kwargs): options["action"] = tables.Column( verbose_name=_(settings.VEGA_ACTION_COLUMN_NAME), accessor=settings.VEGA_ACTION_COLUMN_ACCESSOR_FIELD, - orderable=False) + orderable=False, + ) options["render_action"] = render_actions_fn # create the table dynamically using type - table_class = type( - f"{model.__name__.title()}{settings.VEGA_TABLE_LABEL}", - (tables.Table, ), - options, - ) + table_class = type(f"{model.__name__.title()}{settings.VEGA_TABLE_LABEL}", + (tables.Table, ), options) return table_class @@ -225,10 +254,7 @@ def get_filterclass(model: object, fields: list = None): :return: filter class """ # the Meta class - meta_options = { - "model": model, - "fields": fields, - } + meta_options = {"model": model, "fields": fields} meta_class = type("Meta", (), meta_options) # the attributes of our new table class @@ -236,9 +262,7 @@ def get_filterclass(model: object, fields: list = None): # create the filter_class dynamically using type filter_class = type( - f"{model.__name__.title()}{settings.VEGA_FILTER_LABEL}", - (FilterSet, ), - options, - ) + f"{model.__name__.title()}{settings.VEGA_FILTER_LABEL}", (FilterSet, ), + options) return filter_class From 28a8a49a8df2aa508fcdaad3b938191ad84da95f Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:51:05 +0300 Subject: [PATCH 17/47] Use `get_form_helper_class` to keep code DRY --- vega_admin/contrib/users/forms.py | 43 ++++++++++--------------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 57d7d71..174d7df 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -6,10 +6,9 @@ from django.conf import settings from crispy_forms.bootstrap import Field -from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout -from vega_admin.utils import get_form_actions +from vega_admin.utils import get_form_actions, get_form_helper_class try: # pylint: disable=import-error @@ -68,13 +67,9 @@ def __init__(self, *args, **kwargs): self.fields["username"].required = False self.fields["username"].help_text = _(settings.VEGA_USERNAME_HELP_TEXT) self.fields["email"].help_text = _(settings.VEGA_OPTIONAL_TXT) - self.helper = FormHelper() - self.helper.form_tag = True - self.helper.form_method = "post" - self.helper.render_required_fields = True - self.helper.form_show_labels = True - self.helper.html5_required = True - self.helper.include_media = False + self.helper = get_form_helper_class( + form_tag=True, form_method="POST", render_required_fields=True, + form_show_labels=True, html5_required=True, include_media=True) self.helper.form_id = "add-user-form" self.helper.layout = Layout( Field("first_name"), @@ -121,13 +116,11 @@ def save(self, commit=True): unique_username = self.cleaned_data.get("username") if not unique_username: try: - unique_username = get_adapter().generate_unique_username( - [ - self.cleaned_data.get("first_name"), - self.cleaned_data.get("last_name"), - self.cleaned_data.get("email"), - ] - ) + unique_username = get_adapter().generate_unique_username([ + self.cleaned_data.get("first_name"), + self.cleaned_data.get("last_name"), + self.cleaned_data.get("email"), + ]) except NameError: unique_username = self.cleaned_data.get("email") user_data = dict( @@ -147,13 +140,7 @@ class EditUserForm(UserFormMixin, forms.ModelForm): class Meta: model = User - fields = [ - "first_name", - "last_name", - "username", - "email", - "is_active", - ] + fields = ["first_name", "last_name", "username", "email", "is_active"] def __init__(self, *args, **kwargs): """init method for edit user form""" @@ -161,13 +148,9 @@ def __init__(self, *args, **kwargs): self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) super().__init__(*args, **kwargs) self.fields["email"].help_text = _(settings.VEGA_OPTIONAL_TXT) - self.helper = FormHelper() - self.helper.form_tag = True - self.helper.form_method = "post" - self.helper.render_required_fields = True - self.helper.form_show_labels = True - self.helper.html5_required = True - self.helper.include_media = False + self.helper = get_form_helper_class( + form_tag=True, form_method="POST", render_required_fields=True, + form_show_labels=True, html5_required=True, include_media=True) self.helper.form_id = "edit-user-form" self.helper.layout = Layout( Field("first_name"), From 7e219825388f87c8b40bb1e5844397e9d1c6818e Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 20:57:59 +0300 Subject: [PATCH 18/47] Add stub tests for user forms --- tests/contrib/__init__.py | 0 tests/contrib/users/__init__.py | 0 tests/contrib/users/test_forms.py | 23 +++++++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/contrib/__init__.py create mode 100644 tests/contrib/users/__init__.py create mode 100644 tests/contrib/users/test_forms.py diff --git a/tests/contrib/__init__.py b/tests/contrib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/contrib/users/__init__.py b/tests/contrib/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/contrib/users/test_forms.py b/tests/contrib/users/test_forms.py new file mode 100644 index 0000000..dcee714 --- /dev/null +++ b/tests/contrib/users/test_forms.py @@ -0,0 +1,23 @@ +"""Test vega_admin.contrib.users.forms module""" + +from django.test import TestCase + + +class TestForms(TestCase): + """ + Test class for vega_admin.contrib.users.forms + """ + + def test_adduserform(self): + """ + Test AddUserForm + + """ + self.fail() + + def test_edituserform(self): + """ + Test EditUserForm + + """ + self.fail() From 70a458a4d9368f2eb729973c6ba800f30302be30 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 22:14:19 +0300 Subject: [PATCH 19/47] change PasswordChangeForm --- vega_admin/contrib/users/forms.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 174d7df..89ef3ab 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -2,6 +2,7 @@ from django import forms from django.contrib.auth import password_validation from django.contrib.auth.models import User +from django.contrib.auth.forms import AdminPasswordChangeForm from django.utils.translation import ugettext as _ from django.conf import settings @@ -169,3 +170,24 @@ def validate_unique_email(self, value): value, user=self.instance) except NameError: return value + + +class PasswordChangeForm(AdminPasswordChangeForm): + """Custom form for admins to change user passwords""" + + def __init__(self, *args, **kwargs): + """init method for password change form""" + self.instance = kwargs.pop("instance", None) + self.request = kwargs.pop("request", None) + self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) + super().__init__(user=self.instance, *args, **kwargs) + self.helper = get_form_helper_class( + form_tag=True, form_method="POST", render_required_fields=True, + form_show_labels=True, html5_required=True, include_media=True) + self.helper.form_id = "change-password-form" + self.helper.layout = Layout( + Field("password1"), + Field("password2"), + get_form_actions( + cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), + ) From f93d3976c0f261f052480b1fc5d7980dc88ce46b Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 22:14:49 +0300 Subject: [PATCH 20/47] Add change password view --- vega_admin/contrib/users/views.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/vega_admin/contrib/users/views.py b/vega_admin/contrib/users/views.py index b77d1e0..4c48a33 100644 --- a/vega_admin/contrib/users/views.py +++ b/vega_admin/contrib/users/views.py @@ -1,7 +1,24 @@ """Views module for Vega Admin users app""" -from vega_admin.views import VegaCRUDView from django.contrib.auth.models import User -from vega_admin.contrib.users.forms import AddUserForm, EditUserForm +from django.conf import settings +from django.urls import reverse_lazy + +from vega_admin.contrib.users.forms import (AddUserForm, PasswordChangeForm, + EditUserForm) +from vega_admin.views import VegaCRUDView, VegaUpdateView + + +class ChangePassword(VegaUpdateView): # pylint: disable=too-many-ancestors + """ + Change Password view + """ + + form_class = PasswordChangeForm + model = User + + def get_success_url(self): # pylint: disable=no-self-use + """Get success_url""" + return reverse_lazy("auth.user-list") class UserCRUD(VegaCRUDView): @@ -15,3 +32,12 @@ class UserCRUD(VegaCRUDView): update_form_class = EditUserForm protected_actions = None permissions_actions = None + view_classes = { + "change password": ChangePassword, + } + table_actions = [ + settings.VEGA_READ_ACTION, + settings.VEGA_UPDATE_ACTION, + "change password", + settings.VEGA_DELETE_ACTION, + ] From 899b0aba87296d76550ff9d2083c3175c7dbbf7d Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 22:16:06 +0300 Subject: [PATCH 21/47] Add test for change password form --- tests/contrib/users/test_forms.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/contrib/users/test_forms.py b/tests/contrib/users/test_forms.py index dcee714..292daa5 100644 --- a/tests/contrib/users/test_forms.py +++ b/tests/contrib/users/test_forms.py @@ -21,3 +21,9 @@ def test_edituserform(self): """ self.fail() + + def test_password_change_form(self): + """ + Test PasswordChangeForm + """ + self.fail() From a2ef85a0d95c19624cdee484daa8e0e96fefb417 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Tue, 29 Jan 2019 22:18:36 +0300 Subject: [PATCH 22/47] Add test for ChangePassword view --- tests/contrib/users/test_views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/contrib/users/test_views.py diff --git a/tests/contrib/users/test_views.py b/tests/contrib/users/test_views.py new file mode 100644 index 0000000..a416ac0 --- /dev/null +++ b/tests/contrib/users/test_views.py @@ -0,0 +1,16 @@ +"""Test vega_admin.contrib.users.forms module""" + +from django.test import TestCase + + +class TestViewss(TestCase): + """ + Test class for vega_admin.contrib.users.views + """ + + def test_changepassword(self): + """ + Test ChangePassword + + """ + self.fail() From 4cfdb5328f951ea2b1e74639dff3f4a58ccd6561 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 30 Jan 2019 20:45:52 +0300 Subject: [PATCH 23/47] Add CRUD view for django.contrib.auth.groups --- example/templates/vega_admin/badmin/sidebar.html | 5 +++++ vega_admin/contrib/users/urls.py | 5 +++-- vega_admin/contrib/users/views.py | 13 ++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/example/templates/vega_admin/badmin/sidebar.html b/example/templates/vega_admin/badmin/sidebar.html index 67ce171..7db73d5 100644 --- a/example/templates/vega_admin/badmin/sidebar.html +++ b/example/templates/vega_admin/badmin/sidebar.html @@ -11,6 +11,11 @@
  • {% trans "View Users" %}
  • {% trans "Create User" %}
  • + +
    \ No newline at end of file diff --git a/vega_admin/contrib/users/urls.py b/vega_admin/contrib/users/urls.py index aa272e3..ccaa964 100644 --- a/vega_admin/contrib/users/urls.py +++ b/vega_admin/contrib/users/urls.py @@ -1,4 +1,5 @@ """example URL Configuration""" -from vega_admin.contrib.users import views +from vega_admin.contrib.users.views import UserCRUD, GroupCRUD -urlpatterns = views.UserCRUD().url_patterns() # pylint: disable=invalid-name +# pylint: disable=invalid-name +urlpatterns = UserCRUD().url_patterns() + GroupCRUD().url_patterns() diff --git a/vega_admin/contrib/users/views.py b/vega_admin/contrib/users/views.py index 4c48a33..0d808c3 100644 --- a/vega_admin/contrib/users/views.py +++ b/vega_admin/contrib/users/views.py @@ -1,5 +1,5 @@ """Views module for Vega Admin users app""" -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.conf import settings from django.urls import reverse_lazy @@ -41,3 +41,14 @@ class UserCRUD(VegaCRUDView): "change password", settings.VEGA_DELETE_ACTION, ] + + +class GroupCRUD(VegaCRUDView): + """ + CRUD view for Groups + """ + + model = Group + protected_actions = None + permissions_actions = None + # list_fields = ['id', 'name'] From 44d007fea013843701de9964cbd72f6ab1518fba Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 30 Jan 2019 20:47:47 +0300 Subject: [PATCH 24/47] Format code --- vega_admin/contrib/users/views.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/vega_admin/contrib/users/views.py b/vega_admin/contrib/users/views.py index 0d808c3..4b6e410 100644 --- a/vega_admin/contrib/users/views.py +++ b/vega_admin/contrib/users/views.py @@ -1,10 +1,10 @@ """Views module for Vega Admin users app""" -from django.contrib.auth.models import User, Group from django.conf import settings +from django.contrib.auth.models import Group, User from django.urls import reverse_lazy -from vega_admin.contrib.users.forms import (AddUserForm, PasswordChangeForm, - EditUserForm) +from vega_admin.contrib.users.forms import (AddUserForm, EditUserForm, + PasswordChangeForm) from vega_admin.views import VegaCRUDView, VegaUpdateView @@ -27,14 +27,12 @@ class UserCRUD(VegaCRUDView): """ model = User - list_fields = ['id', 'username', 'email', 'first_name', 'last_name'] + list_fields = ["id", "username", "email", "first_name", "last_name"] create_form_class = AddUserForm update_form_class = EditUserForm protected_actions = None permissions_actions = None - view_classes = { - "change password": ChangePassword, - } + view_classes = {"change password": ChangePassword} table_actions = [ settings.VEGA_READ_ACTION, settings.VEGA_UPDATE_ACTION, @@ -51,4 +49,4 @@ class GroupCRUD(VegaCRUDView): model = Group protected_actions = None permissions_actions = None - # list_fields = ['id', 'name'] + list_fields = ['id', 'name'] From e4af47d7781a0d8c041d884ec84cb3a777e4d0a8 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 30 Jan 2019 21:07:52 +0300 Subject: [PATCH 25/47] Add `get_permissions` method --- vega_admin/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vega_admin/views.py b/vega_admin/views.py index bca5b10..32c8225 100644 --- a/vega_admin/views.py +++ b/vega_admin/views.py @@ -180,6 +180,11 @@ def get_permissions_actions(self): return self.permissions_actions return [] + def get_permissions(self): + """Get list of permission names associated with this CRUD view""" + actions = self.get_actions() + return [self.get_permission_for_action(action) for action in actions] + def get_search_fields(self): """Get search fields for list view""" return self.search_fields From 7621f63698b6fb3e0280e3eb77ace48d0e521ae4 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 30 Jan 2019 21:08:02 +0300 Subject: [PATCH 26/47] Add test for get_permissions --- tests/test_crud.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_crud.py b/tests/test_crud.py index 260881d..329f897 100644 --- a/tests/test_crud.py +++ b/tests/test_crud.py @@ -360,6 +360,24 @@ def test_paginate_by(self): list_res = self.client.get(list_url) self.assertEqual(list_res.context["object_list"].count(), 10) + def test_get_permissions(self): + """Test get_permissions""" + actions = [ + "create", + "update", + "delete", + "template", + "view", + "list", + "artists", + ] + expected = [ + f"artist_app.{action}_song" for action in actions + ] + + self.assertEqual( + set(expected), set(CustomSongCRUD().get_permissions())) + @override_settings(LOGIN_URL='/list/artists/') def test_login_protection(self): """ From 1e3468fa1f990f826dfe08c6bc26d56fa5ad2e4c Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 21:24:14 +0300 Subject: [PATCH 27/47] Better env definitions for tox --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 10ec7a3..86ec1fb 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,7 @@ envlist = flake8 pylint - py36-django{20,21} - py37-django{20,21} + py{36,37}-django{20,21} [testenv:flake8] deps = From ded27440114209dffbe0fe62b81636911cf321f3 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 21:29:11 +0300 Subject: [PATCH 28/47] Upgrade django --- requirements.txt | 2 +- requirements/dev.txt | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index c8a6bdc..c91a78c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-braces==1.13.0 django-crispy-forms==1.7.2 django-filter==2.0.0 django-tables2==2.0.3 -django==2.1.5 +django==2.1.7 et-xmlfile==1.0.1 # via openpyxl jdcal==1.4 # via openpyxl odfpy==1.3.6 # via tablib diff --git a/requirements/dev.txt b/requirements/dev.txt index fa215cf..9d244c4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements/dev.txt requirements/dev.in +# pip-compile requirements/dev.in # appdirs==1.4.3 # via black astroid==2.0.4 # via pylint @@ -16,7 +16,7 @@ django-braces==1.13.0 django-crispy-forms==1.7.2 django-filter==2.0.0 django-tables2==2.0.3 -django==2.1.5 # via django-filter, django-tables2, model-mommy +django==2.1.7 # via django-filter, django-tables2, model-mommy et-xmlfile==1.0.1 # via openpyxl filelock==3.0.10 # via tox flake8==3.6.0 diff --git a/setup.py b/setup.py index c8155f6..ac6a8f0 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ url='https://github.com/moshthepitt/django-vega-admin', packages=find_packages(exclude=['docs', 'tests']), install_requires=[ - 'Django >= 2.0', + 'Django >=2.0.11', 'django-crispy-forms', 'django-braces', 'django-filter', From 3b252f30edab3545897c61b0017ee1cdd53df18b Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 21:41:49 +0300 Subject: [PATCH 29/47] Add test for good data with AddUserForm --- tests/contrib/users/test_forms.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/contrib/users/test_forms.py b/tests/contrib/users/test_forms.py index 292daa5..99404f7 100644 --- a/tests/contrib/users/test_forms.py +++ b/tests/contrib/users/test_forms.py @@ -2,6 +2,8 @@ from django.test import TestCase +from vega_admin.contrib.users.forms import AddUserForm + class TestForms(TestCase): """ @@ -13,7 +15,24 @@ def test_adduserform(self): Test AddUserForm """ - self.fail() + good_data = { + "first_name": "mosh", + "last_name": "pitt", + "username": "moshthepitt", + "email": "mosh@example.com", + "password": "TestAddUserForm", + } + + form = AddUserForm(data=good_data) + self.assertTrue(form.is_valid()) + user = form.save() + self.assertEqual("mosh", user.first_name) + self.assertEqual("pitt", user.last_name) + self.assertEqual("moshthepitt", user.username) + self.assertEqual("mosh@example.com", user.email) + self.assertTrue( + self.client.login( + username="moshthepitt", password="TestAddUserForm")) def test_edituserform(self): """ From 63a040de7e6cf95e5e69e1f3a8549ea4b9fa764d Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 21:52:06 +0300 Subject: [PATCH 30/47] Add test for bad data with AddUserForm --- tests/contrib/users/test_forms.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/contrib/users/test_forms.py b/tests/contrib/users/test_forms.py index 99404f7..a4c2302 100644 --- a/tests/contrib/users/test_forms.py +++ b/tests/contrib/users/test_forms.py @@ -34,6 +34,32 @@ def test_adduserform(self): self.client.login( username="moshthepitt", password="TestAddUserForm")) + # weak password + bad_data = { + "first_name": "mosh", + "last_name": "pitt", + "username": "moshthepitt2", + "email": "mosh@example.com", + "password": "mosh@example.com", + } + form = AddUserForm(data=bad_data) + self.assertFalse(form.is_valid()) + self.assertEqual(1, len(form.errors.keys())) + self.assertEqual('The password is too similar to the email address.', + form.errors['password'][0]) + + # missing email and username + bad_data = { + "first_name": "mosh", + "last_name": "pitt", + "password": "TestAddUserForm", + } + form = AddUserForm(data=bad_data) + self.assertFalse(form.is_valid()) + self.assertEqual(1, len(form.errors.keys())) + self.assertEqual('You must provide one of email or username', + form.errors['__all__'][0]) + def test_edituserform(self): """ Test EditUserForm From 151eb448d93f40ab608dac0786123b04272c9636 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 22:10:30 +0300 Subject: [PATCH 31/47] Add tests for PasswordChangeForm --- tests/contrib/users/test_forms.py | 65 +++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/tests/contrib/users/test_forms.py b/tests/contrib/users/test_forms.py index a4c2302..bc1322c 100644 --- a/tests/contrib/users/test_forms.py +++ b/tests/contrib/users/test_forms.py @@ -2,7 +2,10 @@ from django.test import TestCase -from vega_admin.contrib.users.forms import AddUserForm +from model_mommy import mommy + +from vega_admin.contrib.users.forms import (AddUserForm, EditUserForm, + PasswordChangeForm) class TestForms(TestCase): @@ -45,8 +48,10 @@ def test_adduserform(self): form = AddUserForm(data=bad_data) self.assertFalse(form.is_valid()) self.assertEqual(1, len(form.errors.keys())) - self.assertEqual('The password is too similar to the email address.', - form.errors['password'][0]) + self.assertEqual( + "The password is too similar to the email address.", + form.errors["password"][0], + ) # missing email and username bad_data = { @@ -57,18 +62,64 @@ def test_adduserform(self): form = AddUserForm(data=bad_data) self.assertFalse(form.is_valid()) self.assertEqual(1, len(form.errors.keys())) - self.assertEqual('You must provide one of email or username', - form.errors['__all__'][0]) + self.assertEqual("You must provide one of email or username", + form.errors["__all__"][0]) def test_edituserform(self): """ Test EditUserForm """ - self.fail() + user = mommy.make("auth.User") + + good_data = { + "first_name": "mosh", + "last_name": "pitt", + "username": "moshthepitt22", + "email": "mosh22@example.com", + } + form = EditUserForm(instance=user, data=good_data) + self.assertTrue(form.is_valid()) + user = form.save() + self.assertEqual("mosh", user.first_name) + self.assertEqual("pitt", user.last_name) + self.assertEqual("moshthepitt22", user.username) + self.assertEqual("mosh22@example.com", user.email) def test_password_change_form(self): """ Test PasswordChangeForm """ - self.fail() + user = mommy.make("auth.User", username="softie") + + good_data = { + "password1": "PasswordChangeForm", + "password2": "PasswordChangeForm", + } + form = PasswordChangeForm(instance=user, data=good_data) + self.assertTrue(form.is_valid()) + form.save() + self.assertTrue( + self.client.login( + username="softie", password="PasswordChangeForm")) + + # weak password + bad_data = {"password1": "123456789", "password2": "123456789"} + form = PasswordChangeForm(instance=user, data=bad_data) + self.assertFalse(form.is_valid()) + self.assertEqual(1, len(form.errors.keys())) + self.assertEqual("This password is too common.", + form.errors["password2"][0]) + self.assertEqual("This password is entirely numeric.", + form.errors["password2"][1]) + + # different passwords + bad_data = { + "password1": "PasswordChangeForm", + "password2": "123456789" + } + form = PasswordChangeForm(instance=user, data=bad_data) + self.assertFalse(form.is_valid()) + self.assertEqual(1, len(form.errors.keys())) + self.assertEqual("The two password fields didn't match.", + form.errors["password2"][0]) From 5c6a5a5e6ed9aac7ca8f29844a1d1b360a3d739c Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 22:10:52 +0300 Subject: [PATCH 32/47] Add password validators --- tests/settings.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/settings.py b/tests/settings.py index 751136c..7cb6be1 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -59,6 +59,28 @@ ] MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' +# Password validation +# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': + 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa + }, + { + 'NAME': + 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': + 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': + 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + TIME_ZONE = 'Africa/Nairobi' USE_I18N = True USE_L10N = True From c7b718e00993f409a08e638c4e0b807805ef2036 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 22:36:38 +0300 Subject: [PATCH 33/47] Add test for ChangePassword view --- tests/contrib/users/test_views.py | 34 ++++++++++++++++++++++++++++--- vega_admin/settings.py | 18 ++++++++-------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/tests/contrib/users/test_views.py b/tests/contrib/users/test_views.py index a416ac0..f1ceea5 100644 --- a/tests/contrib/users/test_views.py +++ b/tests/contrib/users/test_views.py @@ -1,9 +1,17 @@ """Test vega_admin.contrib.users.forms module""" -from django.test import TestCase +from django.test import TestCase, override_settings +from model_mommy import mommy -class TestViewss(TestCase): +from vega_admin.contrib.users.forms import PasswordChangeForm +from vega_admin.contrib.users.views import ChangePassword +from vega_admin.views import VegaUpdateView + + +@override_settings( + ROOT_URLCONF="vega_admin.contrib.users.urls", VEGA_TEMPLATE="basic") +class TestViews(TestCase): """ Test class for vega_admin.contrib.users.views """ @@ -13,4 +21,24 @@ def test_changepassword(self): Test ChangePassword """ - self.fail() + user = mommy.make("auth.User", username="TestChangePasswordView") + data = { + "password1": "Extension-I-School-5", + "password2": "Extension-I-School-5", + } + + res = self.client.get(f"/auth.user/change%20password/{user.id}/") + self.assertEqual(res.status_code, 200) + self.assertIsInstance(res.context["form"], PasswordChangeForm) + self.assertIsInstance(res.context["view"], ChangePassword) + self.assertIsInstance(res.context["view"], VegaUpdateView) + self.assertTemplateUsed(res, "vega_admin/basic/update.html") + res = self.client.post(f"/auth.user/change%20password/{user.id}/", + data) + self.assertEqual(res.status_code, 302) + self.assertRedirects(res, "/auth.user/list/") + user.refresh_from_db() + self.assertTrue( + self.client.login( + username="TestChangePasswordView", + password="Extension-I-School-5")) diff --git a/vega_admin/settings.py b/vega_admin/settings.py index 5a297c8..e128f96 100644 --- a/vega_admin/settings.py +++ b/vega_admin/settings.py @@ -16,11 +16,11 @@ VEGA_LIST_ACTION, VEGA_DELETE_ACTION, ] -VEGA_TEMPLATE = 'basic' +VEGA_TEMPLATE = "basic" # crispy forms -VEGA_CRISPY_TEMPLATE_PACK = getattr( - settings, "CRISPY_TEMPLATE_PACK", "bootstrap3") +VEGA_CRISPY_TEMPLATE_PACK = getattr(settings, "CRISPY_TEMPLATE_PACK", + "bootstrap3") # strings VEGA_FORM_VALID_CREATE_TXT = "Created successfully!" @@ -28,8 +28,7 @@ VEGA_FORM_VALID_DELETE_TXT = "Deleted successfully!" VEGA_FORM_INVALID_TXT = "Please correct the errors on the form." VEGA_DELETE_PROTECTED_ERROR_TXT = ( - "You cannot delete this item, it is referenced by other items." -) + "You cannot delete this item, it is referenced by other items.") VEGA_PERMREQUIRED_NOT_SET_TXT = "PermissionRequiredMixin not set for" VEGA_LISTVIEW_SEARCH_TXT = "Search" VEGA_LISTVIEW_SEARCH_QUERY_TXT = "Search Query" @@ -44,14 +43,15 @@ VEGA_ACTION_COLUMN_NAME = "" VEGA_ACTION_COLUMN_ACCESSOR_FIELD = "pk" VEGA_ACTION_LINK_SEPARATOR = " | " +VEGA_CHANGE_PASSWORD_LABEL = "change password" # exceptions VEGA_INVALID_ACTION = "Invalid Action" # contrib # users -VEGA_USERNAME_HELP_TEXT =\ - "Optional. 150 characters or fewer. Letters, digits and @/./+/-/_ only." +VEGA_USERNAME_HELP_TEXT = ( + "Optional. 150 characters or fewer. Letters, digits and @/./+/-/_ only.") VEGA_OPTIONAL_TXT = "Optional." -VEGA_EMAIL_OR_USERNAME_REQUIRED_TXT =\ - "You must provide one of email or username" +VEGA_EMAIL_OR_USERNAME_REQUIRED_TXT = ( + "You must provide one of email or username") From 8337735027950f683a248822b22864c9807c7daf Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 22:37:41 +0300 Subject: [PATCH 34/47] Remove magic strings --- vega_admin/contrib/users/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vega_admin/contrib/users/views.py b/vega_admin/contrib/users/views.py index 4b6e410..438724e 100644 --- a/vega_admin/contrib/users/views.py +++ b/vega_admin/contrib/users/views.py @@ -32,11 +32,11 @@ class UserCRUD(VegaCRUDView): update_form_class = EditUserForm protected_actions = None permissions_actions = None - view_classes = {"change password": ChangePassword} + view_classes = {settings.VEGA_CHANGE_PASSWORD_LABEL: ChangePassword} table_actions = [ settings.VEGA_READ_ACTION, settings.VEGA_UPDATE_ACTION, - "change password", + settings.VEGA_CHANGE_PASSWORD_LABEL, settings.VEGA_DELETE_ACTION, ] @@ -49,4 +49,4 @@ class GroupCRUD(VegaCRUDView): model = Group protected_actions = None permissions_actions = None - list_fields = ['id', 'name'] + list_fields = ["id", "name"] From 82722706542b3ca0c989d3475078ae408d03b6cd Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:02:26 +0300 Subject: [PATCH 35/47] Add VegaOrderedQuerysetMixin --- vega_admin/mixins.py | 22 ++++++++++++++++++++++ vega_admin/settings.py | 3 +++ vega_admin/views.py | 3 ++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/vega_admin/mixins.py b/vega_admin/mixins.py index 53c6408..28e11ca 100644 --- a/vega_admin/mixins.py +++ b/vega_admin/mixins.py @@ -36,6 +36,28 @@ class VegaFormMixin(VegaFormKwargsMixin): pass +class VegaOrderedQuerysetMixin: + """ + Optionally ensures querysets are ordered + """ + order_by = None + + def get_order_by(self): + """Get the field to use when ordering""" + return self.order_by or settings.VEGA_ORDERING_FIELD + + def get_queryset(self): + """ + Get the queryset + """ + queryset = super().get_queryset() + if not queryset.ordered and settings.VEGA_FORCE_ORDERING: + order_by = self.get_order_by() + queryset = queryset.order_by(order_by) + + return queryset + + class ListViewSearchMixin: """ Adds search to listview diff --git a/vega_admin/settings.py b/vega_admin/settings.py index e128f96..fd1152f 100644 --- a/vega_admin/settings.py +++ b/vega_admin/settings.py @@ -17,6 +17,9 @@ VEGA_DELETE_ACTION, ] VEGA_TEMPLATE = "basic" +# ensures that listview queries are ordered +VEGA_FORCE_ORDERING = True +VEGA_ORDERING_FIELD = "-pk" # crispy forms VEGA_CRISPY_TEMPLATE_PACK = getattr(settings, "CRISPY_TEMPLATE_PACK", diff --git a/vega_admin/views.py b/vega_admin/views.py index 32c8225..f066cb8 100644 --- a/vega_admin/views.py +++ b/vega_admin/views.py @@ -19,7 +19,7 @@ ListViewSearchMixin, ObjectTitleMixin, ObjectURLPatternMixin, PageTitleMixin, SimpleURLPatternMixin, VegaFormMixin, - VerboseNameMixin) + VegaOrderedQuerysetMixin, VerboseNameMixin) from vega_admin.utils import (get_filterclass, get_listview_form, get_modelform, get_table) @@ -33,6 +33,7 @@ class VegaListView( ExportMixin, SingleTableView, SimpleURLPatternMixin, + VegaOrderedQuerysetMixin, ListView,): """ vega-admin Generic List View From 42fc960ca9e00a6f07e22a873b57b4764fd96875 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:03:04 +0300 Subject: [PATCH 36/47] Add VegaOrderedQuerysetMixin tests --- tests/contrib/users/test_views.py | 16 ++++++++++++++++ tests/test_views.py | 8 ++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/contrib/users/test_views.py b/tests/contrib/users/test_views.py index f1ceea5..03016f9 100644 --- a/tests/contrib/users/test_views.py +++ b/tests/contrib/users/test_views.py @@ -42,3 +42,19 @@ def test_changepassword(self): self.client.login( username="TestChangePasswordView", password="Extension-I-School-5")) + + @override_settings(VEGA_FORCE_ORDERING=True, VEGA_ORDERING_FIELD="pk") + def test_list_view_ordering(self): + """ + Test VEGA_FORCE_ORDERING=True + """ + res = self.client.get("/auth.user/list/") + self.assertTrue(res.context["object_list"].ordered) + + @override_settings(VEGA_FORCE_ORDERING=False) + def test_list_view_ordering_off(self): + """ + Test VEGA_FORCE_ORDERING=False + """ + res = self.client.get("/auth.user/list/") + self.assertFalse(res.context["object_list"].ordered) diff --git a/tests/test_views.py b/tests/test_views.py index 4452976..60dc88e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -9,7 +9,7 @@ from model_mommy import mommy from vega_admin.views import (VegaCreateView, VegaCRUDView, VegaDeleteView, - VegaListView, VegaDetailView, VegaUpdateView) + VegaDetailView, VegaListView, VegaUpdateView) from .artist_app.forms import ArtistForm from .artist_app.models import Artist, Song @@ -101,7 +101,7 @@ def tearDown(self): @override_settings( ROOT_URLCONF="tests.artist_app.urls", - VEGA_TEMPLATE="basic", + VEGA_TEMPLATE="basic" ) class TestViews(TestViewsBase): """ @@ -188,17 +188,20 @@ class ArtistCrud(VegaCRUDView): view.get_url_name_for_action(action) ) + @override_settings(VEGA_FORCE_ORDERING=True, VEGA_ORDERING_FIELD="pk") def test_vega_list_view(self): """ Test VegaListView """ artist = mommy.make("artist_app.Artist", name="Bob") mommy.make("artist_app.Artist", _quantity=7) + res = self.client.get("/list/artists/") self.assertEqual(res.status_code, 200) self.assertIsInstance(res.context["view"], ArtistListView) self.assertIsInstance(res.context["view"], VegaListView) self.assertEqual(res.context["object_list"].count(), 8) + self.assertTrue(res.context["object_list"].ordered) self.assertTemplateUsed(res, "vega_admin/basic/list.html") res = self.client.get("/list/artists/?q=Bob") @@ -210,6 +213,7 @@ def test_vega_list_view(self): set(res.context["vega_listview_search_form"].fields.keys()) ) self.assertEqual(res.context["object_list"].count(), 1) + self.assertTrue(res.context["object_list"].ordered) self.assertEqual(res.context["object_list"].first(), artist) def test_vega_view_view(self): From cff7779addb6dbbdc59e2e59e3d747976c1b1f4f Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:28:11 +0300 Subject: [PATCH 37/47] Ensure order_by is passed to listview --- vega_admin/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vega_admin/views.py b/vega_admin/views.py index f066cb8..2dbee52 100644 --- a/vega_admin/views.py +++ b/vega_admin/views.py @@ -144,6 +144,7 @@ class VegaCRUDView: # pylint: disable=too-many-public-methods table_class = None paginate_by = 25 crud_path = None + order_by = None def __init__(self, model=None): """ @@ -436,6 +437,7 @@ def get_view_class_for_action( # pylint: disable=too-many-branches if settings.VEGA_LIST_ACTION in self.get_actions(): options["list_url"] = reverse_lazy( self.get_url_name_for_action(settings.VEGA_LIST_ACTION)) + options["order_by"] = self.order_by if settings.VEGA_CREATE_ACTION in self.get_actions(): options["create_url"] = reverse_lazy( self.get_url_name_for_action(settings.VEGA_CREATE_ACTION)) From 09403ec773100f94988c8ed0c1e3635f9bc7c409 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:29:09 +0300 Subject: [PATCH 38/47] Change to using list for order_by --- tests/test_views.py | 2 +- vega_admin/contrib/users/views.py | 1 + vega_admin/mixins.py | 2 +- vega_admin/settings.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index 60dc88e..26ebe5a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -188,7 +188,7 @@ class ArtistCrud(VegaCRUDView): view.get_url_name_for_action(action) ) - @override_settings(VEGA_FORCE_ORDERING=True, VEGA_ORDERING_FIELD="pk") + @override_settings(VEGA_FORCE_ORDERING=True, VEGA_ORDERING_FIELD=["pk"]) def test_vega_list_view(self): """ Test VegaListView diff --git a/vega_admin/contrib/users/views.py b/vega_admin/contrib/users/views.py index 438724e..3af350c 100644 --- a/vega_admin/contrib/users/views.py +++ b/vega_admin/contrib/users/views.py @@ -39,6 +39,7 @@ class UserCRUD(VegaCRUDView): settings.VEGA_CHANGE_PASSWORD_LABEL, settings.VEGA_DELETE_ACTION, ] + order_by = ["-last_login", "first_name"] class GroupCRUD(VegaCRUDView): diff --git a/vega_admin/mixins.py b/vega_admin/mixins.py index 28e11ca..a3ab17e 100644 --- a/vega_admin/mixins.py +++ b/vega_admin/mixins.py @@ -53,7 +53,7 @@ def get_queryset(self): queryset = super().get_queryset() if not queryset.ordered and settings.VEGA_FORCE_ORDERING: order_by = self.get_order_by() - queryset = queryset.order_by(order_by) + queryset = queryset.order_by(*order_by) return queryset diff --git a/vega_admin/settings.py b/vega_admin/settings.py index fd1152f..f043c53 100644 --- a/vega_admin/settings.py +++ b/vega_admin/settings.py @@ -19,7 +19,7 @@ VEGA_TEMPLATE = "basic" # ensures that listview queries are ordered VEGA_FORCE_ORDERING = True -VEGA_ORDERING_FIELD = "-pk" +VEGA_ORDERING_FIELD = ["-pk"] # crispy forms VEGA_CRISPY_TEMPLATE_PACK = getattr(settings, "CRISPY_TEMPLATE_PACK", From 2dd2099281c6c114faf2028f158bb575a5240287 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:29:29 +0300 Subject: [PATCH 39/47] Add ordering test for UserCRUD --- tests/contrib/users/test_views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/contrib/users/test_views.py b/tests/contrib/users/test_views.py index 03016f9..faa8c1d 100644 --- a/tests/contrib/users/test_views.py +++ b/tests/contrib/users/test_views.py @@ -43,13 +43,18 @@ def test_changepassword(self): username="TestChangePasswordView", password="Extension-I-School-5")) - @override_settings(VEGA_FORCE_ORDERING=True, VEGA_ORDERING_FIELD="pk") + @override_settings(VEGA_FORCE_ORDERING=True) def test_list_view_ordering(self): """ Test VEGA_FORCE_ORDERING=True """ + mommy.make("auth.User", _quantity=7) res = self.client.get("/auth.user/list/") self.assertTrue(res.context["object_list"].ordered) + self.assertEqual("-last_login", + res.context["object_list"].query.order_by[0]) + self.assertEqual("first_name", + res.context["object_list"].query.order_by[1]) @override_settings(VEGA_FORCE_ORDERING=False) def test_list_view_ordering_off(self): From c4aa83f0707c5a39b3fc12f45109a1d6f9ebb109 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:49:09 +0300 Subject: [PATCH 40/47] Use requirements/dev.txt --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 86ec1fb..8d23c67 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ basepython = py36: python3.6 py37: python3.7 commands = - pip install -r requirements/dev.in + pip install -r requirements/dev.txt coverage erase coverage run --include="vega_admin/**.*" --omit="tests/**.*,vega_admin/migrations/**.*" manage.py test {toxinidir}/tests coverage report From 816eaa95d2c8a1ed94ea58b6a24cb3ced25aa725 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:50:10 +0300 Subject: [PATCH 41/47] Install allauth for tests --- requirements/dev.in | 3 +++ requirements/dev.txt | 12 +++++++++++- tests/settings.py | 11 +++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/requirements/dev.in b/requirements/dev.in index dda7359..edf717c 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -1,6 +1,9 @@ # load from setup.py -e . +# allauth +django-allauth + # others pylint pylint-django diff --git a/requirements/dev.txt b/requirements/dev.txt index 9d244c4..bcad2ea 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -9,17 +9,22 @@ astroid==2.0.4 # via pylint attrs==18.2.0 # via black backcall==0.1.0 # via ipython black==18.9b0 +certifi==2018.11.29 # via requests +chardet==3.0.4 # via requests click==7.0 # via black coverage==4.5.2 decorator==4.3.0 # via ipython, traitlets +defusedxml==0.5.0 # via python3-openid +django-allauth==0.39.1 django-braces==1.13.0 django-crispy-forms==1.7.2 django-filter==2.0.0 django-tables2==2.0.3 -django==2.1.7 # via django-filter, django-tables2, model-mommy +django==2.1.7 # via django-allauth, django-filter, django-tables2, model-mommy et-xmlfile==1.0.1 # via openpyxl filelock==3.0.10 # via tox flake8==3.6.0 +idna==2.8 # via requests ipdb==0.11 ipython-genutils==0.2.0 # via traitlets ipython==7.1.1 # via ipdb @@ -29,6 +34,7 @@ jedi==0.13.1 # via ipython lazy-object-proxy==1.3.1 # via astroid mccabe==0.6.1 # via flake8, pylint model-mommy==1.6.0 +oauthlib==3.0.1 # via requests-oauthlib odfpy==1.3.6 # via tablib openpyxl==2.5.10 # via tablib parso==0.3.1 # via jedi @@ -46,8 +52,11 @@ pygments==2.2.0 # via ipython pylint-django==2.0.2 pylint-plugin-utils==0.4 # via pylint-django pylint==2.1.1 +python3-openid==3.1.0 # via django-allauth pytz==2018.7 # via django pyyaml==4.2b4 # via tablib +requests-oauthlib==1.2.0 # via django-allauth +requests==2.21.0 # via django-allauth, requests-oauthlib six==1.11.0 # via astroid, model-mommy, prompt-toolkit, tox, traitlets tablib==0.12.1 toml==0.10.0 # via black, tox @@ -55,6 +64,7 @@ tox==3.5.3 traitlets==4.3.2 # via ipython typed-ast==1.1.0 # via astroid unicodecsv==0.14.1 # via tablib +urllib3==1.24.1 # via requests virtualenv==16.1.0 # via tox wcwidth==0.1.7 # via prompt-toolkit wrapt==1.10.11 # via astroid diff --git a/tests/settings.py b/tests/settings.py index 7cb6be1..ad20189 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -19,6 +19,10 @@ 'crispy_forms', 'django_tables2', 'django_filters', + # just for testing + 'allauth', + 'allauth.account', + 'allauth.socialaccount', # custom 'vega_admin', 'vega_admin.contrib.users', @@ -59,6 +63,13 @@ ] MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' +AUTHENTICATION_BACKENDS = ( + # Needed to login by username in Django admin, regardless of `allauth` + 'django.contrib.auth.backends.ModelBackend', + # `allauth` specific authentication methods, such as login by e-mail + 'allauth.account.auth_backends.AuthenticationBackend' +) + # Password validation # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators From 303f71820d4cfc479c9d705af4b51cd2188c9347 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:50:36 +0300 Subject: [PATCH 42/47] Add more tests for AddUserForm --- tests/contrib/users/test_forms.py | 35 ++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/contrib/users/test_forms.py b/tests/contrib/users/test_forms.py index bc1322c..597874b 100644 --- a/tests/contrib/users/test_forms.py +++ b/tests/contrib/users/test_forms.py @@ -23,7 +23,7 @@ def test_adduserform(self): "last_name": "pitt", "username": "moshthepitt", "email": "mosh@example.com", - "password": "TestAddUserForm", + "password": "Miserable-Water-9", } form = AddUserForm(data=good_data) @@ -35,14 +35,27 @@ def test_adduserform(self): self.assertEqual("mosh@example.com", user.email) self.assertTrue( self.client.login( - username="moshthepitt", password="TestAddUserForm")) + username="moshthepitt", password="Miserable-Water-9")) + + # good data no username + good_data = { + "first_name": "mosh", + "last_name": "pitt", + "email": "mosh2@example.com", + "password": "Bean-Quarrel-0", + } + + form = AddUserForm(data=good_data) + self.assertTrue(form.is_valid()) + user = form.save() + self.assertEqual("mosh", user.username) # was generated # weak password bad_data = { "first_name": "mosh", "last_name": "pitt", "username": "moshthepitt2", - "email": "mosh@example.com", + "email": "mosh3@example.com", "password": "mosh@example.com", } form = AddUserForm(data=bad_data) @@ -53,6 +66,22 @@ def test_adduserform(self): form.errors["password"][0], ) + # email not unique + bad_data = { + "first_name": "mosh", + "last_name": "pitt", + "username": "moshthepitt3", + "email": "mosh@example.com", + "password": "Every-Actor-1", + } + form = AddUserForm(data=bad_data) + self.assertFalse(form.is_valid()) + self.assertEqual(1, len(form.errors.keys())) + self.assertEqual( + "A user is already registered with this e-mail address.", + form.errors["email"][0], + ) + # missing email and username bad_data = { "first_name": "mosh", From 3333d0b66795f0d5f622a23e93a5250027327059 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Wed, 6 Mar 2019 23:50:52 +0300 Subject: [PATCH 43/47] Code cleanup --- vega_admin/contrib/users/forms.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 89ef3ab..5951218 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -99,7 +99,7 @@ def clean_password(self): def validate_unique_email(self, value): # pylint: disable=no-self-use """validate unique email while adding users""" try: - return get_adapter().validate_unique_email(value) + return get_adapter().validate_unique_email(email=value) except NameError: return value @@ -166,8 +166,7 @@ def __init__(self, *args, **kwargs): def validate_unique_email(self, value): """Validate unique email when editting users""" try: - return get_adapter().validate_unique_email( - value, user=self.instance) + return get_adapter().validate_unique_email(email=value) except NameError: return value From 425d583c35c5167bdd7458785a2aff069ccea69a Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 7 Mar 2019 00:07:54 +0300 Subject: [PATCH 44/47] Ensure cancel url on password change is set --- tests/contrib/users/test_forms.py | 20 +++++++++++++++++++- vega_admin/contrib/users/forms.py | 6 +++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/contrib/users/test_forms.py b/tests/contrib/users/test_forms.py index 597874b..231f3ba 100644 --- a/tests/contrib/users/test_forms.py +++ b/tests/contrib/users/test_forms.py @@ -1,6 +1,7 @@ """Test vega_admin.contrib.users.forms module""" -from django.test import TestCase +from django.test import TestCase, override_settings +from unittest.mock import patch from model_mommy import mommy @@ -8,6 +9,8 @@ PasswordChangeForm) +@override_settings( + ROOT_URLCONF="vega_admin.contrib.users.urls", VEGA_TEMPLATE="basic") class TestForms(TestCase): """ Test class for vega_admin.contrib.users.forms @@ -152,3 +155,18 @@ def test_password_change_form(self): self.assertEqual(1, len(form.errors.keys())) self.assertEqual("The two password fields didn't match.", form.errors["password2"][0]) + + @patch('vega_admin.contrib.users.forms.get_form_actions') + def test_password_change_form_cancel_url(self, mock): + """Test cancel url on password change form""" + user = mommy.make("auth.User") + + good_data = { + "password1": "PasswordChangeForm", + "password2": "PasswordChangeForm", + } + form = PasswordChangeForm(instance=user, data=good_data) + self.assertTrue(form.is_valid()) + form.save() + + mock.assert_called_with(cancel_url="/auth.user/list/") diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 5951218..5617cc8 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -10,6 +10,7 @@ from crispy_forms.layout import Layout from vega_admin.utils import get_form_actions, get_form_helper_class +from django.urls import reverse_lazy try: # pylint: disable=import-error @@ -18,7 +19,7 @@ except ModuleNotFoundError: UNIQUE_EMAIL = False else: - UNIQUE_EMAIL = app_settings.UNIQUE_EMAIL + UNIQUE_EMAIL = app_settings.UNIQUE_EMAIL # pylint: disable=no-member class UserFormMixin: # pylint: disable=too-few-public-methods @@ -187,6 +188,5 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( Field("password1"), Field("password2"), - get_form_actions( - cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), + get_form_actions(cancel_url=reverse_lazy("auth.user-list")), ) From 7f860904dd1d44f722bbd93f09cba88d47c079e4 Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 7 Mar 2019 00:11:10 +0300 Subject: [PATCH 45/47] Fix pylint issue --- vega_admin/contrib/users/forms.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 5617cc8..f49bf02 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -22,6 +22,14 @@ UNIQUE_EMAIL = app_settings.UNIQUE_EMAIL # pylint: disable=no-member +def validate_unique_email(value): + """Validate unique email when editting users""" + try: + return get_adapter().validate_unique_email(email=value) + except NameError: + return value + + class UserFormMixin: # pylint: disable=too-few-public-methods """User forms mixin""" @@ -33,7 +41,7 @@ def clean_email(self): except NameError: pass if value and UNIQUE_EMAIL: - value = self.validate_unique_email(value) + value = validate_unique_email(value) return value @@ -164,13 +172,6 @@ def __init__(self, *args, **kwargs): cancel_url=self.vega_extra_kwargs.get("cancel_url", "/")), ) - def validate_unique_email(self, value): - """Validate unique email when editting users""" - try: - return get_adapter().validate_unique_email(email=value) - except NameError: - return value - class PasswordChangeForm(AdminPasswordChangeForm): """Custom form for admins to change user passwords""" From 3a0f2de656532d0ee070d4096e95313ac529ec9d Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 7 Mar 2019 00:14:49 +0300 Subject: [PATCH 46/47] Fix broken test --- tests/test_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_views.py b/tests/test_views.py index 26ebe5a..5992241 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -193,6 +193,8 @@ def test_vega_list_view(self): """ Test VegaListView """ + Artist.objects.all().delete() + artist = mommy.make("artist_app.Artist", name="Bob") mommy.make("artist_app.Artist", _quantity=7) From f1ecb08eec4364dd21d923214e4a4a9ffd58e89e Mon Sep 17 00:00:00 2001 From: Kelvin Jayanoris Date: Thu, 7 Mar 2019 00:15:49 +0300 Subject: [PATCH 47/47] =?UTF-8?q?=E2=86=91=20bump=20to=20version=200.0.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vega_admin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vega_admin/__init__.py b/vega_admin/__init__.py index 6481db0..bbdf165 100644 --- a/vega_admin/__init__.py +++ b/vega_admin/__init__.py @@ -1,7 +1,7 @@ """ Main init file for vega_admin """ -VERSION = (0, 0, 6) +VERSION = (0, 0, 7) __version__ = '.'.join(str(v) for v in VERSION) # pylint: disable=invalid-name default_app_config = 'vega_admin.apps.VegaAdminConfig' # noqa