├── annoying ├── __init__.py ├── templatetags │ ├── __init__.py │ ├── annoying.py │ └── smart_if.py ├── exceptions.py ├── utils.py ├── functions.py ├── middlewares.py ├── fields.py └── decorators.py ├── AUTHORS.txt ├── .hgignore ├── .hgtags ├── INSTALL.txt ├── LICENSE.txt ├── setup.py └── README.md /annoying/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /annoying/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Anderson 2 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | .*\.pyc 2 | django_annoying.egg-info 3 | dist 4 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | a33d0428600309b739baee9b6b7ff40553677b00 release-0.7.7 2 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | Installation instruction: 2 | 3 | * Copy annoying directory to your django project or put in PYTHONPATH. 4 | 5 | That's it. 6 | -------------------------------------------------------------------------------- /annoying/exceptions.py: -------------------------------------------------------------------------------- 1 | class Redirect(Exception): 2 | def __init__(self, *args, **kwargs): 3 | self.args = args 4 | self.kwargs = kwargs 5 | 6 | -------------------------------------------------------------------------------- /annoying/templatetags/annoying.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django import template 3 | 4 | from smart_if import smart_if 5 | 6 | 7 | register = template.Library() 8 | 9 | 10 | try: 11 | if int(django.get_version()[-5:]) < 11806: 12 | register.tag('if', smart_if) 13 | except ValueError: 14 | pass 15 | -------------------------------------------------------------------------------- /annoying/utils.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.utils.encoding import iri_to_uri 3 | 4 | 5 | class HttpResponseReload(HttpResponse): 6 | """ 7 | Reload page and stay on the same page from where request was made. 8 | 9 | example: 10 | 11 | def simple_view(request): 12 | if request.POST: 13 | form = CommentForm(request.POST): 14 | if form.is_valid(): 15 | form.save() 16 | return HttpResponseReload(request) 17 | else: 18 | form = CommentForm() 19 | return render_to_response('some_template.html', {'form': form}) 20 | """ 21 | status_code = 302 22 | 23 | def __init__(self, request): 24 | HttpResponse.__init__(self) 25 | referer = request.META.get('HTTP_REFERER') 26 | self['Location'] = iri_to_uri(referer or "/") 27 | -------------------------------------------------------------------------------- /annoying/functions.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import _get_queryset 2 | from django.conf import settings 3 | 4 | 5 | def get_object_or_None(klass, *args, **kwargs): 6 | """ 7 | Uses get() to return an object or None if the object does not exist. 8 | 9 | klass may be a Model, Manager, or QuerySet object. All other passed 10 | arguments and keyword arguments are used in the get() query. 11 | 12 | Note: Like with get(), a MultipleObjectsReturned will be raised if more than one 13 | object is found. 14 | """ 15 | queryset = _get_queryset(klass) 16 | try: 17 | return queryset.get(*args, **kwargs) 18 | except queryset.model.DoesNotExist: 19 | return None 20 | 21 | 22 | 23 | def get_config(key, default=None): 24 | """ 25 | Get settings from django.conf if exists, 26 | return default value otherwise 27 | 28 | example: 29 | 30 | ADMIN_EMAIL = get_config('ADMIN_EMAIL', 'default@email.com') 31 | """ 32 | return getattr(settings, key, default) 33 | -------------------------------------------------------------------------------- /annoying/middlewares.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.conf import settings 4 | from django.views.static import serve 5 | from django.shortcuts import redirect 6 | 7 | from .exceptions import Redirect 8 | 9 | 10 | class StaticServe(object): 11 | """ 12 | Django middleware for serving static files instead of using urls.py 13 | """ 14 | regex = re.compile(r'^%s(?P.*)$' % settings.MEDIA_URL) 15 | 16 | def process_request(self, request): 17 | if settings.DEBUG: 18 | match = self.regex.search(request.path) 19 | if match: 20 | return serve(request, match.group(1), settings.MEDIA_ROOT) 21 | 22 | 23 | class RedirectMiddleware(object): 24 | """ 25 | You must add this middleware to MIDDLEWARE_CLASSES list, 26 | to make work Redirect exception. All arguments passed to 27 | Redirect will be passed to django built in redirect function. 28 | """ 29 | def process_exception(self, request, exception): 30 | if not isinstance(exception, Redirect): 31 | return 32 | return redirect(*exception.args, **exception.kwargs) 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | setup( 3 | name="django-annoying", 4 | version="0.7.7", 5 | packages=find_packages(), 6 | author="Anderson", 7 | author_email="stavros@korokithakis.net", 8 | description="This is a django application that tries to eliminate annoying things in the Django framework.", 9 | long_description=""" 10 | **Features:** 11 | 12 | - render_to decorator - Reduce typing in django views. 13 | - signals decorator - Allow using signals as decorators. 14 | - ajax_request decorator - Returns JsonResponse with dict as content. 15 | - autostrip decorator - Strip form text fields before validation. 16 | - get_object_or_None function - Similar to get_object_or_404, but returns None if the object is not found. 17 | - get_config function - Get settings from django.conf if exists, return a default value otherwise. 18 | - AutoOneToOne field - Creates related object on first call if it doesn't exist yet. 19 | - HttpResponseReload - Reload and stay on same page from where request was made. 20 | - StaticServer middleware - Instead of configuring urls.py, just add this middleware and it will serve you static files. 21 | - JSONField - A field that stores a Python object as JSON and retrieves it as a Python object. 22 | 23 | 24 | **Installation instructions:** 25 | 26 | - Copy the "annoying" directory to your Django project or put it in your PYTHONPATH. 27 | - You can also run "sudo python setup.py install" or "sudo easy_install django-annoying". 28 | 29 | 30 | **Download:** 31 | 32 | - git clone git://github.com/skorokithakis/django-annoying.git 33 | - hg clone http://bitbucket.org/Stavros/django-annoying/ 34 | 35 | """, 36 | license="BSD", 37 | keywords="django", 38 | url="https://github.com/skorokithakis/django-annoying", 39 | ) 40 | -------------------------------------------------------------------------------- /annoying/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import OneToOneField 3 | from django.core.serializers.json import DjangoJSONEncoder 4 | from django.db.models.fields.related import SingleRelatedObjectDescriptor 5 | 6 | # Try to be compatible with Django 1.5+. 7 | try: 8 | import json 9 | except ImportError: 10 | from django.utils import simplejson as json 11 | 12 | 13 | class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor): 14 | def __get__(self, instance, instance_type=None): 15 | try: 16 | return super(AutoSingleRelatedObjectDescriptor, self).__get__(instance, instance_type) 17 | except self.related.model.DoesNotExist: 18 | obj = self.related.model(**{self.related.field.name: instance}) 19 | obj.save() 20 | # Don't return obj directly, otherwise it won't be added 21 | # to Django's cache, and the first 2 calls to obj.relobj 22 | # will return 2 different in-memory objects 23 | return super(AutoSingleRelatedObjectDescriptor, self).__get__(instance, instance_type) 24 | 25 | 26 | class AutoOneToOneField(OneToOneField): 27 | ''' 28 | OneToOneField creates related object on first call if it doesnt exist yet. 29 | Use it instead of original OneToOne field. 30 | 31 | example: 32 | 33 | class MyProfile(models.Model): 34 | user = AutoOneToOneField(User, primary_key=True) 35 | home_page = models.URLField(max_length=255, blank=True) 36 | icq = models.IntegerField(max_length=255, null=True) 37 | ''' 38 | def contribute_to_related_class(self, cls, related): 39 | setattr(cls, related.get_accessor_name(), AutoSingleRelatedObjectDescriptor(related)) 40 | 41 | 42 | class JSONField(models.TextField): 43 | """ 44 | JSONField is a generic textfield that neatly serializes/unserializes 45 | JSON objects seamlessly. 46 | Django snippet #1478 47 | 48 | example: 49 | class Page(models.Model): 50 | data = JSONField(blank=True, null=True) 51 | 52 | 53 | page = Page.objects.get(pk=5) 54 | page.data = {'title': 'test', 'type': 3} 55 | page.save() 56 | """ 57 | 58 | __metaclass__ = models.SubfieldBase 59 | 60 | def to_python(self, value): 61 | if value == "": 62 | return None 63 | 64 | try: 65 | if isinstance(value, basestring): 66 | return json.loads(value) 67 | except ValueError: 68 | pass 69 | return value 70 | 71 | def get_db_prep_save(self, value, *args, **kwargs): 72 | if value == "": 73 | return None 74 | if isinstance(value, dict) or isinstance(value, list): 75 | value = json.dumps(value, cls=DjangoJSONEncoder) 76 | return super(JSONField, self).get_db_prep_save(value, *args, **kwargs) 77 | 78 | def value_from_object(self, obj): 79 | value = super(JSONField, self).value_from_object(obj) 80 | if self.null and value is None: 81 | return None 82 | return json.dumps(value) 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Description 2 | ----------- 3 | 4 | This django application eliminates certain annoyances in the Django 5 | framework. 6 | 7 | ### Features 8 | 9 | - render\_to decorator - Reduce typing in django views. 10 | - signals decorator - Allow using signals as decorators. 11 | - ajax\_request decorator - Returns JsonResponse with dict as content. 12 | - autostrip decorator - Strip form text fields before validation 13 | - get\_object\_or\_None function - Similar to get\_object\_or\_404, but returns None if the object is not found. 14 | - get\_config function - Get settings from django.conf if exists, return a default value otherwise. 15 | - AutoOneToOne field - Creates a related object on first call if it doesn't exist yet. 16 | - JSONField - A field that stores a Python object as JSON and retrieves it as a Python object. 17 | - HttpResponseReload - Reload and stay on same page from where the request 18 | was made. 19 | - StaticServer middleware - Instead of configuring urls.py, just add 20 | this middleware and it will serve your static files when you are in 21 | debug mode. 22 | 23 | ### Installation instructions 24 | 25 | - Copy the `annoying` directory to your django project or put in on your PYTHONPATH. 26 | - You can also run `sudo python setup.py install`, `sudo easy\_install django-annoying`, 27 | or `sudo pip install django-annoying`. 28 | - Add `"annoying"` under INSTALLED\_APPS in your `settings.py` file. 29 | 30 | Examples 31 | -------- 32 | 33 | ### render\_to decorator 34 | 35 | from annoying.decorators import render_to 36 | 37 | # 1. Template name in decorator parameters 38 | 39 | @render_to('template.html') 40 | def foo(request): 41 | bar = Bar.object.all() 42 | return {'bar': bar} 43 | 44 | # equals to 45 | def foo(request): 46 | bar = Bar.object.all() 47 | return render_to_response('template.html', 48 | {'bar': bar}, 49 | context_instance=RequestContext(request)) 50 | 51 | 52 | # 2. Template name as TEMPLATE item value in return dictionary 53 | 54 | @render_to() 55 | def foo(request, category): 56 | template_name = '%s.html' % category 57 | return {'bar': bar, 'TEMPLATE': template_name} 58 | 59 | #equals to 60 | def foo(request, category): 61 | template_name = '%s.html' % category 62 | return render_to_response(template_name, 63 | {'bar': bar}, 64 | context_instance=RequestContext(request)) 65 | 66 | ### signals decorator 67 | 68 | Note: Django now [includes this by default](https://docs.djangoproject.com/en/1.5/topics/signals/#connecting-receiver-functions). 69 | 70 | from annoying.decorators import signals 71 | 72 | # connect to registered signal 73 | @signals.post_save(sender=YourModel) 74 | def sighandler(instance, **kwargs): 75 | pass 76 | 77 | # connect to any signal 78 | signals.register_signal(siginstance, signame) # and then as in example above 79 | 80 | #or 81 | 82 | @signals(siginstance, sender=YourModel) 83 | def sighandler(instance, **kwargs): 84 | pass 85 | 86 | #In any case defined function will remain as is, without any changes. 87 | 88 | ### ajax\_request decorator 89 | 90 | from annoying.decorators import ajax_request 91 | 92 | @ajax_request 93 | def my_view(request): 94 | news = News.objects.all() 95 | news_titles = [entry.title for entry in news] 96 | return {'news_titles': news_titles} 97 | 98 | ### autostrip decorator 99 | 100 | from annoying.decorators import autostrip 101 | 102 | class PersonForm(forms.Form): 103 | name = forms.CharField(min_length=2, max_length=10) 104 | email = forms.EmailField() 105 | 106 | PersonForm = autostrip(PersonForm) 107 | 108 | #or in python >= 2.6 109 | 110 | @autostrip 111 | class PersonForm(forms.Form): 112 | name = forms.CharField(min_length=2, max_length=10) 113 | email = forms.EmailField() 114 | 115 | ### get\_object\_or\_None function 116 | 117 | from annoying.functions import get_object_or_None 118 | 119 | def get_user(request, user_id): 120 | user = get_object_or_None(User, id=user_id) 121 | if not user: 122 | ... 123 | 124 | ### AutoOneToOneField 125 | 126 | from annoying.fields import AutoOneToOneField 127 | 128 | 129 | class MyProfile(models.Model): 130 | user = AutoOneToOneField(User, primary_key=True) 131 | home_page = models.URLField(max_length=255, blank=True) 132 | icq = models.IntegerField(blank=True, null=True) 133 | 134 | ### JSONField 135 | 136 | from annoying.fields import JSONField 137 | 138 | 139 | #model 140 | class Page(models.Model): 141 | data = JSONField(blank=True, null=True) 142 | 143 | 144 | 145 | # view or another place.. 146 | page = Page.objects.get(pk=5) 147 | page.data = {'title': 'test', 'type': 3} 148 | page.save() 149 | 150 | ### get\_config function 151 | 152 | from annoying.functions import get_config 153 | 154 | ADMIN_EMAIL = get_config('ADMIN_EMAIL', 'default@email.com') 155 | 156 | ### StaticServer middleware 157 | 158 | Add this middleware as first item in MIDDLEWARE\_CLASSES 159 | 160 | example: 161 | 162 | MIDDLEWARE_CLASSES = ( 163 | 'annoying.middlewares.StaticServe', 164 | 'django.middleware.common.CommonMiddleware', 165 | 'django.contrib.sessions.middleware.SessionMiddleware', 166 | 'django.middleware.doc.XViewMiddleware', 167 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 168 | ) 169 | 170 | It will serve static files in debug mode. Also it helps when you debug 171 | one of your middleware by responding to static requests before they get 172 | to debugged middleware and will save you from constantly typing "continue" 173 | in debugger. 174 | 175 | Used on [python](http://pyplanet.org) community portal. 176 | -------------------------------------------------------------------------------- /annoying/decorators.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render_to_response 2 | from django import forms 3 | from django.template import RequestContext 4 | from django.db.models import signals as signalmodule 5 | from django.http import HttpResponse 6 | from django.utils import simplejson 7 | 8 | import datetime 9 | 10 | __all__ = ['render_to', 'signals', 'ajax_request', 'autostrip'] 11 | 12 | 13 | try: 14 | from functools import wraps 15 | except ImportError: 16 | def wraps(wrapped, assigned=('__module__', '__name__', '__doc__'), 17 | updated=('__dict__',)): 18 | def inner(wrapper): 19 | for attr in assigned: 20 | setattr(wrapper, attr, getattr(wrapped, attr)) 21 | for attr in updated: 22 | getattr(wrapper, attr).update(getattr(wrapped, attr, {})) 23 | return wrapper 24 | return inner 25 | 26 | 27 | def render_to(template=None, mimetype=None): 28 | """ 29 | Decorator for Django views that sends returned dict to render_to_response 30 | function. 31 | 32 | Template name can be decorator parameter or TEMPLATE item in returned 33 | dictionary. RequestContext always added as context instance. 34 | If view doesn't return dict then decorator simply returns output. 35 | 36 | Parameters: 37 | - template: template name to use 38 | - mimetype: content type to send in response headers 39 | 40 | Examples: 41 | # 1. Template name in decorator parameters 42 | 43 | @render_to('template.html') 44 | def foo(request): 45 | bar = Bar.object.all() 46 | return {'bar': bar} 47 | 48 | # equals to 49 | def foo(request): 50 | bar = Bar.object.all() 51 | return render_to_response('template.html', 52 | {'bar': bar}, 53 | context_instance=RequestContext(request)) 54 | 55 | 56 | # 2. Template name as TEMPLATE item value in return dictionary. 57 | if TEMPLATE is given then its value will have higher priority 58 | than render_to argument. 59 | 60 | @render_to() 61 | def foo(request, category): 62 | template_name = '%s.html' % category 63 | return {'bar': bar, 'TEMPLATE': template_name} 64 | 65 | #equals to 66 | def foo(request, category): 67 | template_name = '%s.html' % category 68 | return render_to_response(template_name, 69 | {'bar': bar}, 70 | context_instance=RequestContext(request)) 71 | 72 | """ 73 | def renderer(function): 74 | @wraps(function) 75 | def wrapper(request, *args, **kwargs): 76 | output = function(request, *args, **kwargs) 77 | if not isinstance(output, dict): 78 | return output 79 | tmpl = output.pop('TEMPLATE', template) 80 | return render_to_response(tmpl, output, \ 81 | context_instance=RequestContext(request), mimetype=mimetype) 82 | return wrapper 83 | return renderer 84 | 85 | 86 | 87 | class Signals(object): 88 | ''' 89 | Convenient wrapper for working with Django's signals (or any other 90 | implementation using same API). 91 | 92 | Example of usage:: 93 | 94 | 95 | # connect to registered signal 96 | @signals.post_save(sender=YourModel) 97 | def sighandler(instance, **kwargs): 98 | pass 99 | 100 | # connect to any signal 101 | signals.register_signal(siginstance, signame) # and then as in example above 102 | 103 | or 104 | 105 | @signals(siginstance, sender=YourModel) 106 | def sighandler(instance, **kwargs): 107 | pass 108 | 109 | In any case defined function will remain as is, without any changes. 110 | 111 | (c) 2008 Alexander Solovyov, new BSD License 112 | ''' 113 | def __init__(self): 114 | self._signals = {} 115 | 116 | # register all Django's default signals 117 | for k, v in signalmodule.__dict__.iteritems(): 118 | # that's hardcode, but IMHO it's better than isinstance 119 | if not k.startswith('__') and k != 'Signal': 120 | self.register_signal(v, k) 121 | 122 | def __getattr__(self, name): 123 | return self._connect(self._signals[name]) 124 | 125 | def __call__(self, signal, **kwargs): 126 | def inner(func): 127 | signal.connect(func, **kwargs) 128 | return func 129 | return inner 130 | 131 | def _connect(self, signal): 132 | def wrapper(**kwargs): 133 | return self(signal, **kwargs) 134 | return wrapper 135 | 136 | def register_signal(self, signal, name): 137 | self._signals[name] = signal 138 | 139 | signals = Signals() 140 | 141 | 142 | date_time_handler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None 143 | 144 | FORMAT_TYPES = { 145 | 'application/json': lambda response: simplejson.dumps(response, default=date_time_handler), 146 | 'text/json': lambda response: simplejson.dumps(response, default=date_time_handler), 147 | } 148 | 149 | try: 150 | import yaml 151 | FORMAT_TYPES.update({ 152 | 'application/yaml': yaml.dump, 153 | 'text/yaml': yaml.dump, 154 | }) 155 | except ImportError: 156 | pass 157 | 158 | def ajax_request(func): 159 | """ 160 | If view returned serializable dict, returns response in a format requested 161 | by HTTP_ACCEPT header. Defaults to JSON if none requested or match. 162 | 163 | Currently supports JSON or YAML (if installed), but can easily be extended. 164 | 165 | example: 166 | 167 | @ajax_request 168 | def my_view(request): 169 | news = News.objects.all() 170 | news_titles = [entry.title for entry in news] 171 | return {'news_titles': news_titles} 172 | """ 173 | @wraps(func) 174 | def wrapper(request, *args, **kwargs): 175 | for accepted_type in request.META.get('HTTP_ACCEPT', '').split(','): 176 | if accepted_type in FORMAT_TYPES.keys(): 177 | format_type = accepted_type 178 | break 179 | else: 180 | format_type = 'application/json' 181 | response = func(request, *args, **kwargs) 182 | if isinstance(response, dict) or isinstance(response, list): 183 | data = FORMAT_TYPES[format_type](response) 184 | response = HttpResponse(data, content_type=format_type) 185 | response['content-length'] = len(data) 186 | return response 187 | return wrapper 188 | 189 | def autostrip(cls): 190 | """ 191 | strip text fields before validation 192 | 193 | example: 194 | class PersonForm(forms.Form): 195 | name = forms.CharField(min_length=2, max_length=10) 196 | email = forms.EmailField() 197 | 198 | PersonForm = autostrip(PersonForm) 199 | 200 | #or you can use @autostrip in python >= 2.6 201 | 202 | Author: nail.xx 203 | """ 204 | fields = [(key, value) for key, value in cls.base_fields.iteritems() if isinstance(value, forms.CharField)] 205 | for field_name, field_object in fields: 206 | def get_clean_func(original_clean): 207 | return lambda value: original_clean(value and value.strip()) 208 | clean_func = get_clean_func(getattr(field_object, 'clean')) 209 | setattr(field_object, 'clean', clean_func) 210 | return cls 211 | 212 | -------------------------------------------------------------------------------- /annoying/templatetags/smart_if.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | __author__ = "SmileyChris" 4 | 5 | #============================================================================== 6 | # Calculation objects 7 | #============================================================================== 8 | 9 | class BaseCalc(object): 10 | def __init__(self, var1, var2=None, negate=False): 11 | self.var1 = var1 12 | self.var2 = var2 13 | self.negate = negate 14 | 15 | def resolve(self, context): 16 | try: 17 | var1, var2 = self.resolve_vars(context) 18 | outcome = self.calculate(var1, var2) 19 | except: 20 | outcome = False 21 | if self.negate: 22 | return not outcome 23 | return outcome 24 | 25 | def resolve_vars(self, context): 26 | var2 = self.var2 and self.var2.resolve(context) 27 | return self.var1.resolve(context), var2 28 | 29 | def calculate(self, var1, var2): 30 | raise NotImplementedError() 31 | 32 | 33 | class Or(BaseCalc): 34 | def calculate(self, var1, var2): 35 | return var1 or var2 36 | 37 | 38 | class And(BaseCalc): 39 | def calculate(self, var1, var2): 40 | return var1 and var2 41 | 42 | 43 | class Equals(BaseCalc): 44 | def calculate(self, var1, var2): 45 | return var1 == var2 46 | 47 | 48 | class Greater(BaseCalc): 49 | def calculate(self, var1, var2): 50 | return var1 > var2 51 | 52 | 53 | class GreaterOrEqual(BaseCalc): 54 | def calculate(self, var1, var2): 55 | return var1 >= var2 56 | 57 | 58 | class In(BaseCalc): 59 | def calculate(self, var1, var2): 60 | return var1 in var2 61 | 62 | 63 | OPERATORS = { 64 | '=': (Equals, True), 65 | '==': (Equals, True), 66 | '!=': (Equals, False), 67 | '>': (Greater, True), 68 | '>=': (GreaterOrEqual, True), 69 | '<=': (Greater, False), 70 | '<': (GreaterOrEqual, False), 71 | 'or': (Or, True), 72 | 'and': (And, True), 73 | 'in': (In, True), 74 | } 75 | BOOL_OPERATORS = ('or', 'and') 76 | 77 | 78 | class IfParser(object): 79 | error_class = ValueError 80 | 81 | def __init__(self, tokens): 82 | self.tokens = tokens 83 | 84 | def _get_tokens(self): 85 | return self._tokens 86 | 87 | def _set_tokens(self, tokens): 88 | self._tokens = tokens 89 | self.len = len(tokens) 90 | self.pos = 0 91 | 92 | tokens = property(_get_tokens, _set_tokens) 93 | 94 | def parse(self): 95 | if self.at_end(): 96 | raise self.error_class('No variables provided.') 97 | var1 = self.get_bool_var() 98 | while not self.at_end(): 99 | op, negate = self.get_operator() 100 | var2 = self.get_bool_var() 101 | var1 = op(var1, var2, negate=negate) 102 | return var1 103 | 104 | def get_token(self, eof_message=None, lookahead=False): 105 | negate = True 106 | token = None 107 | pos = self.pos 108 | while token is None or token == 'not': 109 | if pos >= self.len: 110 | if eof_message is None: 111 | raise self.error_class() 112 | raise self.error_class(eof_message) 113 | token = self.tokens[pos] 114 | negate = not negate 115 | pos += 1 116 | if not lookahead: 117 | self.pos = pos 118 | return token, negate 119 | 120 | def at_end(self): 121 | return self.pos >= self.len 122 | 123 | def create_var(self, value): 124 | return TestVar(value) 125 | 126 | def get_bool_var(self): 127 | """ 128 | Returns either a variable by itself or a non-boolean operation (such as 129 | ``x == 0`` or ``x < 0``). 130 | 131 | This is needed to keep correct precedence for boolean operations (i.e. 132 | ``x or x == 0`` should be ``x or (x == 0)``, not ``(x or x) == 0``). 133 | """ 134 | var = self.get_var() 135 | if not self.at_end(): 136 | op_token = self.get_token(lookahead=True)[0] 137 | if isinstance(op_token, basestring) and (op_token not in 138 | BOOL_OPERATORS): 139 | op, negate = self.get_operator() 140 | return op(var, self.get_var(), negate=negate) 141 | return var 142 | 143 | def get_var(self): 144 | token, negate = self.get_token('Reached end of statement, still ' 145 | 'expecting a variable.') 146 | if isinstance(token, basestring) and token in OPERATORS: 147 | raise self.error_class('Expected variable, got operator (%s).' % 148 | token) 149 | var = self.create_var(token) 150 | if negate: 151 | return Or(var, negate=True) 152 | return var 153 | 154 | def get_operator(self): 155 | token, negate = self.get_token('Reached end of statement, still ' 156 | 'expecting an operator.') 157 | if not isinstance(token, basestring) or token not in OPERATORS: 158 | raise self.error_class('%s is not a valid operator.' % token) 159 | if self.at_end(): 160 | raise self.error_class('No variable provided after "%s".' % token) 161 | op, true = OPERATORS[token] 162 | if not true: 163 | negate = not negate 164 | return op, negate 165 | 166 | 167 | #============================================================================== 168 | # Actual templatetag code. 169 | #============================================================================== 170 | 171 | class TemplateIfParser(IfParser): 172 | error_class = template.TemplateSyntaxError 173 | 174 | def __init__(self, parser, *args, **kwargs): 175 | self.template_parser = parser 176 | return super(TemplateIfParser, self).__init__(*args, **kwargs) 177 | 178 | def create_var(self, value): 179 | return self.template_parser.compile_filter(value) 180 | 181 | 182 | class SmartIfNode(template.Node): 183 | def __init__(self, var, nodelist_true, nodelist_false=None): 184 | self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false 185 | self.var = var 186 | 187 | def render(self, context): 188 | if self.var.resolve(context): 189 | return self.nodelist_true.render(context) 190 | if self.nodelist_false: 191 | return self.nodelist_false.render(context) 192 | return '' 193 | 194 | def __repr__(self): 195 | return "" 196 | 197 | def __iter__(self): 198 | for node in self.nodelist_true: 199 | yield node 200 | if self.nodelist_false: 201 | for node in self.nodelist_false: 202 | yield node 203 | 204 | def get_nodes_by_type(self, nodetype): 205 | nodes = [] 206 | if isinstance(self, nodetype): 207 | nodes.append(self) 208 | nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype)) 209 | if self.nodelist_false: 210 | nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) 211 | return nodes 212 | 213 | 214 | def smart_if(parser, token): 215 | """ 216 | A smarter {% if %} tag for django templates. 217 | 218 | While retaining current Django functionality, it also handles equality, 219 | greater than and less than operators. Some common case examples:: 220 | 221 | {% if articles|length >= 5 %}...{% endif %} 222 | {% if "ifnotequal tag" != "beautiful" %}...{% endif %} 223 | 224 | Arguments and operators _must_ have a space between them, so 225 | ``{% if 1>2 %}`` is not a valid smart if tag. 226 | 227 | All supported operators are: ``or``, ``and``, ``in``, ``=`` (or ``==``), 228 | ``!=``, ``>``, ``>=``, ``<`` and ``<=``. 229 | """ 230 | bits = token.split_contents()[1:] 231 | var = TemplateIfParser(parser, bits).parse() 232 | nodelist_true = parser.parse(('else', 'endif')) 233 | token = parser.next_token() 234 | if token.contents == 'else': 235 | nodelist_false = parser.parse(('endif',)) 236 | parser.delete_first_token() 237 | else: 238 | nodelist_false = None 239 | return SmartIfNode(var, nodelist_true, nodelist_false) 240 | 241 | --------------------------------------------------------------------------------