Django CreateView and validation - django

I'm trying to implement generic views in my Django 1.8 app, so that Django can take care of the validation/redirection loop for me.
I've created a model:
class Customer(models.Model):
custid = models.CharField(max_length=4, verbose_name='CID (4 alphanumeric uppercase)', validators=[validators.CIDValidator])
customer_shortcode = models.CharField(max_length=7, verbose_name='Customer Code (7 chars, uppercase, no spaces)', validators=[validators.ShortnameValidator])
description = models.CharField(max_length=30, blank=True)
and defined a validator for each of my two validated fields:
class CIDValidator(RegexValidator):
regex = r'^[A-Z0-9]{4}$'
message = 'CID is a 4-character uppercase alphanumeric value'
class ShortnameValidator(RegexValidator):
regex = r'^[A-Z0-9_]{1,7}$'
message = 'Shortname should be uppercase, no spaces, alphanumeric'
(At this point, I expected that the admin interface would use the validators when I added a Customer, but it doesn't)
For the actual app, I've created a ModelForm for the Customer class:
class CustomerForm(ModelForm):
class Meta:
model = Customer
fields = ['custid', 'customer_shortcode', 'description']
and a View class inherited from CreateView:
class CustomerCreateView(CreateView):
model = Customer
form_class = CustomerForm
def get_success_url(self):
return reverse('customer_list')
And I still don't get validation errors when I enter invalid data in the generated form.
As far as I can follow from the docs, I should only need to override clean() or clean_xxx() on the ModelForm for additional validation, not for this, but it's really unclear. I'd like to keep the knowledge about what constitutes a valid value in as few places as possible - which the validator on the ModelField would do.
What is missing here? I suspect I'm getting confused between model validation and form validation...

TL;DR: when specifying this kind of validators in model field definitions, you should pass instances rather than classes (validators.CIDValidator() instead of validators.CIDValidator).
Longer explanation
Django validators need to be callables. Trying to call the class you are passing now will go through python's creation sequence for an instance, calling __new__ and __init__, and it would return an instance of that class - but it won't do anything in terms of validating the field value.
The Django validators you are subclassing also have a __call__ method, that is run when you try to call an instance of that class, and it takes care of validating and raising ValidationErrors

Related

Django: How to Properly Use ManyToManyField with Factory Boy Factories & Serializers?

The Problem
I am using a model class Event that contains an optional ManyToManyField to another model class, User (different events can have different users), with a factory class EventFactory (using the Factory Boy library) with a serializer EventSerializer. I believe I have followed the docs for factory-making and serializing, but am receiving the error:
ValueError: "< Event: Test Event >" needs to have a value for field "id"
before this many-to-many relationship can be used.
I know that both model instances must be created in a ManyToMany before linking them, but I do not see where the adding is even happening!
The Question
Can someone clarify how to properly use a ManyToManyField using models, factory boy, and serializers in a way I am not already doing?
The Set-Up
Here is my code:
models.py
#python_2_unicode_compatible
class Event(CommonInfoModel):
users = models.ManyToManyField(User, blank=True, related_name='events')
# other basic fields...
factories.py
class EventFactory(factory.django.DjangoModelFactory):
class Meta:
model = models.Event
#factory.post_generation
def users(self, create, extracted, **kwargs):
if not create:
# Simple build, do nothing.
return
if extracted:
# A list of users were passed in, use them
# NOTE: This does not seem to be the problem. Setting a breakpoint
# here, this part never even fires
for users in extracted:
self.users.add(users)
serializers.py
class EventSerializer(BaseModelSerializer):
serialization_title = "Event"
# UserSerializer is a very basic serializer, with no nested
# serializers
users = UserSerializer(required=False, many=True)
class Meta:
model = Event
exclude = ('id',)
test.py
class EventTest(APITestCase):
#classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(email='test#gmail.com',
password='password')
def test_post_create_event(self):
factory = factories.EventFactory.build()
serializer = serializers.EventSerializer(factory)
# IMPORTANT: Calling 'serializer.data' is the exact place the error occurs!
# This error does not occur when I remove the ManyToManyField
res = self.post_api_call('/event/', serializer.data)
Version Info
Django 1.11
Python 2.7.10
Thank you for any help you can give!
Regarding the error:
It seems like the missing id is due to the use of .build() instead of .create() (or just EventFactory()). The former does not save the model, and therefore it does not get an id value, whereas the latter does (refer to factory docs and model docs).
I suspect the serializer still expects the object to have an id, even though the many-to-many relationship is optional, because it cannot enforce a potential relationship without an id.
However, there might be a simpler solution to the actual task. The above method is a way of generating the POST data passed to post_api_call(). If this data was instead created manually, then both the factory and serializer become unnecessary. The explicit data method might even be better from a test-perspective, because you can now see the exact data which has to produce an expected outcome. Whereas with the factory and serializer method it is much more implicit in what is actually used in the test.

How to set a model field based on the current user in a Django CreateView [duplicate]

