I'm working on a project that manages school staff. The relationship between the two is poorly implemented in my opinion so I am trying to refactor it. However, to minimize impact, since this is not a priority I'm trying to do this without breaking the front-end, which should be refactored at a later date. The current Staff model looks like this:
class Staff(models.Model):
principal_at = models.ForeignKey(School, null=True)
teacher_at = models.ForeignKey(School, null=True)
# I'm only showing these two roles for the sake of simplicity but there are more.
I'm refactoring it to this:
class Staff(models.Model):
ROLE_CHOICES = (
"principal",
"teacher",
# ...
)
school = models.ForeignKey(School)
role = models.CharField(choices=ROLE_CHOICES)
# ...
#property
def principal_at(self):
return self.school if self.role == "principal" else None
# ...
My StaffSerializer looks like this:
class StaffSerializer(serializers.ModelSerializer):
class Meta:
model = Staff
fields = (
"principal_at",
"teacher_at",
# ...
)
# There is also some custom validation to ensure the Staff can only have
# one role.
If I just keep my it unchanged, the principal_at field becomes a ReadOnlyField and it doesn't get saved, which makes sense since my model attribute is now a property and not an actual writeable field. I changed, therefore, my serializer to this:
class StaffSerializer(serializers.ModelSerializer):
kwargs = {"required": False, "queryset": School.objects.all()}
principal_at = serializers.PrimaryKeyRelatedField(**kwargs)
teacher_at = serializers.PrimaryKeyRelatedField(**kwargs)
class Meta:
model = Staff
fields = (
"principal_at",
"teacher_at",
# ...
)
def validate(self, data):
principal_at = data.pop("principal_at", None)
teacher_at = data.pop("teacher_at", None)
# ...
if principal_at:
data.update(school_id=principal_at, role="principal")
elif teacher_at:
data.update(school_id=teacher_at, role="teacher")
# ...
return data
Now the model is saved correctly (I checked with ipdb) but in the process of building the response I get this:
TypeError: Object of type School is not JSON serializable
After this point I honestly don't remember what I've tried, but I've tried many things and nothing works. One thing I do remember trying and that blew my mind was creating a custom field and overriding to_representation:
class RelatedSchoolSerializer(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
import ipdb; ipdb.set_trace()
return value.pk
class StaffSerializer(serializers.ModelSerializer):
kwargs = {"required": False, "queryset": School.objects.all()}
principal_at = serializers.PrimaryKeyRelatedField(**kwargs)
teacher_at = serializers.PrimaryKeyRelatedField(**kwargs)
# ...
This kept failing with the same error, however, prompting me to add a breakpoint and check what the f**k is going on. Inside the debugger, to my absolute dismay, I got the following:
ipdb> value
<rest_framework.relations.PKOnlyObject object at 0x7f9563c87f90>
ipdb> value.pk
<School: School 0>
ipdb> value.pk.pk
1
ipdb>
If I return value.pk.pk instead of value.pk, everything works. But even though my test suite is passing, I'm afraid that such a weird hack may have mysterious unintended consequences down the road. What on earth is going on? What is the proper way of doing this?
Related
I'm building a simple recipe storage application that uses the Graphene package for GraphQL. I've been able to use Django Forms so far very easily in my mutations, however one of my models fields is really an Enum and I'd like to expose it in Graphene/GraphQL as such.
My enum:
class Unit(Enum):
# Volume
TEASPOON = "teaspoon"
TABLESPOON = "tablespoon"
FLUID_OUNCE = "fl oz"
CUP = "cup"
US_PINT = "us pint"
IMPERIAL_PINT = "imperial pint"
US_QUART = "us quart"
IMPERIAL_QUART = "imperial quart"
US_GALLON = "us gallon"
IMPERIAL_GALLON = "imperial gallon"
MILLILITER = "milliliter"
LITER = "liter"
# Mass and Weight
POUND = "pound"
OUNCE = "ounce"
MILLIGRAM = "milligram"
GRAM = "gram"
KILOGRAM = "kilogram"
My Model:
class RecipeIngredient(TimeStampedModel):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name='ingredients')
direction = models.ForeignKey(RecipeDirection, on_delete=models.CASCADE, null=True, related_name='ingredients')
quantity = models.DecimalField(decimal_places=2, max_digits=10)
unit = models.TextField(choices=Unit.as_tuple_list())
My form:
class RecipeIngredientForm(forms.ModelForm):
class Meta:
model = RecipeIngredient
fields = (
'recipe',
'direction',
'quantity',
'unit',
)
My Mutation:
class CreateRecipeIngredientMutation(DjangoModelFormMutation):
class Meta:
form_class = RecipeIngredientForm
exclude_fields = ('id',)
I've created this graphene enum UnitEnum = Enum.from_enum(Unit) however I haven't been able to get graphene to pick it up. I've tried adding it to the CreateRecipeIngredientMutation as a regular field like unit = UnitEnum() as well as an Input class on that mutation. So far, the closest I've gotten is this Github issue from awhile ago. After playing around with the class in an iPython shell, I think I could just do CreateRecipeIngredientMutation.Input.unit.type.of_type = UnitEnum() but this feels awful.
I came up with a solution that works but is not pretty. I used the https://github.com/hzdg/django-enumfields package to help with this.
I created my own form field:
class EnumChoiceField(enumfields.forms.EnumChoiceField):
def __init__(self, enum, *, coerce=lambda val: val, empty_value='', **kwargs):
if isinstance(enum, six.string_types):
self.enum = import_string(enum)
else:
self.enum = enum
super().__init__(coerce=coerce, empty_value=empty_value, **kwargs)
And used it in my Django form. Then in my custom AppConfig I did this:
class CoreAppConfig(AppConfig):
name = 'myapp.core'
def ready(self):
registry = get_global_registry()
#convert_form_field.register(EnumChoiceField)
def convert_form_field_to_enum(field: EnumChoiceField):
converted = registry.get_converted_field(field.enum)
if converted is None:
raise ImproperlyConfigured("Enum %r is not registered." % field.enum)
return converted(description=field.help_text, required=field.required)
And finally in my schema:
UnitEnum = Enum.from_enum(Unit)
get_global_registry().register_converted_field(Unit, UnitEnum)
I really don't like this, but couldn't think of a better way to handle this. I came across this idea when searching down another graphene django issue here https://github.com/graphql-python/graphene-django/issues/481#issuecomment-412227036.
I feel like there has to be a better way to do this.
I've an Order model and others models which related with it. An user can delete any of this items and I must perform a check if the order is empty after deletion and set as active False in case true. Some basic code to ilustrate it
class Order(models.Model):
paid = models.BooleanField(default=False)
active = models.BooleanField(default=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
def empty_order():
"""
I must implement it
"""
class HomeOrder(models.Model):
...
order = models.OneToOneField(Order, related_name='primary_home')
class TourOrder(models.Model):
...
order = models.ForeignKey(Order, related_name='tours')
I have a post_delete signals that are connected with every of this Models related to Order:
post_delete.connect(delete_order_if_empty, sender=HomeOrder)
post_delete.connect(delete_order_if_empty, sender=TourOrder)
def delete_order_if_empty(sender, instance, **kwargs):
if instance.order.empty_order():
instance.order.active = False
instance.order.save()
An Order can have one Home, so if the Home exists I can do order.primary_home, if Home does not exist it will raise an AttributeError because it is an OneToOne relationship.
An Order can have many Tours, so in the empty_order method I thought to do some checks as following.
def empty_order():
home = hasattr(self, 'primary_home') # Avoid AttributeError exception
tours = self.tours.exists()
this_order_has_something = primary_home or tours
return not this_order_has_something
Now, when I delete an HomeOrder the signal is raised but the empty_method never realized that this HomeOrder does not exists any more. Example:
>>>o=Order.objects.create(...)
>>>o.primary_home # raise AttributeError
>>>h=HomeOrder.objetcs.create(order=o, ...)
>>>o.primary_home # <HomeOrder: home-xx>
>>>h.delete()
>>>o.primary_home # still <HomeOrder: home-xx> Why?
>>>o.refresh_from_db()
>>>o.primary_home # again <HomeOrder: home-xx>
I'm in the process of creating an assessment system using Django; however, I have an integrated test that passes and I'm not sure as to why (it should be failing). In the test, I set the grade field of the bobenrollment object to "Excellent". As you can see from the models below, the Enrollment model doesn't have a grade field (none of the models do). I was under the impression that dot notation of model objects would access the model fields (I'm probably incorrect about this). I don't want to write ineffective tests, so I would like to know what makes this test pass and what I should do to make it break. Thanks!
class ClassAndSemesterModelTest(TestCase):
def add_two_classes_to_semester_add_two_students_to_class(self):
first_semester = Semester.objects.create(text='201530')
edClass = EdClasses.objects.create(name='EG 5000')
edClass2 = EdClasses.objects.create(name='EG 6000')
first_semester.classes.add(edClass)
first_semester.classes.add(edClass2)
bob = Student.objects.create(name="Bob DaBuilder")
jane = Student.objects.create(name="Jane Doe")
bobenrollment = Enrollment.objects.create(student=bob, edclass=edClass)
janeenrollment = Enrollment.objects.create(student=jane,edclass=edClass)
bobenrollment2 = Enrollment.objects.create(student=bob,edclass=edClass2)
janeenrollment2 = Enrollment.objects.create(student=jane,edclass=edClass2)
def test_students_link_to_enrollments(self):
self.add_two_classes_to_semester_add_two_students_to_class()
edclass1 = EdClasses.objects.get(name="EG 5000")
bob = Student.objects.get(name="Bob DaBuilder")
#The three lines below are the subject of my question
bobenrollment = Enrollment.objects.get(edclass=edclass1, student=bob)
bobenrollment.grade = "Excellent"
self.assertEqual(bobenrollment.grade, "Excellent")
And the models below:
from django.db import models
class Student(models.Model):
name = models.TextField(default="")
def __str__(self):
return self.name
#TODO add models
class EdClasses(models.Model):
name = models.TextField(default='')
students = models.ManyToManyField(Student, through="Enrollment")
def __str__(self):
return self.name
class Semester(models.Model):
text = models.TextField(default='201530')
classes = models.ManyToManyField(EdClasses)
def __str__(self):
return self.text
class Enrollment(models.Model):
student = models.ForeignKey(Student)
edclass = models.ForeignKey(EdClasses)
Requirements.txt
beautifulsoup4==4.4.1
Django==1.5.4
ipython==3.1.0
LiveWires==2.0
nose==1.3.3
Pillow==2.7.0
projectname==0.1
pyperclip==1.5.11
pytz==2015.2
requests==2.10.0
selenium==2.53.6
six==1.9.0
South==1.0.2
swampy==2.1.7
virtualenv==1.11.5
I was under the impression that dot notation of model objects would access the model fields (I'm probably incorrect about this)
You're correct about this. What you're not taking into account is the fact that you can dynamically add properties to python objects. For instance:
In [1]: class MyClass():
...: pass
...:
In [2]: a = MyClass()
In [3]: a.im_a_property = 'hello'
In [4]: print a.im_a_property
hello
As you can see, the a instance will have the im_a_propery property even though it's not defined by the class. The same applies for the following line in your code:
bobenrollment.grade = "Excellent"
Django models override this behavior so you can seamlessly get DB values as properties of your model instance, but the instance is just a regular python object.
If you want to test the grade property gets saved correctly, you should modify your test to add the value of grade when creating the record and making sure the instance you assert against is the one you read from your DB (i.e. not modifying it beforehand).
Apart from one example in the docs, I can't find any documentation on how exactly django chooses the name with which one can access the child object from the parent object. In their example, they do the following:
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
def __unicode__(self):
return u"%s the place" % self.name
class Restaurant(models.Model):
place = models.OneToOneField(Place, primary_key=True)
serves_hot_dogs = models.BooleanField()
serves_pizza = models.BooleanField()
def __unicode__(self):
return u"%s the restaurant" % self.place.name
# Create a couple of Places.
>>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton')
>>> p1.save()
>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
>>> p2.save()
# Create a Restaurant. Pass the ID of the "parent" object as this object's ID.
>>> r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)
>>> r.save()
# A Restaurant can access its place.
>>> r.place
<Place: Demon Dogs the place>
# A Place can access its restaurant, if available.
>>> p1.restaurant
So in their example, they simply call p1.restaurant without explicitly defining that name. Django assumes the name starts with lowercase. What happens if the object name has more than one word, like FancyRestaurant?
Side note: I'm trying to extend the User object in this way. Might that be the problem?
If you define a custom related_name then it will use that, otherwise it will lowercase the entire model name (in your example .fancyrestaurant). See the else block in django.db.models.related code:
def get_accessor_name(self):
# This method encapsulates the logic that decides what name to give an
# accessor descriptor that retrieves related many-to-one or
# many-to-many objects. It uses the lower-cased object_name + "_set",
# but this can be overridden with the "related_name" option.
if self.field.rel.multiple:
# If this is a symmetrical m2m relation on self, there is no reverse accessor.
if getattr(self.field.rel, 'symmetrical', False) and self.model == self.parent_model:
return None
return self.field.rel.related_name or (self.opts.object_name.lower() + '_set')
else:
return self.field.rel.related_name or (self.opts.object_name.lower())
And here's how the OneToOneField calls it:
class OneToOneField(ForeignKey):
... snip ...
def contribute_to_related_class(self, cls, related):
setattr(cls, related.get_accessor_name(),
SingleRelatedObjectDescriptor(related))
The opts.object_name (referenced in the django.db.models.related.get_accessor_name) defaults to cls.__name__.
As for
Side note: I'm trying to extend the
User object in this way. Might that be
the problem?
No it won't, the User model is just a regular django model. Just watch out for related_name collisions.
[Update: Changed question title to be more specific]
Sorry if I didn't make the question very well, I can't figure how to do this:
class WhatEver():
number = model.IntegerField('Just a Field', default=callablefunction)
...
Where callablefunction does this query:
from myproject.app.models import WhatEver
def callablefunction():
no = WhatEver.objects.count()
return no + 1
I want to automatically write the next number, and I don't know how to do it.
I have errors from callablefunction stating that it cannot import the model, and I think there must be an easier way to do this. There's no need even to use this, but I can't figure how to do it with the pk number.
I've googled about this and the only thing I found was to use the save() method for auto incrementing the number... but I wanted to show it in the <textfield> before saving...
What would you do?
Got it! I hope this will help everyone that has any problems making a auto-filled and auto-incrementing field in django. The solution is:
class Cliente(models.Model):
"""This is the client data model, it holds all client information. This
docstring has to be improved."""
def number():
no = Cliente.objects.count()
if no == None:
return 1
else:
return no + 1
clientcode = models.IntegerField(_('Code'), max_length=6, unique=True, \
default=number)
[... here goes the rest of your model ...]
Take in care:
The number function doesn't take any arguments (not even self)
It's written BEFORE everything in the model
This was tested on django 1.2.1
This function will automatically fill the clientcode field with the next number (i.e. If you have 132 clients, when you add the next one the field will be filled with clientcode number 133)
I know that this is absurd for most of the practical situations, since the PK number is also auto-incrementing, but there's no way to autofill or take a practical use for it inside the django admin.
[update: as I stated in my comment, there's a way to use the primary key for this, but it will not fill the field before saving]
Every Django model already has an auto-generated primary key:
id = models.AutoField(primary_key=True)
It seems you are trying to duplicate an already existing behavior, just use the object primary key.
I, too, came across this problem, my instance of it was customer.number which was relative to the customers Store. I was tempted to use something like:
# Don't do this:
class Customer(models.Model):
# store = ...
number = models.IntegerField(default=0)
def save(self, *args, **kwargs):
if self.number == 0:
try:
self.number = self.store.customer_set.count() + 1
else:
self.number = 1
super(Customer, self).save(*args, **kwargs)
The above can cause several problems: Say there were 10 Customers, and I deleted customer number 6. The next customer to be added would be (seemingly) the 10th customer, which would then become a second Customer #10. (This could cause big errors in get() querysets)
What I ended up with was something like:
class Store(models.Model):
customer_number = models.IntegerField(default=1)
class Customer(models.Model):
store = models.ForeignKey(Store)
number = models.IntegerField(default=0)
def save(self, *args, **kwargs):
if self.number == 0:
self.number = self.store.customer_number
self.store.number += 1
self.store.save()
super(Customer, self).save(*args, **kwargs)
PS:
You threw out several times that you wanted this field filled in "before". I imagine you wanted it filled in before saving so that you can access it. To that I would say: this method allows you to access store.customer_number to see the next number to come.
You have errors in code, that's why you can't import it:
from django.db import models
class WhatEver(models.Model):
number = models.IntegerField('Just a Field', default=0)
and Yuval A is right about auto-incrementing: you don't even need to declare such a field. Just use the pk or id, they mean the same unless there's a composite pk in the model:
> w = Whatever(number=10)
> w
<Whatever object>
> w.id
None
> w.save()
> w.id
1
[update] Well, I haven't tried a callable as a default. I think if you fix these errors, it must work.