├── .gitignore ├── MANIFEST.in ├── README.rst ├── django_widgets ├── __init__.py ├── base.py ├── loading.py ├── settings.py └── templatetags │ ├── __init__.py │ └── widgets.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | *.egg-info 4 | build 5 | dist 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-widgets 2 | ============== 3 | 4 | An alternative for templatetags. Based on http://code.google.com/p/django-widgets/ 5 | 6 | Background 7 | ---------- 8 | 9 | Django template tags are great, but writing renderes and compiler functions is 10 | often boring (ie. parsing options). 11 | 12 | Common usage of widgets is similar to ``{% with %}..{% include %}`` pair, but 13 | provides easy-to-use interface for more complex view-logic code. 14 | 15 | Major features are: 16 | 17 | - class-based style 18 | - widgets registry with autodiscovering. 19 | 20 | 21 | Django-widgets provides two useful templatetags with option parsers: 22 | 23 | - **include_widget** 24 | 25 | Simply includes widget instance found in registry by name, calls widget`s 26 | ``render()`` method with provided optional value and configuration. 27 | 28 | Syntax: 29 | 30 | ``{% include_widget widget_name [value] [option1=value1, [option2=value2, ...]] %}`` 31 | 32 | - ``value``: optional value passed as ``value`` in ``get_context()`` and ``render()`` methods 33 | - ``opt1=val1``: dictionary key-value pairs passed as ``options`` in ``get_context()`` and ``render()`` 34 | 35 | 36 | 37 | - **widget** 38 | 39 | Same as include_widget tag, but template source is taken from tag content 40 | instead of widget`s default template. 41 | 42 | Everything within ``{% widget %}`` and ``{% endwidget %}`` is used as template source. 43 | Also this template tag does **NOT** call widget`s ``render()`` method, but 44 | ``get_context()`` instead. 45 | 46 | 47 | Hints: 48 | 49 | - Context of your view will be unchanged. 50 | - Context of widget contains all view variables, similar to ``{% with %}`` tag. 51 | **No more hacks** like ``{{ settings.MEDIA_URL }}`` nor ``{% get_media_url %}`` :) 52 | 53 | 54 | Defining a Widget 55 | ----------------- 56 | 57 | Widgets should extend from ``django_widgets.base.Widget`` class. 58 | Every widget, placed in **widgets.py** module of your app, 59 | is automatically registered by name (from ``class.__name__``) 60 | 61 | Simplest "hello world" widget example (place it in ``yourapp/widgets.py`` module): 62 | 63 | :: 64 | 65 | from django_widgets import Widget 66 | 67 | class HelloWorld(Widget): 68 | def render(self, context, value=None, options=None): 69 | return u'Hello world!' 70 | 71 | 72 | Calling from template: 73 | 74 | :: 75 | 76 | {% load widgets %} 77 | 78 |

{% include_widget HelloWorld %}

