├── .gitignore
├── AUTHORS
├── CHANGES.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── django_inlines
├── __init__.py
├── admin_urls.py
├── forms.py
├── inlines.py
├── media
│ └── django_inlines
│ │ ├── inlines.css
│ │ ├── inlines.js
│ │ └── jquery-fieldselection.js
├── samples.py
├── templates
│ ├── admin
│ │ └── django_inlines
│ │ │ ├── inline_form.html
│ │ │ └── js_inline_config.js
│ └── inlines
│ │ └── youtube.html
├── templatetags
│ ├── __init__.py
│ └── inlines.py
└── views.py
├── setup.py
└── tests
├── __init__.py
├── core
├── __init__.py
├── fixtures
│ └── users.json
├── models.py
├── templates
│ ├── inlines
│ │ ├── user.contact.html
│ │ └── user.html
│ └── youtube_inlines
│ │ └── youtube.html
└── tests
│ ├── __init__.py
│ ├── base.py
│ ├── modelinline.py
│ ├── templateinline.py
│ ├── templatetags.py
│ └── test_inlines.py
├── manage.py
└── settings.py
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | django_inlines.egg-info
3 | MANIFEST
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Christian Metts
2 |
3 | Additional contributions from:
4 | Daniel Lindsley
5 | Jannis Leidel
6 | Martin Mahner
7 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changes:
2 | ========
3 |
4 | 0.7.2
5 | *****
6 |
7 | Bug fixes:
8 |
9 | * Arguments now work correctly (thanks to Martin Mahner).
10 | * Fixed a bug in the regex that would split passed values incorrectly occasionally.
11 |
12 | New:
13 |
14 | * START_TAG and END_TAG can now be controlled via settings.
15 |
16 |
17 | 0.7.1
18 | *****
19 |
20 | * Includes fixes from Jannis Leidel so it actually works on Pypi now. Thanks Jannis!
21 |
22 |
23 | 0.7
24 | ***
25 |
26 | * Initial public release
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009, Christian Metts
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of the django_inlines nor the names of its contributors may
15 | be used to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS
2 | include README.rst
3 | include CHANGES.rst
4 | include LICENSE
5 | recursive-include django_inlines/templates *.html
6 | recursive-include django_inlines/templates *.js
7 | recursive-include django_inlines/media *.js
8 | recursive-include django_inlines/media *.css
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | What's this then?
2 | =================
3 |
4 | Django Inlines is an app to let you use include other objects and special
5 | bits in your text fields.
6 |
7 | It uses a registration style so it's easy to set up inlines for any of your apps
8 | or third party applications.
9 |
10 |
11 | Example:
12 | ********
13 |
14 | Register your inlines::
15 |
16 | from django_inlines import inlines
17 | from django_inlines.samples import YoutubeInline
18 |
19 | inlines.registry.register('youtube', YoutubeInline)
20 |
21 | In your `entry.body`::
22 |
23 |
44 |
45 |
46 | Creating inlines
47 | ****************
48 |
49 | An inline can be any class that provides a `render` method and has an
50 | `__init__` that can take these arguments::
51 |
52 | __init__(self, value, variant=None, context=None, template_dir="", **kwargs)
53 |
54 | Django Inlines comes with the base inline classes you can subclass to create
55 | your own inlines.
56 |
57 |
58 | ``inlines.InlineBase``
59 | ----------------------
60 |
61 | A base class for overriding to provide simple inlines.
62 | The `render` method is the only required override. It should return a string.
63 | or at least something that can be coerced into a string.
64 |
65 |
66 | ``inlines.TemplateInline``
67 | --------------------------
68 |
69 | A base class for overriding to provide templated inlines.
70 | The `get_context` method is the only required override. It should return
71 | dictionary-like object that will be fed to the template as the context.
72 |
73 | If you instantiate your inline class with a context instance, it'll use
74 | that to set up your base context.
75 |
76 | Any extra arguments assigned to your inline are passed directly though to
77 | the context.
78 |
79 | See ``samples.YoutubeInline`` for an example of a ``TemplateInline``
80 | subclass.
81 |
82 | Template inlines render a template named the same as the name they were
83 | registered as. The youtube inline uses ``inlines/youtube.html``
84 |
85 |
86 | ``inlines.ModelInline``
87 | -----------------------
88 |
89 | A base class for creating inlines for Django models. The `model` class
90 | attribute is the only required override. It should be assigned a django
91 | model class.
92 |
93 | A sample model inline::
94 |
95 | from myapp.models import Photo
96 |
97 | class PhotoInline(inlines.ModelInline):
98 | model = Photo
99 |
100 | inlines.registry.register('photo', PhotoInline)
101 |
102 | And in use::
103 |
104 | {{ photo 1 }}
105 |
106 | ModelInlines take an object's `id` as it's only value and pass that object into
107 | the context as ``object``.
108 |
109 | Since model inlines will be used very often there is a ``inline_for_model``
110 | shortcut method for this. It can be used to register models as inlines directly::
111 |
112 | from django_inlines.inlines import inline_for_model
113 | inlines.registry.register('photo', inline_for_model(Photo))
114 |
115 |
116 | Inline syntax
117 | *************
118 |
119 | Django inlines use this syntax ``{{ name[:variant] value [argument=value ...] }}``
120 |
121 | ``name``
122 |
123 | The name the inline has been registered under. Template inlines use this as
124 | the base name for their templates.
125 |
126 | ``value``
127 |
128 | Any string. It's the requirement of the inline class to parse this string.
129 |
130 | ``variant`` `optional`
131 |
132 | Variants can be used by the inline class to alter behavior. By default any
133 | inline that renders a template uses this to check for an alternate template.
134 | ``{{ youtube:hd }}`` would first check for ``inlines/youtube.hd.html``
135 | before checking for ``inlines/youtube.html``.
136 |
137 | ``arguments`` `optional`
138 |
139 | Any number of key=value pairs are allowed at the end of an inline. These
140 | are passed directly to the template as context vars.
141 | ``{{ youtube:hd width=400 height=200 }}``
142 |
143 |
144 | The template tag
145 | ****************
146 |
147 | Searches through the provided content and applies inlines where ever they are
148 | found. The current context of your template is passed into to your inline templates.
149 |
150 | Syntax::
151 |
152 | {% process_inlines entry.body [in template_dir] [as varname] }
153 |
154 |
155 | Example::
156 |
157 | {% process_inlines entry.body %}
158 |
159 | {% process_inlines entry.body as body %}
160 |
161 | {% process_inlines entry.body in 'inlines/sidebar' %}
162 |
163 | {% process_inlines entry.body in 'inlines/sidebar' as body %}
164 |
165 | If given the optional template_dir argument inlines will first check in that
166 | directory for their template before falling back to ``inlines/.html``
167 |
168 | If given [as varname] the tag won't return anything but will instead populate
169 | varname in your context. Then you can apply filters or test against the output.
170 |
171 |
172 | Settings
173 | ********
174 |
175 | You can override some settings within your ``settings.py``:
176 |
177 | - ``INLINE_DEBUG = True``: Normally a error with your inlines would fail silently.
178 | Turning this to ``True`` would raise all exceptions your inlines might produce.
179 | Default: ``False``
180 |
181 | - ``INLINES_START_TAG = '{{'``: The start tag used in the inline syntax.
182 | Default: ``'{{'``
183 |
184 | - ``INLINES_END_TAG = '}}'``: The end tag used in the inline syntax.
185 | Default: ``'}}'``
186 |
187 |
188 | To do:
189 | ******
190 |
191 | **Warning:** Django inlines is still under development. Every thing here is
192 | well tested and functional, but stability isn't promised yet. Important bits
193 | don't exist yet. These include:
194 |
195 | * Better documentation.
196 | * Admin style auto discovery of inlines.py in your apps.
197 | * Adding validation hooks to the base classes.
198 | * A model field and a widget for validation and improved adding in the admin.
--------------------------------------------------------------------------------
/django_inlines/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mintchaos/django_inlines/1912e508d04884713a6c44a068c21fbd217d478a/django_inlines/__init__.py
--------------------------------------------------------------------------------
/django_inlines/admin_urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 |
3 |
4 | urlpatterns = patterns('django_inlines.views',
5 | url(r'^inline_config\.js$', 'js_inline_config', name='js_inline_config'),
6 | url(r'^get_inline_form/$', 'get_inline_form', name='get_inline_form'),
7 | )
8 |
--------------------------------------------------------------------------------
/django_inlines/forms.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.admin.widgets import AdminTextareaWidget
3 |
4 |
5 | class DelayedUrlReverse(object):
6 | def __init__(self, reverse_arg):
7 | self.reverse_arg = reverse_arg
8 |
9 | def __str__(self):
10 | from django.core.urlresolvers import reverse, NoReverseMatch
11 | try:
12 | url = reverse(self.reverse_arg)
13 | except NoReverseMatch:
14 | url = ''
15 | return url
16 |
17 | def startswith(self, value):
18 | return str(self).startswith(value)
19 |
20 |
21 | class InlineWidget(AdminTextareaWidget):
22 | def __init__(self, attrs=None):
23 | final_attrs = {'class': 'vLargeTextField vInlineTextArea'}
24 | if attrs is not None:
25 | final_attrs.update(attrs)
26 | super(InlineWidget, self).__init__(attrs=final_attrs)
27 |
28 | class Media:
29 | css = { 'all': [ 'django_inlines/inlines.css' ] }
30 |
31 | js = [
32 | 'admin/jquery.js',
33 | DelayedUrlReverse('js_inline_config'),
34 | 'js/admin/RelatedObjectLookups.js',
35 | 'django_inlines/jquery-fieldselection.js',
36 | 'django_inlines/inlines.js'
37 | ]
38 |
39 |
40 | class InlineField(models.TextField):
41 | def formfield(self, **kwargs):
42 | defaults = {}
43 | defaults.update(kwargs)
44 | defaults = {'widget': InlineWidget}
45 | return super(InlineField, self).formfield(**defaults)
--------------------------------------------------------------------------------
/django_inlines/inlines.py:
--------------------------------------------------------------------------------
1 | import re
2 | from django.template.loader import render_to_string
3 | from django.template import Context, RequestContext
4 | from django.db.models.base import ModelBase
5 | from django.conf import settings
6 |
7 | INLINE_SPLITTER = re.compile(r"""
8 | (?P[a-z_]+) # Must start with a lowercase + underscores name
9 | (?::(?P\w+))? # Variant is optional, ":variant"
10 | (?:(?P[^\Z]+))? # args is everything up to the end
11 | """, re.VERBOSE)
12 |
13 | INLINE_KWARG_PARSER = re.compile(r"""
14 | (?P(?:\s\b[a-z_]+=\w+\s?)+)?\Z # kwargs match everything at the end in groups " name=arg"
15 | """, re.VERBOSE)
16 |
17 |
18 | class InlineUnrenderableError(Exception):
19 | """
20 | Any errors that are children of this error will be silenced by inlines.process
21 | unless settings.INLINE_DEBUG is true.
22 | """
23 | pass
24 |
25 | class InlineInputError(InlineUnrenderableError):
26 | pass
27 |
28 | class InlineValueError(InlineUnrenderableError):
29 | pass
30 |
31 | class InlineAttributeError(InlineUnrenderableError):
32 | pass
33 |
34 | class InlineNotRegisteredError(InlineUnrenderableError):
35 | pass
36 |
37 | class InlineUnparsableError(InlineUnrenderableError):
38 | pass
39 |
40 |
41 | def parse_inline(text):
42 | """
43 | Takes a string of text from a text inline and returns a 3 tuple of
44 | (name, value, **kwargs).
45 | """
46 |
47 | m = INLINE_SPLITTER.match(text)
48 | if not m:
49 | raise InlineUnparsableError
50 | args = m.group('args')
51 | name = m.group('name')
52 | value = ""
53 | kwtxt = ""
54 | kwargs = {}
55 | if args:
56 | kwtxt = INLINE_KWARG_PARSER.search(args).group('kwargs')
57 | value = re.sub("%s\Z" % kwtxt, "", args)
58 | value = value.strip()
59 | if m.group('variant'):
60 | kwargs['variant'] = m.group('variant')
61 | if kwtxt:
62 | for kws in kwtxt.split():
63 | k, v = kws.split('=')
64 | kwargs[str(k)] = v
65 | return (name, value, kwargs)
66 |
67 |
68 | def inline_for_model(model, variants=[], inline_args={}):
69 | """
70 | A shortcut function to produce ModelInlines for django models
71 | """
72 |
73 | if not isinstance(model, ModelBase):
74 | raise ValueError("inline_for_model requires it's argument to be a Django Model")
75 | d = dict(model=model)
76 | if variants:
77 | d['variants'] = variants
78 | if inline_args:
79 | d['args'] = inline_args
80 | class_name = "%sInline" % model._meta.module_name.capitalize()
81 | return type(class_name, (ModelInline,), d)
82 |
83 |
84 | class InlineBase(object):
85 | """
86 | A base class for overriding to provide simple inlines.
87 | The `render` method is the only required override. It should return a string.
88 | or at least something that can be coerced into a string.
89 | """
90 |
91 | def __init__(self, value, variant=None, context=None, template_dir="", **kwargs):
92 | self.value = value
93 | self.variant = variant
94 | self.kwargs = kwargs
95 |
96 | def render(self):
97 | raise NotImplementedError('This method must be defined in a subclass')
98 |
99 |
100 | class TemplateInline(object):
101 | """
102 | A base class for overriding to provide templated inlines.
103 | The `get_context` method is the only required override. It should return
104 | dictionary-like object that will be fed to the template as the context.
105 |
106 | If you instantiate your inline class with a context instance, it'll use
107 | that to set up your base context.
108 |
109 | Any extra arguments assigned to your inline are passed directly though to
110 | the context.
111 | """
112 |
113 | def __init__(self, value, variant=None, context=None, template_dir=None, **kwargs):
114 | self.value = value
115 | self.variant = variant
116 | self.context = context
117 | self.kwargs = kwargs
118 |
119 | self.template_dirs = []
120 | if template_dir:
121 | self.template_dirs.append(template_dir.strip('/').replace("'", '').replace('"', ''))
122 | self.template_dirs.append('inlines')
123 |
124 | def get_context(self):
125 | """
126 | This method must be defined in a subclass
127 | """
128 | raise NotImplementedError('This method must be defined in a subclass')
129 |
130 | def get_template_name(self):
131 | templates = []
132 | name = self.__class__.name
133 | for dir in self.template_dirs:
134 | if self.variant:
135 | templates.append('%s/%s.%s.html' % (dir, name, self.variant))
136 | templates.append('%s/%s.html' % (dir, name))
137 | return templates
138 |
139 | def render(self):
140 | if self.context:
141 | context = self.context
142 | else:
143 | context = Context()
144 | context.update(self.kwargs)
145 | context['variant'] = self.variant
146 | output = render_to_string(self.get_template_name(), self.get_context(), context)
147 | context.pop()
148 | return output
149 |
150 |
151 | class ModelInline(TemplateInline):
152 | """
153 | A base class for creating inlines for Django models. The `model` class
154 | attribute is the only required override. It should be assigned a django
155 | model class.
156 | """
157 |
158 | model = None
159 | help_text = "Takes the id of the desired object"
160 |
161 | @classmethod
162 | def get_app_label(self):
163 | return "%s/%s" % (self.model._meta.app_label, self.model._meta.module_name)
164 |
165 | def get_context(self):
166 | model = self.__class__.model
167 | if not isinstance(model, ModelBase):
168 | raise InlineAttributeError('ModelInline requires model to be set to a django model class')
169 | try:
170 | value = int(self.value)
171 | object = model.objects.get(pk=value)
172 | except ValueError:
173 | raise InlineInputError("'%s' could not be converted to an int" % self.value)
174 | except model.DoesNotExist:
175 | raise InlineInputError("'%s' could not be found in %s.%s" % (self.value, model._meta.app_label, model._meta.module_name))
176 | return { 'object': object }
177 |
178 |
179 | class Registry(object):
180 |
181 | def __init__(self):
182 | self._registry = {}
183 | self.START_TAG = getattr(settings, 'INLINES_START_TAG', '{{')
184 | self.END_TAG = getattr(settings, 'INLINES_END_TAG', '}}')
185 |
186 | @property
187 | def inline_finder(self):
188 | return re.compile(r'%(start)s\s*(.+?)\s*%(end)s' % {'start':self.START_TAG, 'end':self.END_TAG})
189 |
190 | def register(self, name, cls):
191 | if not hasattr(cls, 'render'):
192 | raise TypeError("You may only register inlines with a `render` method")
193 | cls.name = name
194 | self._registry[name] = cls
195 |
196 | def unregister(self, name):
197 | if not name in self._registry:
198 | raise InlineNotRegisteredError("Inline '%s' not registered. Unable to remove." % name)
199 | del(self._registry[name])
200 |
201 | def process(self, text, context=None, template_dir=None, **kwargs):
202 | def render(matchobj):
203 | try:
204 | text = matchobj.group(1)
205 | name, value, inline_kwargs = parse_inline(text)
206 | try:
207 | cls = self._registry[name]
208 | except KeyError:
209 | raise InlineNotRegisteredError('"%s" was not found as a registered inline' % name)
210 | inline = cls(value, context=context, template_dir=template_dir, **inline_kwargs)
211 | return str(inline.render())
212 | # Silence any InlineUnrenderableErrors unless INLINE_DEBUG is True
213 | except InlineUnrenderableError:
214 | debug = getattr(settings, "INLINE_DEBUG", False)
215 | if debug:
216 | raise
217 | else:
218 | return ""
219 | text = self.inline_finder.sub(render, text)
220 | return text
221 |
222 |
223 | # The default registry.
224 | registry = Registry()
225 |
--------------------------------------------------------------------------------
/django_inlines/media/django_inlines/inlines.css:
--------------------------------------------------------------------------------
1 | .inline_control:after { /* Clear fix */ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
2 | .inline_control { /* IE fix */ zoom: 1; }
3 |
4 | p.insert { float:left; margin-right:10px; }
5 |
6 | div.inlineinserter { float:left; width:500px; padding-top:3px; }
7 |
8 | div.inlineinserter p { padding:0; margin:0 0 1.3em 0;}
9 | div.inlineinserter .help_text { color:#666; font-size:10px; }
10 | div.inlineinserter label { width:6em; }
11 | div.inlineinserter .insert { margin-right:10px;}
--------------------------------------------------------------------------------
/django_inlines/media/django_inlines/inlines.js:
--------------------------------------------------------------------------------
1 | var DjangoInlines = DjangoInlines || {}
2 |
3 |
4 | $(function() {
5 |
6 | $('.vInlineTextArea').each(function(){
7 | var id = this.id;
8 | var div = $('
\n')
80 |
81 | def test_asvar_and_template_dir(self):
82 | """
83 | The template tag shouldn't care what order the arguments are in.
84 | """
85 | inlines.registry.register('youtube', YoutubeInline)
86 |
87 | template = "{% load inlines %}{% process_inlines body as body in 'youtube_inlines' %}
\n green')
134 |
135 | def test_usage_with_template_dirs_fallback(self):
136 | """
137 | A if the a template in the specified dir doesn't exist it should fallback
138 | to using the default of inlines.
139 | """
140 |
141 | from django.conf import settings
142 | inlines.registry.register('youtube', YoutubeInline)
143 |
144 | template = "{% load inlines %}
{% process_inlines body in 'nonexistent_inlines' %}