Does anyone know how to OR together Django model field validators?
Something like this:
example_field = models.CharField(max_length=255, validators=[validator1|validator2])
I am guessing that there is a way and it involves the Q operator, but I can't find what it is exactly.
You can do the validation in a function itself:
from django.core.exceptions import ValidationError
from django.db import models
def combined_validator(value):
try:
return validator1(value)
except ValidationError:
return validator2(value)
class MyModel(models.Model):
example_field = models.CharField(
max_length=255,
validators=[combined_validator]
)
If validator1 does not detect any trouble, then the control flow is returned, and thus we are safe. If it raises a ValidationError, then we fallback on validator2. If that does not raises an error, then we are again safe. Otherwise, the error will raise out of combined_validator.
I needed an OR-validator as well, so I made a little reusable validator that
can accept any number of sub-validators that are OR-ed together.
Inspired by Django's own validators, so should work everywhere (only tested in Django Rest Framework serializer)
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator, RegexValidator
from django.utils.deconstruct import deconstructible
from rest_framework import serializers
#deconstructible # allows usage in migrations
class OrValidator:
message = 'Enter a valid value.' # customize this based on the sub-validators
code = 'invalid'
def __init__(self, validators, message=None, code=None):
self.validators = validators
self._errors = []
if code is not None:
self.code = code
if message is not None:
self.message = message
def __call__(self, value):
for validator in self.validators:
try:
return validator(value)
except ValidationError as e:
# save error for debugging
self._errors.append(e)
# non matched, raise error
raise ValidationError(self.message, code=self.code)
def __eq__(self, other):
return (
self.validators == other.validators and
isinstance(other, self.__class__) and
self.message == other.message and
self.code == other.code
)
class UsernameValidator(RegexValidator):
regex = re.compile(r'^[-\w]+\Z', re.UNICODE)
# example usage in Django Rest Framework (should work in forms and models as well)
class ResetPasswordSerializer(serializers.Serializer):
email_or_username = serializers.CharField(
required=True,
validators=[
OrValidator(
[EmailValidator(), UsernameValidator()],
message='Enter a valid email or username',
),
],
)
Related
I'm trying to add custom functionality to django router methods.
This is my router that exposes the standard methods on an user.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [BasePermission]
I'm validating the user using serializer validation methods.
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
MOBILE_ERROR = 'Mobile number should be 10 digits long and only contain numbers.'
EMAIL_ERROR = 'Incorrect email format'
USERNAME_ERROR = 'Username must be at least 6 characters long and contain only letters and numbers.'
class Meta:
model = User
fields = '__all__'
def validate_mobile(self, value):
regexp = re.compile(r'^[0-9]{10}$')
if regexp.search(value):
return value
raise serializers.ValidationError(self.MOBILE_ERROR)
def validate_email(self, value):
if validate_email(value):
return value
raise serializers.ValidationError(self.EMAIL_ERROR)
def validate_username(self, value):
regexp = re.compile(r'^[a-zA-Z0-9]{6,}$')
if regexp.search(value):
return value
raise serializers.ValidationError(self.USERNAME_ERROR)
And this is my route.
router = DefaultRouter(trailing_slash=False)
router.register(r'user', UserViewSet),
urlpatterns = router.urls
I want to add a method send_activation_code if the user is created successfully. How do I do this?
For such purpose you can use signals. Every time when your app creates new User instance - some action should be performed. In your case you should connect build-in signal post_save and your existed send_activation_code function
Example for your case:
yourapp/signals.py:
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=User)
def send_activation_code_signal(sender, instance, created, **kwargs):
if created:
send_activation_code(instance.phone_number)
Also, you need to import signals in your app config file
yourapp/app.py:
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class YourAppConfig(AppConfig):
name = 'yourproject.yourapp'
verbose_name = _('yourapp')
def ready(self):
import yourproject.yourapp.signals
yourapp/__init__.py:
default_app_config = 'yourproject.yourapp.apps.YourAppConfig'
If you dont need to send code every time User instance created - you can specify more statements, for example:
if created and instance.validated:
send_activation_code(instance.phone_number)
There are some more useful built-in signals in Django, check docs
Django signals docs: https://docs.djangoproject.com/en/3.0/ref/signals/
I want to validate my email field like: if email contains [gmail.com,outlook.com,yahoo.com] then I want to raise validation Error. But it's not working, I don't know what i am doing wrong. plz help me
views.py
from django.shortcuts import render
from django.views.generic import View
from access.utils import is_valid
from access.mixin import HttpResponseMixin
import json
from access.forms import Employer_Form
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
#method_decorator(csrf_exempt,name = 'dispatch')
class Emp_Registration_view(View,HttpResponseMixin):
def post (self,request,*args,**kwargs):
data = request.body
json_data = is_valid(data)
if not json_data:
return self.render_http_response(json.dumps({'msg':'Please send valid json only'}),status=400)
emp_data = json.loads(data)
form= Employer_Form(emp_data)
if form.is_valid():
form.save(commit=True)
return self.render_http_response(json.dumps({'msg':'Registered Successfully'}))
if form.errors:
return self.render_http_response(json.dumps(form.errors),status=400)
forms.py
from access.models import Employer_Registration
from django import forms
class Employer_Form(forms.ModelForm):
def clean_email(self):
email = self.cleaned_data['emp_email']
email_lists = ['gmail.com','yahoo.com','outlook.com','hotmail.com']
data = emp_email.split('#')
if data in email_lists:
raise forms.ValidationError("email is not valid")
return email
class Meta:
model = Employer_Registration
fields = '__all__'
Your method should be named clean_emp_email, because the field is named emp_email. Otherwise it won't be called.
Seems like you are splitting on # so if you split example#gmail.com it will be [example , gmail.com] and that you are comparing in this line exactly
if [example , gmail.com] in email_lists
so it is not found I suggest you to do you can omit splitting and find in substring as
for i in email_lists:
if i in self.cleaned_data['emp_email']:
raise forms.ValidationError("email is not valid")
After #Alexandr Tatarinov answer you should also call the clean_field_name or simply clean
In django-rest-framework-simplejwt plugin username and password are used by default. But I wanted to use email instead of username. So, I did like below:
In serializer:
class MyTokenObtainSerializer(Serializer):
username_field = User.EMAIL_FIELD
def __init__(self, *args, **kwargs):
super(MyTokenObtainSerializer, self).__init__(*args, **kwargs)
self.fields[self.username_field] = CharField()
self.fields['password'] = PasswordField()
def validate(self, attrs):
# self.user = authenticate(**{
# self.username_field: attrs[self.username_field],
# 'password': attrs['password'],
# })
self.user = User.objects.filter(email=attrs[self.username_field]).first()
print(self.user)
if not self.user:
raise ValidationError('The user is not valid.')
if self.user:
if not self.user.check_password(attrs['password']):
raise ValidationError('Incorrect credentials.')
print(self.user)
# Prior to Django 1.10, inactive users could be authenticated with the
# default `ModelBackend`. As of Django 1.10, the `ModelBackend`
# prevents inactive users from authenticating. App designers can still
# allow inactive users to authenticate by opting for the new
# `AllowAllUsersModelBackend`. However, we explicitly prevent inactive
# users from authenticating to enforce a reasonable policy and provide
# sensible backwards compatibility with older Django versions.
if self.user is None or not self.user.is_active:
raise ValidationError('No active account found with the given credentials')
return {}
#classmethod
def get_token(cls, user):
raise NotImplemented(
'Must implement `get_token` method for `MyTokenObtainSerializer` subclasses')
class MyTokenObtainPairSerializer(MyTokenObtainSerializer):
#classmethod
def get_token(cls, user):
return RefreshToken.for_user(user)
def validate(self, attrs):
data = super(MyTokenObtainPairSerializer, self).validate(attrs)
refresh = self.get_token(self.user)
data['refresh'] = text_type(refresh)
data['access'] = text_type(refresh.access_token)
return data
In view:
class MyTokenObtainPairView(TokenObtainPairView):
"""
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
"""
serializer_class = MyTokenObtainPairSerializer
And it works!!
Now my question is, how can I do it more efficiently? Can anyone give suggestion on this? Thanks in advance.
This answer is for future readers and therefore contains extra information.
In order to simplify the authentication backend, you have multiple classes to hook into. I would suggest to do option 1 (and optionally option 3, a simplified version of yours) below. Couple of notes before you read on:
Note 1: django does not enforce email as required or being unique on user creation (you can override this, but it's off-topic)! Option 3 (your implementation) might therefore give you issues with duplicate emails.
Note 1b: use User.objects.filter(email__iexact=...) to match the emails in a case insensitive way.
Note 1c: use get_user_model() in case you replace the default user model in future, this really is a life-saver for beginners!
Note 2: avoid printing the user to console. You might be printing sensitive data.
As for the 3 options:
Adjust django authentication backend with f.e. class EmailModelBackend(ModelBackend) and replace authenticate function.
Does not adjust token claims
Not dependent on JWT class/middleware (SimpleJWT, JWT or others)
Also adjusts other authentication types (Session/Cookie/non-API auth, etc.)
The required input parameter is still username, example below. Adjust if you dont like it, but do so with care. (Might break your imports/plugins and is not required!)
Replace django authenticate(username=, password=, **kwarg) from django.contrib.auth
Does not adjust token claims
You need to replace token backend as well, since it should use a different authentication, as you did above.
Does not adjust other apps using authenticate(...), only replaces JWT auth (if you set it up as such)
parameters is not required and therefore this option is less adviced).
Implement MyTokenObtainPairSerializer with email as claim.
Now email is sent back as JWT data (and not id).
Together with option 1, your app authentication has become username agnostic.
Option 1 (note that this also allows username!!):
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
class EmailorUsernameModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
try:
user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
except UserModel.DoesNotExist:
return None
else:
if user.check_password(password):
return user
return None
Option 2:
Skipped, left to reader and not adviced.
Option 3:
You seem to have this covered above.
Note: you dont have to define MyTokenObtainPairView, you can use TokenObtainPairView(serializer_class=MyTokenObtainPairSerializer).as_view() in your urls.py. Small simplification which overrides the used token serializer.
Note 2: You can specify the identifying claim and the added data in your settings.py (or settings file) to use email as well. This will make your frontend app use the email for the claim as well (instead of default user.id)
SIMPLE_JWT = {
'USER_ID_FIELD': 'id', # model property to attempt claims for
'USER_ID_CLAIM': 'user_id', # actual keyword in token data
}
However, heed the uniqueness warnings given by the creators:
For example, specifying a "username" or "email" field would be a poor choice since an account's username or email might change depending on how account management in a given service is designed.
If you can guarantee uniqueness, you are all set.
Why did you copy and paste so much instead of subclassing? I got it to work with:
# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainSerializer
class EmailTokenObtainSerializer(TokenObtainSerializer):
username_field = User.EMAIL_FIELD
class CustomTokenObtainPairSerializer(EmailTokenObtainSerializer):
#classmethod
def get_token(cls, user):
return RefreshToken.for_user(user)
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data["refresh"] = str(refresh)
data["access"] = str(refresh.access_token)
return data
And
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
class EmailTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
And of course
#urls.py
from rest_framework_simplejwt.views import TokenRefreshView
from .views import EmailTokenObtainPairView
url("token/", EmailTokenObtainPairView.as_view(), name="token_obtain_pair"),
url("refresh/", TokenRefreshView.as_view(), name="token_refresh"),
The question has been a while but, I add +1 for #Mic's answer. By the way, wasn't it sufficient to update to TokenObtainPairSerializer only as following?:
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import (
TokenObtainPairSerializer, User
)
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = User.EMAIL_FIELD
class EmailTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
Let summarize the above solutions:
1- Create two app by Django command. One for the new token and the other for the user:
python manage.py startapp m_token # modified token
python manage.py startapp m_user # modified user
2- In the m_token, create the serializers.py and override the serializer to replace username with email field:
# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, User
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = User.EMAIL_FIELD
3- In the m_token, override the view to replace the serializer with the new one:
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializer import CustomTokenObtainPairSerializer
class EmailTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
4- In the m_token, create the urls.py and give the paths as follows:
# urls.py
from django.urls import path
from .views import TokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
urlpatterns = [
path(r'token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path(r'token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path(r'token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
5- In the m_user, override the user model as follows:
# models.py
from django.contrib.auth.models import AbstractUser
class MUser(AbstractUser):
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['username']
6- In the django project root, add AUTH_USER_MODEL = 'm_user.MUser' to setting.py.
I tested it in my project and it worked perfectly. I hope I did not miss anything. This way the swagger also shows "email" instead of "username" in the token parameters:
And in addition to #Mic's answer, remember to set USERNAME_FIELD = 'email' and may be REQUIRED_FIELDS = ['username'] in the User model.
For those using a custom User model, you simply can add those lines:
class User(AbstractUser):
...
email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
Then, in urls.py:
from rest_framework_simplejwt.views import TokenObtainPairView
urlpatterns = [
...
path('api/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
Using this code you can allow users to login using either username or email in the username field. You can add some lines to validate the email.
class TokenPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
raw_username = attrs["username"]
users = User.objects.filter(email=raw_username)
if(users):
attrs['username'] = users.first().username
# else:
# raise serializers.ValidationError("Only email is allowed!")
data = super(TokenPairSerializer, self).validate(attrs)
return data
This is my serializers.py (I want to create a serializer for the built-in User model):
from rest_framework import serializers
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'password', 'email', )
I'm aware that Django Rest Framework has it's own field validators, because when I try to create a user using a username which already exists, it raises an error saying:
{'username': [u'This field must be unique.']}
I want to customize the error message and make it say "This username is already taken. Please try again" rather than saying "This field must be unique".
It also has a built-in regex validator, because when I create a username with an exclamation mark, it says:
{'username': [u'Enter a valid username. This value may contain only letters, numbers and #/./+/-/_ characters.']}
I want to customize the regex validator so that it just says "Invalid username".
How do I customize all of the error messages which each field has?
Note: according to this post: Custom error messages in Django Rest Framework serializer I can do:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
self.fields['username'].error_messages['required'] = u'My custom required msg'
But what do I do for the 'unique' and 'regex' validators? I tried doing
self.fields['username'].error_messages['regex'] = u'My custom required msg'
and
self.fields['username'].error_messages['validators'] = u'My custom required msg'
but neither worked.
In order to replace unique or regex error messages you should change message member of corresponding validator object. This could be done using separate mixin class:
from django.core.validators import RegexValidator
from rest_framework.validators import UniqueValidator
from django.utils.translation import ugettext_lazy as _
class SetCustomErrorMessagesMixin:
"""
Replaces built-in validator messages with messages, defined in Meta class.
This mixin should be inherited before the actual Serializer class in order to call __init__ method.
Example of Meta class:
>>> class Meta:
>>> model = User
>>> fields = ('url', 'username', 'email', 'groups')
>>> custom_error_messages_for_validators = {
>>> 'username': {
>>> UniqueValidator: _('This username is already taken. Please, try again'),
>>> RegexValidator: _('Invalid username')
>>> }
>>> }
"""
def __init__(self, *args, **kwargs):
# noinspection PyArgumentList
super(SetCustomErrorMessagesMixin, self).__init__(*args, **kwargs)
self.replace_validators_messages()
def replace_validators_messages(self):
for field_name, validators_lookup in self.custom_error_messages_for_validators.items():
# noinspection PyUnresolvedReferences
for validator in self.fields[field_name].validators:
if type(validator) in validators_lookup:
validator.message = validators_lookup[type(validator)]
#property
def custom_error_messages_for_validators(self):
meta = getattr(self, 'Meta', None)
return getattr(meta, 'custom_error_messages_for_validators', {})
Then you could just inherit this mixin and update Meta class:
class UserSerializer(SetCustomErrorMessagesMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')
custom_error_messages_for_validators = {
'username': {
UniqueValidator: _('This username is already taken. Please, try again'),
RegexValidator: _('Invalid username')
}
}
You also can do that just customizing your language translation, look here:
https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#message-files
All you have to do is to create message files, change translation in there and compile your message into .mo files.
This method is not so much unacceptable as i might seems. Look here:
https://www.django-rest-framework.org/topics/internationalization/
DRF docs say that its common practice customizing error messages (your own and default ones) via translation.
My tips for those who`d want to try path above:
Make default django.po file. Most of default errors will be there.
If you are using DRF, then open this file https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po , chose error that you want to customize and copy only that error into your django.po flle above.
Customize it.
Compile it.
(!) NOTE Make sure that your translation file named exacly as django.po, not mylovelydjango.po, not myproject.po but EXACTLY django.po, or nothing will work.
My app has users who create pages. In the Page screen of the admin, I'd like to list the User who created the page, and in that list, I'd like the username to have a link that goes to the user page in admin (not the Page).
class PageAdmin(admin.ModelAdmin):
list_display = ('name', 'user', )
list_display_links = ('name','user',)
admin.site.register(Page, PageAdmin)
I was hoping that by making it a link in the list_display it would default to link to the actual user object, but it still goes to Page.
I'm sure I'm missing something simple here.
Modifying your model isn't necessary, and it's actually a bad practice (adding admin-specific view-logic into your models? Yuck!) It may not even be possible in some scenarios.
Luckily, it can all be achieved from the ModelAdmin class:
from django.urls import reverse
from django.utils.safestring import mark_safe
class PageAdmin(admin.ModelAdmin):
# Add it to the list view:
list_display = ('name', 'user_link', )
# Add it to the details view:
readonly_fields = ('user_link',)
def user_link(self, obj):
return mark_safe('{}'.format(
reverse("admin:auth_user_change", args=(obj.user.pk,)),
obj.user.email
))
user_link.short_description = 'user'
admin.site.register(Page, PageAdmin)
Edit 2016-01-17:
Updated answer to use make_safe, since allow_tags is now deprecated.
Edit 2019-06-14:
Updated answer to use django.urls, since as of Django 1.10 django.core.urls has been deprecated.
Add this to your model:
def user_link(self):
return '%s' % (reverse("admin:auth_user_change", args=(self.user.id,)) , escape(self.user))
user_link.allow_tags = True
user_link.short_description = "User"
You might also need to add the following to the top of models.py:
from django.template.defaultfilters import escape
from django.core.urls import reverse
In admin.py, in list_display, add user_link:
list_display = ('name', 'user_link', )
No need for list_display_links.
You need to use format_html for modern versions of django
#admin.register(models.Foo)
class FooAdmin(admin.ModelAdmin):
list_display = ('ts', 'bar_link',)
def bar_link(self, item):
from django.shortcuts import resolve_url
from django.contrib.admin.templatetags.admin_urls import admin_urlname
url = resolve_url(admin_urlname(models.Bar._meta, 'change'), item.bar.id)
return format_html(
'{name}'.format(url=url, name=str(item.bar))
)
I ended up with a simple helper:
from django.shortcuts import resolve_url
from django.utils.safestring import SafeText
from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.utils.html import format_html
def model_admin_url(obj: Model, name: str = None) -> str:
url = resolve_url(admin_urlname(obj._meta, SafeText("change")), obj.pk)
return format_html('{}', url, name or str(obj))
Then you can use the helper in your model-admin:
class MyAdmin(admin.ModelAdmin):
readonly_field = ["my_link"]
def my_link(self, obj):
return model_admin_url(obj.my_foreign_key)
I needed this for a lot of my admin pages, so I created a mixin for it that handles different use cases:
pip install django-admin-relation-links
Then:
from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
#admin.register(Group)
class MyModelAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
# ...
change_links = ['field_name']
See the GitHub page for more info. Try it out and let me know how it works out!
https://github.com/gitaarik/django-admin-relation-links
I decided to make a simple admin mixin that looks like this (see docstring for usage):
from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html
from rest_framework.reverse import reverse
class RelatedObjectLinkMixin(object):
"""
Generate links to related links. Add this mixin to a Django admin model. Add a 'link_fields' attribute to the admin
containing a list of related model fields and then add the attribute name with a '_link' suffix to the
list_display attribute. For Example a Student model with a 'teacher' attribute would have an Admin class like this:
class StudentAdmin(RelatedObjectLinkMixin, ...):
link_fields = ['teacher']
list_display = [
...
'teacher_link'
...
]
"""
link_fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.link_fields:
for field_name in self.link_fields:
func_name = field_name + '_link'
setattr(self, func_name, self._generate_link_func(field_name))
def _generate_link_func(self, field_name):
def _func(obj, *args, **kwargs):
related_obj = getattr(obj, field_name)
if related_obj:
content_type = ContentType.objects.get_for_model(related_obj.__class__)
url_name = 'admin:%s_%s_change' % (content_type.app_label, content_type.model)
url = reverse(url_name, args=[related_obj.pk])
return format_html('{}', url, str(related_obj))
else:
return None
return _func
If anyone is trying to do this with inline admin, consider a property called show_change_link since Django 1.8.
Your code could then look like this:
class QuestionInline(admin.TabularInline):
model = Question
extra = 1
show_change_link = True
class TestAdmin(admin.ModelAdmin):
inlines = (QuestionInline,)
admin.site.register(Test, TestAdmin)
This will add a change/update link for each foreign key relationship in the admin's inline section.