Sharing Objects with other users in Django
Asked Answered
R

1

11

I'm modeling a quite complex system in Django. I will post here only the relevant part of it and I will show simplified use cases diagrams to better express my ideas.

I basically have 2 type of users: Seller and Customer.

  1. A Seller "acquires" a Customer, that means that the seller now has the customer's personal information and can interact with him/her.
    A seller cannot interact with a customer that he didn't acquire. enter image description here

  2. A Seller creates a model hierarchy of related objects (each model in a sublevel is connected with a Foreign key to its parent)
    enter image description here

  3. A Seller shares the created box object and all its related objects with some customers enter image description here

  4. The Authorized Customers can:

    • R-ead some attributes of the Box model
    • R-ead and U-pdate some attributes of the Item model
    • Can't access some attributes of the Item model

    enter image description here

Questions:

  • A: What is the best approach to create relationship between users?
    I've seen that django-relationship could be useful.
  • B: What is the best approach to share Model instances in Django with other users?
    In my case, by default a Customer can't access any model created by the Seller if not explicitly shared.
  • C: If I share only the Box model, does it implicitly mean that also all its related model (Foreign keys to Box model) will be shared?
  • D: What is the best approach to control field level permission per user?
    Can django-guardian be useful here?
Roasting answered 15/7, 2013 at 2:33 Comment(0)
D
17

A: What is the best approach to create relationship between users?

All users are not created equal. django-relationship creates arbitrary relations between arbitrary users, which is likely not what you want. What you really want is to constrain this relationship to be strictly Seller -> Customer

# This example assumes that both customers and sellers have user table entries.
from django.contrib.auth.models import User

class Customer(User): pass

class Seller(User):
    acquired_customers = ManyToManyField(Customer,
        related_name="acquired_sellers")

    def acquire(customer):
        " A convenience function to acquire customers "
        self.acquired_customers.add(customer.id)

B: What is the best approach to share Model instances in Django with other users?

You can use custom "through" model of a ManyToManyField to add extra information that you want to track. In this case, we're adding the seller, and an automatic timestamp when was shared. This allows you to do things like show the products that have been shared with you sorted by when they were shared, along with the name of the seller that sent it to you.

# Install mptt for heirararchical data.
from mptt.models import MPTTModel

class Box(MPTTModel):
    " Nestable boxen for your Items "
    owner       = ForeignKey(Seller)
    title       = CharField(max_length=255)
    shared_with = ManyToManyField(Customer,
        related_name='boxes_sharedwithme', through=SharedBox)

class Item(Model):
    " A shareable Item "
    box         = ForeignKey(Box)
    title       = CharField(max_length=255)

class SharedBox(Model):
    " Keeps track of who shares what to whom, and when "
    when        = DateTimeField(auto_now_add=True)
    box         = ForeignKey(Box)
    seller      = ForeignKey(Seller)
    customer    = ForeignKey(Customer)

#----------------------------

# share an Item with a Customer
def share_item(request, box_id, customer_id, **kwargs):
    # This will fail if the user is not a seller
    seller   = request.user.seller
    # This will fail if the seller is not the owner of the item's box
    box      = Box.objects.get(
        id=box_id, owner=seller)
    # This will fail if the seller has not acquired the customer
    customer = Customer.objects.get(
        id=customer_id, acquired_sellers=seller)
    # This will share the item if it has not already been shared.
    SharedBox.objects.create_or_update(
        box=box, seller=seller, customer=customer)
    return HttpResponse("OK") 

C: If I share only the Box model, does it implicitly mean that also all its related model (#Foreign keys to Box model) will be shared?

Implicit permissions are "business logic" which means you'll likely need to implement it yourself. Fortunately, Django's permission system is pluggable so that you can add your own rules that recurse up the hierarchy to check permissions. Or, you can create custom Managers that add the appropriate rules to the the query wherever used.

from django.db.models import Manager
from django.db.models.query import EmptyQuerySet

class ItemManager(Manager):

    def visible(user):
        iqs = self.get_query_set()
        oqs = EmptyQuerySet()
        # add all the items a user can see as a seller
        try: oqs |= iqs.filter(box__owner=user.seller)
        except Seller.DoesNotExist: pass
        # add all the items a user can see as a customer
        try: oqs |= iqs.filter(box__shared_with=user.customer)
        except Customer.DoesNotExist: pass
        # return the complete list of items.
        return oqs

class Item(Model): objects = ItemManager()

class ItemListView(ListView):
    model = Item

    def get_queryset(request):
        return self.model.objects.visible(request.user)

D: What is the best approach to control field level permission per user?

If this needs to be super granular or per-user then django-guardian is the way to go. If permissions are rule-based, then you may be better off with a simple field in order to keep the complexity of your database queries down.

class Property(Model):
    title = CharField(max_length=255)
    units = CharField(max_length=10,
        choices=UNIT_TYPES, null=True, blank=True)

# -- A simple field that toggles properties for all users

class ItemProperty(Model):
    item     = ForeignKey(Item)
    property = ForeignKey(Property)
    value    = CharField(max_length=100)
    customer_viewable = BooleanField(default=False)
    customer_editable = BooleanField(default=False)

# -- A simple field that defines user classes who can view/edit

from django.contrib.auth.models import Group

class ItemProperty(Model):
    item     = ForeignKey(Item)
    property = ForeignKey(Property)
    value    = CharField(max_length=100)
    viewable_by = ForeignKey(Group)
    editable_by = ForeignKey(Group)

Disclaimer: These are my opinions on the subject. Views on authorisation within the Django community are heavily fragmented. There will never be one "Best" way to do anything, so consider carefully whether you need something really generic and can afford the database hits, or whether you need something speedy & business-specific and can afford the loss of flexibility.

Decrease answered 15/7, 2013 at 9:21 Comment(2)
Thanks, very insight answer! I'm wondering, about question D: if I don't expose in the template some of the attributes of an object (let's say an Item) and if I don't include those attributes in any of the Form accessible to the Customers, that basically means that the Customers will never be able to access those attributes, am I right? So it would be something I shouldn't care about. I guess.Roasting
As long as those attributes don't have a corresponding guessable url, and that view doesn't check permissions, then no you don't have to worry about it.Decrease

© 2022 - 2024 — McMap. All rights reserved.