Link Django models together via GenericKey? - python

i have the following models:
class Team(models.Model):
users = models.ManyToManyField("User")
class User(AbstractUser):
...
class Subscription(models.Model):
team = models.ForeignKey("Team", on_delete=models.CASCADE)
name = models.CharField(max_length=64)
class Package(models.Model):
name = models.CharField(max_length=64) # packageA, packageB
max_activation_number = models.PositiveIntegerField(default=1)
class Activation(models.Model):
subscription = models.ForeignKey("Subscription", on_delete=models.CASCADE)
package = models.ForeignKey("Package", on_delete=models.CASCADE)
created = models.DatetimeField()
class PackageA(models.Model):
...
class PackageB(models.Model):
...
A team has one subscription and it can activate one or more package and the same package could be activated more than one time. (number of times specified with "max_ativation_number")
Example:
A team has a subscription called Suite and the available packages are: EmailAccount and Calendar
The team choose to activate 3 EmailAccount and 2 Calendar (packages are not tied to each other)
For that reason the team could activate the same package more times.
For every activation i need to create a new instance on PackageA or PackageB (it depends on the choice a team made) and then i should "link" to that instance somehow.
Should i use GenericKey field inside Activation model? I not only need the name of the chosen package but I also need to figure out which instance.

As far as I understand, you are trying to make a Many-To-Many relationship actually https://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships:
class Team(models.Model):
users = models.ManyToManyField("User")
class User(AbstractUser):
...
class Subscription(models.Model):
team = models.ForeignKey("Team", on_delete=models.CASCADE)
name = models.CharField(max_length=64)
packages = models.ManyToManyField(Package, through=Activation)
class Package(models.Model):
name = models.CharField(max_length=64) # packageA, packageB
class Activation(models.Model):
subscription = models.ForeignKey("Subscription", on_delete=models.CASCADE)
package = models.ForeignKey("Package", on_delete=models.CASCADE)
max_activation_number = models.PositiveIntegerField(default=1)
created = models.DatetimeField()
So in order to create Subscription Suite with 3 EmailAccount and 2 Calendar
suite_subscription = Subscription.objects.create(name='Suite', team=team_1)
email_package = Package.objects.create(name='EmailAccount')
calendar_package = Package.objects.create(name='Calendar')
suite_subscription.activation_set.create(package=email_package, max_activation_number=3)
suite_subscription.activation_set.create(package=calendar_package, max_activation_number=2)
Now you have suite_subscription with 3 max email packages and 2 max calendar packages. Similarly to this, you can create extra packages and subscriptions...
And you can add as many custom attributes as you want to that "pivot" Activation table.

This can be solved in multiple ways as you want to create a new package instance one solution is using Django signals to create the new instance of packages by using post_save signal from django.db.models.signals import post_save.
from django.db.models.signals import post_save
#receiver(post_save, sender= Activation)
def post_activation_save(sender, instance, **kwargs):
if kwargs.get('created'):
# create new package instance
....

Related

Is it possible to use a related field as a choice field in Django?

In Django, if I have something like this:
class Library(models.Model):
name = models.CharField(...)
address = models.CharField(...)
book_of_the_week = ?
class Book(models.Model):
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name="books")
name = models.CharField(...)
This gives me the ability to create multiple libraries, each with a large number of books.
For book_of_the_week, I want this to be a reference to a Book instance, so that in Django Admin, the field is represented as a dropdown that lets you select from the books in the current library, and in code, you can use .book_of_the_week to access a specific instance of Book.
Is this possible?
Sure, it's possible. But if you do this, you'll only ever be able to save the current book of the week. What happens if you want to show a library's book-of-the-week history? Seems like a likely scenario to me. Consider doing something like:
class Library(models.Model):
name = models.CharField(...)
address = models.CharField(...)
book_of_the_week = ?
class Book(models.Model):
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name="books")
name = models.CharField(...)
class BookOfTheWeek(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_of_the_week')
library = models.ForeignKey(Library, ...etc)
week_of = models.DateField()
In this manner every book of the week will be a discrete database record, and you'll be able to track a history of each library's book of the week over time.
However...
That said, if you do not need to do this and a single instance record is fine, you should be able to create a ForeignKey relation from Library to Book. However, you may run into some circular and/or hierarchical reference issues depending on how where your model classes are written. In the example you show above, you'd need to declare your ForeignKey model name as a string, like so:
book_of_the_week = models.ForeignKey('Book', on_delete ... etc)
instead of:
book_of_the_week = models.ForeignKey(Book, on_delete ... etc)
...otherwise Django will throw an error because the Book model class is referenced before it is defined. Making 'Book' a string will let Django parse the full models.py file, build the logic, and avoid the error.
Sidenote:
Depending on your specific case you may also consider a ForeignKey to self, like so:
class Book(models.Model):
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name="books")
name = models.CharField(...)
book_of_the_week = models.ForeignKey('self', on_delete=models.SET_NULL, ...etc)
This would allow any Book on the database table to reference another single Book instance.