I have a model named Domain which looks like this:
class Domain(models.Model):
"""
Model for storing the company domains
"""
user = models.ForeignKey(
User
)
host = models.CharField(
null=False, verbose_name="Host", max_length=128, unique=True
)
I'd like to use Django's generic views for doing CRUD operations on this. There is one field in this model that needs user input but the foreign key field doesn't need any user input. How can I exclude that field from the form that my generic view generates but assign it the value of the current authenticated user.
Thanks.
Have a look at Russel's answer to a similar question on the django-users group earlier this week.
Quoting the answer*:
Forms and Views solve different problems.
The View is solving the problem of "how do I handle this request and
convert it into a response?". The Form is solving the problem of "How
do I convert the POST data in this request into a model object (or a
change to a model object)?".
Very roughly, a view is doing the following:
View gets a request
View works out whether this is a GET or a POST
If its a POST, View asks the Form to turn the Post into a model change
Form returns success or failure
View responds to the success or failure of the Form.
View returns a response.
The functionality of the Form is a complete subset of the
functionality of the View -- and for this reason, it's a completely
interchangable internal component.
Now, in simple situations, it's possible for a View to guess all the
defaults for the form -- all it needs to know is that you're dealing
with a Foo model, and it can construct a default Foo ModelForm.
However, if you have more sophisticated form requirements, you're
going to need a customized Form.
We could have implemented this by exposing all the options of
ModelForm on the View class; but in order to keep everything clean, we
kept the ModelForm isolated, and provided the View with a way to
specify which Form class it's going to use.
So - to cover your use case of excluding fields, you define a
ModelForm that excludes the fields, then let the CreateView know the
form you want to use:
class CampaignForm(forms.ModelForm):
class Meta:
model = Campaign
exclude = ('user', 'name', 'content_inlined')
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
I'm guessing when you say "fix a values for a field", you mean setting
the values of user, name and content_inlined before you save the new
Campaign instance; to do this, you need to inject some extra code into
the form processing logic of the form:
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
def form_valid(self, form):
form.instance.user = ... (something meaningful.. e.g., self.request.user)
return super(CreateCampaignView, self).form_valid(form)
This overrides the default behavior when the form is valid, and sets
the extra values. The super() implementation of form_valid() will then
save the instance.
For the record, this could also be done by overriding the save()
method on the ModelForm -- however, if you do that, you lose the
request object, which you will need if you're trying to set the
instance values to something that is request-sensitive.
*the original answer set self.object.user instead of form.instance.user. This gives an AttributeError so I have changed it above.

UniqueValidator with source field

I have model UserProfile:
class UserProfile(models.Model):
user = models.OneToOneField(User)
and serializer:
email = serializers.CharField(source='user.email', required=False,
validators=[UniqueValidator(queryset=User.objects.all())])
Serialization works fine, but deserialization doesn't — it tries to file 'user.email' field in User model, and, of course, fails.
If I change User to UserProfile in the queryset, it fails with another error:
invalid literal for int() with base 10: 'admin#localhost'
Is it possible to set different sources for serialization and deserialization?
By default, the UniqueValidator expects that the source (or field name, if no source is given) can be used in the queryset that is provided to filter out the existing object. So by using a source that spans a relation (and has a dot), it will try to use that name when filtering the queryset which fails, as you've noticed.
You can fix this by subclassing UniqueValidator to override the filter_queryset method to filter it differently.
class CustomUniqueValidator(UniqueValidator):
def filter_queryset(self, value, queryset):
"""
Filter the queryset to all instances matching the given attribute.
"""
filter_kwargs = {"email": value}
return queryset.filter(**filter_kwargs)
This hard codes email as the filter, one possible option for not hard coding it would be to split self.field_name and get the last part of the dotted source.

How to reference model joined across foreign key in django admin

