├── .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 |

Check out my new video:

24 | 25 | {{ youtube http://www.youtube.com/watch?v=RXJKdh1KZ0w }} 26 | 27 | In your template:: 28 | 29 | {% load inlines %} 30 | {% process_inlines entry.body %} 31 | 32 | Output:: 33 | 34 |

Check out my new video:

35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 |
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 = $('

Insert inline:

'); 9 | var select = $(''); 10 | for (inline in DjangoInlines.inlines) { select.append($(''))}; 11 | div.append($('
')) 12 | 13 | select.change(function(){ 14 | var inline = $(this).val(); 15 | var inserter = $('#'+id+'_inlineinserter'); 16 | if (inline == '') { inserter.html(''); return false } 17 | inserter.load(DjangoInlines.get_inline_form_url + "?inline="+$(this).val()+"&target="+id); 18 | }); 19 | 20 | div.find('p').append(select); 21 | $(this).after(div); 22 | }); 23 | 24 | 25 | $("div.inlineinserter .insert").live("click", function(){ 26 | target = $(this).attr('rel'); 27 | parent = $(this).parents('div.inline_control'); 28 | var inline_text = "" 29 | inline_text += parent.find('p.insert select').val(); 30 | variant = $('#'+target+'_variant').val(); 31 | if (variant) { 32 | inline_text += ':'+variant 33 | } 34 | inline_text += ' ' 35 | inline_text += $('#id_'+target+'_value').val(); 36 | parent.find('p.arg .value').each(function() { 37 | if ($(this).val() != '') { 38 | inline_text += ' '+$(this).attr('rel')+'='+$(this).val(); 39 | } 40 | }); 41 | $('#'+target).replaceSelection("{{ "+inline_text+" }} "); 42 | }); 43 | 44 | $("div.inlineinserter a.cancel").live("click", function(){ 45 | $(this).parents('div.inline_control').find('p.insert select').val(['']).change(); 46 | return false; 47 | }); 48 | 49 | }); -------------------------------------------------------------------------------- /django_inlines/media/django_inlines/jquery-fieldselection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16 3 | * (c) 2006 Alex Brem - http://blog.0xab.cd 4 | */ 5 | 6 | (function() { 7 | 8 | var fieldSelection = { 9 | 10 | getSelection: function() { 11 | 12 | var e = this.jquery ? this[0] : this; 13 | 14 | return ( 15 | 16 | /* mozilla / dom 3.0 */ 17 | ('selectionStart' in e && function() { 18 | var l = e.selectionEnd - e.selectionStart; 19 | return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) }; 20 | }) || 21 | 22 | /* exploder */ 23 | (document.selection && function() { 24 | 25 | e.focus(); 26 | 27 | var r = document.selection.createRange(); 28 | if (r == null) { 29 | return { start: 0, end: e.value.length, length: 0 } 30 | } 31 | 32 | var re = e.createTextRange(); 33 | var rc = re.duplicate(); 34 | re.moveToBookmark(r.getBookmark()); 35 | rc.setEndPoint('EndToStart', re); 36 | 37 | return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text }; 38 | }) || 39 | 40 | /* browser not supported */ 41 | function() { 42 | return { start: 0, end: e.value.length, length: 0 }; 43 | } 44 | 45 | )(); 46 | 47 | }, 48 | 49 | replaceSelection: function() { 50 | 51 | var e = this.jquery ? this[0] : this; 52 | var text = arguments[0] || ''; 53 | 54 | return ( 55 | 56 | /* mozilla / dom 3.0 */ 57 | ('selectionStart' in e && function() { 58 | e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length); 59 | return this; 60 | }) || 61 | 62 | /* exploder */ 63 | (document.selection && function() { 64 | e.focus(); 65 | document.selection.createRange().text = text; 66 | return this; 67 | }) || 68 | 69 | /* browser not supported */ 70 | function() { 71 | e.value += text; 72 | return this; 73 | } 74 | 75 | )(); 76 | 77 | } 78 | 79 | }; 80 | 81 | jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; }); 82 | 83 | })(); -------------------------------------------------------------------------------- /django_inlines/samples.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django_inlines.inlines import TemplateInline 3 | 4 | 5 | class YoutubeInline(TemplateInline): 6 | """ 7 | An inline that takes a youtube URL or video id and returns the proper embed. 8 | 9 | Examples:: 10 | 11 | {{ youtube http://www.youtube.com/watch?v=4R-7ZO4I1pI&hd=1 }} 12 | {{ youtube 4R-7ZO4I1pI }} 13 | 14 | The inluded template supports width and height arguments:: 15 | 16 | {{ youtube 4R-7ZO4I1pI width=850 height=500 }} 17 | 18 | """ 19 | help_text = "Takes a youtube URL or video ID: http://www.youtube.com/watch?v=4R-7ZO4I1pI or 4R-7ZO4I1pI" 20 | inline_args = [ 21 | dict(name='height', help_text="In pixels"), 22 | dict(name='width', help_text="In pixels"), 23 | ] 24 | 25 | def get_context(self): 26 | video_id = self.value 27 | match = re.search(r'(?<=v\=)[\w]+', video_id) 28 | if match: 29 | video_id = match.group() 30 | return { 'video_id': video_id } 31 | -------------------------------------------------------------------------------- /django_inlines/templates/admin/django_inlines/inline_form.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | {% if inline.get_app_label %} 5 | Lookup 6 | {% endif %} 7 | {% if inline.help_text %}{{ inline.help_text }}{% endif %} 8 |

9 | 10 | {% if inline.variants %} 11 |

12 | 13 | 19 |

20 | {% endif %} 21 | 22 | {% for arg in inline.inline_args %} 23 |

24 | {% if arg.options %} 25 | 31 | {% else %} 32 | 33 | {% endif %} 34 | {% if arg.help_text %}{{ arg.help_text }}{% endif %} 35 |

36 | {% endfor %} 37 | 38 |

Cancel

39 | -------------------------------------------------------------------------------- /django_inlines/templates/admin/django_inlines/js_inline_config.js: -------------------------------------------------------------------------------- 1 | var DjangoInlines = DjangoInlines || {} 2 | 3 | DjangoInlines.inlines = []; 4 | {% for inline in inlines %}DjangoInlines.inlines.push('{{ inline.name }}'); 5 | {% endfor %} 6 | DjangoInlines.get_inline_form_url = "{% url get_inline_form %}"; 7 | 8 | {% comment %} 9 | {% for inline in inlines %} 10 | obj = {}; 11 | obj.name = "{{ inline.name|escapejs }}"; 12 | obj.help = "{{ inline.help|escapejs }}"; 13 | obj.variants = {{ inline.variants }}; 14 | obj.args = []; {% for arg in inline.args %}obj.args.push({{ arg|safe }});{% endfor %} 15 | {% if inline.app_path %}obj.app_path = "{{ inline.app_path }}";{% else %}obj.app_path = false;{% endif %} 16 | DjangoInlines.registered.push(obj); 17 | {% endfor %} 18 | {% endcomment %} 19 | -------------------------------------------------------------------------------- /django_inlines/templates/inlines/youtube.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /django_inlines/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mintchaos/django_inlines/1912e508d04884713a6c44a068c21fbd217d478a/django_inlines/templatetags/__init__.py -------------------------------------------------------------------------------- /django_inlines/templatetags/inlines.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.filter 8 | def stripinlines(value): 9 | from django_inlines.inlines import registry 10 | return registry.inline_finder.sub('', value) 11 | 12 | 13 | class InlinesNode(template.Node): 14 | 15 | def __init__(self, var_name, template_directory=None, asvar=None): 16 | self.var_name = template.Variable(var_name) 17 | self.template_directory = template_directory 18 | self.asvar = asvar 19 | 20 | def render(self, context): 21 | try: 22 | from django_inlines.inlines import registry 23 | 24 | if self.template_directory is None: 25 | rendered = registry.process(self.var_name.resolve(context), context=context) 26 | else: 27 | rendered = registry.process(self.var_name.resolve(context), context=context, template_dir=self.template_directory) 28 | if self.asvar: 29 | context[self.asvar] = rendered 30 | return '' 31 | else: 32 | return rendered 33 | except: 34 | if getattr(settings, 'INLINE_DEBUG', False): # Should use settings.TEMPLATE_DEBUG? 35 | raise 36 | return '' 37 | 38 | 39 | @register.tag 40 | def process_inlines(parser, token): 41 | """ 42 | Searches through the provided content and applies inlines where ever they 43 | are found. 44 | 45 | Syntax:: 46 | 47 | {% process_inlines entry.body [in template_dir] [as varname] } 48 | 49 | Examples:: 50 | 51 | {% process_inlines entry.body %} 52 | 53 | {% process_inlines entry.body as body %} 54 | 55 | {% process_inlines entry.body in 'inlines/sidebar' %} 56 | 57 | {% process_inlines entry.body in 'inlines/sidebar' as body %} 58 | 59 | """ 60 | 61 | args = token.split_contents() 62 | 63 | if not len(args) in (2, 4, 6): 64 | raise template.TemplateSyntaxError("%r tag requires either 1, 3 or 5 arguments." % args[0]) 65 | 66 | var_name = args[1] 67 | 68 | ALLOWED_ARGS = ['as', 'in'] 69 | kwargs = { 'template_directory': None } 70 | if len(args) > 2: 71 | tuples = zip(*[args[2:][i::2] for i in range(2)]) 72 | for k,v in tuples: 73 | if not k in ALLOWED_ARGS: 74 | raise template.TemplateSyntaxError("%r tag options arguments must be one of %s." % (args[0], ', '.join(ALLOWED_ARGS))) 75 | if k == 'in': 76 | kwargs['template_directory'] = v 77 | if k == 'as': 78 | kwargs['asvar'] = v 79 | 80 | return InlinesNode(var_name, **kwargs) 81 | -------------------------------------------------------------------------------- /django_inlines/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.views.decorators import staff_member_required 2 | from django.shortcuts import render_to_response 3 | from django.http import Http404 4 | from django.conf import settings 5 | 6 | from django_inlines import inlines 7 | 8 | 9 | @staff_member_required 10 | def js_inline_config(request): 11 | registered = [] 12 | sorted_inlines = sorted(inlines.registry._registry.items()) 13 | for inline in sorted_inlines: 14 | d = {'name': inline[0]} 15 | inline_cls = inline[1] 16 | d['help'] = getattr(inline_cls, 'help', '') 17 | d['variants'] = getattr(inline_cls, 'variants', []) 18 | args = getattr(inline_cls, 'inline_args', []) 19 | d['args'] = sorted(args) 20 | if issubclass(inline_cls, inlines.ModelInline): 21 | d['app_path'] = "%s/%s" % (inline_cls.model._meta.app_label, inline_cls.model._meta.module_name) 22 | registered.append(d) 23 | return render_to_response('admin/django_inlines/js_inline_config.js', { 'inlines': registered }, mimetype="text/javascript") 24 | 25 | @staff_member_required 26 | def get_inline_form(request): 27 | inline = request.GET.get('inline', None) 28 | target = request.GET.get('target', None) 29 | if not inline or not target: 30 | raise Http404('"inline" and "target" must be specified as a GET args') 31 | inline_cls = inlines.registry._registry.get(inline, None) 32 | if not inline_cls: 33 | raise Http404('Requested inline does not exist') 34 | admin_media_prefix = settings.ADMIN_MEDIA_PREFIX 35 | return render_to_response('admin/django_inlines/inline_form.html', { 'inline': inline_cls, 'target':target, 'ADMIN_MEDIA_PREFIX': admin_media_prefix }) 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.core import setup 3 | 4 | def read(fname): 5 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 6 | 7 | README = read('README.rst') 8 | CHANGES = read('CHANGES.rst') 9 | 10 | setup( 11 | name = "django_inlines", 12 | version = "0.7.4", 13 | url = 'http://github.com/mintchaos/django_inlines', 14 | license = 'BSD', 15 | description = "For embedding anything you'd like into text in your django apps.", 16 | long_description='\n\n\n'.join([README, CHANGES]), 17 | 18 | author = 'Christian Metts', 19 | author_email = 'xian@mintchaos.com', 20 | packages = [ 21 | 'django_inlines', 22 | 'django_inlines.templatetags', 23 | ], 24 | package_data={'django_inlines': ['templates/inlines/*.html', 'templates/admin/django_inlines/*.html', 'templates/admin/django_inlines/*.js', 'media/django_inlines/*.css', 'media/django_inlines/*.js']}, 25 | classifiers = [ 26 | 'Development Status :: 4 - Beta', 27 | 'Framework :: Django', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python', 32 | 'Topic :: Internet :: WWW/HTTP', 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mintchaos/django_inlines/1912e508d04884713a6c44a068c21fbd217d478a/tests/__init__.py -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mintchaos/django_inlines/1912e508d04884713a6c44a068c21fbd217d478a/tests/core/__init__.py -------------------------------------------------------------------------------- /tests/core/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "core.user", 5 | "fields": { 6 | "name": "Xian", 7 | "title": "Ponymonger", 8 | "email": "xian@example.com", 9 | "phone": "(708) 555-1212" 10 | } 11 | }, 12 | { 13 | "pk": 2, 14 | "model": "core.user", 15 | "fields": { 16 | "name": "Evil Xian", 17 | "title": "Ponydestroyer", 18 | "email": "ex@example.com", 19 | "phone": "(666) 555-1212" 20 | } 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /tests/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class User(models.Model): 4 | name = models.CharField(max_length=255) 5 | title = models.CharField(blank=True, max_length=255) 6 | email = models.EmailField() 7 | phone = models.CharField(blank=True, max_length=255) -------------------------------------------------------------------------------- /tests/core/templates/inlines/user.contact.html: -------------------------------------------------------------------------------- 1 | {{ object.name }}{% if object.phone %}, {{ object.phone }}{% endif %}{% if object.email %}, {{ object.email }}{% endif %} -------------------------------------------------------------------------------- /tests/core/templates/inlines/user.html: -------------------------------------------------------------------------------- 1 | {{ object.name }} -------------------------------------------------------------------------------- /tests/core/templates/youtube_inlines/youtube.html: -------------------------------------------------------------------------------- 1 |
2 | {% if bold %}{% endif %}{{ video_id }}{% if bold %}{% endif %} 3 |
4 | -------------------------------------------------------------------------------- /tests/core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | from templateinline import * 3 | from modelinline import * 4 | from templatetags import * 5 | -------------------------------------------------------------------------------- /tests/core/tests/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django.conf import settings 3 | from django_inlines.inlines import Registry, parse_inline, InlineUnparsableError 4 | from core.tests.test_inlines import DoubleInline, QuineInline, KeyErrorInline 5 | 6 | class ParserTestCase(unittest.TestCase): 7 | 8 | def testParser(self): 9 | OUT = ('simple', '', {}) 10 | self.assertEqual(parse_inline('simple'), OUT) 11 | OUT = ('with', 'a value', {}) 12 | self.assertEqual(parse_inline('with a value'), OUT) 13 | OUT = ('with', 'a value', {'and': 'args'}) 14 | self.assertEqual(parse_inline('with a value and=args'), OUT) 15 | OUT = ('with', '', {'just': 'args'}) 16 | self.assertEqual(parse_inline('with just=args'), OUT) 17 | OUT = ('with', 'complex value http://www.youtube.com/watch?v=nsBAj6eopzc&hd=1&feature=hd#top', {}) 18 | self.assertEqual(parse_inline('with complex value http://www.youtube.com/watch?v=nsBAj6eopzc&hd=1&feature=hd#top'), OUT) 19 | OUT = ('with', 'complex value http://www.youtube.com/watch?v=nsBAj6eopzc&hd=1&feature=hd#top', {'and': 'args'}) 20 | self.assertEqual(parse_inline('with complex value http://www.youtube.com/watch?v=nsBAj6eopzc&hd=1&feature=hd#top and=args'), OUT) 21 | OUT = (u'with', u'complex value http://www.youtube.com/watch?v=nsBAj6eopzc', {'and': 'args'}) 22 | self.assertEqual(parse_inline(u'with complex value http://www.youtube.com/watch?v=nsBAj6eopzc and=args'), OUT) 23 | OUT = ('with', 'a value', {'variant': 'variant', 'and': 'args', 'more': 'arg'}) 24 | self.assertEqual(parse_inline('with:variant a value and=args more=arg'), OUT) 25 | OUT = ('with', '', {'variant': 'avariant'}) 26 | self.assertEqual(parse_inline('with:avariant'), OUT) 27 | 28 | class RegistrySartEndTestCase(unittest.TestCase): 29 | 30 | def setUp(self): 31 | inlines = Registry() 32 | inlines.register('double', DoubleInline) 33 | inlines.START_TAG = '<<' 34 | inlines.END_TAG = '>>' 35 | self.inlines = inlines 36 | 37 | def testDifferentSartEnds(self): 38 | # self.assertEqual(self.inlines.START_TAG, "<<") 39 | IN = """<< double makes more >>""" 40 | OUT = """makes moremakes more""" 41 | self.assertEqual(self.inlines.process(IN), OUT) 42 | IN = """<< double 2 >> / << double 2 multiplier=3 >>""" 43 | OUT = """4 / 6""" 44 | self.assertEqual(self.inlines.process(IN), OUT) 45 | 46 | class InlineTestCase(unittest.TestCase): 47 | 48 | def setUp(self): 49 | inlines = Registry() 50 | inlines.register('quine', QuineInline) 51 | inlines.register('double', DoubleInline) 52 | self.inlines = inlines 53 | 54 | def tearDown(self): 55 | settings.INLINE_DEBUG = False 56 | 57 | def testQuineInline(self): 58 | IN = """{{ quine should be the same }}""" 59 | OUT = """{{ quine should be the same }}""" 60 | self.assertEqual(self.inlines.process(IN), OUT) 61 | IN = """the {{ quine }}""" 62 | OUT = """the {{ quine }}""" 63 | self.assertEqual(self.inlines.process(IN), OUT) 64 | IN = """the {{ quine with value }} 65 | {{ quine with=args }} 66 | {{ quine:with_variant }} 67 | {{ quine:with everything even=args }} 68 | """ 69 | OUT = """the {{ quine with value }} 70 | {{ quine with=args }} 71 | {{ quine:with_variant }} 72 | {{ quine:with everything even=args }} 73 | """ 74 | self.assertEqual(self.inlines.process(IN), OUT) 75 | 76 | def testDoubleInline(self): 77 | IN = """{{ double makes more }}""" 78 | OUT = """makes moremakes more""" 79 | self.assertEqual(self.inlines.process(IN), OUT) 80 | IN = """{{ double 2 }} / {{ double 2 multiplier=3 }}""" 81 | OUT = """4 / 6""" 82 | self.assertEqual(self.inlines.process(IN), OUT) 83 | 84 | def testMultipleInlines(self): 85 | IN = """{{ quine }} and {{ nothing }}""" 86 | OUT = """{{ quine }} and """ 87 | self.assertEqual(self.inlines.process(IN), OUT) 88 | 89 | def testRemovalOfUnassignedInline(self): 90 | IN = """this {{ should }} be removed""" 91 | OUT = """this be removed""" 92 | self.assertEqual(self.inlines.process(IN), OUT) 93 | 94 | def test_empty_inline(self): 95 | IN = """this {{ 234 }} be removed""" 96 | OUT = """this be removed""" 97 | self.assertEqual(self.inlines.process(IN), OUT) 98 | settings.INLINE_DEBUG = True 99 | self.assertRaises(InlineUnparsableError, self.inlines.process, IN) 100 | 101 | def test_keyerrors(self): 102 | """ 103 | A regression test to make sure KeyErrors thrown by inlines 104 | aren't silenced in render anymore. 105 | """ 106 | self.inlines.register('keyerror', KeyErrorInline) 107 | IN = "{{ keyerror fail! }}" 108 | self.assertRaises(KeyError, self.inlines.process, IN) -------------------------------------------------------------------------------- /tests/core/tests/modelinline.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django_inlines.inlines import Registry, inline_for_model, InlineInputError 3 | from django.conf import settings 4 | from test_inlines import UserInline 5 | from core.models import User 6 | 7 | class ModelInlineTestCase(TestCase): 8 | 9 | fixtures = ['users'] 10 | 11 | def setUp(self): 12 | inlines = Registry() 13 | inlines.register('user', UserInline) 14 | self.inlines = inlines 15 | 16 | def testModelInlines(self): 17 | self.assertEqual(self.inlines.process("{{ user 1 }}"), "Xian") 18 | self.assertEqual(self.inlines.process("{{ user 1 }} vs {{ user 2 }}"), "Xian vs Evil Xian") 19 | 20 | def testModelInlineVariants(self): 21 | self.assertEqual(self.inlines.process("{{ user:contact 1 }}"), "Xian, (708) 555-1212, xian@example.com") 22 | self.assertEqual(self.inlines.process("{{ user:nonexistant_variant 1 }}"), "Xian") 23 | 24 | 25 | class BadInputModelInlineTestCase(TestCase): 26 | 27 | fixtures = ['users'] 28 | 29 | def setUp(self): 30 | inlines = Registry() 31 | inlines.register('user', UserInline) 32 | self.inlines = inlines 33 | 34 | def tearDown(self): 35 | settings.INLINE_DEBUG = False 36 | 37 | def testAgainstNonexistentObject(self): 38 | self.assertEqual(self.inlines.process("{{ user 111 }}"), "") 39 | 40 | def testAgainstCrapInput(self): 41 | self.assertEqual(self.inlines.process("{{ user asdf }}"), "") 42 | 43 | def testErrorRaising(self): 44 | settings.INLINE_DEBUG = True 45 | process = self.inlines.process 46 | self.assertRaises(InlineInputError, process, "{{ user 111 }}",) 47 | self.assertRaises(InlineInputError, process, "{{ user asdf }}",) 48 | 49 | class InlineForModelTestCase(TestCase): 50 | 51 | fixtures = ['users'] 52 | 53 | def setUp(self): 54 | inlines = Registry() 55 | self.inlines = inlines 56 | 57 | def testInlineForModel(self): 58 | self.inlines.register('user', inline_for_model(User)) 59 | self.assertEqual(self.inlines.process("{{ user 1 }}"), "Xian") 60 | self.assertEqual(self.inlines.process("{{ user 1 }} vs {{ user 2 }}"), "Xian vs Evil Xian") 61 | 62 | def testInlineForModelBadInput(self): 63 | self.assertRaises(ValueError, inline_for_model, "User") 64 | -------------------------------------------------------------------------------- /tests/core/tests/templateinline.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django_inlines.inlines import Registry 3 | from django_inlines.samples import YoutubeInline 4 | 5 | 6 | class YoutubeTestCase(unittest.TestCase): 7 | 8 | def setUp(self): 9 | inlines = Registry() 10 | inlines.register('youtube', YoutubeInline) 11 | self.inlines = inlines 12 | 13 | def testYoutubeInlines(self): 14 | IN = """{{ youtube RXJKdh1KZ0w }}""" 15 | OUT = """
\n\n \n \n \n \n \n
\n""" 16 | self.assertEqual(self.inlines.process(IN), OUT) 17 | IN = """{{ youtube RXJKdh1KZ0w width=200 height=100 }}""" 18 | OUT = """
\n\n \n \n \n \n \n
\n""" 19 | self.assertEqual(self.inlines.process(IN), OUT) 20 | IN = """{{ youtube http://www.youtube.com/watch?v=RXJKdh1KZ0w&hd=1&feature=hd }}""" 21 | OUT = """
\n\n \n \n \n \n \n
\n""" 22 | self.assertEqual(self.inlines.process(IN), OUT) 23 | -------------------------------------------------------------------------------- /tests/core/tests/templatetags.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.template import Template, Context 3 | from django_inlines import inlines 4 | from django_inlines.samples import YoutubeInline 5 | from django_inlines.templatetags.inlines import stripinlines 6 | from test_inlines import QuineInline, DoubleInline 7 | 8 | 9 | class StripInlinesTestCase(TestCase): 10 | def render(self, template_string, context_dict=None): 11 | """A shortcut for testing template output.""" 12 | if context_dict is None: 13 | context_dict = {} 14 | 15 | c = Context(context_dict) 16 | t = Template(template_string) 17 | return t.render(c) 18 | 19 | def test_strip_inlines(self): 20 | IN = "This is my YouTube video: {{ youtube C_ZebDKv1zo }}" 21 | self.assertEqual(stripinlines(IN), "This is my YouTube video: ") 22 | 23 | def test_simple_usage(self): 24 | inlines.registry.register('youtube', YoutubeInline) 25 | 26 | template = u"{% load inlines %}

{{ body|stripinlines }}

" 27 | context = { 28 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo }}", 29 | } 30 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

') 31 | 32 | 33 | class ProcessInlinesTestCase(TestCase): 34 | def render(self, template_string, context_dict=None): 35 | """A shortcut for testing template output.""" 36 | if context_dict is None: 37 | context_dict = {} 38 | 39 | c = Context(context_dict) 40 | t = Template(template_string) 41 | return t.render(c) 42 | 43 | def setUp(self): 44 | super(ProcessInlinesTestCase, self).setUp() 45 | 46 | # Stow. 47 | self.old_registry = inlines.registry 48 | inlines.registry = inlines.Registry() 49 | 50 | def tearDown(self): 51 | inlines.registry = self.old_registry 52 | super(ProcessInlinesTestCase, self).tearDown() 53 | 54 | def test_simple_usage(self): 55 | inlines.registry.register('youtube', YoutubeInline) 56 | 57 | template = u"{% load inlines %}

{% process_inlines body %}

" 58 | context = { 59 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo }}", 60 | } 61 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\n\n \n \n \n \n \n
\n

') 62 | 63 | def test_usage_with_args_and_unicode(self): 64 | inlines.registry.register('youtube', YoutubeInline) 65 | 66 | template = u"{% load inlines %}

{% process_inlines body %}

" 67 | context = { 68 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo height=295 width=480 }}", 69 | } 70 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\n\n \n \n \n \n \n
\n

') 71 | 72 | def test_asvar(self): 73 | inlines.registry.register('youtube', YoutubeInline) 74 | 75 | template = u"{% load inlines %}{% process_inlines body as body %}

{{ body|safe }}

" 76 | context = { 77 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo }}", 78 | } 79 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\n\n \n \n \n \n \n
\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' %}

{{ body|safe }}

" 88 | context = { 89 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo }}", 90 | } 91 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\nC_ZebDKv1zo\n
\n

') 92 | 93 | template = "{% load inlines %}{% process_inlines body in 'youtube_inlines' as body %}

{{ body|safe }}

" 94 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\nC_ZebDKv1zo\n
\n

') 95 | 96 | def test_usage_with_multiple_inlines(self): 97 | inlines.registry.register('quine', QuineInline) 98 | inlines.registry.register('double', DoubleInline) 99 | 100 | template = "{% load inlines %}

{% process_inlines body %}

" 101 | context = { 102 | 'body': u"Some text {{ quine Why hello }} but {{ double your fun }}.", 103 | } 104 | self.assertEqual(inlines.registry.process(context['body']), 'Some text {{ quine Why hello }} but your funyour fun.') 105 | self.assertEqual(self.render(template, context), u'

Some text {{ quine Why hello }} but your funyour fun.

') 106 | 107 | def test_usage_with_template_dirs(self): 108 | inlines.registry.register('youtube', YoutubeInline) 109 | 110 | template = "{% load inlines %}

{% process_inlines body in 'youtube_inlines' %}

" 111 | context = { 112 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo }}", 113 | } 114 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\nC_ZebDKv1zo\n
\n

') 115 | 116 | def test_that_context_gets_passed_through(self): 117 | inlines.registry.register('youtube', YoutubeInline) 118 | 119 | template = "{% load inlines %}

{% with 'b' as bold %}{% process_inlines body in 'youtube_inlines' %}{% endwith %}

" 120 | context = { 121 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo }}", 122 | } 123 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\nC_ZebDKv1zo\n
\n

') 124 | 125 | def test_that_context_gets_popped(self): 126 | inlines.registry.register('youtube', YoutubeInline) 127 | 128 | template = """{% load inlines %}

{% process_inlines body in 'youtube_inlines' %} {{ test }}

""" 129 | context = { 130 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo bold=bold }} {{ youtube C_ZebDKv1zo }}", 131 | 'test': u"green" 132 | } 133 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\nC_ZebDKv1zo\n
\n
\nC_ZebDKv1zo\n
\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' %}

" 145 | context = { 146 | 'body': u"This is my YouTube video: {{ youtube C_ZebDKv1zo }}", 147 | } 148 | self.assertEqual(self.render(template, context), u'

This is my YouTube video:

\n\n \n \n \n \n \n
\n

') 149 | -------------------------------------------------------------------------------- /tests/core/tests/test_inlines.py: -------------------------------------------------------------------------------- 1 | from django_inlines.inlines import InlineBase, ModelInline 2 | from core.models import User 3 | 4 | 5 | class QuineInline(InlineBase): 6 | """ 7 | A simple inline that returns itself. 8 | """ 9 | def render(self): 10 | bits = [] 11 | if self.variant: 12 | bits.append(':%s' % self.variant) 13 | else: 14 | bits.append('') 15 | if self.value: 16 | bits.append(self.value) 17 | for k, v in self.kwargs.items(): 18 | bits.append("%s=%s" % (k,v)) 19 | else: 20 | return "{{ quine%s }}" % " ".join(bits) 21 | 22 | 23 | class DoubleInline(InlineBase): 24 | """ 25 | A simple inline that doubles itself. 26 | """ 27 | def render(self): 28 | value = self.value 29 | multiplier = 2 30 | if self.kwargs.has_key('multiplier'): 31 | try: 32 | multiplier = int(self.kwargs['multiplier']) 33 | except ValueError: 34 | pass 35 | try: 36 | value = int(self.value) 37 | except ValueError: 38 | pass 39 | return value*multiplier 40 | 41 | 42 | class KeyErrorInline(InlineBase): 43 | """ 44 | An inline that raises a KeyError. For regression testing. 45 | """ 46 | def render(self): 47 | empty = {} 48 | return empty['this will fail'] 49 | 50 | 51 | class UserInline(ModelInline): 52 | """ 53 | A inline for the mock user model. 54 | """ 55 | model = User 56 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import sys 4 | from os.path import dirname, abspath 5 | sys.path += [dirname(dirname(abspath(__file__)))] 6 | 7 | try: 8 | import settings # Assumed to be in the same directory. 9 | except ImportError: 10 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 11 | sys.exit(1) 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | DATABASE_ENGINE = 'sqlite3' 2 | DATABASE_NAME = 'django_inlines_tests.db' 3 | 4 | INSTALLED_APPS = [ 5 | 'core', 6 | 'django_inlines', 7 | ] 8 | TEMPLATE_LOADERS = ( 9 | 'django.template.loaders.app_directories.load_template_source', 10 | ) 11 | --------------------------------------------------------------------------------