Admin and Meta don't see the fields of parent abstract classes - django

I use Django 1.10. I have the following model structure:
class GenericPage(models.Model):
"""Abstract page, other pages inherit from it."""
book = models.ForeignKey('Book', on_delete=models.CASCADE)
class Meta:
abstract = True
class GenericColorPage(models.Model):
"""Abstract page that is sketchable and colorable, other pages inherit from it."""
sketched = models.BooleanField(default=False)
colored = models.BooleanField(default=False)
class Meta:
abstract = True
class GenericBookPage(GenericColorPage):
"""A normal book page, with a number. Needs to be storyboarded and edited."""
###
#various additional fields
###
class Meta:
# unique_together = (('page_number', 'book'),) # impedes movement of pages
ordering = ('-book', '-page_number',)
abstract = True
objects = BookPageManager() # the manager for book pages
class BookPage(GenericBookPage):
"""Just a regular book page with text (that needs to be proofread)"""
proofread = models.BooleanField(default=False)
Additionally, an excerpt from Admin:
class BookPageAdmin(admin.ModelAdmin):
# fields NOT to show in Edit Page.
list_display = ('__str__', 'page_name', 'sketched', 'colored', 'edited', 'proofread',)
list_filter = ('book',)
readonly_fields = ('page_number',) # valid page number is assigned via overridden save() in model
actions = ['delete_selected',]
I tried to do ./manage.py makemigrations but if throws the following errors:
<class 'progress.admin.BookPageAdmin'>: (admin.E116) The value of 'list_filter[0]' refers to 'book', which does not refer to a Field.
progress.BookPage: (models.E015) 'ordering' refers to the non-existent field 'book'.
In the past, when I did not use the abstracts and just put everything into BookPage model, it all worked fine. But it seems that Meta and Admin don't see the fields in parent classes. Am I missing something? Is there a way to make them read fields from abstract parents?

In the past, when I did not use the abstracts and just put everything into BookPage model, it all worked fine
Of course it worked fine because you put everything inside BookPage which is not an abstract class which means that table (and, thus, fields) will be created.
But it seems that Meta and Admin don't see the fields in parent classes. Am I missing something?
You're missing the fact that none of your models inherits from the GenericPage abstract model. Thus, the book field is never created.
Is there a way to make them read fields from abstract parents?
You must create/modify a model that inherits from an abstract model. Maybe, do this:
class GenericBookPage(GenericColorPage, GenericPage):
which allows you to inherit both GenericColorPage and GenericPage fields. When I say inherit I mean when the migrate command runs to actually create the database table and the relevant columns (model fields).

Related

Django equivalent of ASP.NET Parameter Binding or Ruby on Rails Action Controller Parameters

I'm wondering what's mentioned in the title. This are links to the examples mentioned, regarding other techs:
ASP.NET Parameter Binding
Ruby on Rails Action Controller
Parameters
Currently I'm building an API using DRF and using custom code in views or serializers validate methods to validate parameters, like this:
class AnimalWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
fields = '__all__'
def validate_dicose_category(self, value):
raise serializers.ValidationError('Dicose Category cannot be set manually.')
Is there a better way?
Since in your example you are telling the serializer to support __all__ fields, then you need to disable updating that one manually.
You probably mean to use use exclude as in the example below, which will simply remove the field from "all". The primary difference between exclude and using read_only is that the output will include the dicose_category.
Use the exclude= to exclude this field. This is the opposite of fields=, and you can only use one at a time.
class AnimalWriteSerializer(serializers.ModelSerializer):
dicose_category = serializers.CharField(read_only=True)
class Meta:
model = Animal
exclude = ["dicose_category"]
You can declare the field as read only (directly or using extra kwarg). You can't write it but it will include be in the output. I'm not sure why you would want to do this, but it can be helpful if you are using the return data for something and need it there.
class AnimalWriteSerializer(serializers.ModelSerializer):
dicose_category = serializers.CharField(read_only=True)
class Meta:
model = Animal
fields = "__all__"
# or declare an extra_kwarg which does the same thing:
class AnimalWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
fields = "__all__"
extra_kwargs = {
"dicose_category": { "read_only": True }
}
And lastly, I strongly suggest listing all the fields you intended to be updated directly, rather than using __all__ or exclude=.
New fields added to the model are not automatically updateable
All updateable fields are explicitly and clearly listed
Unit tests can now be explicit, and the output format is consistent
class AnimalWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
fields = [
"name",
"mission",
"favorite_color",
]

How to create an admin mixin for common/abstract model in Django

