Login

fancy_if

Author:
Scanner
Posted:
April 15, 2007
Language:
Python
Version:
.96
Tags:
row-level-permissions compound-conditional
Score:
-2 (after 2 ratings)

I am working on some apps that use row level permissions. For many permission tests I intend to make custom tags so I can locate the permission test used by the template and by the view in a common module.

However, frequently, before I decide what the actual end-tag is going to be I need a way to expression a more powerful 'if' structure in the template language.

Frequently the existing if and if_has_perm tags are just not powerful enough to express these things.

This may make the template language unacceptably more like a programming language but it has helped me quickly modify complex permission statements without having to next and repeat four or five clauses.

As you can see this is intended to work with django off of the row level permissions branch.

Here is an example of a template that is using the 'fancy_if' statement:

{% fancy_if or ( and not discussion.locked not discussion.closed "asforums.post_discussion" discussion ) "asforums.moderate_forum" discussion.forum %} <li><a href="./create_post/?in_reply_to={{ post.id }}">{% icon "comment_add" "Reply to post" %}</a></li> {% end_fancy_if %} {% fancy_if or "asforums.moderate_forum" discussion.forum ( and not discussion.closed ( or eq post.author user eq discussion.author user ) ) %} {% fancy_if or "asforums.moderate_forum" discussion.forum eq post.author user %} <li><a href="{{ post.get_absolute_url }}update/">{% icon "comment_edit" "Edit Post" %}</a></li> {% end_fancy_if %} <li><a href="{{ post.get_absolute_url }}delete/">{% icon "comment_delete" "Delete Post" %}</a></li> {% end_fancy_if %}

  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
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
##############################################################################
#
@register.tag(name = 'fancy_if')
def fancy_if(parser, token):
    """A fancy if tag. It has two notable features:

    1) you can combine and, or, and not statements nested as much as you like.
    2) you can test object like {% if %} AND you can test row level permissions
       in the same expression.

    NOTE: To simplify my sanity, I use a simple prefix expression grammar.

    So, for example, to test if a discussion is not locked or the user
    has moderate permission on the forum the discussion is in:

    {% fancy_if or not discussion.locked 'asforums.has_moderate' discussion.forum %} ... {% else %} {% end_fancy_if %}

    The syntax is:

    {% fancy_if expr %}

    An expr may be:
    
       ( expr )
       or expr ...   (a list of one or more expr's or'd together)
       and expr ...  (a list of one or more expr's or'd together)
       not expr
       'permission code name' <variable>|None
       eq <variable>|'string' <variable>|'string'
       <variable>

    If you wish to nest or & and you use '(' ')' to express the
    sub-expression. These characters MUST be separated by white space, ie:

    Good: ( and foo bar biz bat )
    Bad:  (and foo bar biz bat)

    NOTE: Permissions are expressed by being surrounded by matching
    double or single quotes. ie: 'asforums.forum_moderate' or
    'asforums.disc_read'.

    NOTE: The permission expression MUST be two tokens. A permission
    and a variable reference. If you wish to do class level permission
    checking using the value 'None' (with no quotes!) as the variable
    reference.
    """

    bits = token.contents.split()
    tag = bits.pop(0)
    if not bits:
        raise template.TemplateSyntaxError, \
            "'fancy_if' statement requires at least one argument"
    expr = parse_fi_expressions(bits, parser)
    
    nodelist_true = parser.parse(('else', 'end_' + tag))
    token = parser.next_token()
    if token.contents == 'else':
        nodelist_false = parser.parse(('end_' + tag,))
        parser.delete_first_token()
    else:
        nodelist_false = template.NodeList()

    return FancyIfNode(expr, nodelist_true, nodelist_false)

##############################################################################
#
class FancyIfNode(template.Node):
    """The template Node object that represents our 'fancy if'.
    """
    def __init__(self, expr, nodelist_true, nodelist_false):
        self.expr = expr
        self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false

    def __repr__(self):
        return "<FancyIf: %s >" % repr(self.expr)

    def __iter__(self):
        for node in self.nodelist_true:
            yield node
        for node in self.nodelist_false:
            yield node

    def get_nodes_by_type(self, nodetype):
        nodes = []
        if isinstance(self, nodetype):
            nodes.append(self)
        nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
        nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
        return nodes

    def render(self, context):
        if self.expr.eval(context):
            return self.nodelist_true.render(context)
        return self.nodelist_false.render(context)

        