79 | 80 | 81 | Base Widget class has render() method which uses class-property 82 | **template** for rendering. But if not set ``render()`` raises 83 | NotImplementedError. 84 | 85 | 86 | Example of widget with custom template: 87 | 88 | :: 89 | 90 | from django_widgets import Widget 91 | from catalog import Category 92 | 93 | class CategoryTree(Widget): 94 | template = 'catalog/category_tree_widget.html' 95 | 96 | def get_context(self, root_category, options, context=None): 97 | return { 98 | 'tree': Category.objects.get_tree_for(root_category), 99 | 'root_category': root_category, 100 | 'max_level': options.get('max_level', 3), 101 | } 102 | 103 | 104 | 105 | In template: 106 | 107 | :: 108 | 109 | {% load widgets %} 110 | 111 | {% include_widget CategoryTree max_level=1 %} 112 | 113 | 114 | The ``catalog/category_tree_widget.html`` template will be used for 115 | rendering tree of categories. 116 | 117 | 118 | Or use inline tempalte with ``{% widget %}`` tag: 119 | 120 | 121 | :: 122 | 123 | {% load widgets %} 124 | 125 | {% widget CategoryTree max_level=1 %} 126 |
127 | ... widget template ... 128 |
129 | {% endwidget %} 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /django_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from loading import autodiscover 2 | from base import Widget 3 | 4 | autodiscover() 5 | -------------------------------------------------------------------------------- /django_widgets/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | django widgets base module 3 | 4 | author: Marcin Nowak 5 | license: BSD 6 | """ 7 | 8 | from django.forms.widgets import MediaDefiningClass 9 | from django.forms.widgets import Widget as BaseWidget 10 | from django.template.loader import get_template 11 | from loading import registry 12 | 13 | def context_wrapper(wrapped_method, context): 14 | """ 15 | This decorator prepares args and kwargs for wrapped method. 16 | Backward compatibility is achieved by inspecting method args. 17 | 18 | Background: 19 | Sometimes access to template Context is needed within widget. 20 | render() method has context argument, but get_context() not. 21 | 22 | get_context() args are inspected and context is set as keyword 23 | argument, when `context` argument exist in method`s declaration. 24 | """ 25 | import inspect 26 | kwargs = {} 27 | 28 | _ctxargs = inspect.getargspec(wrapped_method)[0] # Python2.5 compatibility 29 | if 'context' in _ctxargs: 30 | kwargs['context'] = context 31 | 32 | def wrapper(widget, *args): 33 | return wrapped_method(widget, *args, **kwargs) 34 | 35 | return wrapper 36 | 37 | 38 | class WidgetMeta(MediaDefiningClass): 39 | """ 40 | initializes widget classes 41 | automatically adds widget instance into registry 42 | """ 43 | 44 | def __init__(mcs, name, bases, attrs): 45 | MediaDefiningClass.__init__(mcs, name, bases, attrs) 46 | if 'template' not in attrs: 47 | mcs.template = None 48 | mcs.template_instance = None 49 | if name is not 'Widget': 50 | registry.register(name, mcs()) 51 | 52 | 53 | class Widget(BaseWidget): 54 | """ 55 | base widget class 56 | """ 57 | __metaclass__ = WidgetMeta 58 | template = None 59 | 60 | def __init__(self, *args, **kwargs): 61 | super(BaseWidget, self).__init__(*args, **kwargs) 62 | self.template_instance = None 63 | 64 | def get_context(self, value, options): 65 | """ 66 | returns context dictionary 67 | output my be customized by options dict 68 | """ 69 | return {} 70 | 71 | def render(self, context, value=None, attrs=None): 72 | """ 73 | main render method 74 | uses "template" class property for rendering 75 | or needs to be overriden for custom non-template widgets 76 | """ 77 | if not self.template_instance: 78 | if not self.template: 79 | raise RuntimeError('Abstract method Widget.render()\ 80 | is not implemented') 81 | self.template_instance = get_template(self.template) 82 | 83 | context.push() 84 | context.update(context_wrapper(self.get_context, context)( 85 | value, attrs or {})) 86 | result = self.template_instance.render(context) 87 | context.pop() 88 | return result 89 | 90 | -------------------------------------------------------------------------------- /django_widgets/loading.py: -------------------------------------------------------------------------------- 1 | """ 2 | widgets cache module 3 | """ 4 | 5 | class WidgetsCache(object): 6 | """ 7 | Widgets registry class 8 | contains all registered widget instances 9 | """ 10 | 11 | def __init__(self): 12 | """ 13 | initialize widgets cache 14 | """ 15 | self.widgets = {} 16 | 17 | def register(self, name, widget): 18 | """ 19 | registering widget instance using custom name 20 | """ 21 | if self.widgets.has_key(name): 22 | raise KeyError('Widget "%s" is already registered' % name) 23 | self.widgets[name] = widget 24 | 25 | def unregister(self, widget): 26 | if isinstance(widget, (str, unicode)): 27 | self.widgets.pop(str(widget)) 28 | for k, v in self.widgets.items(): 29 | if isinstance(v, widget): 30 | self.widgets.pop(k) 31 | 32 | def get(self, name): 33 | """ 34 | returns widget instance by name 35 | """ 36 | return self.widgets[name] 37 | 38 | 39 | def autodiscover(): 40 | from django.conf import settings 41 | for app in settings.INSTALLED_APPS: 42 | # Just loading the module will do the trick 43 | __import__(app, {}, {}, ['widgets']) 44 | 45 | 46 | registry = WidgetsCache() 47 | -------------------------------------------------------------------------------- /django_widgets/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | -------------------------------------------------------------------------------- /django_widgets/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcinn/django-widgets/1fbacae615d506b27d63a2f062fecd413271fa76/django_widgets/templatetags/__init__.py -------------------------------------------------------------------------------- /django_widgets/templatetags/widgets.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-widgets template tags 3 | """ 4 | 5 | from django.template import Library, Node, TemplateSyntaxError 6 | from django_widgets.loading import registry 7 | from django_widgets.base import context_wrapper 8 | 9 | register = Library() 10 | 11 | 12 | def _parse_args(bits, parser): 13 | if len(bits) < 2: 14 | raise TemplateSyntaxError("'%s' takes at least one argument" 15 | " (registered widget name)" % bits[0]) 16 | value = None 17 | if len(bits)>2 and not '=' in bits[2]: 18 | # there is a value arg 19 | value = parser.compile_filter(bits[2]) 20 | return (bits[1], value) 21 | 22 | 23 | def _parse_options(bits, parser): 24 | options = {} 25 | opts_arg = [] 26 | if len(bits) > 2: 27 | if '=' in bits[2]: 28 | # there is no value arg provided 29 | bits = iter(bits[2:]) 30 | else: 31 | # skip value arg 32 | bits = iter(bits[3:]) 33 | 34 | for bit in bits: 35 | if '=' in bit: 36 | k, v = bit.split('=', 1) 37 | k = k.strip() 38 | options[k] = parser.compile_filter(v) 39 | elif bit: 40 | opts_arg.append(bit) 41 | return (options, opts_arg,) 42 | 43 | 44 | class IncludeWidgetNode(Node): 45 | def __init__(self, widget_name, value, opts_arg, options): 46 | self.widget_name = widget_name 47 | self.value = value 48 | self.options = options 49 | self.opts_arg = opts_arg 50 | 51 | def render(self, context): 52 | resolved_options = dict(zip(self.options.keys(), [self.options[v].resolve(context) for v in self.options])) 53 | if self.opts_arg: 54 | resolved_options.update(self.opts_arg.resolve(context)) 55 | return registry.get(self.widget_name).render(context, 56 | self.value.resolve(context) if self.value else None, 57 | resolved_options) 58 | 59 | 60 | class WidgetNode(Node): 61 | def __init__(self, widget_name, value, opts_arg, options, nodelist): 62 | self.widget_name = widget_name 63 | self.value = value 64 | self.options = options 65 | self.opts_arg = opts_arg 66 | self.nodelist = nodelist 67 | 68 | def render(self, context): 69 | resolved_options = dict(zip(self.options.keys(), 70 | [self.options[v].resolve(context) for v in self.options])) 71 | if self.opts_arg: 72 | # create dictionary from arguments (all values are set to True) 73 | resolved_options.update(dict.fromkeys(self.opts_arg, True)) 74 | widget = registry.get(self.widget_name) 75 | 76 | ctx = context_wrapper(widget.get_context, context)( 77 | self.value.resolve(context) if self.value else None, 78 | resolved_options) 79 | 80 | context.update(ctx) 81 | output = self.nodelist.render(context) 82 | context.pop() 83 | return output 84 | 85 | class WidgetMediaNode(Node): 86 | def __init__(self, widget_name): 87 | self.widget_name = widget_name 88 | 89 | def render(self, context): 90 | widget = registry.get(self.widget_name) 91 | if hasattr(widget, 'media'): 92 | return widget.media 93 | 94 | 95 | @register.tag(name='include_widget') 96 | def include_widget(parser, token): 97 | bits = token.split_contents() 98 | name, value = _parse_args(bits, parser) 99 | options, opts_arg = _parse_options(bits, parser) 100 | return IncludeWidgetNode(name, value, opts_arg, options) 101 | 102 | 103 | @register.tag(name='widget') 104 | def widget(parser, token): 105 | bits = token.split_contents() 106 | name, value = _parse_args(bits, parser) 107 | options, opts_arg = _parse_options(bits, parser) 108 | nodelist = parser.parse(('endwidget',)) 109 | parser.delete_first_token() 110 | return WidgetNode(name, value, opts_arg, options, nodelist) 111 | 112 | @register.tag(name='widget_media') 113 | def widget_media(parser, token): 114 | bits = token.split_contents() 115 | if len(bits) != 2: 116 | raise TemplateSyntaxError("'%s' takes only one argument" 117 | " (registered widget name)" % bits[0]) 118 | name = bits[1] 119 | return WidgetMediaNode(name) 120 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='django-widgets', 5 | version='1.0', 6 | packages=find_packages(), 7 | include_package_data=True, 8 | 9 | description='An alternative for templatetags.', 10 | maintainer='Marcin Nowak', 11 | maintainer_email='marcin.j.nowak@gmail.com', 12 | url='https://github.com/marcinn/django-widgets', 13 | classifiers=[ 14 | 'Framework :: Django', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: BSD License', 17 | 'Operating System :: OS Independent', 18 | 'Programming Language :: Python :: 2.5', 19 | 'Programming Language :: Python :: 2.6', 20 | ], 21 | ) 22 | --------------------------------------------------------------------------------