Optional additional data on ManyToManyField - django

I have a ManyToManyField in Django, and I want to save additional information for the relation. What I am doing is
class Speaker(models.Model):
name = models.CharField(max_length=50)
title = models.CharField(max_length=100, blank=True)
description = models.TextField(blank=True)
class Event(models.Model):
title = models.CharField(max_length=120)
speakers = models.ManyToManyField(Speaker, blank=True, null=True, through='Role')
class Role(models.Model):
speaker = models.ForeignKey(Speaker)
event = models.ForeignKey(Event)
role = models.CharField(max_length=50, blank=True)
As per documentation, this prevents Django from doing some automatic stuff. What is particularly annoying is that it makes the Speaker list not available when creating an Event in the admin.
I realize that in general Django does not know what to put in the Role.role field. But that is optional (blank=True). I would expect that
either Django recognizes that Role has only optional fields and lets me use the many to many relation as usual (creating the fields with an empty default), or
Django admin lets me add Speakers to a newly created event, and for each such Speaker it asks for the additional information (the value of Role.role).
The second possibility would be more useful and more general than the first. Still Django admin does none of the two: instead the speakers field is removed from the Event.
Is there a way to make Django admin behave as described above?

The solution lies in this answer. Briefly, one should use InlineModelAdmin, as documented here. This realizes exactly the second behaviour I described.

Related

Possibility of linking a user/profile to a previously-created name linked to a model via a foreign key

I have a Django application where registered users can add, through an input form, details of performances of their music ensemble. This application also has a a section for composers, where they add their own composition. I'm using a custom user model, with profiles linked to user accounts:
class User(AbstractBaseUser):
email = models.EmailField(verbose_name="email", unique=True, max_length=255)
first_name = models.CharField(max_length=30, blank=True, null=True)
[...]
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
[...]
This is my 'composition' model:
class Composition(models.Model):
title = models.CharField(max_length=120) # max_length = required
composer = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
[...]
And this is my 'performance' model. The performance information links to the piece performed (performed):
class Performance(models.Model):
performed = models.ManyToManyField(Composition, blank=True)
[...]
So far, so good. Now, I'd like the performers to be able to add pieces by composers who are not (yet) registered to the website. Let's say that the performer performed a piece by John Lennon and is adding information about that performance. So, the performer will need to be able to add both John Lennon, his composition, and link the two.
The most important bit is: if the ghost of John Lennon tomorrow wants to register to the website, the administrator of the website will need to be able to easily link the compositions already added by the performers to John Lennon's newly created account. Is that possible? I suppose the key to solving this issue is changing composer = models.ForeignKey(settings.AUTH_USER_MODEL... with something else, i.e. using a intermediary model. Any suggestion will be greatly appreciated.
There are different ways to do this, you should choose one based on your taste:
When you know that the actual user is registered, just delete the old, fake user referred in the composition and replace it with the actual user.
Create a new model named something like Artist and change the composer relationship to refer to the Artist model. then, link the Artist model to the actual user with a nullable foreign key.

Creating a model with two optional, but one mandatory foreign key