I am creating a multi tenant app and have the following abstract class that all relevant tenant specific models inherit from:
class TenantAwareModel(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
class Meta:
abstract = True
When registering models in the Django admin, I want to keep my code DRY and not have to add 'tenant' to every single search_fields, list_display, list_filter etc so I tried creating a super class to inherit from.
class TenantAwareAdmin(admin.ModelAdmin):
class Meta:
abstract = True
# change_form_template = "admin/professional_enquiry_change_form.html"
search_fields = ('tenant__id', 'tenant__name', 'tenant__domain',)
list_display = ('tenant',)
list_filter = ('tenant',)
And then trying to inherit that in the registration of the other models. E.g.
class UserAdmin(TenantAwareAdmin, admin.ModelAdmin ):
...
This approach matches the way the TenantAwareModel works.
class TenantAwareModel(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
class Meta:
abstract = True
class User(AbstractUser, Uuidable, Timestampable, TenantAwareModel):
...
and it is of course giving me:
django.core.exceptions.ImproperlyConfigured: The model TenantAwareModel is abstract, so it cannot be registered with admin.
Does anybody know the proper way to do this to avoid all the duplicated code?
The Meta class and abstract=True can only be defined in the model definition, not in the ModelAdmin.
The TenantAwareModel is an abstract model that allows you to avoid repeating the common fields in the definition of others models, but you can't register a abstract model in the admin site. The abstract model is not related with any table, its only task is to be a base from which inherit in other models that will represent tables(not abstract models) and which will be able to be registered in the admin site.
To avoid repeat the same fields in search_fields, list_display, etc, you should create a mixin like this:
class TenantAwareAdminMixin:
search_fields = [..., ...]
list_display = [..., ...]
...
and inherit from this
class UserAdmin(TenantAwareAdminMixin, admin.ModelAdmin):
...
But there is a problem... If you override some field defined in the TenantAwareAdminMixin, this will overwrite the value provided by the mixin forcing you to explicitly add the inherited values plus those new values, like this:
class UserAdmin(TenantAwareAdminMixin, admin.ModelAdmin):
list_display = TenantAwareAdminMixin.list_display + ['new_value']
...
It is possible to obtain a similar result by overriding the get_list_display, etc. methods.

Wagtail Inline Panel - Sort Order

So I'm struggling with ordering the choices within an InlinePanel (for an orderable) on my site. In the admin page, when adding a new item, the options are presented in the order they were added to the site (so, essentially the 'id' for that item); this is less than ideal considering there are hundreds of options presented in a manner that is not user friendly.
I'm assuming this needs to be defined as ordering meta within the orderable, but I can't seem to get it to work. This is what my orderable looks like:
class RelatedPeople(Orderable):
service = ParentalKey('service.Services', related_name='related_person')
person = models.ForeignKey('person.People', null=True, on_delete=models.SET_NULL, related_name='related_service')
panels = [
FieldPanel('person')
]
I've tried the following with no success:
class Meta:
ordering = 'person'
and, trying to append the field within 'person' that I want to sort by, 'name':
class Meta:
ordering = 'person.name'
There must be an obvious way to solve this that I'm over looking. A default sort order of the 'id' (in this case, for 'person.People') is rarely ever going to be suitable from the perspective of the content creator.
Any advice would be greatly appreciated!
Thanks in advance,
Rob
Person model should have:
ordering = ['name']
instead of
ordering = 'name'
And your Orderable object should have it's meta changed to
class Meta(Orderable.Meta):
Via Django Docs, this is the example of abstract base classes, and ordering:
Meta and multi-table inheritance¶
In the multi-table inheritance situation, it doesn’t make sense for a
child class to inherit from its parent’s Meta class. All the Meta
options have already been applied to the parent class and applying
them again would normally only lead to contradictory behavior (this is
in contrast with the abstract base class case, where the base class
doesn’t exist in its own right).
So a child model does not have access to its parent’s Meta class.
However, there are a few limited cases where the child inherits
behavior from the parent: if the child does not specify an ordering
attribute or a get_latest_by attribute, it will inherit these from its
parent.
If the parent has an ordering and you don’t want the child to have any
natural ordering, you can explicitly disable it:
class ChildModel(ParentModel):
# ...
class Meta:
# Remove parent's ordering effect
ordering = []
When an abstract base class is created, Django makes any Meta inner
class you declared in the base class available as an attribute. If a
child class does not declare its own Meta class, it will inherit the
parent’s Meta. If the child wants to extend the parent’s Meta class,
it can subclass it. For example:
from django.db import models
class CommonInfo(models.Model):
# ...
class Meta:
abstract = True
ordering = ['name']
class Student(CommonInfo):
# ...
class Meta(CommonInfo.Meta):
db_table = 'student_info'
I am not familiar with Wagtail, but can you take a look at this issue :
https://github.com/wagtail/wagtail/issues/4477#issuecomment-382277375
Update:
Maybe you just need to update your Person model like this:
class Person(models.Model):
...
class Meta:
ordering = 'name'
In your files, you try to order RelatedPeople by Person, but what you need is to order the Person list by name in your wagtail dropdown

Conditionally nest Django serializers

So, I have a foreign key to my User model in many of my models. Now, the serializers for these models are nested, in that they include the entire user object rather than just the id. I have done so as shown bellow:
class BadgeSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
class Meta:
model = Badge
fields = '__all__'
It works as expected. However, I seldom find myself in a situation where I want just the id. I was wondering what is the best way to conditionally nest my BadgeSerializer...
Now, the best solution I can think of is to have a non-nested BadgeSerializer, which includes only the user id. And then have a NestedBadgeSerializer (extending BadgeSerializer) which does nest the User model and include the entire user object.
class BadgeSerializer(serializers.ModelSerializer):
class Meta:
model = Badge
fields = '__all__'
class NestedBadgeSerializer(BadgeSerializer):
user = UserSerializer(read_only=True)
class Meta:
model = Badge
fields = '__all__'
I am NOT sure if that's the proper way though.

Error using a base class field in subclass unique_together meta option

Using the following code:
class Organization(models.Model):
name = models.CharField(max_length="100",)
alias = models.SlugField()
...
class Division(Organization):
parent_org = models.ForeignKey(Organization)
class Meta:
unique_together=['parent_org', 'alias']
...
Trying to syncdb give me this error:
Error: One or more models did not validate:
organizations.division: "unique_together" refers to alias. This is not in the
same model as the unique_together statement.
Any help is appreciated,
Thanks,
Eric
This is by design. Reading the documentation for the unique_together option, it states that:
It's used in the Django admin and is enforced at the database level.
If you look at the table that a subclass creates, you'll see that it doesn't actually have the fields that its parent has. Instead, it gets a soft Foreign Key to the parent table with a field name called [field]_ptr_id, where [field] is the name of the table you're inheriting from excluding the app name. So your division table has a Primary Foreign Key called organization_ptr_id.
Now because unique_together is enforced at the database level using the UNIQUE constraint, there's no way that I know of for the database to actually apply that to a field not in the table.
Your best bet is probably through using Validators at your business-logic level, or re-thinking your database schema to support the constraint.
Edit: As Manoj pointed out, you could also try using Model Validators such as validate_unique.
[Model] Validators would work for you. Perhaps simplest, though, would be to use:
class BaseOrganization(models.Model):
name = models.CharField(max_length="100",)
alias = models.SlugField()
class Meta:
abstract = True
class Organization(BaseOrganization):
pass
class Division(BaseOrganization):
parent_org = models.ForeignKey(Organization)
class Meta:
unique_together=['parent_org', 'alias']
Note: as with your current code, you could not have subdivisions of divisions.
This is a solution I recently used in Django 1.6 (thanks to Manoj Govindan for the idea):
class Organization(models.Model):
name = models.CharField(max_length="100",)
alias = models.SlugField()
...
class Division(Organization):
parent_org = models.ForeignKey(Organization)
# override Model.validate_unique
def validate_unique(self, exclude=None):
# these next 5 lines are directly from the Model.validate_unique source code
unique_checks, date_checks = self._get_unique_checks(exclude=exclude)
errors = self._perform_unique_checks(unique_checks)
date_errors = self._perform_date_checks(date_checks)
for k, v in date_errors.items():
errors.setdefault(k, []).extend(v)
# here I get a list of all pairs of parent_org, alias from the database (returned
# as a list of tuples) & check for a match, in which case you add a non-field
# error to the error list
pairs = Division.objects.exclude(pk=self.pk).values_list('parent_org', 'alias')
if (self.parent_org, self.alias) in pairs:
errors.setdefault(NON_FIELD_ERRORS, []).append('parent_org and alias must be unique')
# finally you raise the ValidationError that includes all validation errors,
# including your new unique constraint
if errors:
raise ValidationError(errors)
This does not strictly apply the question but is very closely related; unique_together will work if the base class is abstract. You can mark abstract model classes as such using:
class Meta():
abstract = True
This will prevent django from creating a table for the class, and its fields will be directly included in any subclasses. In this situation, unique_together is possible because all fields are in the same table.
https://docs.djangoproject.com/en/1.5/topics/db/models/#abstract-base-classes
I had similar challenge when trying to apply unique_together to a permission group created by a given client.
class ClientGroup(Group):
client = models.ForeignKey(Client, on_delete=models.CASCADE)
is_active = models.BooleanField()
class Meta():
# looking at the db i found a field called group_ptr_id.
# adding group_ptr did solve the problem.
unique_together = ('group_ptr', 'client', 'is_active')