How to make a django model "commentable", "likeable" and "rateable"

I am using Django 2.0.8 and Python 3.5 for a project. I have different models in my project, some of which, I want to allow commenting on - with both the object (e.g. a blogpost) and comments to the blogpost being likeable.
I am using the threaded comments django app to provide commenting functionality.
Assuming I have a model Foo (see below):
from django.db import models
from django.conf import settings
class Foo(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, default=1, on_delete = models.PROTECT)
# ...
class Likeable():
pass
class Rateable():
pass
How could I use mixins (or any other mechanism for that matter), to make the object Foo "commentable" (i.e. an object which can be commented upon), "likeable" (i.e. an object which can be commented upon) and "rateable" (i.e. an object which can be rated?)- bearing in mind that comments on an objects may be BOTH liked and rated.
According to django documentation , you can achieve this using the Content types Framework. ContentType is a generic model that permits you to track all the models included in INSTALLED_APPS using for that their app_label, model_name and pk. The way it works is easy:
Your generic Comment model
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
class Comment(models.Model):
# Generic relation fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# Model specific fields
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
comment = models.TextField()
created = models.DatetimeField(auto_now_add=True)
# ...
Your reusable generic relation model. The best way is using abstract model classes or mixins. For example, using abstract models:
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
class Commentable(models.Model):
comments = GenericRelation(Comment)
class Meta:
abstract = True
Your Commentable model:
from django.db import models
class Foo(Commentable, Likeable, ...):
# your stuff
How to use it:
# Add a new comment to Foo
foo = new Foo()
foo.save()
foo.comments.create(author=author, comment="Your comment")
# Retrieve all comments from an specific user no matter the base model
comments = Comment.objects.filter(author=author)
EDIT As #ozren1983 said, each approach has its own downsides, but this is the standard way to do it.
The main advantages are:
You can retrieve all the comments (for example) made in all your commentable models in just one query. Using the approach of having a comment, like, etc table per model, you would need to concatenate a query per model. This could be problematic and a bit challenging if you have a lot of models or if you want to merge the results and order them, for example.
Just one table per functionality (comments, likes) implies just one database migration in case of change. This could be key if your database is huge.
The main disadvantage is the lack of integrity checks of this generic relationship in database. But if you plan to use the django ORM strictly, nothing should be broken.
BONUS: Another approach that many projects use is inheriting the models (one to one relationship) from an specific one called Item or Thread. Then, you can add all the comments, likes, etc functionalities to this model. This is called multi-table inheritance. An example:
from django.db import models
class Thread(models.Model):
pass
class Comment(models.Model):
# Relation with thread
thread = models.ForeignKey(
Thread,
on_delete=models.CASCADE,
related_name="comments"
)
# Model specific fields
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
comment = models.TextField()
created = models.DatetimeField(auto_now_add=True)
# ...
class Foo(Thread):
pass
Unlike using the generic relationships, the main advantage of this method is that, this way, you have database integrity checks.
The main disadvantage is that your database structure could become complex.
Based on my experience and recommendations in Two scoops of Django, I would advise against using GenericForeignKey and GenericRelation. Two big downsides of that approach are:
slow queries
danger of data corruption
Instead, I would use following approach. Let's say you have 3 models:
class User(models.Model):
username = models.CharField(max_length=255)
class Author(models.Model):
name = models.CharField(max_length=255)
class Post(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author)
Add abstract Like model, and use it as base class for other models that will implement liking functionality.
class Like(models.Model):
user = models.ForeignKey(User)
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class AuthorLike(Like):
author = models.ForeignKey(Author)
class PostLike(Like):
post = models.ForeignKey(Post)
Similarly, add abstract Rating model and use it as a base class:
class Rating(models.Model):
user = models.ForeignKey(User)
rate = models.PositiveSmallIntegerField()
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class AuthorRating(Rating):
author = models.ForeignKey(Author)
class PostRating(Rating):
post = models.ForeignKey(Post)
You can use same approach to enable liking and rating to the Comments model you are using:
from threadedcomments.models import ThreadedComment
class ThreadedCommentRating(Rating):
threadedcomment = models.ForeignKey(ThreadedComment)
class ThreadedCommentLike(Like):
threadedcomment = models.ForeignKey(ThreadedComment)
The django-contrib-comments app, according to documentation, makes use of GenericForeignKey, meaning its own model can create a relation to any other model in your project.
A simple solution would be to just copy that existing functionality, creating your own Like/Rate application based on the same concept (i.e. storing the Like/Rate models in that application's models).
I think you would get very far starting out by forking the https://github.com/django/django-contrib-comments codebase.
(I assume you have searched and failed to find an already existing application that already does this).

unique_together with a field from a foreign key in a through table for a ManyToMany relation

I am developing a Django 2.0 project app. It has a (non-working) models.py file, which looks something like this:
from django.db import models
from django.utils import timezone
class Computer(models.Model):
name = models.CharField(max_length=25)
def __str__(self):
return "Computer {}".format(self.name)
class Software(models.Model):
name = models.CharField(max_length=25)
description = models.CharField(max_length=1024, blank=True)
def __str__(self):
return self.name
class SoftwareVersion(models.Model):
software = models.ForeignKey(Software, on_delete=models.CASCADE, related_name="versions")
version = models.CharField(max_length=100)
released_at = models.DateTimeField(default=timezone.now)
def __str__(self):
return "{} {}".format(self.software, self.version)
class ComputerSoftwareBundle(models.Model):
computer = models.ForeignKey(Computer, on_delete=models.CASCADE, related_name="bundles")
installed_at = models.DateTimeField(default=timezone.now)
versions = models.ManyToManyField(SoftwareVersion, through="BundleSoftwareVersion", related_name="bundles")
class BundleSoftwareVersion(models.Model):
bundle = models.ForeignKey(ComputerSoftwareBundle, on_delete=models.CASCADE)
version = models.ForeignKey(SoftwareVersion, on_delete=models.CASCADE)
class Meta:
unique_together = (("bundle", "version__software"),)
The app tracks software bundles currently or previously installed on computers. The thing here is that a bundle should not contain more than one version of the same software. Also, SoftwareVersion should contain a reference to Software, because the same version string has a different meaning for different pieces of software.
The code does not work as described in this Stackoverflow answer. I left the unique_together line in to illustrate what I am trying to achieve.
I've tried to work around this limitation of Django (not being able to use fields referred to via a foreign key in unique_together) by overriding the save and validate_unique methods in BundleSoftwareVersion but that did not work out completely well. Here's the implementation I have tried:
class BundleSoftwareVersion(models.Model):
bundle = models.ForeignKey(ComputerSoftwareBundle, on_delete=models.CASCADE)
version = models.ForeignKey(SoftwareVersion, on_delete=models.CASCADE)
def save(self, *args, **kwargs):
self.validate_unique()
super().save(*args, **kwargs)
def validate_unique(self, exclude=None):
super().validate_unique(exclude)
bundle_versions = BundleSoftwareVersion.objects.filter(bundle=self.bundle,
version__software=self.version.software)
count = len(bundle_versions)
if not self.pk:
# if this instance is not stored in the database,
# we need to increment the count to take this instance
# into account
count += 1
if count > 1:
raise ValidationError("There already is an instance of software '{}' in this bundle.".format(self.version.software))
I have thus far tried out these models via the admin site. The checks work when changing an existing ComputerSoftwareBundle (the admin site displays a message next to the offending entry), but adding results in an uncaught exception.
Is there a better way to enforce this kind of uniqueness?
I have come up with a workaround:
class BundleSoftwareVersion(models.Model):
bundle = models.ForeignKey(ComputerSoftwareBundle, on_delete=models.CASCADE)
version = models.ForeignKey(SoftwareVersion, on_delete=models.CASCADE)
_software = models.ForeignKey(Software, on_delete=models.CASCADE, null=True, editable=False)
class Meta:
unique_together = (("bundle", "_software"),)
def save(self, *args, **kwargs):
self._software = self.version.software
super().save(*args, **kwargs)
As you can see, I now have a helper field _software which is used in unique_together and into which the self.version.software is stored on each save.
So far, I have experienced one downside with this approach: trying to save a ComputerSoftwareBundle containing duplicate software instances results in an error page for IntegrityError being displayed instead of an error message within the form.
I would appreciate suggestions on how to fix this downside, or even suggestions for a different approach altogether.

How to authenticate user for a specific object rather than whole class in django?

I am working on making an app to add clubs in website. This is my model.py file
from django.db import models
from stdimage import StdImageField
# Create your models here.
class Club(models.Model):
ClubName = models.CharField(max_length=200)
ClubLogo = StdImageField(upload_to='club_logo', variations={'thumbnail':(150, 200, True)})
ClubDetails = models.TextField()
ClubStartDate = models.DateField()
def __str__(self):
return self.ClubName
class Notice(models.Model):
NOTICE = 'NOTICE'
UPDATES = 'UPDATES'
EVENTS = 'EVENTS'
NOTICE_IN_CHOICES = (
(NOTICE, 'Notice'),
(UPDATES, 'Updates'),
(EVENTS, 'Events'),)
NoticeType = models.CharField(
max_length=20, choices=NOTICE_IN_CHOICES, default=NOTICE)
NoticeTag = models.CharField(max_length=30)
NoticeStartDate = models.DateField(auto_now_add=True)
NoticeEndDate = models.DateField()
NoticeFile = models.FileField(default='#', upload_to='notice/%Y/%m/%d')
NoticeContent = models.TextField(default='NA')
NoticeClub = models.ForeignKey(Club)
def __str__(self):
return self.NoticeTag
class Members(models.Model):
MemeberName = models.CharField(max_length=200)
MemberImage = StdImageField(upload_to='member_photo', variations={'thumbnail':(150, 120, True)})
MemberEmail = models.EmailField()
MemberClub = models.ForeignKey(Club)
def __str__(self):
return self.MemeberName
Now when i am making users via django's inbuilt admin panel i have option to give permission to users to change member of any club but i want to give access to change members of only that particular club which he is member of.
As you can see in this picture that all club are in dropdown option when someone who has access to add notices adding otices. But instead of that i want only one option in the dropdown for the useradmin to which he is associated.
this is my admin.py file
from django.contrib import admin
# Register your models here.
from club.models import Club, Members, Notice
admin.site.register(Club),
admin.site.register(Members),
admin.site.register(Notice),
This is a problem with which many users have been struggling with.
I have been using couple of external packages, and couple of self made solutions. But the best one I have found so far is Django Guardian It's an implementation of per object permission .This means you can manage users and permissions to which they have access to.

How to reference object in class view in django

In my models, I have both tasks and projects. What I'm trying to do is add related tasks into the context of the page in which projects are updated.
The error I'm getting is:
type object 'project' has no attribute 'id'
In models.py I have the task class:
class task(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
....
related_project = models.ForeignKey(project, blank=True, null=True)
Also in models.py, I have the project class:
class project(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
...
The view I'm using to update the Project object is:
class ProjectUpdate(UpdateView):
"""
This will be used to view and update projects
"""
template_name = 'tasks/update-project.html'
model = project
fields = ['name','status', 'purpose', 'vision', 'big_steps', 'context', 'priority', 'due_date', 'related_project']
def get_context_data(self, **kwargs):
"""
This puts the tasks related to the particular project in context
"""
# Call base implementation first to get a context
context = super(ProjectUpdate, self).get_context_data(**kwargs)
# TO DO: Add in querysets of related tasks
context = context['related_tasks'] = task.objects.filter(related_project__id=project.id)
The problem seems to be that "project.id" does not actually access the "id" of the particular project object. How to I add the tasks that are linked to the particular project object being updated via the task's "related_project" attribute (which has as a foreign key the project object)?
You can't use project.id because project is the model, not the instance you are updating. Normally you would call your model Project and a model instance project. Calling your model project will confuse other users, as they'll assume it's an instance instead of a model.
In the get_context_data method, you can access the object with self.object.
context['related_tasks'] = task.objects.filter(related_project__id=self.object.id)
Make sure you don't set context = context['related_tasks'] = ....

Categories