My problem is that I have a model that can take one of two foreign keys to say what kind of model it is. I want it to take at least one but not both. Can I have this still be one model or should I split it into two types. Here is the code:
class Inspection(models.Model):
InspectionID = models.AutoField(primary_key=True, unique=True)
GroupID = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
SiteID = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)
#classmethod
def create(cls, groupid, siteid):
inspection = cls(GroupID = groupid, SiteID = siteid)
return inspection
def __str__(self):
return str(self.InspectionID)
class InspectionReport(models.Model):
ReportID = models.AutoField(primary_key=True, unique=True)
InspectionID = models.ForeignKey('Inspection', on_delete=models.CASCADE, null=True)
Date = models.DateField(auto_now=False, auto_now_add=False, null=True)
Comment = models.CharField(max_length=255, blank=True)
Signature = models.CharField(max_length=255, blank=True)
The problem is the Inspection model. This should be linked to either a group or a site, but not both. Currently with this set up it needs both.
I'd rather not have to split this up into two nearly identical models GroupInspection and SiteInspection, so any solution that keeps it as one model would be ideal.
I would suggest that you do such validation the Django way
by overriding the clean method of Django Model
class Inspection(models.Model):
...
def clean(self):
if <<<your condition>>>:
raise ValidationError({
'<<<field_name>>>': _('Reason for validation error...etc'),
})
...
...
Note, however, that like Model.full_clean(), a model’s clean() method is not invoked when you call your model’s save() method.
it needs to be called manually to validate model's data, or you can override model's save method to make it always call the clean() method before triggering the Model class save method
Another solution that might help is using GenericRelations,
in order to provide a polymorphic field that relates with more than one table, but it can be the case if these tables/objects can be used interchangeably in the system design from the first place.
As mentionned in comments, the reason that "with this set up it needs both" is just that you forgot to add the blank=True to your FK fields, so your ModelForm (either custom one or the default generated by the admin) will make the form field required. At the db schema level, you could fill both, either one or none of those FKs, it would be ok since you made those db fields nullable (with the null=True argument).
Also, (cf my other comments), your may want to check that your really want to FKs to be unique. This technically turns your one to many relationship into a one to one relationship - you're only allowed one single 'inspection' record for a given GroupID or SiteId (you can't have two or more 'inspection' for one GroupId or SiteId). If that's REALLY what you want, you may want to use an explicit OneToOneField instead (the db schema will be the same but the model will be more explicit and the related descriptor much more usable for this use case).
As a side note: in a Django Model, a ForeignKey field materializes as a related model instance, not as a raw id. IOW, given this:
class Foo(models.Model):
name = models.TextField()
class Bar(models.Model):
foo = models.ForeignKey(Foo)
foo = Foo.objects.create(name="foo")
bar = Bar.objects.create(foo=foo)
then bar.foo will resolve to foo, not to foo.id. So you certainly want to rename your InspectionID and SiteID fields to proper inspection and site. BTW, in Python, the naming convention is 'all_lower_with_underscores' for anything else than class names and pseudo-constants.
Now for your core question: there's no specific standard SQL way of enforcing a "one or the other" constraint at the database level, so it's usually done using a CHECK constraint, which is done in a Django model with the model's meta "constraints" option.
This being said, how constraints are actually supported and enforced at the db level depends on your DB vendor (MySQL < 8.0.16 just plain ignore them for example), and the kind of constraint you will need here will not be enforced at the form or model level validation, only when trying to save the model, so you also want to add validation either at the model level (preferably) or form level validation, in both cases in the (resp.) model or form's clean() method.
So to make a long story short:
first double-check that you really want this unique=True constraint, and if yes then replace your FK field with a OneToOneField.
add a blank=True arg to both your FK (or OneToOne) fields
add the proper check constraint in your model's meta - the doc is succint but still explicit enough if you know to do complex queries with the ORM (and if you don't it's time you learn ;-))
add a clean() method to your model that checks your have either one or the other field and raises a validation error else
and you should be ok, assuming your RDBMS respects check constraints of course.
Just note that, with this design, your Inspection model is a totally useless (yet costly !) indirection - you'd get the exact same features at a lower cost by moving the FKs (and constraints, validation etc) directly into InspectionReport.
Now there might be another solution - keep the Inspection model, but put the FK as a OneToOneField on the other end of the relationship (in Site and Group):
class Inspection(models.Model):
id = models.AutoField(primary_key=True) # a pk is always unique !
class InspectionReport(models.Model):
# you actually don't need to manually specify a PK field,
# Django will provide one for you if you don't
# id = models.AutoField(primary_key=True)
inspection = ForeignKey(Inspection, ...)
date = models.DateField(null=True) # you should have a default then
comment = models.CharField(max_length=255, blank=True default="")
signature = models.CharField(max_length=255, blank=True, default="")
class Group(models.Model):
inspection = models.OneToOneField(Inspection, null=True, blank=True)
class Site(models.Model):
inspection = models.OneToOneField(Inspection, null=True, blank=True)
And then you can get all the reports for a given Site or Group with yoursite.inspection.inspectionreport_set.all().
This avoids having to add any specific constraint or validation, but at the cost of an additional indirection level (SQL join clause etc).
Which of those solution would be "the best" is really context-dependent, so you have to understand the implications of both and check how you typically use your models to find out which is more appropriate for your own needs. As far as I'm concerned and without more context (or in doubt) I'd rather use the solution with the less indirection levels, but YMMV.
NB regarding generic relations: those can be handy when you really have a lot of possible related models and / or don't know beforehand which models one will want to relate to your own. This is specially useful for reusable apps (think "comments" or "tags" etc features) or extensible ones (content management frameworks etc). The downside is that it makes querying much heavier (and rather impractical when you want to do manual queries on your db). From experience, they can quickly become a PITA bot wrt/ code and perfs, so better to keep them for when there's no better solution (and/or when the maintenance and runtime overhead is not an issue).
My 2 cents.
Django has a new (since 2.2) interface for creating DB constraints: https://docs.djangoproject.com/en/3.0/ref/models/constraints/
You can use a CheckConstraint to enforce one-and-only-one is non-null. I use two for clarity:
class Inspection(models.Model):
InspectionID = models.AutoField(primary_key=True, unique=True)
GroupID = models.OneToOneField('PartGroup', on_delete=models.CASCADE, blank=True, null=True)
SiteID = models.OneToOneField('Site', on_delete=models.CASCADE, blank=True, null=True)
class Meta:
constraints = [
models.CheckConstraint(
check=~Q(SiteID=None) | ~Q(GroupId=None),
name='at_least_1_non_null'),
),
models.CheckConstraint(
check=Q(SiteID=None) | Q(GroupId=None),
name='at_least_1_null'),
),
]
This will only enforce the constraint at the DB level. You will need to validate inputs in your forms or serializers manually.
As a side note, you should probably use OneToOneField instead of ForeignKey(unique=True). You'll also want blank=True.
It might be late to answer your question, but I thought my solution might fit to some other person's case.
I would create a new model, let's call it Dependency, and apply the logic in that model.
class Dependency(models.Model):
Group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
Site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)
Then I would write the logic to be applicable very explicitly.
class Dependency(models.Model):
group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)
_is_from_custom_logic = False
#classmethod
def create_dependency_object(cls, group=None, site=None):
# you can apply any conditions here and prioritize the provided args
cls._is_from_custom_logic = True
if group:
_new = cls.objects.create(group=group)
elif site:
_new = cls.objects.create(site=site)
else:
raise ValueError('')
return _new
def save(self, *args, **kwargs):
if not self._is_from_custom_logic:
raise Exception('')
return super().save(*args, **kwargs)
Now you just need to create a single ForeignKey to your Inspection model.
In your view functions, you need to create a Dependency object and then assign it to your Inspection record. Make sure that you use create_dependency_object in your view functions.
This pretty much makes your code explicit and bug proof. The enforcement can be bypassed too very easily. But the point is that it needs prior knowledge to this exact limitation to be bypassed.
I think you're talking about Generic relations, docs.
Your answer looks similar to this one.
Sometime ago I needed to use Generic relations but I read in a book and somewhere else that the usage should be avoided, I think it was Two Scoops of Django.
I ended up creating a model like this:
class GroupInspection(models.Model):
InspectionID = models.ForeignKey..
GroupID = models.ForeignKey..
class SiteInspection(models.Model):
InspectionID = models.ForeignKey..
SiteID = models.ForeignKey..
I‘m not sure if it is a good solution and as you mentioned you'd rather not use it, but this is worked in my case.

