Groups per object using Django and django-guardian object permissions - django

I'm currently creating a structure where I have employees which belong to a company.
Within this company I need to be able to create several groups. Ranks if you will. You could assign less permissions to lower ranks and more permissions to higher ranks.
I want to go for object level permissions and I noticed the django-guardian project gave me exactly what I needed. It works with the native User and Group objects so I'm now trying to find a way to implement the native group object in a company object.
Problems I face is that name in group is unique. So if 2 companies add the same group, errors will occur.
I found an implementation that works in a way but seems quite 'hacky' to me. In my company I declared a group variable that references Group:
class Company(models.Model):
...
groups = models.ManyToManyField(Group, through='CompanyRole')
CompanyRole basically houses the group name and a reference to company and group
class CompanyRole(models.Model):
group = models.ForeignKey(Group)
company = models.ForeignKey(Company)
real_name = models.CharField(max_length=60, verbose_name=_('Real name'))
objects = CompanyGroupManager()
I created a custom manager with a convenient method to add a new 'company group'
class CompanyGroupManager(models.Manager):
def create_group(self, company, group_name):
un_group_name = str(company.id) + '#' + group_name
group = Group.objects.create(name=un_group_name)
company_group = self.model(
real_name=group_name,
company=company,
group=group
)
company_group.save(using=self._db)
return company_group
Here's the part I don't really feel confortable about. In order to change the problem with the unique name on the Group model I used a combination of the company id, a hash sign and the actual group name to avoid clashes.
Now my question is: are there better methods in my scenario, am I missing something or is this a good way of accomplishing what I need?

Unfortunately there is no way of getting around the unique requirement, that is because this field is used as the id:
https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.Field.unique
Your options are the following:
1) Mocking the model.
You would basically just create a new Group model that doesn't have the unique requirement. The downside here is that you'd need to use it everywhere, so if this requires updating 3rd party apps, it might not be worth it.
2) make the name you unique. (As you did)
Make sure that you document your convention well, so that all future coders will know what they are looking at.Something like "company name"#"group name" could make more intuitive sense than an id. If the a hash might appear in either then use a more certain delimiter ("__" is a relatively common way of connecting related concepts in django, I might go for this).
I would recommend that you add the following to make it easy for you to access the name.
def get_name(self):
# Explain how you get the group name from your uniqueified name
return self.name.split('#')[1]
Group.add_to_class('get_name', get_name)
When you access your group's name in your app, just do:
my_group.get_name()
You might also want to put the generating the uniqueified name into an overridden version of the save(). This would give you nicer split between model and view...

Related

Django Custom Group model

By default, in django the group model has the name as unique=True. Is it possible to remove this attribute and how? Does it have any major consequence?
It's probably better to prefix the name of the group with something distinctive rather than try to make it non-unique. By default Group.name is used as a natural key by Django, for serialization purposes.
You could work around display issues by doing something during display, like:
def get_group_name(group):
if "|" in group.name:
return group.name.split("|")[1]
return group.name
group = Group.objects.create(name="COMPANY_X|Sales")
print(get_group_name(group))
# Sales
You can still define your own Group model but it would require customizing your user model quite significantly, which is a lot of work, and there may still be things that rely on Group name uniqueness in Django internals.

Best approach to model contacts in the Django modelling language

I am designing a contact relationship application that needs to store contacts in groups. Basically I have 7 "group types" (simplified it to 3 for my image), each group type shares the same fields so I thought that it would make sense to use an abstract "group", and let all group types inherit the methods from this abstract group.
So this is basically the idea:
However, this approach results in a couple of unexpected difficulties. For example:
I am not able to use a foreignkey of an abstract class, so if I would want to model a relationship between a group and a contact, I have to use the following approach:
limit = (models.Q(app_label='groups', model="Group type A") |
models.Q(app_label='groups', model="Group type B") |
models.Q(app_label='groups', model="Group type C")
)
group_type = models.ForeignKey(ContentType, limit_choices_to=limit)
group_id = models.PositiveIntegerField()
group = GenericForeignKey('group_type', 'group_id')
This seems quite hacky, and with this approach I am forced to do some hard coding as well. I am not able to call all groups with a simple query, maybe a new group will be added in the future.
Is there a better approach to model a relationship like this? Am I using the abstract class completely wrong?
Edit: some extra explanation in response to the questions.
A user is connected to a group with another object called "WorkRelation", because there is some extra data that is relevant when assigning a user to a group (for example his function).
I initially went for an abstract class because I thought that this would give me the flexibility to get all Group types be just calling Group.objects.all(). If I would use a base model, the groups aren't connected and I will also have to hard-code all group names.
Since your child models do not have additional fields, you can make them proxy models of the base group model. Proxy models do not create new database tables, they just allow having different programmatic interfaces over the same table.
You could then define your ForeignKey to the base group model:
group = ForeignKey(BaseGroup)
Use django-polymodels or a similar app to have the groups casted to the right type when queried.
More on model inheritance in the doc.
Why don't use solid base model instead of abstract model? Then you just put contacts as either ForeignKey or ManyToMany to the base model.

Database methods to get a list of non duplicated instances?

