3

I'm working on a user/group API (using django-rest-framework for most things), and am getting hung up on the settings dict that is part of each object.

My basic models look like this:

class Group(models.Model):
    name = models.CharField(null=False, unique=True)
    is_active = models.BooleanField(null=False, default=True)
    description = models.CharField(max_length=512)

class User(models.Model):
    name = models.CharField(null=False, unique=True, db_index=True)
    group = models.ForeignKey('Group', related_name='users')
    is_active = models.BooleanField(null=False, default=True)

The JSON returned is simple (views omitted, only notable thing is that I'm not using a PrimaryKeyRelatedField for the users field).

Group:

{
   "id": 1,
   "name": "Basic Group",
   "is_active": true,
   "description": "Just a simple group",
   "users": [
      "Test User 1",
      "Test User 2"
   ]
}

Users:

{
   "id": 1,
   "name": "Test User 1",
   "group": 1,
   "is_active": true
}, {
   "id": 2,
   "name": "Test User 2",
   "group": 1,
   "is_active": true
}

I would like to enhance this by having a "settings" dict for both Group and User object types, that are overriding. That is, the settings dict for a Group will include all global settings, unless a setting with the same name has been set on the group. Similarly, the settings dict for a user will have all of the global settings, overridden by group settings, unless a setting with the same name has been set for the user.

Imagine a structure approximately like the following:

name      | value     | group | user
---       | ---       | ---   | ---
setting 1 | value 1   |       | 
setting 1 | value 2   | 1     | 
setting 2 | SKFJDL    | 1     | 
setting 2 | ABCD      | 1     | 2

This would result in the following JSON being returned for Group 1:

{
    "id": 1,
    "name": "Basic Group",
    "is_active": true,
    "description": "Just a simple group",
    "users": [
        "Test User 1",
        "Test User 2"
    ],
    "settings": {
        "setting 1": "value 2",
        "setting 2": "SKFJDL"
    }
}

For User 1, the settings dict would look like:

"settings": {
    "setting 1": "value 2",
    "setting 2": "SKFJDL"
}

For User 2, it looks like:

"settings": {
    "setting 1": "value 2",
    "setting 2": "ABCD"
}

This leaves me with two issues.

Issue 1: The SQL that gets me the desired output is relatively complex. Basically:

SELECT DISTINCT(name) * 
FROM (
    (SELECT * FROM setting WHERE group IS NULL AND user IS NULL) 
  UNION ALL 
    (SELECT * FROM setting WHERE group = 1 AND user IS NULL) 
  UNION ALL 
    (SELECT * FROM setting WHERE group = 1 AND user = 2)) 
  AS temp

I think I'm probably going to have to suck it up and do some custom query writing, instead of relying on anything that the Django ORM provides me out of the box...but I'm more than happy to be wrong about that.

Issue 2 (the bigger one): How in the heck do I actually do this? A ForeignKey relationship doesn't seem to give me a way to customize the fields that are pulled in when doing the reverse lookup, at least not in any way that I've been able to find in a day or so of stumbling around. I have a feeling that a custom reverse manager is almost certainly what I'm looking for, but I haven't actually figured out how to use it in the context of a model, and in particular a model that's having it's downstream views autogenerated by DRF.

Any help appreciated!

2 Answers2

0

I took a quick stab at this. Threw a very basic Django project up on github here (log into the admin with testuser|testpass): https://github.com/junctionapps/so40854954

Basically, three filters using pretty much exactly the sql you have there. Then fill a dictionary (or whatever) with the data, replacing based on the priority. So put in the default (null/null), then group, then user. I renamed the models CustomUser, CustomGroup, and CustomSettings so as to not bump into reserved Django objects.

The settings stored in a model like (I think this is what you meant, I may have misunderstood this bit).

class CustomSettings(models.Model):
    name = models.CharField(max_length=64)
    value = models.CharField(max_length=64)
    group = models.ForeignKey(CustomGroup, null=True, blank=True)
    user = models.ForeignKey(CustomUser, null=True, blank=True)

and in a view something like the following (not totally happy with the part with the three loops; there is probably a slicker way).

if user_id:
    # a place to store the end values
    custom_settings = {}

    # get the user's group
    custom_user = get_object_or_404(CustomUser, id=user_id)

    # get the default values
    # see some tips at http://stackoverflow.com/a/844572/4872140
    default_settings = CustomSettings.objects.filter(group__isnull=True,
                                                     user__isnull=True)
    # get the group values
    group_settings = CustomSettings.objects.filter(group=custom_user.group,
                                                   user__isnull=True)
    # get the user values
    user_settings = CustomSettings.objects.filter(group=custom_user.group,
                                                  user=custom_user)
    # probably a more 'pythonic' way to conflate, however:
    # set the values:
    for s in default_settings:
        custom_settings[s.name] = s.value
    for s in group_settings:
        custom_settings[s.name] = s.value
    for s in user_settings:
        custom_settings[s.name] = s.value

Then custom_settings should have the values you want to use in DRF in some serializer.

AMG
  • 1,606
  • 1
  • 14
  • 25
0
class Hop(models.Model):
    migration = models.ForeignKey('Migration')
    host = models.ForeignKey(User, related_name='host_set')

    class Meta:
        unique_together = (("migration", "host"),)

https://docs.djangoproject.com/en/2.1/ref/models/options/#unique-together

yan
  • 1,382
  • 14
  • 11