Model with recursive self relation in Django's admin - django

Say we have a model with two self-recursive relations:
class Article(Item): # Item in this case is an abstract class
date = models.DateField()
parent = models.OneToOneField('self', null=True, blank=True)
subatricles = models.ForeignKey('self', null=True, blank=True, related_name='subs')
Article acts here as a node - it can has many children (if supplied) and one parent (if any). However, when I register my model in Django's admin my subatricles are shown as "one-to-one' - in both cases there are choice boxes but in the latter multiple values cannot be selected, though.
How can I add children via the admin pane to this Article object and later list them?
What I would like to have is this:
instead of normal drop-down.
Thanks.

You only need one field parent with subarticles as related_name to provide the reverse lookup:
class Article(Item): # Item in this case is an abstract class
date = models.DateField()
parent = models.ForeignKey('self', null=True, blank=True, related_name='subarticles')
so if you have an article object and you want to get its parent, use:
article.parent
if you want to get its children, you use:
article.subarticles
In the admin interface to show the subarticles the easiest way is to use the InlineModelAdmin:
class ArticleInline(admin.StackedInline):
model = Article
class ArticleAdmin(admin.ModelAdmin):
inlines = [
ArticleInline,
]

Related

how to filter data from parant model based on if there relationship with child model

I have these models
class Tree(models.Model):
field = models.TextField()
class TaskProgress(models.Model):
base_task = models.ForeignKey(BaseTask, on_delete=models.CASCADE)
tree = models.ForeignKey(Tree, on_delete=models.CASCADE)
class BaseTask(models.Model):
trees=models.ManyToManyField(Tree, through='TaskProgress')
class TaskType1(BaseTask):
child1_field = models.TextField()
class TaskType2(BaseTask):
child2_field = models.TextField()
how to get all taskprogress when related to TaskType2 ,
TaskProgress.objects.filter(???)
I added extra field on BaseTask class
TASK_TYPE =[('I','Irrigation'),('C','Care'),('A','Assessment'),('O','Other')]
class BaseTask(models.Model):
trees=models.ManyToManyField(Tree, through='TaskProgress')
worker = models.ManyToManyField(User)
task_type = models.CharField(max_length=1,choices=TASK_TYPE,null=True)
And the filter will be like this
TaskProgress.objects.filter(base_task__task = "I")
I do not think what you are asking is possible, if the models are designed like described. The base_task ForeignKey is specifically pointing at a BaseTask. Even though TaskType1 and TaskType2 inherit from BaseTask, they have no relation in the database. They only look similar.
Option 1: Look into Generic Relations in Django. Basically it allows you to have a ForeignKey relation with more than one type of model. I would not recommend it though. Generic relations are a mess if you don't know want you are doing.
Option 2: Rethink your layout. Maybe you can move the relation to the two TaskTypes instead and adress them via related_name.
class TaskProgress(models.Model):
# base_task = models.ForeignKey(BaseTask, on_delete=models.CASCADE)
tree = models.ForeignKey(Tree, on_delete=models.CASCADE)
class TaskType1(BaseTask):
task_progress = models.OneToOneField(TaskProgress, related_name='task_type_1'
child1_field = models.TextField()
class TaskType2(BaseTask):
task_progress = models.OneToOneField(TaskProgress, related_name='task_type_2'
child2_field = models.TextField()
This way you create a one-to-one-relation between the TaskProgress and the TaskType. You should be able to query one or the other by checking whether a relation exists, e.g. all TaskProgress instances with a relation to a TaskType1 instance.
# Query all TaskProgress instances, that have a TaskType1
TaskProgress.objects.filter(task_type_1__isnull=False)

What is the simplest way to handle M2M through fields in wagtail FieldPanel?

I recently added a "through" model to allow sorting connected objects.
In the example below, a Stage has an ordered list of Blocks linked through StageBlock (with the StageBlock.order field)
#register_snippet
class Block(index.Indexed, models.Model):
title = models.CharField(max_length=100, verbose_name=_("Block Name"))
#register_snippet
class Stage(index.Indexed, models.Model):
title = models.CharField(max_length=100, verbose_name=_("Stage Name"))
blocks = models.ManyToManyField(
to="app.Block",
blank=True,
help_text=_("Blocks associated to this stage"),
related_name="stages",
verbose_name=_("Blocks"),
through="StageBlock",
)
panels = [
FieldPanel("title", classname="title full"),
FieldPanel(
"blocks",
widget=autocomplete.ModelSelect2Multiple(
url="block-autocomplete",
attrs={"data-maximum-selection-length": 3},
),
),
class StageBlock(models.Model):
block = models.ForeignKey("app.Block", on_delete=models.CASCADE)
stage = models.ForeignKey("app.Stage", on_delete=models.CASCADE)
order = models.PositiveSmallIntegerField()
The problem is that the related Wagtail admin form breaks, since it tries to associate Block objects to Stage, without providing the "through" model "order" field value.
I'm wondering what is the cleanest/least effort solution to allow an ordered selection of elements in the admin panel, then to properly save the Stage instance with its blocks and related stageblocks.
For the moment, I will add a custom form to the snippet, and auto-assign the order from the position of blocks in the form data (hoping that it always matches the order of blocks as selected in the fieldpanel).
It feels like this use-case could be auto-handled, either by the wagtail-autocomplete plugin, or by wagtail fieldpanel.
But as far as I understand, fieldpanel will simply re-use the Django ModelMultipleChoiceField field, which returns a html element.
A many-to-many relation with a 'through' model is structurally the same as a one-to-many child relationship on that 'through' model, so one possibility is to implement this with an InlinePanel (as described here):
from django_modelcluster.fields import ParentalKey
from django_modelcluster.models import ClusterableModel
from wagtail.core.models import Orderable
#register_snippet
class Stage(index.Indexed, ClusterableModel):
title = models.CharField(max_length=100, verbose_name=_("Stage Name"))
panels = [
FieldPanel("title", classname="title full"),
InlinePanel("blocks", label="Blocks"),
]
class StageBlock(Orderable):
stage = ParentalKey("app.Stage", on_delete=models.CASCADE, related_name='blocks')
block = models.ForeignKey("app.Block", on_delete=models.CASCADE)
panels = [
FieldPanel('block'),
]

Make a nested reference in one Model class Table

I'd like to make a table with such a structure
1. urls
2. views
3. models
4. templates
4.1 html
4.2 format
4.3 test
html, format and test live in the same level with others but reference to 4. templates
I could start from the simple easy model as:
class Topic(models.Model):
"""A topic the user is learning about."""
title = models.CharField(max_length=200)
sub_title = models.CharField(max_length=200)
How to reference 'sub_title' to 'title'
This looks like a tree-like structure. A tree is basically a set of nodes (here topics) where a node can have at most one parent. There are no cycles in the parent relation (hence a node's grandparent can not be itself for example).
One can define such tree-like structure, by adding a reference to the parent, and that reference can be None (for top level nodes). So we can model this with:
class Topic(models.Model):
"""A topic the user is learning about."""
title = models.CharField(max_length=200)
parent = models.ForeignKey(
'Topic',
on_delete=models.SET_NULL,
null=True,
default=None,
)
Since your topics have an index number as well, this means that we can add an IntegerField with the index, and prefereably, the index and parent need to be unique together, such that we can not define two 4.2s:
class Topic(models.Model):
"""A topic the user is learning about."""
index = models.IntegerField()
title = models.CharField(max_length=200)
parent = models.ForeignKey(
'Topic',
on_delete=models.SET_NULL,
null=True,
default=None,
)
class Meta:
unique_together = (('parent', 'index'),)
ordering = ('index',)
So now we model the hierarchy with:
Topic.objects.create(index=1, title='urls')
Topic.objects.create(index=2, title='views')
Topic.objects.create(index=3, title='models')
templates = Topic.objects.create(index=4, title='templates')
Topic.objects.create(index=1, title='html', parent=templates)
Topic.objects.create(index=2, title='format', parent=templates)
tests = Topic.objects.create(index=3, title='tests', parent=templates)
Now we can obtain the parent of a topic, by calling .parent:
tests.parent # templates
We can also obtain an (ordered) set of subtopics with .topic_set:
templates.topic_set # queryset with [html, format, tests]

"already exists" error when using ManyToMany relationship between the same two models

class Product( models.Model ):
name = models.CharField(verbose_name="Name", max_length=255, null=True, blank=True)
the_products_inside_combo = models.ManyToManyField('self', verbose_name="Products Inside Combo", help_text="Only for Combo Products", blank=True)
However, I got this error when I tried to put the duplicate values:
From_product-to_product relationship with this From product and To
product already exists.
Screencap of the error.
Each pair (Product, Product) must be unique. This is why you get already exists error.
Behind the scenes, Django creates an intermediary join table to
represent the many-to-many relationship.
What do you want to do is to have many-to-many relationship between two models (nevermind that they are the same) with additional information stored - quantity (so you would have ProductA = 2x ProductB + ....
In order to model this relationship you will have to create intermediary model and use through option. Documentation explains it very well, so have a look:
https://docs.djangoproject.com/en/dev/topics/db/models/#intermediary-manytomany
Update
Here is minimal working example:
class Product(models.Model):
name = models.CharField(verbose_name='Name', max_length=255, null=True, blank=True)
products = models.ManyToManyField('self', through='ProductGroup', symmetrical=False)
def __str__(self):
return self.name
class ProductGroup(models.Model):
parent = models.ForeignKey('Product', related_name='+')
child = models.ForeignKey('Product', related_name='+')
and admin models:
class ProductGroupInline(admin.TabularInline):
model = Product.products.through
fk_name = 'parent'
class ProductAdmin(admin.ModelAdmin):
inlines = [
ProductGroupInline,
]
admin.site.register(Product, ProductAdmin)
admin.site.register(ProductGroup)
As you can see recursive Product-Product relation is modeled with ProductGroup (through parameter). Couple of notes:
Many-to-many fields with intermediate tables must not be symmetrical, hence symmetrical=False. Details.
Reverse accessors for ProductGroup are disabled ('+') (in general you can just rename them, however, you don't want to work with ProductGroup directly). Otherwise we would get Reverse accessor for 'ProductGroup.child' clashes with reverse accessor for 'ProductGroup.parent'..
In order to have a nice display of ManyToMany in admin we have to use inline models (ProductGroupInline). Read about them in documentation. Please note, however, fk_name field. We have to specify this because ProductGroup itself is ambiguous - both fields are foreign keys to the same model.
Be cautious with recurrency. If you would define, for example, __str__ on Product as: return self.products having ProductGroup with the same parent as the child you would loop infinitely.
As you can see in the screencap pairs can be duplicated now. Alternatively you would just add quantity field to ProductGroup and check for duplication when creating objects.

Display a many-to-many relation in admin without need to edit children

My model is:
class CustomerAccount(models.Model):
name = models.CharField(max_length=50)
class MyUser(AbstractUser):
customer_account = models.ManyToManyField(CustomerAccount, related_name='users', blank=True)
default_customer_account = models.ForeignKey(CustomerAccount, related_name='users_using_default_account', null=True, blank=True)
I want to display in the admin interface of the CustomerAccount this sort of thing:
I don't need to add a MyUser in the CustomerAccount interface.
Most SO questions and docs are related to show an Inline class in the admin, but I don't need it.
How should I do?
The functinality shown above you get by making adding the desired field to the filter_horizontal list (docu).