best way to implement privacy on each field in model django
Asked Answered
I

3

2

I am trying to provide privacy settings at the model's field level for users. So a user can decide which data he wants to display and which data he wants to hide.

Example:

class Foo(models.Model):
    user = models.OneToOneField("auth.User")
    telephone_number = models.CharField(blank=True, null=True, max_length=10)
    image = models.ImageField(upload_to=get_photo_storage_path, null=True, blank=True)

I want to provide an option to the user to select the fields which he wants to display and which he doesn't. Consider a user doesn't want to display telephone_number, so he should have the option for that.

Which is the best way to approach this?

Isle answered 28/5, 2015 at 6:44 Comment(1)
Well, there are multiple things that you can do, this can reside in a classmethod, inside the Foo class. There you will have access to the class via cls variable. Secondly, now we also have the JSONField type available in Django. That can be used directly to store fieldnames as a list.Besmirch
B
2

You can create a CommaSeparatedIntegerField field inside the model, and use it to store a list of field_names (Integers that denote a field_name) that the user wants to hide.

You can create a mapping between the field_names and integers as a constant inside your models.py. And check whichever field_names are those the the user had checked.

Example mapping:

FIELD_NAME_CHOICES = (
    (1, Foo._meta.get_field('telephone_number')),
    (2, Foo._meta.get_field('name')),
    .
    .
)

Check the following link for reference https://docs.djangoproject.com/en/1.8/ref/models/fields/#commaseparatedintegerfield

Besmirch answered 28/5, 2015 at 7:3 Comment(1)
One should have to create a seperate class for this right, Because we cannot access class Foo inside Foo.Schwab
C
0

The very obvious plain stupid solution would be to add a boolean 'show_xxx' for each field, ie:

class Foo(models.Model):
    user = models.OneToOneField("auth.User")
    telephone_number = models.CharField(blank=True, null=True, max_length=10)
    show_telephone_number = models.BooleanField(default=True)
    image = models.ImageField(upload_to=get_photo_storage_path, null=True, blank=True)
    show_image = models.BooleanField(default=True)

Then in your templates check for the show_xxx field's value:

{% if foo.telephone_number and foo.show_telephone_number %}
  <p>Telephone number: {{ foo.telephone_number }}</p>
{% endif %}

etc...

Of course you can also use a single integer field and the old bitmask trick:

class Foo(models.Model):
    user = models.OneToOneField("auth.User")
    telephone_number = models.CharField(blank=True, null=True, max_length=10)
    image = models.ImageField(upload_to=get_photo_storage_path, null=True, blank=True)

    foo = models.TextField(blank=True)

    perms = models.IntegerField(default=0)
    SHOW_TEL = 1
    SHOW_IMG = 2
    SHOW_FOO = 4

    def _show(self, flag):
        return (self.perms & flag) == flag

    def show_telephone_number(self):
        return self._show(self.SHOW_TEL)

    def show_image(self):
        return self._show(self.SHOW_IMG)

    def show_foo(self):
        return self._show(self.SHOW_FOO)

but I'm not sure this really is an "optimisation"... And you'll have to manually take care of the checkboxes etc in your edit forms.

Cristie answered 28/5, 2015 at 7:15 Comment(6)
That would be a wastage of DB space and a lot of extra fields, what if my model had 20 fields. I would require another 20 flag fields for that. Not an optimized solutionBesmirch
Added a "db space optimisation" solution... But I'm not sure I'd call this an optimisation.Cristie
I totally agree. Space is cheap, waste it. But I'd like to see a way to autogenerate these with a mixinFilipino
@SebastianWozny this can be done with a decorator (to mark which fields should be 'flaggable') and a custom metaclass.Cristie
Can you demonstrate that? It seems like a most intricate solution.Filipino
@SebastianWozny it's not that tricky if you understand metaclasses and django models, but it would obviously take at least as much time than just adding the "show_xxx" fields manually, and I don't think it's a recurrent need enough to be worth the added complexity.Cristie
R
0

You could also use a MultiSelectField... The code below is from a Django Snippets, so please no credits to me as I'm only sharing the work of someone else!

class MultiSelectField(models.CharField):
""" Choice values can not contain commas. """

def __init__(self, *args, **kwargs):
    self.max_choices = kwargs.pop('max_choices', None)
    super(MultiSelectField, self).__init__(*args, **kwargs)
    self.max_length = get_max_length(self.choices, self.max_length)
    self.validators[0] = MaxValueMultiFieldValidator(self.max_length)
    if self.max_choices is not None:
        self.validators.append(MaxChoicesValidator(self.max_choices))

@property
def flatchoices(self):
    return None

def get_choices_default(self):
    return self.get_choices(include_blank=False)

def get_choices_selected(self, arr_choices):
    choices_selected = []
    for choice_selected in arr_choices:
        choices_selected.append(string_type(choice_selected[0]))
    return choices_selected

def value_to_string(self, obj):
    value = self._get_val_from_obj(obj)
    return self.get_prep_value(value)

def validate(self, value, model_instance):
    arr_choices = self.get_choices_selected(self.get_choices_default())
    for opt_select in value:
        if (opt_select not in arr_choices):
            if django.VERSION[0] == 1 and django.VERSION[1] >= 6:
                raise ValidationError(self.error_messages['invalid_choice'] % {"value": value})
            else:
                raise ValidationError(self.error_messages['invalid_choice'] % value)

def get_default(self):
    default = super(MultiSelectField, self).get_default()
    if isinstance(default, (int, long)):
        default = string_type(default)
    return default

def formfield(self, **kwargs):
    defaults = {'required': not self.blank,
                'label': capfirst(self.verbose_name),
                'help_text': self.help_text,
                'choices': self.choices,
                'max_length': self.max_length,
                'max_choices': self.max_choices}
    if self.has_default():
        defaults['initial'] = self.get_default()
    defaults.update(kwargs)
    return MultiSelectFormField(**defaults)

def get_prep_value(self, value):
    return '' if value is None else ",".join(value)

def to_python(self, value):
    if value:
        return value if isinstance(value, list) else value.split(',')

def contribute_to_class(self, cls, name):
    super(MultiSelectField, self).contribute_to_class(cls, name)
    if self.choices:
        def get_list(obj):
            fieldname = name
            choicedict = dict(self.choices)
            display = []
            if getattr(obj, fieldname):
                for value in getattr(obj, fieldname):
                    item_display = choicedict.get(value, None)
                    if item_display is None:
                        try:
                            item_display = choicedict.get(int(value), value)
                        except (ValueError, TypeError):
                            item_display = value
                    display.append(string_type(item_display))
            return display

        def get_display(obj):
            return ", ".join(get_list(obj))

        setattr(cls, 'get_%s_list' % self.name, get_list)
        setattr(cls, 'get_%s_display' % self.name, get_display)

MultiSelectField = add_metaclass(models.SubfieldBase)(MultiSelectField)
Roop answered 4/7, 2016 at 17:50 Comment(1)
this is currently also available as a PyPi module, called django-multiselectRoop

© 2022 - 2024 — McMap. All rights reserved.