Editing dynamic choice fields in Django

I have a standard Django blog with a Post model, only on the model I have added a ManyToManyField for approvers, the idea being that the backend passes the post to 2 or more approvers to confirm the content before it is published.
class Post(models.Model):
author = models.ForeignKey(
get_user_model(), null=True, on_delete=models.SET_NULL)
title = models.CharField(max_length=30)
content = models.CharField(max_length=30)
approvers = models.ManyToManyField(Approvers)
I will probably learn towards something like django-fsm to create a finite state machine for the Post model to govern whether it is draft/in approval/published, but I would like to be able to change the approvers field so that the number and order of approvers (users) can be changed dynamically by the user.
What is the best way to do this? I thought I could try and change the approvers field to a JSONField so that users can add / delete / change the order of approvers and then handle the interpretation in the frontend and write some function to interface with django-fsm, but this feels like it conflates things too much. Am I missing a simpler route?
Why not make another model to do so like
class PostApprover(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='approvers')
user = models.ForeignKey(Approver, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
To access order in which post(let say with id 5) is approved (descending).you can do like
post = Post.objects.get(id=5)
post.approvers.order_by('-created_at')
you can change the value of created_at to change the order.
Or you can also make an integer field that determines your order

Creating submodels, user friendly way

Django 1.9.8
There is a model. And there are submodels.
One to many relationships.
Examples: person and bank accounts.
class Person(models.Model):
name = models.CharField(max_length=200, unique=True, blank=False)
class Account(models.Model):
account = models.CharField(max_length=20, unique=True, blank=False)
owner = models.ForeignKey(Person, on_delete=models.CASCADE)
Sometimes it is much more practical to let a user create submodels not one by one, but as if in a list of input fields. S/he presses a plus sign and another input field for a new submodel is added. Then another etc. This way everything is observable, table-like. And one submit button which triggers validation and creation of submodels.
Much more friendly than creating submodels one by one.
I have prepared a prototype: https://whispering-everglades-55047.herokuapp.com/
Ajax used.
This must be a typical pattern. Could you suggest me a ready made application for this purpose. I can't find myself.
The excellent django-autocomplete-light package should be exactly what you're looking for.
It allows the end user to create owners in an AccountFormView, for example, through the use of a "+" button with 'add another' functionality (outside the admin).

Fetch Data from ForeignKey Relationahip in Django - Filtering

I have two models:
class Card(BaseModel):
name = models.CharField(max_length=100, blank=True)
content = models.TextField(max_length=500)
mentioner = models.ForeignKey(Profile, related_name='Mentioner')
class Profile(BaseModel):
name = models.CharField(max_length=100)
card_mention = models.ForeignKey(Card, related_name='Mentionee')
When a user creates a card, he or she can "mention" another user in this card (as the mentioner). He or she can actually mention zero users, him or herself, and any number of other users (or profiles actually, since users and profiles are linked).
If I pass the data into the context for a template, how would I set this up in a view, for example, such that I can get get the mentioned profiles from the card?
In other words, in a template, lets say I'm building a feed of these cards with mentions in them. I want to list all mentioned profiles in the card (in the template). How would I fetch these profiles for this purpose?
I'm stuck with using some kind of filter maybe. I can fetch all the profile objects, but then I need a way to filter based on which profiles are associated with each card according to the above relationship.
Consider rewriting your model so you are using a ManyToManyField as it closer models your idea, being:
Cards have one author (Profile)
Cards can mention zero or more mentions of other users (Profile)
So your code would be:
class Card(BaseModel):
name = models.CharField(max_length=100, blank=True)
content = models.TextField(max_length=500)
author = models.ForeignKey(Profile, related_name='cards_authored')
mentions = models.ManyToManyField(Profile, related_name='cards_mentioned_in')
class Profile(BaseModel):
name = models.CharField(max_length=100)
This means in a view you are then able to show all the mentions for the current user:
def show_users_mentions(request):
user = request.user
mentions = Card.objects.filter(mentions=user)
# do something with mentions
Or get all of the users mentioned on a particular card:
def show_mentions_on_card(request,card_id):
card = Card.objects.get(pk=card_id)
mentions = card.mentions
# do something with mentions