I have a django 1.6 app with the following (trimmed for clarity)
classes defined. User is the standard django.contrib.auth User class.
class Event(models.Model):
user = models.ForeignKey(User, related_name='events')
name = models.CharField(max_length=64)
class Profile(models.Model):
user = models.ForeignKey(User, related_name='aprofile')
class MemberProfile(Profile):
pass
Here are my admin classes:
class ProfileAdmin(ModelAdmin):
model = Profile
fields = ('user', )
class MemberProfileAdmin(ModelAdmin):
model = MemberProfile
fields = ('user', )
readonly_fields = ('user', )
What I'd like to do is display a read-only list of all events for a given member, or at least profile. Of course joining across the User foreign key seems like the way to go, but I am drawing a blank as to how to accomplish this. Here's a summary of attempts so far.
Define an inline admin on the Event class directly referencing the user field, and add it to the ProfileAdmin:
class EventInlineAdmin(TabularInline):
model = Event
fk_name = 'user' # Fails - fk_name 'user' is not a ForeignKey to <class 'solo365.solo_profile.models.profile.Profile'>
...well, no, it sure isn't. But our User has an 'aprofile' field, so...
class EventInlineAdmin(TabularInline):
model = Event
fk_name = 'user__aprofile' # Fails - EventInlineAdmin.fk_name' refers to field 'user__aprofile' that is missing from model 'admin_fk_test.Event'.
Ok, those fields look like they should sync up, but perhaps we need to be a little more aggressive:
class EventInlineAdmin(TabularInline):
model = Event
fk_name = 'user__aprofile__pk' # Fails - 'EventInlineAdmin.fk_name' refers to field 'user__aprofile__pk' that is missing from model 'admin_fk_test.Event'.
I've also tried messing with formfield_for_foreignkey() and friends in both the inline and the regular model admins, but without that fk_name having a valid value, those methods never get called.
I then considered trying to access the events field directly from a Profile's user:
class ProfileAdmin(ModelAdmin):
model = Profile
fields = ('user', 'user__events') # Fails - Unknown field(s) (user__events) specified for Profile. Check fields/fieldsets/exclude attributes of class ProfileAdmin.
What about with a custom formfield_for_foreignkey() method? Sadly that never gets called for anything other than the 'user' field. I've also considered a custom get_formsets() method, but frankly I'm not sure how I could use that without a working EventInlineAdmin.
I could of course define a custom field that simply concatenates all of the events and returns that as a string, but ideally I would prefer something like a fully-featured inline (even read-only) than just a chunk o' text. IOW such a custom field would have a method that (ideally) would return an inline form without requiring any sort of custom template, setting of allow_tags, etc.
Am I doomed to have to create a completely custom Form for the Profile admin class? Or is there a simple way to accomplish what I'm trying to do, that I'm just missing?
Update:
Bonus points if a provided solution works for the MemberProfileAdmin class, not just the ProfileAdmin class.
The relation between User and Profile should be a 1:1 relation which would allow the referencing via user__aprofile. Otherwise, the reverse relation of a foreing key is a queryset because one foreign key can be assigned to multiple instances. This is might be the reason why your code failed.
Change it to:
class Profile(models.Model):
user = models.OneToOneKey(User, related_name='aprofile')
This is a bit like using ForeignKey(unique=True).
To know the attributes, it might help to call dir(model_instance) on the model instance in question, or try around in the Django shell (./manage.py shell).
Also, I've experienced that it might be more confusing to assign a custom related_name like in your case where you would expect one profile by looking at the related name but you would actually get back a queryset.
The generated name in that case would be profile_set, and you would have to call profile_set.all() or profile_set.values() to get some actual profiles.

Why is my forms clean method not doing anything?

I have two basic models that use model forms in the Django admin.
Models.py is similar to:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField()
class OtherModel(models.Model):
model = models.ForeignKey(FirstModel)
##Other fields that show up fine and save fine, but include some localflavor
Forms.py looks similar to:
class FirstModelForm(forms.ModelForm):
def clean(self):
#call the super as per django docs
cleaned_data = super(FirstModelForm, self).clean()
print cleaned_data
class Meta:
model = FirstModel
#other modelform is the same with the appropriate word substitutions and one field that gets overridden to a USZipCodeField
These are a stacked inline ModelAdmin with nothing special in the admin.py:
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (#my list of fields works correctly)
readonly_fields = (#couple read onlys that work correctly)
class FirstModelAdmin(admin.ModelAdmin):
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
I do have a User model, form and ModelAdmin that subclasses the User and UserCreationForm and overrides it's own clean method.This works exactly as expected.
The problem is with FirstModel and OtherModel. The clean methods I override in the ModelForm subclasses of FirstModelForm and OtherModelForm don't do anything. No exception thrown or a print of the cleaned_data. Just nothing. Everything else works as expected, but it's like my clean method isn't even there.
I got to be missing something simple, but I can't see what is. Any help would be great. Thanks!
By default, Django dynamically generates a model form for your model admins. You must specify that you want to use your custom forms by setting the form attribute.
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (...) # if this doesn't work after specifying the form, set fields for the model form instead
readonly_fields = (#couple read onlys that work correctly)
form = OtherModelForm
class FirstModelAdmin(admin.ModelAdmin):
form = FirstModelForm
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
You need to return the cleaned_data from the clean method in the form. If you look at the documentation for cleaning fields that rely on each other you'll notice:
...
# Always return the full collection of cleaned data.
return cleaned_data
It is possible that nothing survived the parent 'clean' method. If you are submitting data that won't validate because of the way your models are set up, cleaned_data will be empty. This is mentioned in the same doc linked by Timmy, where it says:
By the time the form’s clean() method is called, all the individual field clean methods will have been run (the previous two sections), so self.cleaned_data will be populated with any data that has survived so far. So you also need to remember to allow for the fact that the fields you are wanting to validate might not have survived the initial individual field checks.
In this case, if you have a URLField, the field validation is very strict, and unless you define 'verify_exists=False', it will also check if you are putting in a URL that returns a 404. In your case you would need to do this if you wanted to allow that:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField(verify_exists=False)
Outside of that, I have no idea what could be going on.