##############################################################################
#
def parse_fi_expressions(tokens, parser):
    """Given a list of tokens that comprised the 'fancy_if' statement
    parse them in to a list of node trees that represents the expressions to
    be evaluated.
    """
    try:
        token = tokens.pop(0)
        if token == "(":
            res = parse_fi_expressions(tokens, parser)
            if tokens.pop(0) != ")":
                raise template.TemplateSyntaxError, "Missing matching ')'"
            return res
        elif token  == "not":
            if len(tokens) < 1:
                raise TemplateSyntaxError, "'%s' must come with an expres" \
                    "sion to operate on." % token
            return FiNot(parse_fi_expressions(tokens, parser))
        elif token in ("and", "or"):
            if len(tokens) < 1:
                raise TemplateSyntaxError, "'%s' must come with a list of " \
                    "expressions to operate on." % token
            res = []
            while len(tokens) > 0:
                if tokens[0] == ")":
                    break
                res.append(parse_fi_expressions(tokens, parser))
            if token == "and":
                return FiAnd(res)
            return FiOr(res)

        elif token[0] == token[-1] and token[0] in ('"', "'"):
            return FiPerm(token[1:-1],
                          parser.compile_filter(tokens.pop(0)))
        elif token == "eq":
            return FiEquals(parser.compile_filter(tokens.pop(0)),
                            parser.compile_filter(tokens.pop(0)))

        # Otherwise it is just a variable reference.
        #
        return FiVar(parser.compile_filter(token))
    except IndexError:
        raise template.TemplateSyntaxError, "Mis-matched terms and " \
              "operators. Ran out of tokens whilst parsing arguments."
    
##############################################################################
#
# Here we have a simple set of classes to define our parsed expression
# syntax in a format that is easy to eval when we have a context we need
# to render. We define a sub-class for each type of operation. They must
# have an eval() method which returns True or False, and a __repr__ method
# that gives us a simple string representation.
#
class FiExpr(object):
    """The root object for the expression tree in our fancy-if node.
    """
    def __repr__(self):
        raise NotImplementedError
        
    def eval(self, context):
        raise NotImplementedError

class FiNot(FiExpr):
    def __init__(self, expr):
        self.expr = expr

    def __repr__(self):
        return "(not: %s )" % repr(self.expr)

    def eval(self, context):
        return not self.expr.eval(context)

class FiAnd(FiExpr):
    def __init__(self, expr_list):
        self.expr_list = expr_list

    def __repr__(self):
        return "(and:" + " ".join([repr(x) for x in self.expr_list]) + " )"

    def eval(self, context):
        for elt in self.expr_list:
            if not elt.eval(context):
                return False
        return True

class FiOr(FiExpr):
    def __init__(self, expr_list):
        self.expr_list = expr_list

    def __repr__(self):
        return "(or:" + " ".join([repr(x) for x in self.expr_list]) + " )"

    def eval(self, context):
        for elt in self.expr_list:
            if elt.eval(context):
                return True
        return False

class FiEquals(FiExpr):
    def __init__(self, obj_var1, obj_var2):
        self.obj_var1 = obj_var1
        self.obj_var2 = obj_var2

    def __repr__(self):
        return "( eq %s %s )" % (self.obj_var1, self.obj_var2)

    def eval(self, context):
        try:
            obj1 = self.obj_var1.resolve(context)
        except template.VariableDoesNotExist:
            obj1 = None
        try:
            obj2 = self.obj_var2.resolve(context)
        except template.VariableDoesNotExist:
            obj2 = None
        return obj1 == obj2

class FiPerm(FiExpr):
    def __init__(self, perm, obj_var):
        self.perm = perm
        self.obj_var = obj_var

    def __repr__(self):
        return "(has perm '%s' on %s )" % (self.perm,
                                           self.obj_var.var)

    def eval(self, context):
        if self.obj_var == None:
            obj = None
        else:
            try:
                obj = self.obj_var.resolve(context)
            except template.VariableDoesNotExist:
                obj = None
        try:
            user = template.resolve_variable("user", context)
        except template.VariableDoesNotExist:
            return settings.TEMPLATE_STRING_IF_INVALID

        return user.has_perm(self.perm, object=obj)

class FiVar(FiExpr):
    def __init__(self, obj_var):
        self.obj_var = obj_var

    def __repr__(self):
        return self.obj_var.var

    def eval(self, context):
        try:
            obj = self.obj_var.resolve(context)
        except template.VariableDoesNotExist:
            obj = None
        if obj:
            return True
        return False

More like this

  1. testdata tag for templates by showell 6 years, 2 months ago
  2. Row-Level, URL-based permissions for FlatPages by bradmontgomery 6 years ago
  3. FieldAccessForm (per-field user access for forms derived from models) by Killarny 6 years, 8 months ago
  4. a template tag to invoke a method on an object with a variable by Scanner 8 years, 1 month ago
  5. load m2m fields objects by dirol 5 years ago

Comments

Please login first before commenting.