In my models I have a class like the following:
class Contact(models.Model):
group = models.CharField(max_length=200, blank=True)
name = models.CharField(max_length=100)
I'd like to find the better way of getting a list of all the groups. So far
I have two solutions:
groups=[]
for contact in Contact.objects.all():
if not contact.group in groups:
groups.append(contact.group)
and the second one:
groups=set(contact.group for contact in Contact.objects.all())
I think that the second one is much better because it uses generators, but I'd like to know if there is some database method like filter, exclude , etc that could allow me to do this.
The point of doing this is to optimize when an user has a lot of contacts but just a few groups. (In that case maybe making a class group would be better, but I'd really like to avoid that)
Best way is to use distinct
groups = Contact.objects.values_list('group', flat=True).distinct()

Django and writing queries with lots of joins

I have trouble to make these kind of queries with lots of joins. I didn't found examples, but I guess they are not so complicated to write. It's just there are several FKs.
Here is the models.py (not complicated)
class User(AbstractBaseUser, PermissionsMixin): # Django custom user model
# Some stuff
class CliProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
class BizProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
class Card(models.Model):
linked_client = models.ForeignKey(CliProfile, blank=True, null=True)
class Points(models.Model):
benef_card = models.ForeignKey(Card)
at_owner = models.ForeignKey(BizProfile)
creation_date = models.DateTimeField(auto_now_add=True)
Quick description of the model
a user can be a client (using CliProfile) or a business (using BizProfile)
each card is linked to a client
each card contains a [points - business] association
This way: a client has a card and can has 3 points at Pizza Hut, and 5 points at McDonalds with the same card)
The request I'm trying to write
Functionally speaking, the purpose is a owner (like PizzaHut) can see all his clients (client who have cards which has points at Pizza Hut)
Technically speaking, I'm trying to write a query to get all clients (ie. a CliProfile queryset) whose cards (at least 1 of all) whose points (at least 1 of all) whose owner (there is only 1) whose user (there is only 1) = request.user ?
Do you have any idea how to write such a query? Thanks a lot.
To match fields within models in filter() you need to use two underscores. The following worked for me
CliProfile.objects.filter(card__points__at_owner=request.user)
But #Alex's suggestion makes the most sense unless this was just an example of what you are trying to do.
If you wanted profiles that are associated with one of several cards you can use the __in field lookup:
CliProfile.objects.filter(card__in=IterableOfCards)
Also you don't use == in filter(). That would return True or False and then pass that value in the filter() call effectively making the call filter(True or False) which won't do anything useful. you have to use = because you are passing a named parameter into the filter function.
Why card instead of card_set()?
cart_set only exists within an instance of a CliProfile. You are not in an instance of a CliProfile, you are trying to get a list of them.
You can try it in the terminal and it will tell you the valid choices.
#Note that it doesn't matter what you put after=, since it fails before that is checked.
>>> CliProfile.objects.filter(card_set=True)
FieldError: Cannot resolve keyword 'card_set' into field. Choices are: card, id, user
a CliProfile can be referenced by multiple cards, which is why card_set exists in it but you are trying to match one card. The card whose points at_owner field is request.user.
You would use a_cliprofile_instance.card_set.filter() to get a subset of their cards or a_cliprofile_instance.card_set.all() to display all of their cards

Find the most recent rating for a user in a django queryset

I'm looking for a method to get the most recent rating for a specific Person for all Resources.
Currently I'm using a query like Rating.objects.filter(Person = person).order_by('-timestamp')
then passing that through a unique_everseen with a key on the resource and user attributes and then re-looking up with a Rating.objects.filter(id__in = uniquelist). Is there a more elegant way to do this with the django queryset functions?
These are the relevant models.
class Person(models.Model):
pass
class Resource(models.Model):
pass
class Rating(models.Model):
rating = models.IntegerField()
timestamp = models.DateField()
resource = models.ForeignKey('Resource')
user = models.ForeignKey('Person')
I need to keep all of the old Ratings around since other functions need to be able to keep a history of how things are 'changing'.
I am not 100% clear on what you are looking for here, do you want to find the most recent rating by a user for all the resources they have rated? If you can provide detail on what unique_everseen actually does it would help to clarify what you are looking for.
You could rather look from a resource perspective:
resources = Resource.objects.filter(rating__user=person).order_by('-rating__timestamp')
resource_rating = [(resource, resource.rating_set.filter(person=person).get_latest('timestamp')) for resource in resources]
You might be able to use Aggregate functions to get to the most recent record per resource, or some clever use of the Q object to limit the SQL requests (my example may save you some requests, and be more elegant but it is not as simple as what you could produce with a raw SQL request). In raw SQL you would be using an inner SELECT or a well executed GROUP BY to get the most recent rating, so mimicking that would be ideal.
You could also create a post_save signal hook and an 'active' or 'current' boolean field on your Rating model, which would iterate other ratings matching user/resource and set their 'active' field to False. i.e. the post_save hook would mark all other ratings as inactive for a user/resource using something like:
if instance.active:
for rating in Rating.objects.filter(user=instance.user,resource=instance.resource).exclude(id=instance.id):
rating.active=False
rating.save()
You could then do a simple query for:
Rating.objects.filter(user=person,active=True).order_by('-timestamp')
This would be the most economical of queries (even if you make the complicated group by/inner select in raw SQL you are doing a more complicated query than necessary). Using the boolean field also means you can provide 'step forward/step backwards'/'undo/redo' behavior for a user's ratings if that is relevant.