Login

FieldAccessForm (per-field user access for forms derived from models)

Author:
Killarny
Posted:
October 16, 2008
Language:
Python
Version:
1.0
Score:
4 (after 4 ratings)

=== version 2 ===

Parts of this code are based off of source from davidcramer on #django and I'd like to thank him for his assistance.

Example:

# forms.py
...
class ForumPostForm(FieldAccessForm):
    class Meta:
        model = ForumPost
    class FieldAccess:
        moderator = FieldAccessLevel(
            lambda user, instance: user.get_profile().is_moderator,
            enable = ('approve', 'delete', 'edit')
        member = FieldAccessLevel(
            lambda user, instance: user.is_active,
            enable = ('edit',),
            exclude = ('approve', 'delete')
...

# template
...
<form action="" method="POST">
    <table>
        {% for field in form %}
            <tr><th>{{ field.label_tag }}</th><td>
                {% if not field.field.disabled %}
                    {{ field }}
                {% else %}
                    {{ field.field.value }}
                {% endif %}
            </td></tr>
        {% endfor %}
    </table>
    <p><input type="submit" value="Update" /></p>
</form>
...

This class will grant or deny access to individual fields according to simple rules. The first argument must be a user object, but otherwise, this class is instantiated the same as a ModelForm.

To utilize this code, inherit your form from FieldAccessForm, and create an inner class on your form called FieldAccess. Variables added to this inner class must have the same structure as that provided by the FieldAccessLevel class, which defines an access level, and the fields which apply to that access level.

FieldAccessLevel takes as it's first argument a callable rule that validates this access level. That rule will be called with two arguments: 'user' (current user requesting access) and 'instance' (model instance in question).

The keyword arguments to FieldAccessLevel are field groups which are used to determine which fields on this form are to be enabled and/or excluded, when the current user matches this access level. The term exclude indicates fields which are not to be rendered in the form at all.

Any fields not grouped in either 'enable' or 'exclude' will be disabled by default.

Superusers are always assumed to have full access. Otherwise, if a field is not specified with the FieldAccess inner class, then it is disabled by default. In other words, all users (except superusers) are denied access to all fields, unless they are specifically given access on a per-field basis.

It is worth mentioning that multiple access levels can apply simultaneously, giving a user access to fields from all levels for which he matches the rule.

If a user is denied access to a field, then the widget for that field is flagged as disabled and readonly. The field is also given two new attributes: a boolean 'disabled', and a 'value' containing the instanced model field. These two attributes allow a template author to have great control over the display of the form. For example, she may render the plain text value of a field instead of the disabled widget.

The FieldAccess inner class also allows one to conditionally exclude fields from being rendered by the form. These exclusions operate very similarly to the standard Meta exclude option, except that they apply only to the access level in question.

Note: The FieldAccess inner class may be used on both the form and the model; however, generally it makes more sense on the form. If you do use FieldAccess on both the form and model, be aware that both definitions will apply simultaneously. All access levels for which the user passes the rule will be processed, regardless of whether they were found on the form or the model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from django import forms

class FieldAccessLevel:
    '''Represents an access level for a form.'''
    def __init__(self, rule, enable=None, exclude=None):
        self.rule = rule
        self.enable = enable
        self.exclude = exclude

class FieldAccessForm(forms.ModelForm):
    '''This class will grant or deny access to individual fields according
    to simple rules.

    Example:
    
    class MyForm(FieldAccessForm):
        class FieldAccess:
            staff = FieldAccessLevel(lambda u, i: u.is_staff,
                enable = ('field1', 'field2'),
                exclude = ('field3',))
    '''
    def __init__(self, request_user, *args, **kwargs):
        super(FieldAccessForm, self).__init__(*args,**kwargs)
        if request_user.is_superuser:
            # superuser has full access to all fields
            return
        instance = kwargs.get('instance',None)
        # get available access levels
        access_levels = list()
        for FieldAccess in (getattr(self, 'FieldAccess', None), 
                            getattr(self.Meta.model, 'FieldAccess', None)):
            if not FieldAccess: continue
            for attr in dir(FieldAccess):
                if attr.startswith('_'): continue
                access_levels += [getattr(FieldAccess, attr)]
        # for any access level which the user falls under, retrieve the field
        # access data for those levels
        enable = None
        exclude = None
        for level in access_levels:
            if not level.rule(request_user, instance): continue
            if level.enable:
                if enable: enable += level.enable
                else: enable = level.enable
            if level.exclude:
                if exclude: exclude += level.exclude
                else: exclude = level.exclude
        # disable all fields except those in enable or exclude
        for fieldname, field in self.fields.items():
            if exclude and fieldname in exclude:
                self.fields.pop(fieldname)
            elif not enable or fieldname not in enable:
                field.widget.attrs['readonly'] = 'readonly'
                field.widget.attrs['disabled'] = 'disabled'
                field.disabled = True
                field.value = getattr(instance, fieldname, '')

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

Killarny (on October 24, 2008):

I've updated this to the newest code that I'm using, where I made a few improvements to the syntax of the FieldAccess inner class.

#

Please login first before commenting.