django guardian https://github.com/lukaszb/django-guardian is a really well written object-level permissions app; and I have actually read up on and used quite a number of other django object level permissions app in various django projects.
In a recent project that I am working on, I decided to use django guardian but I have a model design question relating to the pros and cons of two possible approaches and their respective implications on sql query performance:-
using django.contrib.auth.models.Group and extending that to my custom organization app's models; or
using django.contrib.auth.models.User instead and creating an m2m field for each of the organization type in my organization app.
Approach #1
# Organisation app's models.py
from django.contrib.auth.models import Group
class StudentClass(models.Model):
name = models.CharField('Class Name', max_length=255)
groups = models.ManyToManyField(Group, blank=True)
size = models.IntegerField('Class Size', blank=True)
class SpecialInterestGroup(models.Model):
name = models.CharField('Interest Group Name', max_length=255)
groups = models.ManyToManyField(Group, blank=True)
description = models.TextField('What our group does!', blank=True)
class TeachingTeam(models.Model):
name = models.CharField('Teacher Team Name', max_length=255)
groups = models.ManyToManyField(Group, blank=True)
specialization = models.TextField('Specialty subject matter', blank=True)
In this approach, when a user is added to a group (django group) for the first time, the group object is created and also assigned to one of these 3 classes, if that group object does not yet belong to the class it is added into.
This means that each StudentClass object, sc_A, sc_B etc, can possibly contain a lot of groups.
What that means is that for me to ascertain whether or not a specific user (say myuser) belongs to a particular organization, I have to query for all the groups that the user belong to, via groups_myuser_belongto = myuser.groups and then query for all the groups that are associated to the organization I am interested in, via groups_studentclass = sc_A.groups.all() and since I now have 2 lists that I need to compare, I can do set(groups_myuser_belongto) && set(groups_studentclass), which will return a new set which may contain 1 or more groups that intersect. If there are 1 or more groups, myuser is indeed a member of sc_A.
This model design therefore implies that I have to go through a lot of trouble (and extra queries) just to find out if a user belongs to an organization.
And the reason why I am using m2m to groups is so as to make use of the Group level permissions functionality that django guardian provides for.
Is such a model design practical?
Or am I better off going with a different model design like that...
Approach #2
# Organisation app's models.py
from django.contrib.auth.models import User
class StudentClass(models.Model):
name = models.CharField('Class Name', max_length=255)
users = models.ManyToManyField(User, blank=True)
size = models.IntegerField('Class Size', blank=True)
class SpecialInterestGroup(models.Model):
name = models.CharField('Interest Group Name', max_length=255)
users = models.ManyToManyField(User, blank=True)
description = models.TextField('What our group does!', blank=True)
class TeachingTeam(models.Model):
name = models.CharField('Teacher Team Name', max_length=255)
users = models.ManyToManyField(User, blank=True)
specialization = models.TextField('Specialty subject matter', blank=True)
Obviously, this model design makes it really easy for me to check if a user object belongs to a particular organization or not. All I need to do to find out if user john is part of a TeachingTeam maths_teachers or not is to check:
user = User.objects.get(username='john')
maths_teachers = TeachingTeam.objects.get(name='Maths teachers')
if user in maths_teachers.users.all():
print "Yes, this user is in the Maths teachers organization!"
But what this model design implies is that when I add a user object to a group (recall that I want to use django guardian's Group permissions functionality), I have to make sure that the save() call adds the user object into a "Maths Teachers" group in django.contrib.auth.models.Group AND into my custom TeachingTeam class's "Maths Teachers" object. And that doesn't feel very DRY, not to mention that I have to somehow ensure that the save calls into both the models are done in a single transaction.
Is there a better way to design my models given this use case/requirement - use django groups and yet provide a way to "extend" the django's native group functionality (almost like how we extend django's user model with a "user profile app")?
My take on this (having developed django apps for a long time) is that you should stick with the natural approach (so a StudentClass has Users rather than Groups). Here "natural" means that it correspond to the actual semantics of the involved objects.
If belonging to a specific StudentClass must imply some automatic group (in addition to those granted to the user), add a groups m2m to the StudentClass model, and create a new authentication backend (extending the default one), which provides a custom get_all_permissions(self, user_obj, obj=None) method. It will be hooked by https://github.com/django/django/blob/master/django/contrib/auth/models.py#L201
In this method query for any group associated to any Organization the user belongs to. And you don't need to do 1+N queries, correct use of the ORM will navigate through two *-to-many at once.
The current ModelBackend method in https://github.com/django/django/blob/master/django/contrib/auth/backends.py#L37 queries get_group_permissions(user_obj) and adds them to the perms the user has assigned. You could add similar behavior by adding (cached) get_student_class_permission and other corresponding methods.
(edited for clearer prologue)
Obs: There is another approach which is to use generic relationships, in this approach you would have the User model instance pointing to it's resources through the contenttypes framework. There is a nice question here on SE explaining this approach.
About the performance: There are some clues that a single select with JOIN is cheaper than many simple select (1,2,3). In this case opition 2 would be better.
About the usability: The first approach is hard to explain, hard to understand. IMHO go for no. 2. Or try the generic relations.
Related
I'm building a site where we ask users several personal details (birth date, phone number, address, marital status etc. many more).
Option 1: User model only. Put these personal fields in class User(AbstractUser) model
class User(AbstractUser):
birth_date = ...
phone_number = ...
Option 2: User + UserProfile models: separate login-related data (User) from personal data (UserProfile) like:
class User(AbstractUser):
pass
class UserProfile(models.Model):
user = models.OneToOneField(
User,
to_field="id",
primary_key=True,
on_delete=models.CASCADE,
related_name="user_profile",
)
birth_date = ...
phone_number = ...
Which one is the best practice?
This is subjective, based on each ones opinion/needs. But will go over the technical differences:
Option 1 is easier to use in your code and more efficient as you don't have to do JOINs between tables in order to get all information
Option 2 will make the model look cleaner, with only the most important fields present. This is helpful for data engineers or data analyst who work a lot on SQL visualisation tools
Unless UserProfile has 20+ fields, I would personally go with Option 1.
As always the answer will be "it depends". If your users might have different types of profiles/roles that may require additional set of fields I would go with the second option because it would allow you to have one user model that can combine traits and functionalities of several users. Depending on situation you can call specific method of profile (look for Delegation pattern for more examples).
I have the following two models (only included their relationship to each other). This is a job board site, a business owner can create one or more Business objects (on the off chance they own more than one small business) and then post as many Job objects as they please.
class Business(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Job(models.Model):
business = models.ForeignKey(Business, on_delete= models.CASCADE)
How can I get all Job objects belonging to a User? I know I can get all Job objects belonging to a Business but a user can create multiple businesses.
I know I have to build some kind of chain filter, I am just not sure how to go about that.
Edit: I am trying to achieve this so I can display all of a user's posts in a dashboard type of view.
You can do:
Job.objects.filter(business__user=user)
Note the double underscore after "business". That's how you access the business' attributes
I'm creating a Django project to help people run clubs and community groups. I'm new to Django and I can think of lots of different ways to add users to clubs and manage permissions, but it's not clear what the right way to do it is.
I have a "Club" model and a "Team" model. A club can have many teams, and members of a club may or may not be members of a team, but a user must be a member of the club in order to join the team. There will also be various permissions around what a club/team member can and cannot do (and what a club/team administrator can do)
My current plan for managing membership is to create "ClubMembership" and "TeamMembership" models and create the relationships using ManyToManyField.through, as demonstrated here: https://docs.djangoproject.com/en/2.2/ref/models/fields/#django.db.models.ManyToManyField.through
For permissions, it looks like I should use django-guardian and set permissions when people join/leave a club/team. Would it make sense to also have a "members" foreign key to django.contrib.auth.models.Group in my club/team models to keep track of this?
It feels like I'm doubling up in some areas by having membership managed by membership models and then also using groups to set permissions. Is there a better way to approach this, or anything I should modify/consider?
class Team(models.Model):
# Fields
name = models.CharField(max_length=255)
# Relationship Fields
club = models.ForeignKey(
'organizations.Club',
on_delete=models.CASCADE, related_name="teams",
)
members = models.ManyToManyField(
'organizations.Users.user',
related_name="teams", through='TeamMembership', through_fields=('team', 'user')
)
administrators = models.OneToOneField(
Group,
on_delete=models.CASCADE, related_name="teams",
)
class TeamMembership(models.Model):
# Fields
name = models.CharField(max_length=255)
# Relationship Fields
team = models.ForeignKey(
'organizations.Team',
on_delete=models.CASCADE, related_name="teammemberships",
)
user = models.ForeignKey(
'users.User',
on_delete=models.CASCADE, related_name="teammemberships",
)
To summarize my questions:
Is the "TeamMembership" model the right way to associate users with
Teams?
Should I include Groups in my models to house users for
permissions purposes, or will this cause problems?
Is there another approach I should consider?
Welcome to StackOverflow.
1) Team Membership looks like a good way to do it. I'm not sure what the name field would be used for but it looks like it may be redundant (from Team name or user name).
2) It would not be a bad idea to implement Groups for permissions. That way the security part of your app is not dependent on the functional part, and vice versa.
3) The best way to implement django-guardians permissions with Groups would be to use signals that are sent whenever a Team or TeamMembership instance is created or destroyed. The creation/deletion of a Team would result in the creation/deletion of a corresponding permissions group, and the creation/deletion of a TeamMembership would result in the addition/removal of that person in the Team permissions group.
I've been searching Google and Stackoverflow for the last 3 days now and couldn't find anything similar. I'd like to extend or use the django groups and permissions.
Let's say we got some Projects with different Teams and Users.
class Project(models.Model):
owner = models.ForeignKey(User)
name = models.CharField(max_length=100)
class Team(models.Model):
project = models.ForeignKey(Project)
members = models.ManyToManyField(User)
name = models.CharField(max_length=100)
permission = models.ManyToManyField(Permission)
Now - everything's fine. Now what I want is to extend the existing Auth.Group delivered by django so I could use request.user.has_perm... the direct way.
So, let's change the Team-Model into
class Team(Group):
project = models.ForeignKey(Project)
members = models.ManyToManyField(User)
This extends the existing Group. The name and permissions field comes direct from the Group-Model. If I create a new Team, there will be a Group added, too. That's great.
With a custom save method I could also add, that the user listed in members, will be added to the created Group and gets the permissions granted to the Team/Group.
Now my problem is, the name field of the Group is unique - so I couldn't add an Admins-Team for each Project.
I thought about adding another name-field for the frontend and generate a unique string for the name field of the Group-Model. But hmmm....is there a better solution for multiple custom Groups? Maybe one got an idea? Thank you in advance!
You need to make group names unique. So one way that you found out is having another name field, making it unique for all subgroups.
Another way would be to make use of existing name field and add special chars of yours. For example group name with admin#project1, members#project1 etc. (Note: I'm not sure '#' is allowed in group name, you can choose any special character that is allowed).
So whenever you create Team you update the name field with suffix #<project_name> in Team models save() method.
To display it more sanely, you can add __unicode__() method to return only first part of the group name. Example:
class Team(Group):
...
def __unicode__(self):
try:
return self.name.split('#')[0]
except:
#something wrong!
return self.name
My desire is to have a common location model, and then have the various higher level models who need a location refer to it.
I want to present my user in admin with a multiple part form (an inline) that allows them to enter the higher level info for the Publisher and Building, as well as the location information for each. The inline system doesn't seem to want to work this way.
Clearly, I am doing something very wrong, because this seems like a very standard sort of problem to me. Is my schema design borked ?
Am I stupidly using the inline system ? I don't want to do subclasses of Location for each upper level object, because I want to manipulate locations in different ways independent of whatever high-level objects own them (a mailing list, or geographic look up perhaps)
models.py:
...
class Location(models.Model):
"""
A geographical address
"""
# Standard Location stuff
address_line1 = models.CharField("Address line 1", max_length = 45, null=True, blank=True)
...
class Publisher(models.Model):
"""
Contains Publisher information for publishers of yearbooks. Replaces Institution from 1.x
"""
name = models.CharField(max_length=100, null=False, help_text="Name of publisher, e.g. University of Kansas")
groups = models.ManyToManyField(Group, help_text="Select groups that this publisher owns. Usually just one, but multiple groups are possible.")
is_active = models.BooleanField(help_text="Check this box to enable this publisher.")
location = models.OneToOneField(Location)
...
class Building(models.Model):
"""
Contains Building Information
"""
name = models.CharField(max_length=100, null=False, help_text="Name of building, e.g. Physical Sciences")
is_active = models.BooleanField(help_text="Check this box to enable this building.")
location = models.OneToOneField(Location)
...
admin.py:
...
class LocationInline(generic.GenericStackedInline):
model = Location
max_num = 1
extra = 1
class PublisherAdmin(admin.ModelAdmin):
model = Publisher
inlines = [ LocationInline,
]
class BuildingAdmin(admin.ModelAdmin):
model = Building
inlines = [ LocationInline,
]
admin.site.register(Publisher, PublisherAdmin)
admin.site.register(Building, BuildingAdmin)
I can force the inline to load and present by adding this to the Location model:
# Support reverse lookup for admin
object_id = models.PositiveIntegerField()
content_type = models.ForeignKey(ContentType)
of = generic.GenericForeignKey('content_type', 'object_id' )
But when I do this, even though I do get an inline object, and can edit it, the relationship seems backwards to me, with Location storing an id to the object that created it.
Any help is welcome, either a recommended schema change to make everything work wonderfully (as Django is so good at) or a trick to make the backwards-seeming stuff make sense.
Firstly, I think you want ForeignKey, not OneToOneField. Otherwise, you might as well just add your location fields to the Publisher and Building models. Then you'll simply get a dropdown to choose the location and a link to add a new one if needed in the building and publisher admin.
If you really want to have one location instance per building/publisher, you won't be able to edit it as an inline because an inline model needs to have a ForeignKey pointing to the parent model, unless you add the generic foreign key. This isnt 'backwards' - it's a valid option when you want an object to be able to attach itself to any other, regardless of type.
When it comes to domain model, there's no such thing as a "One Right Way" to do it, it depends on your specific application's requirements.
wrt/ your problem:
The OneToOne field limits your models to one Location per model instance, which (as Greg mentionned) is not conceptually very different from just sticking the Location's fields directly in the model. wrt/ DRY/factorisation/reuse etc, you can get this done using model inheritence too, having an abstract (or eventually concrete if it makes sense for your app) Location model.
The ForeignKey solution still restricts your Publisher and Building models to a single Location (which might - or not - be what you want), but a given location might be shared between different Publisher and / or Building instances. This means that editing one given location will reflect on all the related instances (beware of unwanted side effects here).
Using a GenericForeignKey in the Location model means that a given location instance belongs to one and only one related object. No surprinsing side-effect as with the above solution but you may have duplicate locations (ie one for the building, one for the publisher) with same values, and you won't be able to lookup all related objects for a specific location (or not that easily at least). Also, this won't prevent a Publisher or Building instance to have more than one location, which once again might be fine or not. wrt/ Location instance "storing the id" of the object they belong to, well, that's really what this design choice means : a Location "belongs to" some other object, period.
In any case, designing around the default behaviour of Django's admin app is probably not the wisest thing to do. You have to first decide what makes sense for this application (and you may have different needs for Publishers and Buildings), then possibly extend the admin to match your needs.