I'm trying to instantiate a User instance through a Mock. That mock instance is being passed to another model Profile where I'm checking for any validation errors when a cleaning method is called.
However I'm getting: AttributeError: Mock object has no attribute '_state'
There is this previous post: How to mock users and requests in django. Nevertheless, I want to avoid any database calls.
What can be done differently so that a Mock will in this case?
#models.py
class Profile(models.Model):
hobby = "Hobbyist"
develop = "Developer"
coding_level = (
(hobby, hobby),
(develop, develop)
)
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
birth = models.DateField(verbose_name="Date Of Birth")
coding_level = models.CharField(
verbose_name="Experience",
max_length=20,
choices=coding_level, default=hobby, blank=False
)
bio = models.TextField(
verbose_name="User Bio",
validators=[MinValueValidator(10)]
)
github = models.URLField(
verbose_name="GitHub link",
validators=[check_submitted_link],
unique=True
)
avatar = models.ImageField(upload_to="images/%Y/%m/%d/")
#test_models.py
class TestProfile__001(SimpleTestCase):
def setUp(self):
self.test_user = Mock(
spec=User,
username="test_username",
email="test#email.com"
)
self.profile_data = {
'user': self.test_user,
'birth': '2019-10-07',
'coding_level': 'hobbyist',
'bio': "",
'github': "http://www.test.com",
'avatar': "image.txt"
}
def test_create_profile_fail(self):
with self.assertRaises(ValidationError):
test_profile = Profile(**self.profile_data)
test_profile.clean_fields()
I would argue that there's no point in testing clean_fields, since it's already a part of very well tested Django codebase. But if you insist on testing it, you are definitely should not mock the User out.
Let's look at the code you are trying to test here (this is an excerpt from clean_fields):
raw_value = getattr(self, f.attname)
if f.blank and raw_value in f.empty_values:
continue
try:
setattr(self, f.attname, f.clean(raw_value, self))
except ValidationError as e:
errors[f.name] = e.error_list
We see that it runs through every field on the model trying to call its clean method (source):
def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
OneToOneField itself does not introduce any of these methods, it's done higher in the class hierarchy, in ForeignKey class. And here's the main part of the validate method:
using = router.db_for_read(self.remote_field.model, instance=model_instance)
qs = self.remote_field.model._default_manager.using(using).filter(
**{self.remote_field.field_name: value}
)
qs = qs.complex_filter(self.get_limit_choices_to())
if not qs.exists():
raise exceptions.ValidationError(
self.error_messages['invalid'],
code='invalid',
params={
'model': self.remote_field.model._meta.verbose_name, 'pk': value,
'field': self.remote_field.field_name, 'value': value,
}, # 'pk' is included for backwards compatibility
)
As you can see, the whole validation consists of nothing but a query construction with a database call! So if you are trying to avoid hitting the database, you should just skip the validation of any foreign keys altogether:
def test_create_profile_fail(self):
with self.assertRaises(ValidationError):
test_profile = Profile(**self.profile_data)
test_profile.clean_fields(exclude=['user']) # don't forget to explain your decision in the comments
So to conclude, the best way to mock the User is to actually create it in the database. If you want to avoid the boilerplate, you can use the factory_boy package.
Related
Please be gentle. I'm a Django newb and I find the level of abstraction just plain overwhelming.
My ultimate goal is to modify an image file on its way into the model. That part may or may not be relevant, but assistance came my way in this post which advised me that I should be making changes inside a validator:
REST Django - How to Modify a Serialized File Before it is Put Into Model
Anyway, at the moment I am simply trying to get the context of the request so I can be sure to do the things to the thing only when the request is a POST. However, inside my validator, the self.context is just an empty dictionary. Based on what I have found out there, there should be a value for self.context['request'].
Here is what I have:
Serializer with validator method:
class MediaSerializer(serializers.ModelSerializer):
class Meta:
model = Media
fields = '__all__'
def validate_media(self, data):
print(self.context)
#todo: why is self.context empty?
#if self.context['request'].method == 'POST':
# print('do a thing here')
return data
def to_representation(self, instance):
data = super(MediaSerializer, self).to_representation(instance)
return data
The view along with the post method
class MediaView(APIView):
queryset = Media.objects.all()
parser_classes = (MultiPartParser, FormParser)
permission_classes = [permissions.IsAuthenticated, ]
serializer_class = MediaSerializer
def post(self, request, *args, **kwargs):
user = self.request.user
print(user.username)
request.data.update({"username": user.username})
media_serializer = MediaSerializer(data=request.data)
# media_serializer.update('username', user.username)
if media_serializer .is_valid():
media_serializer.save()
return Response(media_serializer.data, status=status.HTTP_201_CREATED)
else:
print('error', media_serializer.errors)
return Response(media_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The Model:
class Media(models.Model):
objects = None
username = models.ForeignKey(User, to_field='username',
related_name="Upload_username",
on_delete=models.DO_NOTHING)
date = models.DateTimeField(auto_now_add=True)
#temp_media = models.FileField(upload_to='upload_temp', null=True)
media = models.FileField(upload_to='albumMedia', null=True)
#todo: potentially this will go to a temp folder, optimize will be called and then permananent home will be used -jjb
#MEDIA_ROOT path must be /src/upload
file_type = models.CharField(max_length=12)
MEDIA_TYPES = (
('I', "Image"),
('V', "Video")
)
media_type = models.CharField(max_length=1, choices=MEDIA_TYPES, default='I')
ACCESSIBILITY = (
('P', "Public"),
('T', "Tribe"),
('F', "Flagged")
)
user_access = models.CharField(max_length=1, choices=ACCESSIBILITY, default='P')
So I'm just trying to figure out how to fix this context problem. Plus if there are any other tips on how to get where I'm going, I'd be most appreciative.
PS I'm pretty new here. If I wrote this question in a way that is inappropriate for stack overflow, please be kind, and I will correct it. Thanks.
I don't think you need to worry about checking if the request is a POST inside the validate_media() method. Generally validation only occurs during POST, PATCH, and PUT requests. On top of that, validation only occurs when you call is_valid() on the serializer, often explicitly in a view, as you do in your post() function. As long as you never call is_valid() from anywhere other than post(), you know that it is a POST. Since you don't support patch() or put() in your view, then this shouldn't be a problem.
inside my validator, the self.context is just an empty dictionary
You must explicitly pass in context when creating a serializer for it to exist. There is no magic here. As you can see in the source code context defaults to {} when you don't pass it in.
To pass in context, you can do this:
context = {'request': request}
media_serializer = MediaSerializer(data=request.data, context=context)
Even better, just pass in the method:
context = {'method': request.method}
media_serializer = MediaSerializer(data=request.data, context=context)
You can make the context dictionary whatever you want.
I was building my api in DJango and rest framework . Please see my model file
class StaffUser(models.Model):
staff_id=models.CharField(max_length=100,null=True,)
name=models.CharField(max_length=100,null=True)
user=models.OneToOneField(User, on_delete=models.CASCADE,related_name='staffs')
roles=models.ManyToManyField(BranchRole,related_name='holding_staffs')
published_date = models.DateTimeField(blank=True, null=True)
class Meta:
db_table = 'staff_details'
def save(self,*args,**kwargs):
email =kwargs['email']
password=kwargs['password']
del kwargs['email']
del kwargs['password']
self.published_date = timezone.now()
self.user=User.objects.create_user(
email=email,
password=password,
is_staff=True,
is_active=1
)
super(StaffUser,self).save(**kwargs)
return self
def __str__(self):
return self.name
When I am trying to call this save function in viewset , I am getting following exception.
"Exception on crate :TypeError("save() got an unexpected keyword argument 'name'"
Please help me to resolve this error. Please see my code in viewset
class StaffUserViewSet(viewsets.ModelViewSet):
"""
This api deals all operations related with module management
You will have `list`, `create`, `retrieve`,
update` and `destroy` actions.
Additionally we also provide an action to update status.
"""
serializer_class = StaffUserSerializer
permission_classes = [permissions.AllowAny]
queryset = StaffUser.objects.all()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ARG={
'staff_id':str,
'phone_number':str,
'email':str,
'name':str,
'password':str,
'address':str,
'id':int,
'roles':dict,
}
self.REPLACING_VALUES={
'model_name':'content_type__model_name',
'content_type_id':'content_type__id',
'app_name':'content_type__app_label' ,
'app_label':'content_type__app_label'
}
self.DEFAULT_PARAMETERS={
'content_type__status__active':True
}
self.VIEW_PARAMETERS_LIST=[
]
self.detailed_view=False
# if any user argument having another label that model relation
# will replace the smame with replacement values"""
self.api_model=StaffUser()
def create(self, request):
#over ride create method in view set
status='Sucess'
# set up detailed view is False
# alter this value to True if you need to alter thsi
content=[]
message="Sucessfull Created"
try:
query_params = data_formatter.request_to_dic(request.POST) # custom method to get input as dictionary
print(query_params)#it works
if (query_params is not None):
input_params=data_formatter.convert_type(query_params,self.ARG) #it works-custom method for convert
print(input_params)
validated_value=validations.is_data_valid(input_params,{'arg':self.ARG})#it works-custom method for convert
print(validated_value)
if(validated_value==1):
#To create group based on name
staff_obj=StaffUser().save(
**input_params
)
else:
status='Fail'
message=validations.ERROR_MESSAGES[validated_value]
except Exception as e:
message='Exception on crate :'+repr(e)
status='Fail'
return Response({"status":status,"data":content,"message":message})
you're using the wrong signature of save()
instead of
staff_obj=StaffUser().save(**input_params)
you should use
staff_obj=StaffUser.objects.create(**input_params)
or
staff_obj=StaffUser(**input_params)
staff_obj.save()
Read carefully the documentation
One more thing, you can rewrite this
email =kwargs['email']
password=kwargs['password']
del kwargs['email']
del kwargs['password']
in this way
email = kwargs.pop('email')
password = kwargs.pop('password')
UPDATE
Honestly not sure it'a a good idea to do this job in the save() method; why don't create a model's classmethod ?
#classmethod
def create staff_user(cls, **kwargs):
email = kwargs.pop('email')
password = kwargs.pop('password')
kwargs['published_date'] = timezone.now()
kwargs['user'] = User.objects.create_user(
email=email,
password=password,
is_staff=True,
is_active=1
)
return cls.objects.create(**kwargs)
and then call it in your View
staff_obj = StaffUser.create_staff_user(**input_params)
?
Model:
ATTN_TYPE_CHOICES = (
('N', 'Entry'),
('X', 'Exit'),
('L', 'Leave'),
)
class Attn(Timestamp):
emp_id = models.CharField(
max_length=10
)
date = models.DateField()
time = models.TimeField(
default=time(00, 00)
)
type = models.CharField(
max_length=1,
choices=ATTN_TYPE_CHOICES,
default='N'
)
#property
def late(self):
return type == 'N' and self.time > LATE_LIMIT
def save(self, *args, **kwargs):
try:
Attn.objects.get(emp_id=self.emp_id, date=self.date, type='N')
except Attn.DoesNotExist:
pass
else:
try:
exit = Attn.objects.get(emp_id=self.emp_id, date=self.date, type='X')
except Attn.DoesNotExist:
self.type = 'X'
else:
exit.delete()
super(Attn, self).save(*args, **kwargs)
class Meta:
unique_together = ('emp_id', 'date', 'type')
I will create objects thrice. The first time is simple. The type will be N. The second time I want the save method to check if type N already exists, if it does, then change the type to 'X' and save second object. Third time, I want it to check for N and then for X. But this time it will find X and will delete the existing entry for X before saving the new entry with type X.
For some reason, the code seems to get stuck at the unique_together and doesn't let me save data from the admin panel. Should I try and catch the Integrityerror for this problem?
Try editing the save method like this,
def save(self, *args, **kwargs):
try:
Attn.objects.get(emp_id=self.emp_id, date=self.date, type='N')
try:
exit = Attn.objects.get(emp_id=self.emp_id, date=self.date, type=='X')
exit.delete()
except Attn.DoesNotExist:
self.type = 'X'
else:
self.type = 'X'
except Attn.DoesNotExist:
self.type = 'N'
return super(Attn, self).save(*args, **kwargs)
Remove the unique_together constraint, its not needed now, you are explicitly overriding the save method and restricting the app to save objects with the conditions above..
EDIT
From the docs,
The ValidationError raised during model validation when the constraint is violated has the unique_together error code.
That means, if the unique_together constraint is violated then, the ValidationError is raised in the model validation itself. Django never even try to reach near the save method, if the constraint is failed. Thus, django-admin raises error before committing the object to the database.
I have two models like this:
class Sector(models.Model):
name = models.CharField(max_length=100, db_index=True, unique=True) # HERE IF I REMOVE unique=True, it works correctly
class Address(models.Model):
...
sector = models.ForeignKey(Sector, null=True, blank=True)
And a serializer for the Address model:
In the view, I have this:
address_serialized = AddressSerializer(data=request.data)
if address_serialized.is_valid():
address_serialized.save(client=client)
It never gets to the create function. I have a serialized with a create function that looks like this:
class AddressSerializer(serializers.ModelSerializer):
city_gps = CitySerializer(required=False)
sector = SectorSerializer(required=False)
class Meta:
model = Address
fields = (..., "sector")
def create(self, validated_data):
...
sector_dict = validated_data.get("sector", None)
sector = None
if sector_dict and "name" in sector_dict and city_gps:
if Sector.objects.filter(name=sector_dict["name"], city=city_gps).exists():
sector = Sector.objects.get(name=sector_dict["name"], city=city_gps)
# pdb.set_trace()
if "sector" in validated_data:
validated_data.pop("sector")
if "city_gps" in validated_data:
validated_data.pop("city_gps")
address = Address.objects.create(sector=sector, city_gps=city_gps, **validated_data)
return address
The code never touches this function, is_valid() returns False. And the message is
{"sector":{"name":["sector with this name already exists."]}}
I need to be able to create a new address with FK to the already existing sector. How can I achieve that? Any advice will help.
EDIT
The view looks like this:
class ClientProfileAddressCreateView(APIView):
# throttle_scope = '1persecond'
renderer_classes = (JSONRenderer,)
permission_classes = (IsAuthenticated,)
def post(self, request):
try:
client = Client.objects.get(user=request.user)
except ObjectDoesNotExist:
return Response({"error": "A client profile for the logged user does not exit"},
status=status.HTTP_404_NOT_FOUND)
address_serialized = AddressSerializer(data=request.data)
print("address_serialized.is_valid: %s" % address_serialized.is_valid()) # Returns False when unique=True in models
if address_serialized.is_valid():
# print("address_serialized: %s" % address_serialized.data)
address_serialized.save(client=client)
else:
return Response(data=address_serialized.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(data=address_serialized.data, status=status.HTTP_201_CREATED)
This is a known issue with nested serializers and unique constraints.
Really awesome thing to always do is actually print the Serializer - that can give you a lot of extra info.
When you have a json like this:
{
"Sector": {
"name": "Sector XYZ"
},
"address_line_one": “Some Random Address”
}
Django REST framework does not know whether you're creating or getting the Sector object, thus it forces validation on every request.
What you need to do is the following:
class SectorSerializer(serializers.ModelSerializer):
# Your fields.
class Meta:
model = Address
fields = ("Your Fields",)
extra_kwargs = {
'name': {
'validators': [],
}
}
Then to handle validation you would need to redo the create/update part to fit the uniqueness constraint and raise exception/validation error.
I hope this helps.
Helpful links: This SO Answer and Dealing with unique constraints in nested serializers
EDIT :
As per cezar's request: I will add how it might look like to override the create method of the serializer. I have not tried this code, but the logic goes like this.
class SectorSerializer(serializers.ModelSerializer):
# Your fields.
class Meta:
model = Address
fields = ("Your Fields",)
extra_kwargs = {
'name': {
'validators': [],
}
}
def create(self, validated_data):
raise_errors_on_nested_writes('create', self, validated_data)
ModelClass = self.Meta.model
info = model_meta.get_field_info(ModelClass)
many_to_many = {}
for field_name, relation_info in info.relations.items():
if relation_info.to_many and (field_name in validated_data):
many_to_many[field_name] = validated_data.pop(field_name)
# FIELD CHECK
your_field = validated_data.get("your_field","") # or validated_data["your_field"]
try:
YourModel.objects.filter(your_check=your_field).get()
raise ValidationError("Your error")
except YourModel.DoesNotExist:
# if it doesn't exist it means that no model containing that field exists so pass it. You can use YourQuerySet.exists() but then the logic changes
pass
try:
instance = ModelClass.objects.create(**validated_data)
except TypeError:
tb = traceback.format_exc()
msg = (
'Got a `TypeError` when calling `%s.objects.create()`. '
'This may be because you have a writable field on the '
'serializer class that is not a valid argument to '
'`%s.objects.create()`. You may need to make the field '
'read-only, or override the %s.create() method to handle '
'this correctly.\nOriginal exception was:\n %s' %
(
ModelClass.__name__,
ModelClass.__name__,
self.__class__.__name__,
tb
)
)
raise TypeError(msg)
# Save many-to-many relationships after the instance is created.
if many_to_many:
for field_name, value in many_to_many.items():
field = getattr(instance, field_name)
field.set(value)
return instance
I'm currently trying django-tastypie to design a RESTful api. I'm facing a problem:
# the RevisionObject retrieve commits info through pysvn
# This Resource is fully functionnal (RevisionObject code is not here)
class RevisionResource(Resource):
id = fields.CharField(attribute='revision')
description = fields.CharField(attribute='message')
author = fields.CharField(attribute='author')
changed_path = fields.ListField(attribute='changed_paths')
class Meta:
object_class = RevisionObject
allowed_methods = ['get']
resource_name = 'revision'
class RevisionToApplyResource(ModelResource):
#### here's the problem
revision = fields.ToManyField(RevisionResource, 'revision')
####
class Meta:
queryset = RevisionToApply.objects.all()
In my models.py I have:
class RevisionToApply(models.Model):
patch = models.ForeignKey(PatchRequest)
revision = models.PositiveIntegerField()
applied = models.BooleanField(default = False)
My problem is that the RevisionToApply models (for django) uses an int to the revision.
How can I tell tastypie to use the revision field of RevisionToApplyResource as a pointer to a RevisionResource? If the ToXxxxField are only for linking with django models, what is the perfect moment to insert the ResourceObject?
thanks.
class NoForeignKeyToOneField(ToOneField):
def dehydrate(self, bundle):
try:
obj_key = getattr(bundle.obj, self.attribute)
foreign_obj = self.to_class().obj_get(pk=obj_key)
except ObjectDoesNotExist:
foreign_obj= None
if not foreign_obj:
if not self.null:
raise ApiFieldError("The model '%r' has an empty attribute"
"'%s' and doesn't allow null value." % (bundle.obj,
self.attribute))
return None
self.fk_resource = self.get_related_resource(foreign_obj)
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
return self.dehydrate_related(fk_bundle, self.fk_resource)
Here's how I would do it. Taking a look at how the ToOneField class works, you'll notice that the hydrate/dehydrate method pair takes care of getting and setting the actual related instance. By subclassing ToOneField and overriding these two methods, you can get the benefit of Tastypie's automated resource handling without an actual foreign key.
(I'm referring to ToOneField rather than ToManyField because in your model, a given RevisionToApply can only point to one revision, it seems.)
It would look something like this:
class NoForeignKeyToOneField(ToOneField):
def dehydrate(self, bundle):
# Look up the related object manually
try:
obj_key = getattr(bundle.obj, self.attribute)
###
# Get the revision object here. If you want to make it generic,
# maybe pass a callable on __init__ that can be invoked here
###
foreign_obj = revision_object
except ObjectDoesNotExist:
foreign_obj = None
# The rest remains the same
if not foreign_obj:
if not self.null:
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (bundle.obj, self.attribute))
return None
self.fk_resource = self.get_related_resource(foreign_obj)
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
return self.dehydrate_related(fk_bundle, self.fk_resource)
def hydrate(self, bundle):
value = super(NoForeignKeyToOneField, self).hydrate(bundle)
if value is None:
return value
# Here, don't return the full resource, only the primary key
related_resource = self.build_related_resource(value, request=bundle.request)
return related_resource.pk
And then use this field type in your resource rather than the basic ToOneField. I haven't tested it , but I believe the approach is sound, simple and it'll get the job done.