├── .gitignore ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── dev-requirements.txt ├── example_project ├── __init__.py ├── example_project.db ├── manage.py ├── model_blocks ├── pepulator_factory │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto__add_jamb__add_knuckle.py │ │ ├── 0003_auto__add_field_knuckle_img_url.py │ │ ├── 0004_auto__add_field_pepulator_address.py │ │ └── __init__.py │ ├── models.py │ ├── static │ │ └── pepulator_factory │ │ │ └── model_block_extensions.css │ ├── templates │ │ ├── base.html │ │ └── pepulator_factory │ │ │ ├── content_list.html │ │ │ ├── distributor_detail.html │ │ │ ├── distributor_list.html │ │ │ ├── knuckle_detail.html │ │ │ ├── pepulator_detail.html │ │ │ └── pepulator_list.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── settings.py └── urls.py ├── model_blocks ├── __init__.py ├── fixtures │ └── pepulator_factory_data.json ├── models.py ├── static │ └── model_blocks │ │ └── simple.css ├── templates │ └── model_blocks │ │ ├── object_detail.html │ │ ├── object_list.html │ │ └── object_teaser.html ├── templatetags │ ├── __init__.py │ ├── model_blocks.py │ ├── model_filters.py │ ├── model_nodes.py │ └── model_tags.py ├── tests.py └── views.py ├── requirements.txt ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python 2 | *.py[c|o] 3 | 4 | # Gedit backup files 5 | *~ 6 | 7 | # Test coverage generated files 8 | .coverage 9 | 10 | # Setup generated files 11 | build/ 12 | dist/ 13 | *.egg-info/ 14 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.8.9 2 | ===== 3 | 4 | Features: 5 | 6 | * Display URLFields as links by default 7 | 8 | 9 | 0.8.8 10 | ===== 11 | 12 | Features: 13 | 14 | * Teasers more customizable by not forcing them to be wrapped in an anchor 15 | 16 | 17 | 0.8.7 18 | ===== 19 | 20 | Features: 21 | 22 | * More classes to select on in teaser, detail, and list templates 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Mjumbe Wawatu Ukweli 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 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include model_blocks *.html *.css *.png *.gif 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Django Model Blocks 3 | ==================== 4 | 5 | The ``model_blocks`` app provides you with automatically generated, stylable 6 | generic Django model template partials. It fills a gap left by ``admin`` and 7 | ``databrowse`` by providing filters and tags that allow your to painlessly 8 | create templates with the following properties: 9 | 10 | * Automatically generated read-only views 11 | * Can conform to whatever URL structure you want 12 | * Can be placed as blocks on to your existing templates 13 | * Integrate well with the rest of your project 14 | 15 | Add this to the top of any template:: 16 | 17 | {% load model_blocks %} 18 | 19 | And drop the filter anywhere you have a model instance (e.g., DetailViews):: 20 | 21 | {{ object|as_detail_block }} 22 | 23 | Quick Reference 24 | --------------- 25 | 26 | Installing 27 | ~~~~~~~~~~ 28 | 29 | You can install the ``model_blocks`` app from PyPI:: 30 | 31 | $ pip install django-model-blocks 32 | 33 | Settings 34 | ~~~~~~~~ 35 | 36 | Modify your ``INSTALLED_APPS`` setting to include:: 37 | 38 | ... 39 | model_blocks, 40 | ... 41 | 42 | Basic Usage 43 | ~~~~~~~~~~~ 44 | 45 | Near the top of any template you want to use model blocks, or in a base 46 | template, include the following line:: 47 | 48 | {% load model_blocks %} 49 | 50 | Then, where you want to drop a generic model template, use:: 51 | 52 | {{ object|as_detail_block }} 53 | 54 | Or:: 55 | 56 | {{ object_list|as_list_block }} 57 | 58 | (**NOTE:** If your list has many objects, consider using pagination, as the list 59 | may require a long time to render.) 60 | 61 | By default, the title on an object detail block will be the unicode 62 | representation of the object, and the title on a list will be the name of the 63 | model appended with `' List'`. To change the title, pass in a parameter:: 64 | 65 | {{ object|as_detail_block:"My Special Object" }} 66 | 67 | Advanced Usage 68 | ~~~~~~~~~~~~~~ 69 | 70 | While using the filters remains the original and most simple way to render 71 | the blocks, if you want/need greater control over the specifics of how certain 72 | models render, you can use the tag notation:: 73 | 74 | {% detail_block object %} 75 | 76 | {% list_block object_list %} 77 | 78 | You can still override the title by using ``with``:: 79 | 80 | {% with title="My Special Object" %} 81 | {% detail_block object %} 82 | {% endwith %} 83 | 84 | Yeah, if all you need to do is override the title, then stick with the filters. 85 | However, When you drop a detail block into your template, it will automatically 86 | render all of the referenced object's fields, including related model fields. 87 | This potentially results in a tree of objects in your page. The tag notation's 88 | strength is revealed when you need to use a custom template for any model in 89 | your tree. 90 | 91 | The ``example_project`` in the source includes a demonstration of this feature. 92 | In that example, there are ``Pepulator`` objects, and each one may have several 93 | ``Knuckle`` objects and several ``Jamb`` objects. However, each ``Knuckle`` has 94 | a field referring to the URL of an image. On our ``Pepulator`` detail page, we 95 | want all of our ``Kuckle`` objects and ``Jamb`` objects shown. The default 96 | template is sufficient for ``Jamb`` objects, but we have to provide a custom 97 | template (based on the default) for each ``Knuckle``. So, we render the 98 | ``Pepulator`` detail like so:: 99 | 100 | {% with pepulator_factory_knuckle_detail_template="pepulator_factory/knuckle_detail.html" %} 101 | {% detail_block pepulator %} 102 | {% endwith %} 103 | 104 | Voila! For more information, check out the 105 | `pepulator_detail.html `_ 106 | and 107 | `knuckle_detail.html `_ 108 | files. 109 | 110 | Help Out 111 | -------- 112 | 113 | Found a bug? File an issue at `Github 114 | `_. Have an improvement? Fork 115 | it and add it, or if you can’t code it, File an `issue 116 | `_ and we'll do it. 117 | 118 | Are you using or thinking of using ``django-model-filters``? Please `drop a 119 | line `_ and let us know what for. 120 | Knowing how people use it in the wild will help us make it better! 121 | 122 | Development 123 | ~~~~~~~~~~~ 124 | 125 | Download the code and then:: 126 | 127 | $ pip install -r requirements.txt 128 | 129 | Running Tests 130 | ~~~~~~~~~~~~~ 131 | 132 | Even simple packages need tests:: 133 | 134 | $ python tests.py --with-coverage --cover-package=model_block 135 | 136 | Run it before and after you make any changes. Try to not let that number drop. 137 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | mock 3 | django-nose 4 | coverage 5 | -------------------------------------------------------------------------------- /example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjumbewu/django-model-blocks/8175d7353d792cb720b4ac356f4538888bf7747c/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/example_project.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjumbewu/django-model-blocks/8175d7353d792cb720b4ac356f4538888bf7747c/example_project/example_project.db -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | 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" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /example_project/model_blocks: -------------------------------------------------------------------------------- 1 | ../model_blocks/ -------------------------------------------------------------------------------- /example_project/pepulator_factory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjumbewu/django-model-blocks/8175d7353d792cb720b4ac356f4538888bf7747c/example_project/pepulator_factory/__init__.py -------------------------------------------------------------------------------- /example_project/pepulator_factory/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from pepulator_factory.models import Pepulator, Distributor, Jamb, Knuckle 3 | 4 | class KnuckleInline (admin.StackedInline): 5 | model = Knuckle 6 | 7 | class JambInline (admin.StackedInline): 8 | model = Jamb 9 | 10 | class PepulatorAdmin (admin.ModelAdmin): 11 | inlines = [KnuckleInline, JambInline] 12 | 13 | admin.site.register(Pepulator, PepulatorAdmin) 14 | admin.site.register(Distributor) 15 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Pepulator' 12 | db.create_table('pepulator_factory_pepulator', ( 13 | ('serial_number', self.gf('django.db.models.fields.IntegerField')(primary_key=True)), 14 | ('height', self.gf('django.db.models.fields.IntegerField')()), 15 | ('width', self.gf('django.db.models.fields.IntegerField')()), 16 | ('manufacture_date', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), 17 | ('color', self.gf('django.db.models.fields.CharField')(max_length=32)), 18 | ('distributed_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='stock', null=True, to=orm['pepulator_factory.Distributor'])), 19 | )) 20 | db.send_create_signal('pepulator_factory', ['Pepulator']) 21 | 22 | # Adding model 'Distributor' 23 | db.create_table('pepulator_factory_distributor', ( 24 | ('name', self.gf('django.db.models.fields.CharField')(max_length=256, primary_key=True)), 25 | ('capacity', self.gf('django.db.models.fields.IntegerField')()), 26 | )) 27 | db.send_create_signal('pepulator_factory', ['Distributor']) 28 | 29 | 30 | def backwards(self, orm): 31 | 32 | # Deleting model 'Pepulator' 33 | db.delete_table('pepulator_factory_pepulator') 34 | 35 | # Deleting model 'Distributor' 36 | db.delete_table('pepulator_factory_distributor') 37 | 38 | 39 | models = { 40 | 'pepulator_factory.distributor': { 41 | 'Meta': {'object_name': 'Distributor'}, 42 | 'capacity': ('django.db.models.fields.IntegerField', [], {}), 43 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'primary_key': 'True'}) 44 | }, 45 | 'pepulator_factory.pepulator': { 46 | 'Meta': {'object_name': 'Pepulator'}, 47 | 'color': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 48 | 'distributed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'stock'", 'null': 'True', 'to': "orm['pepulator_factory.Distributor']"}), 49 | 'height': ('django.db.models.fields.IntegerField', [], {}), 50 | 'manufacture_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 51 | 'serial_number': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}), 52 | 'width': ('django.db.models.fields.IntegerField', [], {}) 53 | } 54 | } 55 | 56 | complete_apps = ['pepulator_factory'] 57 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/migrations/0002_auto__add_jamb__add_knuckle.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Jamb' 12 | db.create_table('pepulator_factory_jamb', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('power', self.gf('django.db.models.fields.FloatField')()), 15 | ('pepulator', self.gf('django.db.models.fields.related.ForeignKey')(related_name='jambs', to=orm['pepulator_factory.Pepulator'])), 16 | )) 17 | db.send_create_signal('pepulator_factory', ['Jamb']) 18 | 19 | # Adding model 'Knuckle' 20 | db.create_table('pepulator_factory_knuckle', ( 21 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 22 | ('hardness', self.gf('django.db.models.fields.FloatField')()), 23 | ('pepulator', self.gf('django.db.models.fields.related.ForeignKey')(related_name='knuckles', to=orm['pepulator_factory.Pepulator'])), 24 | )) 25 | db.send_create_signal('pepulator_factory', ['Knuckle']) 26 | 27 | 28 | def backwards(self, orm): 29 | 30 | # Deleting model 'Jamb' 31 | db.delete_table('pepulator_factory_jamb') 32 | 33 | # Deleting model 'Knuckle' 34 | db.delete_table('pepulator_factory_knuckle') 35 | 36 | 37 | models = { 38 | 'pepulator_factory.distributor': { 39 | 'Meta': {'object_name': 'Distributor'}, 40 | 'capacity': ('django.db.models.fields.IntegerField', [], {}), 41 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'primary_key': 'True'}) 42 | }, 43 | 'pepulator_factory.jamb': { 44 | 'Meta': {'object_name': 'Jamb'}, 45 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 46 | 'pepulator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jambs'", 'to': "orm['pepulator_factory.Pepulator']"}), 47 | 'power': ('django.db.models.fields.FloatField', [], {}) 48 | }, 49 | 'pepulator_factory.knuckle': { 50 | 'Meta': {'object_name': 'Knuckle'}, 51 | 'hardness': ('django.db.models.fields.FloatField', [], {}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'pepulator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'knuckles'", 'to': "orm['pepulator_factory.Pepulator']"}) 54 | }, 55 | 'pepulator_factory.pepulator': { 56 | 'Meta': {'object_name': 'Pepulator'}, 57 | 'color': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 58 | 'distributed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'stock'", 'null': 'True', 'to': "orm['pepulator_factory.Distributor']"}), 59 | 'height': ('django.db.models.fields.IntegerField', [], {}), 60 | 'manufacture_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 61 | 'serial_number': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}), 62 | 'width': ('django.db.models.fields.IntegerField', [], {}) 63 | } 64 | } 65 | 66 | complete_apps = ['pepulator_factory'] 67 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/migrations/0003_auto__add_field_knuckle_img_url.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Knuckle.img_url' 12 | db.add_column('pepulator_factory_knuckle', 'img_url', self.gf('django.db.models.fields.URLField')(default='http://www.lllsoftware.it/Images/icoQuestion.png', max_length=200), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Knuckle.img_url' 18 | db.delete_column('pepulator_factory_knuckle', 'img_url') 19 | 20 | 21 | models = { 22 | 'pepulator_factory.distributor': { 23 | 'Meta': {'object_name': 'Distributor'}, 24 | 'capacity': ('django.db.models.fields.IntegerField', [], {}), 25 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'primary_key': 'True'}) 26 | }, 27 | 'pepulator_factory.jamb': { 28 | 'Meta': {'object_name': 'Jamb'}, 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'pepulator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jambs'", 'to': "orm['pepulator_factory.Pepulator']"}), 31 | 'power': ('django.db.models.fields.FloatField', [], {}) 32 | }, 33 | 'pepulator_factory.knuckle': { 34 | 'Meta': {'object_name': 'Knuckle'}, 35 | 'hardness': ('django.db.models.fields.FloatField', [], {}), 36 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 37 | 'img_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), 38 | 'pepulator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'knuckles'", 'to': "orm['pepulator_factory.Pepulator']"}) 39 | }, 40 | 'pepulator_factory.pepulator': { 41 | 'Meta': {'object_name': 'Pepulator'}, 42 | 'color': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 43 | 'distributed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'stock'", 'null': 'True', 'to': "orm['pepulator_factory.Distributor']"}), 44 | 'height': ('django.db.models.fields.IntegerField', [], {}), 45 | 'manufacture_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 46 | 'serial_number': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}), 47 | 'width': ('django.db.models.fields.IntegerField', [], {}) 48 | } 49 | } 50 | 51 | complete_apps = ['pepulator_factory'] 52 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/migrations/0004_auto__add_field_pepulator_address.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Pepulator.address' 12 | db.add_column('pepulator_factory_pepulator', 'address', self.gf('django.db.models.fields.URLField')(max_length=200, null=True), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Pepulator.address' 18 | db.delete_column('pepulator_factory_pepulator', 'address') 19 | 20 | 21 | models = { 22 | 'pepulator_factory.distributor': { 23 | 'Meta': {'object_name': 'Distributor'}, 24 | 'capacity': ('django.db.models.fields.IntegerField', [], {}), 25 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'primary_key': 'True'}) 26 | }, 27 | 'pepulator_factory.jamb': { 28 | 'Meta': {'object_name': 'Jamb'}, 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'pepulator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jambs'", 'to': "orm['pepulator_factory.Pepulator']"}), 31 | 'power': ('django.db.models.fields.FloatField', [], {}) 32 | }, 33 | 'pepulator_factory.knuckle': { 34 | 'Meta': {'object_name': 'Knuckle'}, 35 | 'hardness': ('django.db.models.fields.FloatField', [], {}), 36 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 37 | 'img_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), 38 | 'pepulator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'knuckles'", 'to': "orm['pepulator_factory.Pepulator']"}) 39 | }, 40 | 'pepulator_factory.pepulator': { 41 | 'Meta': {'object_name': 'Pepulator'}, 42 | 'address': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), 43 | 'color': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 44 | 'distributed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'stock'", 'null': 'True', 'to': "orm['pepulator_factory.Distributor']"}), 45 | 'height': ('django.db.models.fields.IntegerField', [], {}), 46 | 'manufacture_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 47 | 'serial_number': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}), 48 | 'width': ('django.db.models.fields.IntegerField', [], {}) 49 | } 50 | } 51 | 52 | complete_apps = ['pepulator_factory'] 53 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjumbewu/django-model-blocks/8175d7353d792cb720b4ac356f4538888bf7747c/example_project/pepulator_factory/migrations/__init__.py -------------------------------------------------------------------------------- /example_project/pepulator_factory/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Pepulator (models.Model): 4 | serial_number = models.IntegerField(primary_key=True) 5 | height = models.IntegerField() 6 | width = models.IntegerField() 7 | manufacture_date = models.DateTimeField(auto_now=True) 8 | color = models.CharField(max_length=32) 9 | address = models.URLField(null=True) 10 | 11 | distributed_by = models.ForeignKey('Distributor', null=True, 12 | related_name='stock') 13 | 14 | def __unicode__(self): 15 | return u'Pepulator #%s' % self.serial_number 16 | 17 | @models.permalink 18 | def get_absolute_url(self): 19 | return ('pepulator_detail_view', [str(self.serial_number)]) 20 | 21 | 22 | class Knuckle (models.Model): 23 | hardness = models.FloatField() 24 | img_url = models.URLField() 25 | pepulator = models.ForeignKey('Pepulator', related_name='knuckles') 26 | 27 | def __unicode__(self): 28 | return u'Knuckle of hardness %.2f' % self.hardness 29 | 30 | 31 | class Jamb (models.Model): 32 | power = models.FloatField() 33 | pepulator = models.ForeignKey('Pepulator', related_name='jambs') 34 | 35 | def __unicode__(self): 36 | return u'Jamb with power %.2f' % self.power 37 | 38 | 39 | class Distributor (models.Model): 40 | name = models.CharField(max_length=256, primary_key=True) 41 | capacity = models.IntegerField() 42 | 43 | def __unicode__(self): 44 | return unicode(self.name) 45 | 46 | @models.permalink 47 | def get_absolute_url(self): 48 | return ('distributor_detail_view', [self.name]) 49 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/static/pepulator_factory/model_block_extensions.css: -------------------------------------------------------------------------------- 1 | .pepulator_detail > .pepulator_serial_number, 2 | .knuckle_detail > .knuckle_id, 3 | .knuckle_detail > .knuckle_hardness, 4 | .knuckle_detail > .knuckle_pepulator, 5 | .jamb_detail > .jamb_id, 6 | .jamb_detail > .jamb_power, 7 | .jamb_detail > .jamb_pepulator, 8 | .knuckle_img_url_label { 9 | display: none; 10 | } 11 | 12 | .knuckle_header { 13 | margin-left: 1.5em; 14 | } 15 | 16 | .knuckle_header > h1, 17 | .jamb_header > h1 { 18 | text-decoration: none !important; 19 | } 20 | 21 | .knuckle_img_url_value img { 22 | float: left; 23 | height: 1em; 24 | margin-top: -1em; 25 | } 26 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | {% block content %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/templates/pepulator_factory/content_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load model_filters %} 3 | 4 | {% block title %}Content{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

The Pepulator Factory

10 |
11 | 12 | 16 |
17 | {% endblock %} 18 | 19 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/templates/pepulator_factory/distributor_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load model_blocks %} 3 | 4 | {% block title %}{{ distributor }}{% endblock %} 5 | 6 | {% block content %} 7 | {{ distributor|as_detail_block }} 8 | {% endblock %} 9 | 10 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/templates/pepulator_factory/distributor_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load model_blocks %} 3 | 4 | {% block title %}Distributors{% endblock %} 5 | 6 | {% block content %} 7 | {{ distributors|as_list_block }} 8 | {% endblock %} 9 | 10 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/templates/pepulator_factory/knuckle_detail.html: -------------------------------------------------------------------------------- 1 | {% load model_blocks %} 2 | 3 | {% comment %} 4 | # 5 | # This file is based on the 'model_blocks/object_detail.html' template. When 6 | # creating a custom detail block for a given model, that is probably the best 7 | # place to start. 8 | # 9 | {% endcomment %} 10 | 11 | {% spaceless %} 12 |
13 |
14 | 15 | {# Custom title #} 16 |

{% if instance.hardness > 2 %}Stupendous {% endif %}{{ title|default:instance }}

17 | 18 |
19 | 20 |
21 | {% for name, label, value, is_list in fields %} 22 |
23 | {% if not is_list and value != None %} 24 | {{ label|capfirst }} 25 | 26 | 27 | {% if value.get_absolute_url %} 28 | {{ value }} 29 | {% else %} 30 | 31 | {# Say we have images of knuckles... #} 32 | {% if name == "img_url" %} 33 | Picture of {{ instance }} 34 | 35 | {% else %} 36 | {{ value }} 37 | {% endif %} 38 | {% endif %} 39 | 40 | 41 | {% endif %} 42 | 43 | {% if is_list and value.all|length %} 44 | {% with title=label %} 45 | {% list_block value.all %} 46 | {% endwith %} 47 | {% endif %} 48 |
49 | {% endfor %} 50 |
51 |
52 | {% endspaceless %} 53 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/templates/pepulator_factory/pepulator_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load model_blocks %} 3 | 4 | {% block title %}{{ pepulator }}{% endblock %} 5 | 6 | {% block content %} 7 | 8 | {% comment %} 9 | # 10 | # This template is rendered a little differently than the default. For a 11 | # plain template, see distributor_detail.html. This one is special because 12 | # it replaces the default template for a sub-component of the Pepulator 13 | # model with a custom one. That's how the images get on the page. For more 14 | # information, see the "pepulator_factory/knuckle_detail.html" template. 15 | # 16 | {% endcomment %} 17 | 18 | {% with pepulator_factory_knuckle_detail_template="pepulator_factory/knuckle_detail.html" %} 19 | {% detail_block pepulator %} 20 | {% endwith %} 21 | {% endblock %} 22 | 23 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/templates/pepulator_factory/pepulator_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load model_blocks %} 3 | 4 | {% block title %}Pepulators{% endblock %} 5 | 6 | {% block content %} 7 | {% list_block pepulators %} 8 | {% endblock %} 9 | 10 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | from django.views.generic import TemplateView, ListView, DetailView 3 | 4 | from django.contrib.contenttypes.models import ContentType 5 | import pepulator_factory.models 6 | 7 | urlpatterns = patterns('pepulator_factory', 8 | url(r'^$', TemplateView.as_view( 9 | template_name='pepulator_factory/content_list.html'), 10 | name='content_list_view'), 11 | 12 | url(r'^pepulators/$', ListView.as_view( 13 | model=pepulator_factory.models.Pepulator, 14 | template_name='pepulator_factory/pepulator_list.html', 15 | context_object_name='pepulators'), 16 | name='pepulator_list_view'), 17 | url(r'^pepulators/(?P\d+)/$', DetailView.as_view( 18 | model=pepulator_factory.models.Pepulator, 19 | template_name='pepulator_factory/pepulator_detail.html', 20 | context_object_name='pepulator'), 21 | name='pepulator_detail_view'), 22 | 23 | url(r'^distributors/$', ListView.as_view( 24 | model=pepulator_factory.models.Distributor, 25 | template_name='pepulator_factory/distributor_list.html', 26 | context_object_name='distributors'), 27 | name='distributor_list_view'), 28 | url(r'^distributors/(?P.+)/$', DetailView.as_view( 29 | model=pepulator_factory.models.Distributor, 30 | template_name='pepulator_factory/distributor_detail.html', 31 | context_object_name='distributor'), 32 | name='distributor_detail_view'), 33 | ) 34 | -------------------------------------------------------------------------------- /example_project/pepulator_factory/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /example_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@example.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': 'example_project.db', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # On Unix systems, a value of None will cause Django to use the same 27 | # timezone as the operating system. 28 | # If running in a Windows environment this must be set to the same as your 29 | # system time zone. 30 | TIME_ZONE = 'America/Chicago' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale 44 | USE_L10N = True 45 | 46 | # Absolute filesystem path to the directory that will hold user-uploaded files. 47 | # Example: "/home/media/media.lawrence.com/media/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash. 52 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # Absolute path to the directory static files should be collected to. 56 | # Don't put anything in this directory yourself; store your static files 57 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 58 | # Example: "/home/media/media.lawrence.com/static/" 59 | STATIC_ROOT = '' 60 | 61 | # URL prefix for static files. 62 | # Example: "http://media.lawrence.com/static/" 63 | STATIC_URL = '/static/' 64 | 65 | # URL prefix for admin static files -- CSS, JavaScript and images. 66 | # Make sure to use a trailing slash. 67 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 68 | ADMIN_MEDIA_PREFIX = '/static/admin/' 69 | 70 | # Additional locations of static files 71 | STATICFILES_DIRS = ( 72 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 73 | # Always use forward slashes, even on Windows. 74 | # Don't forget to use absolute paths, not relative paths. 75 | ) 76 | 77 | # List of finder classes that know how to find static files in 78 | # various locations. 79 | STATICFILES_FINDERS = ( 80 | 'django.contrib.staticfiles.finders.FileSystemFinder', 81 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 82 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 83 | ) 84 | 85 | # Make this unique, and don't share it with anybody. 86 | SECRET_KEY = 's6a32pk==8#0lfor)s69^fcnl)6znc3poy3sa%2j$-o$rouny1' 87 | 88 | # List of callables that know how to import templates from various sources. 89 | TEMPLATE_LOADERS = ( 90 | 'django.template.loaders.filesystem.Loader', 91 | 'django.template.loaders.app_directories.Loader', 92 | # 'django.template.loaders.eggs.Loader', 93 | ) 94 | 95 | MIDDLEWARE_CLASSES = ( 96 | 'django.middleware.common.CommonMiddleware', 97 | 'django.contrib.sessions.middleware.SessionMiddleware', 98 | 'django.middleware.csrf.CsrfViewMiddleware', 99 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 100 | 'django.contrib.messages.middleware.MessageMiddleware', 101 | ) 102 | 103 | ROOT_URLCONF = 'example_project.urls' 104 | 105 | TEMPLATE_DIRS = ( 106 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 107 | # Always use forward slashes, even on Windows. 108 | # Don't forget to use absolute paths, not relative paths. 109 | ) 110 | 111 | INSTALLED_APPS = ( 112 | 'django.contrib.auth', 113 | 'django.contrib.contenttypes', 114 | 'django.contrib.sessions', 115 | 'django.contrib.sites', 116 | 'django.contrib.messages', 117 | 'django.contrib.staticfiles', 118 | 'django.contrib.admin', 119 | 120 | 'django_nose', 121 | 'south', 122 | 123 | 'model_blocks', 124 | 125 | 'pepulator_factory', 126 | ) 127 | 128 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 129 | 130 | # A sample logging configuration. The only tangible logging 131 | # performed by this configuration is to send an email to 132 | # the site admins on every HTTP 500 error. 133 | # See http://docs.djangoproject.com/en/dev/topics/logging for 134 | # more details on how to customize your logging configuration. 135 | LOGGING = { 136 | 'version': 1, 137 | 'disable_existing_loggers': False, 138 | 'handlers': { 139 | 'mail_admins': { 140 | 'level': 'ERROR', 141 | 'class': 'django.utils.log.AdminEmailHandler' 142 | } 143 | }, 144 | 'loggers': { 145 | 'django.request': { 146 | 'handlers': ['mail_admins'], 147 | 'level': 'ERROR', 148 | 'propagate': True, 149 | }, 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | url(r'^admin/', include(admin.site.urls)), 8 | url(r'^', include('pepulator_factory.urls')), 9 | ) 10 | -------------------------------------------------------------------------------- /model_blocks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjumbewu/django-model-blocks/8175d7353d792cb720b4ac356f4538888bf7747c/model_blocks/__init__.py -------------------------------------------------------------------------------- /model_blocks/fixtures/pepulator_factory_data.json: -------------------------------------------------------------------------------- 1 | [{"pk": 1234, "model": "pepulator_factory.pepulator", "fields": {"color": "red", "width": 15, "manufacture_date": "2011-06-10 11:12:21", "distributed_by": "Walmart", "height": 12}}, {"pk": 1235, "model": "pepulator_factory.pepulator", "fields": {"address": "ppr://1235/", "color": "red", "width": 15, "manufacture_date": "2011-06-10 11:12:33", "distributed_by": "Walmart", "height": 12}}, {"pk": 1238, "model": "pepulator_factory.pepulator", "fields": {"color": "chartreuse", "width": 17, "manufacture_date": "2011-06-10 11:13:30", "distributed_by": "Mom & Pop", "height": 12}}, {"pk": 2345, "model": "pepulator_factory.pepulator", "fields": {"color": "blue", "width": 15, "manufacture_date": "2011-06-10 11:12:49", "distributed_by": "Walmart", "height": 12}}, {"pk": 2346, "model": "pepulator_factory.pepulator", "fields": {"color": "blue", "width": 15, "manufacture_date": "2011-06-10 11:13:02", "distributed_by": "Walmart", "height": 12}}, {"pk": "Walmart", "model": "pepulator_factory.distributor", "fields": {"capacity": 1000000}}, {"pk": "Home Depot", "model": "pepulator_factory.distributor", "fields": {"capacity": 700000}}, {"pk": "Mom & Pop", "model": "pepulator_factory.distributor", "fields": {"capacity": 175}}, {"pk": 1, "model": "pepulator_factory.knuckle", "fields": {"pepulator": 1235, "hardness": 2.3450000000000002}}, {"pk": 2, "model": "pepulator_factory.knuckle", "fields": {"pepulator": 1235, "hardness": 1.1000000000000001}}] 2 | -------------------------------------------------------------------------------- /model_blocks/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjumbewu/django-model-blocks/8175d7353d792cb720b4ac356f4538888bf7747c/model_blocks/models.py -------------------------------------------------------------------------------- /model_blocks/static/model_blocks/simple.css: -------------------------------------------------------------------------------- 1 | .instance_header > h1, 2 | .instance_list_header > h1 { 3 | font-size: 1em; 4 | font-weight: bold; 5 | 6 | padding-bottom: 0.5em; 7 | border-bottom: 1px solid black; 8 | margin-bottom: 1em; 9 | } 10 | 11 | .instance_list { 12 | margin: 0px auto; 13 | } 14 | 15 | .instance_field_label:after { 16 | content: ': '; 17 | } 18 | 19 | /* ==== INSTANCE DETAILS IN A LIST ==== */ 20 | .instance_list_item > * > .instance_header > h1, 21 | .instance_list_item > * > .instance_list_header > h1 { 22 | font-weight: normal; 23 | text-decoration: underline; 24 | 25 | margin: 0px; 26 | padding: 0px; 27 | border: none; 28 | } 29 | 30 | /* ==== REFERENCE OBJECT AS A FIELD ==== */ 31 | .instance_field > * > .instance_header > h1, 32 | .instance_field > * > .instance_list_header > h1 { 33 | font-weight: normal; 34 | text-decoration: none; 35 | 36 | margin: 0px; 37 | padding: 0px; 38 | border: none; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /model_blocks/templates/model_blocks/object_detail.html: -------------------------------------------------------------------------------- 1 | {% load model_tags %} 2 | 3 | {% spaceless %} 4 |
5 |
6 |

{{ title|default:instance }}

7 |
8 | 9 |
10 | {% for name, label, value, is_list, is_link in fields %} 11 | 12 | {% comment %} 13 | # 14 | # This default template will render each field that belongs to the 15 | # instance, including the default ID primary key. If this is not what 16 | # you want, it is easiest to deal with it in your style sheet. Creating 17 | # a custom detail template just because you want to get rid of one field 18 | # seems like it has a high code to benefit ratio. The one exception to 19 | # this is if your extra fields pose some sort of security vulnerability. 20 | # Otherwise, the set of classes on the generated elements are flexible 21 | # enough to handle most cases. 22 | # 23 | # If you DO want override this template for a given model (say, if you 24 | # want to add more interactivity, or fundamentally change the way a 25 | # field is rendered), you can do so with: 26 | # 27 | # {% load model_tags %} 28 | # ... 29 | # {% with (appname)_(modelname)_detail_template="override_file.html" %} 30 | # {% detail_block (your_instance) %} 31 | # {% endwith %} 32 | # 33 | # See the example_project included with the package source for an 34 | # example of how this can work: 35 | # 36 | # https://github.com/mjumbewu/django-model-filters/blob/master/example_project/pepulator_factory/templates/pepulator_detail.html 37 | # 38 | {% endcomment %} 39 | 40 |
41 | {% if not is_list and value != None %} 42 | {{ label|capfirst }} 43 | 44 | 45 | {% if value.get_absolute_url %} 46 | {{ value }} 47 | {% else %} 48 | {% if is_link %} 49 | {{ value }} 50 | {% else %} 51 | {{ value }} 52 | {% endif %} 53 | {% endif %} 54 | 55 | 56 | {% endif %} 57 | 58 | {% if is_list and value.all|length %} 59 | {% with title=label %} 60 | {% list_block value.all %} 61 | {% endwith %} 62 | {% endif %} 63 |
64 | {% endfor %} 65 |
66 |
67 | {% endspaceless %} 68 | -------------------------------------------------------------------------------- /model_blocks/templates/model_blocks/object_list.html: -------------------------------------------------------------------------------- 1 | {% load model_tags %} 2 | 3 | {% spaceless %} 4 |
5 |
6 |

{{ title|default:model|capfirst }}{% if not title %} List{% endif %}

7 |
8 | 9 |
    10 | {% for instance in instance_list %} 11 |
  • 12 | 13 | {% comment %} 14 | # 15 | # If the model has absolute URLs, then the list will just contain 16 | # links to each individual instance of the model. Otherwise, each 17 | # instance will be listed out in a detail block. If this is not your 18 | # desired behavior, override this template for your model: 19 | # 20 | # {% load model_tags %} 21 | # ... 22 | # {% with (appname)_(modelname)_list_template="override_file.html" %} 23 | # {% list_block (your_queryset) %} 24 | # {% endwith %} 25 | # 26 | {% endcomment %} 27 | 28 | {# Reset the title, so that child blocks don't use it mistakenly #} 29 | {% with title='' %} 30 | 31 | {% if instance.get_absolute_url %} 32 | {% teaser_block instance %} 33 | {% else %} 34 | {% detail_block instance %} 35 | {% endif %} 36 | 37 | {% endwith %} 38 | 39 |
  • 40 | {% endfor %} 41 |
42 |
43 | {% endspaceless %} 44 | -------------------------------------------------------------------------------- /model_blocks/templates/model_blocks/object_teaser.html: -------------------------------------------------------------------------------- 1 | {% load model_tags %} 2 | 3 | {% spaceless %} 4 |
5 | {{ instance }} 6 |
7 | {% endspaceless %} 8 | -------------------------------------------------------------------------------- /model_blocks/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjumbewu/django-model-blocks/8175d7353d792cb720b4ac356f4538888bf7747c/model_blocks/templatetags/__init__.py -------------------------------------------------------------------------------- /model_blocks/templatetags/model_blocks.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | 3 | from model_filters import as_detail_block, as_teaser_block, as_list_block 4 | from model_tags import detail_block, teaser_block, list_block 5 | 6 | register = Library() 7 | 8 | register.tag(detail_block) 9 | register.tag(teaser_block) 10 | register.tag(list_block) 11 | register.filter(as_detail_block) 12 | register.filter(as_teaser_block) 13 | register.filter(as_list_block) 14 | 15 | -------------------------------------------------------------------------------- /model_blocks/templatetags/model_filters.py: -------------------------------------------------------------------------------- 1 | from django.template import Context, Library 2 | from django.template.loader import get_template 3 | 4 | from model_nodes import ModelDetailNode, ModelTeaserNode, ModelListNode 5 | 6 | register = Library() 7 | 8 | @register.filter 9 | def as_detail_block(instance, title=None): 10 | """ 11 | Template filter that returns the given instance as a template-formatted 12 | block. Inserts two objects into the context: 13 | ``instance`` - The model instance 14 | ``fields`` - A list of (name, label, value) tuples representing the 15 | instance's fields 16 | """ 17 | node = ModelDetailNode(instance) 18 | return node.render(Context({'title':title})) 19 | 20 | 21 | @register.filter 22 | def as_teaser_block(instance, title=None): 23 | """ 24 | Template filter that returns the given instance as a template-formatted 25 | teaser. Inserts two objects into the context: 26 | ``instance`` - The model instance 27 | ``fields`` - A list of (name, label, value) tuples representing the 28 | instance's fields 29 | """ 30 | node = ModelTeaserNode(instance) 31 | return node.render(Context({'title':title})) 32 | 33 | 34 | @register.filter 35 | def as_list_block(queryset, list_title=None): 36 | """ 37 | Template filter that returns the given instance list as a template-formatted 38 | block. Inserts into the context: 39 | ``instance_list`` - The list of instances 40 | """ 41 | node = ModelListNode(queryset) 42 | return node.render(Context({'title':list_title})) 43 | 44 | -------------------------------------------------------------------------------- /model_blocks/templatetags/model_nodes.py: -------------------------------------------------------------------------------- 1 | from django.db.models.manager import Manager 2 | from django.template import Context, Node, Variable 3 | from django.template.loader import get_template 4 | 5 | class BaseModelBlockNode (Node): 6 | def __init__(self, thing, resolved=True): 7 | """ 8 | thing -- The thing (probably a model instance or a list of model 9 | instances) to be rendered as a block. 10 | resolved -- If True, then ``thing`` is a resolved value. If False, 11 | then thing is the name of a variable which, in context, 12 | will contain the value of the thing. 13 | """ 14 | self.thing = thing 15 | self.resolved = resolved 16 | 17 | def get_template_variable(self, thing, type_of_thing): 18 | """ 19 | Return the name of the template variable that should be used to render 20 | the thing. If the variable name does not resolve to a value, then a 21 | default template will be used. 22 | """ 23 | if isinstance(thing, (list, tuple)): 24 | template_variable = '' 25 | elif hasattr(thing, 'model') and thing: 26 | template_variable = '%s_%s_%s_template' % \ 27 | (thing.model._meta.app_label, thing.model._meta.module_name, 28 | type_of_thing) 29 | elif hasattr(thing, '_meta') and thing: 30 | template_variable = '%s_%s_%s_template' % \ 31 | (thing._meta.app_label, thing._meta.module_name, type_of_thing) 32 | else: 33 | template_variable = '' 34 | return template_variable 35 | 36 | def get_resolved_value(self, context): 37 | """ 38 | Return a resolved version of the thing being rendered -- either a model 39 | instance or a list of such instances. Specifically, if the name of the 40 | value was passed to the node as a string, resolve the value w.r.t. the 41 | context. If the actual value was passed in, then just return the value. 42 | """ 43 | if not self.resolved: 44 | res_var = Variable(self.thing).resolve(context) 45 | else: 46 | res_var = self.thing 47 | return res_var 48 | 49 | def render(self, context): 50 | # Grab the thing to render. It'll be a model instance, or a queryset, 51 | # or a list or something. 52 | thing = self.get_resolved_value(context) 53 | 54 | # Get the name of the context variable that holds the override template 55 | # file name. 56 | template_variable = self.get_template_variable(thing, self.thing_type) 57 | 58 | # If the override template file name is set, then use that. Otherwise, 59 | # use the default template for the thing type 60 | template_name = context.get(template_variable, 61 | 'model_blocks/object_%s.html' % self.thing_type) 62 | template = get_template(template_name) 63 | 64 | # Get the data necessary for rendering the thing, and add it to the 65 | # context. 66 | context.update(Context(self.get_context_data(thing, context))) 67 | 68 | # The variable 'title' must be in the context. If it's not there, add 69 | # it and set it to None 70 | if 'title' not in context: 71 | context['title'] = None 72 | 73 | # After rendering, pop off of the context so that it's back to normal 74 | # for the next tag. 75 | rendering = template.render(context) 76 | context.pop() 77 | return rendering 78 | 79 | 80 | class ModelDetailNode (BaseModelBlockNode): 81 | 82 | thing_type = 'detail' 83 | 84 | def __get_fields_list_variable(self, instance, var_suffix): 85 | # The variable name is built up from the name of the app, the name of 86 | # the model, and the specified suffix. 87 | fields_list_variable = '%s_%s_%s' % \ 88 | (instance._meta.app_label, instance._meta.module_name, var_suffix) 89 | 90 | return fields_list_variable 91 | 92 | 93 | def __get_field_list(self, instance, context, var_suffix): 94 | # Get the variable name to look for 95 | var = self.__get_fields_list_variable(instance, var_suffix) 96 | fields_str = context.get(var, None) 97 | 98 | # If the variable is not set, just return an empty list 99 | if fields_str is None: 100 | return [] 101 | 102 | # If the variable is set, split the list on comma (,) and return 103 | else: 104 | include_fields = [field.strip() for field in fields_str.split(',')] 105 | return include_fields 106 | 107 | 108 | def get_include_fields(self, instance, context): 109 | """Return a list of fields and order to include in the rendering.""" 110 | return self.__get_field_list(instance, context, 'fields') 111 | 112 | 113 | def get_exclude_fields(self, instance, context): 114 | """Return a list of fields to exclude from the rendering.""" 115 | return self.__get_field_list(instance, context, 'exclude') 116 | 117 | 118 | def get_context_data(self, instance, context): 119 | """ 120 | Calculate additional context data that will be used to render the thing. 121 | 122 | """ 123 | include_fields = self.get_include_fields(instance, context) 124 | exclude_fields = self.get_exclude_fields(instance, context) 125 | 126 | fields = [] 127 | for field in instance._meta.fields: 128 | name = field.name 129 | 130 | if name in exclude_fields: 131 | continue 132 | 133 | if include_fields and name not in include_fields: 134 | continue 135 | 136 | label = field.verbose_name 137 | value = getattr(instance, field.name) 138 | is_list = False 139 | is_link = (type(field).__name__ in ('URLField',)) 140 | model = instance._meta.module_name 141 | 142 | if value is not None: 143 | fields.append(( 144 | name, label, value, is_list, is_link, model, 145 | )) 146 | 147 | for rel_obj, model in instance._meta.get_all_related_objects_with_model(): 148 | name = rel_obj.get_accessor_name() 149 | 150 | if name in exclude_fields: 151 | continue 152 | 153 | if include_fields and name not in include_fields: 154 | continue 155 | 156 | label = name 157 | value = getattr(instance, name) 158 | is_list = isinstance(value, (list, tuple, Manager)) 159 | is_link = False 160 | 161 | if value is not None: 162 | fields.append(( 163 | name, label, value, is_list, is_link, model, 164 | )) 165 | 166 | # If include_fields was defined, then sort by the order. 167 | if include_fields: 168 | fields = sorted(fields, key=lambda field: include_fields.index(field[0])) 169 | 170 | 171 | return {'model':instance._meta.module_name, 172 | 'instance':instance, 173 | 'fields':fields} 174 | 175 | 176 | class ModelTeaserNode (ModelDetailNode): 177 | 178 | thing_type = 'teaser' 179 | 180 | 181 | class ModelListNode (BaseModelBlockNode): 182 | 183 | thing_type = 'list' 184 | 185 | def get_context_data(self, queryset, context): 186 | if hasattr(queryset, 'model') and queryset.model: 187 | model = queryset.model._meta.module_name 188 | else: 189 | model = None 190 | 191 | return {'model':model, 'instance_list':queryset} 192 | 193 | 194 | -------------------------------------------------------------------------------- /model_blocks/templatetags/model_tags.py: -------------------------------------------------------------------------------- 1 | from django.template import Library, TemplateSyntaxError 2 | 3 | from model_nodes import ModelDetailNode, ModelTeaserNode, ModelListNode 4 | 5 | register = Library() 6 | 7 | @register.tag 8 | def detail_block(parser, token): 9 | """ 10 | Template tag that takes a model instance and returns the given instance as 11 | a template-formatted block. Inserts two objects into the context: 12 | ``instance`` - The model instance 13 | ``fields`` - A list of (name, label, value) tuples representing the 14 | instance's fields 15 | 16 | You would want to use the tag instead of the filter primarily because, with 17 | a tag you can override the default template used for a particular model. 18 | For example, in the example project, Pepulators have Jambs, so when we put 19 | a detail block of a Pepulator on a page, a list of Jambs will show up. Say 20 | we don't want the default detail of jambs to display; say we want a custom 21 | Jamb detail template. Then we can say the following: 22 | 23 | {% with pepulator_factory_jamb_detail_template="pepulator_factory/jamb_detail.html" %} 24 | {% detail_block pepulator %} 25 | {% endwith %} 26 | 27 | The custom template is named by the app name (``pepulator_factory``), the 28 | model name in all lowercase (``jamb``) and the suffix ``_template``. 29 | """ 30 | try: 31 | tag_name, instance_name = token.split_contents() 32 | except ValueError: 33 | raise TemplateSyntaxError("%r tag requires exactly two arguments" % 34 | token.contents.split()[0]) 35 | 36 | node = ModelDetailNode(instance_name, resolved=False) 37 | return node 38 | 39 | 40 | @register.tag 41 | def teaser_block(parser, token): 42 | """ 43 | Template tag that takes a model instance and returns the given instance as 44 | a template-formatted block. Inserts two objects into the context: 45 | ``instance`` - The model instance 46 | ``fields`` - A list of (name, label, value) tuples representing the 47 | instance's fields 48 | 49 | You would want to use the tag instead of the filter primarily because, with 50 | a tag you can override the default template used for a particular model. 51 | For example, in the example project, Pepulators have Jambs, so when we put 52 | a detail block of a Pepulator on a page, a list of Jambs will show up. Say 53 | we don't want the default detail of jambs to display; say we want a custom 54 | Jamb detail template. Then we can say the following: 55 | 56 | {% with pepulator_factory_jamb_teaser_template="pepulator_factory/jamb_detail.html" %} 57 | {% teaser_block pepulator %} 58 | {% endwith %} 59 | 60 | The custom template is named by the app name (``pepulator_factory``), the 61 | model name in all lowercase (``jamb``) and the suffix ``_template``. 62 | """ 63 | try: 64 | tag_name, instance_name = token.split_contents() 65 | except ValueError: 66 | raise TemplateSyntaxError("%r tag requires exactly two arguments" % 67 | token.contents.split()[0]) 68 | 69 | node = ModelTeaserNode(instance_name, resolved=False) 70 | return node 71 | 72 | 73 | @register.tag 74 | def list_block(parser, token): 75 | """ 76 | Template tag that takes a model instance manager and returns the given 77 | instances as a list in a template-formatted block. Inserts two objects 78 | into the context: 79 | ``model`` - The name of the model for the list 80 | ``instance_list`` - An iterable of model instances. 81 | 82 | You would want to use the tag instead of the filter primarily because, with 83 | a tag you can override the default template used for a particular model. 84 | For example, in the example project, Pepulators have Jambs, so when we put 85 | a detail block of a Pepulator on a page, a list of Jambs will show up. Say 86 | we don't want the default detail of jambs to display; say we want a custom 87 | Jamb detail template. Then we can say the following: 88 | 89 | {% with pepulator_factory_jamb_template="pepulator_factory/jamb_detail.html" %} 90 | {% detail_block pepulator %} 91 | {% endwith %} 92 | 93 | The custom template is named by the app name (``pepulator_factory``), the 94 | model name in all lowercase (``jamb``) and the suffix ``_template``. 95 | """ 96 | try: 97 | tag_name, list_name = token.split_contents() 98 | except ValueError: 99 | raise TemplateSyntaxError("%r tag requires exactly two arguments" % 100 | token.contents.split()[0]) 101 | 102 | node = ModelListNode(list_name, resolved=False) 103 | return node 104 | 105 | -------------------------------------------------------------------------------- /model_blocks/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the model blocks 3 | """ 4 | 5 | import datetime 6 | 7 | from django.test import TestCase 8 | from mock import Mock 9 | 10 | from django.db.models import Model, IntegerField, DateTimeField, CharField 11 | from django.template import Context, Template, TemplateSyntaxError 12 | 13 | from example_project.pepulator_factory.models import Pepulator, Distributor 14 | from model_blocks.templatetags import model_filters 15 | from model_blocks.templatetags import model_nodes 16 | 17 | class DetailBlockFilterTest (TestCase): 18 | fixtures = ['pepulator_factory_data.json'] 19 | 20 | def setUp(self): 21 | # Mock Django's get_template so that it doesn't load a real file; 22 | # instead just return a template that allows us to verify the context 23 | model_nodes.get_template = Mock( 24 | return_value=Template(('{{ title|default_if_none:instance|safe }}:{{ model|safe }},' 25 | '{% for name, label, value, is_list, is_link in fields %}' 26 | '{{ name|safe }},' 27 | '{{ label|safe }},' 28 | '{% if not is_list %}' 29 | '{% if is_link %}' 30 | '@{{ value }}' 31 | '{% else %}' 32 | '{{ value|safe }}' 33 | '{% endif %}' 34 | '{% else %}' 35 | '[{% for item in value.all %}{{ item|safe }},{% endfor %}]' 36 | '{% endif %},' 37 | '{% endfor %}'))) 38 | 39 | 40 | def test_model_format(self): 41 | """Tests that a given model is formatted as expected.""" 42 | pepulator = Pepulator.objects.get(serial_number=1235) 43 | 44 | expected_detail = (u"Pepulator #1235:pepulator," 45 | "serial_number,serial number,1235," 46 | "height,height,12," 47 | "width,width,15," 48 | "manufacture_date,manufacture date,2011-06-10 11:12:33," 49 | "color,color,red," 50 | "address,address,@ppr://1235/," 51 | "distributed_by,distributed by,Walmart," 52 | "knuckles,knuckles,[Knuckle of hardness 2.35,Knuckle of hardness 1.10,]," 53 | "jambs,jambs,[]," 54 | ) 55 | detail = model_filters.as_detail_block(pepulator) 56 | 57 | model_nodes.get_template.assert_called_with('model_blocks/object_detail.html') 58 | self.assertEqual(detail, expected_detail) 59 | 60 | 61 | def test_filter_is_registered(self): 62 | """Test that the filter can be used from within a template""" 63 | 64 | template = Template(('{% load model_filters %}' 65 | '{{ pepulator|as_detail_block }}')) 66 | 67 | pepulator = Pepulator.objects.get(serial_number=1235) 68 | context = Context({'pepulator':pepulator}) 69 | 70 | expected_detail = (u"Pepulator #1235:pepulator," 71 | "serial_number,serial number,1235," 72 | "height,height,12," 73 | "width,width,15," 74 | "manufacture_date,manufacture date,2011-06-10 11:12:33," 75 | "color,color,red," 76 | "address,address,@ppr://1235/," 77 | "distributed_by,distributed by,Walmart," 78 | "knuckles,knuckles,[Knuckle of hardness 2.35,Knuckle of hardness 1.10,]," 79 | "jambs,jambs,[]," 80 | ) 81 | detail = template.render(context) 82 | 83 | model_nodes.get_template.assert_called_with('model_blocks/object_detail.html') 84 | self.assertEqual(detail, expected_detail) 85 | 86 | 87 | def test_title_is_used(self): 88 | """Test that a title is used if provided""" 89 | 90 | template = Template(('{% load model_filters %}' 91 | '{{ pepulator|as_detail_block:"My Pepulator" }}')) 92 | 93 | pepulator = Pepulator.objects.get(serial_number=1235) 94 | context = Context({'pepulator':pepulator}) 95 | 96 | expected_detail = (u"My Pepulator:pepulator," 97 | "serial_number,serial number,1235," 98 | "height,height,12," 99 | "width,width,15," 100 | "manufacture_date,manufacture date,2011-06-10 11:12:33," 101 | "color,color,red," 102 | "address,address,@ppr://1235/," 103 | "distributed_by,distributed by,Walmart," 104 | "knuckles,knuckles,[Knuckle of hardness 2.35,Knuckle of hardness 1.10,]," 105 | "jambs,jambs,[]," 106 | ) 107 | detail = template.render(context) 108 | 109 | model_nodes.get_template.assert_called_with('model_blocks/object_detail.html') 110 | self.assertEqual(detail, expected_detail) 111 | 112 | 113 | def test_related_fields(self): 114 | """Tests that related fields not defined on the model are included.""" 115 | pepulator = Distributor.objects.get(name="Mom & Pop") 116 | 117 | expected_detail = (u"Mom & Pop:distributor," 118 | "name,name,Mom & Pop," 119 | "capacity,capacity,175," 120 | "stock,stock,[Pepulator #1238,]," 121 | ) 122 | detail = model_filters.as_detail_block(pepulator) 123 | 124 | model_nodes.get_template.assert_called_with('model_blocks/object_detail.html') 125 | self.assertEqual(detail, expected_detail) 126 | 127 | 128 | class TeaserBlockFilterTest (TestCase): 129 | fixtures = ['pepulator_factory_data.json'] 130 | 131 | def setUp(self): 132 | # Mock Django's get_template so that it doesn't load a real file; 133 | # instead just return a template that allows us to verify the context 134 | model_nodes.get_template = Mock( 135 | return_value=Template('{{ title|default_if_none:instance|safe }}:{{ model|safe }},{% for name, label, value, is_list in fields %}{{ name|safe }},{{ label|safe }},{% if not is_list %}{{ value|safe }}{% else %}[{% for item in value.all %}{{ item|safe }},{% endfor %}]{% endif %},{% endfor %}')) 136 | 137 | 138 | def test_model_format(self): 139 | """Tests that a given model is formatted as expected.""" 140 | pepulator = Pepulator.objects.get(serial_number=1235) 141 | 142 | expected_teaser = (u"Pepulator #1235:pepulator," 143 | "serial_number,serial number,1235," 144 | "height,height,12," 145 | "width,width,15," 146 | "manufacture_date,manufacture date,2011-06-10 11:12:33," 147 | "color,color,red," 148 | "address,address,ppr://1235/," 149 | "distributed_by,distributed by,Walmart," 150 | "knuckles,knuckles,[Knuckle of hardness 2.35,Knuckle of hardness 1.10,]," 151 | "jambs,jambs,[]," 152 | ) 153 | teaser = model_filters.as_teaser_block(pepulator) 154 | 155 | model_nodes.get_template.assert_called_with('model_blocks/object_teaser.html') 156 | self.assertEqual(teaser, expected_teaser) 157 | 158 | 159 | class ListBlockFilterTest (TestCase): 160 | fixtures = ['pepulator_factory_data.json'] 161 | 162 | def setUp(self): 163 | # Mock Django's get_template so that it doesn't load a real file; 164 | # instead just return a template that allows us to verify the context 165 | model_nodes.get_template = Mock( 166 | return_value=Template('{{ title|default_if_none:model|capfirst }}{% if not title %}s{% endif %}:{{ instance_list|safe }}')) 167 | 168 | 169 | def test_list_format(self): 170 | """Tests that a given model is formatted as expected.""" 171 | pepulator_list = Pepulator.objects.filter(serial_number__gt=2000) 172 | 173 | expected_rendering = (u"Pepulators:[, " 174 | "]") 175 | rendering = model_filters.as_list_block(pepulator_list) 176 | 177 | model_nodes.get_template.assert_called_with('model_blocks/object_list.html') 178 | self.assertEqual(rendering, expected_rendering) 179 | 180 | 181 | def test_filter_is_registered(self): 182 | """Test that the filter can be used from within a template""" 183 | 184 | template = Template(('{% load model_filters %}' 185 | '{{ pepulators|as_list_block }}')) 186 | pepulator_list = Pepulator.objects.filter(serial_number__gt=2000) 187 | context = Context({'pepulators':pepulator_list}) 188 | 189 | expected_rendering = (u"Pepulators:[, " 190 | "]") 191 | rendering = template.render(context) 192 | 193 | model_nodes.get_template.assert_called_with('model_blocks/object_list.html') 194 | self.assertEqual(rendering, expected_rendering) 195 | 196 | 197 | def test_empty_queryset(self): 198 | """Test that the filter can be used from within a template""" 199 | 200 | template = Template(('{% load model_filters %}' 201 | '{{ pepulators|as_list_block }}')) 202 | pepulator_list = Pepulator.objects.filter(serial_number__gt=5000) 203 | context = Context({'pepulators':pepulator_list}) 204 | 205 | expected_rendering = (u"Pepulators:[]") 206 | rendering = template.render(context) 207 | 208 | model_nodes.get_template.assert_called_with('model_blocks/object_list.html') 209 | self.assertEqual(rendering, expected_rendering) 210 | 211 | 212 | def test_non_query_set_results_in_no_model(self): 213 | """Test that when a non queryset is used, the model is None""" 214 | # Why? Because we try to read the model off of the queryset. If we just 215 | # have a list of objects, then we don't know the model. 216 | 217 | template = Template(('{% load model_filters %}' 218 | '{{ pepulators|as_list_block }}')) 219 | pepulator_list = [p for p in Pepulator.objects.filter(serial_number__gt=2000)] 220 | context = Context({'pepulators':pepulator_list}) 221 | 222 | expected_rendering = (u"Nones:[, " 223 | "]") 224 | rendering = template.render(context) 225 | 226 | model_nodes.get_template.assert_called_with('model_blocks/object_list.html') 227 | self.assertEqual(rendering, expected_rendering) 228 | 229 | 230 | def test_empty_list(self): 231 | """Test that when a non queryset is used, the model is None""" 232 | 233 | template = Template(('{% load model_filters %}' 234 | '{{ pepulators|as_list_block }}')) 235 | pepulator_list = [] 236 | context = Context({'pepulators':pepulator_list}) 237 | 238 | expected_rendering = (u"Nones:[]") 239 | rendering = template.render(context) 240 | 241 | model_nodes.get_template.assert_called_with('model_blocks/object_list.html') 242 | self.assertEqual(rendering, expected_rendering) 243 | 244 | 245 | def test_alternate_title_is_used(self): 246 | """Test that a list title is used if provided""" 247 | template = Template(('{% load model_filters %}' 248 | '{{ pepulators|as_list_block:"Some Pepulators" }}')) 249 | pepulator_list = Pepulator.objects.filter(serial_number__gt=2000) 250 | context = Context({'pepulators':pepulator_list}) 251 | 252 | expected_rendering = (u"Some Pepulators:[, " 253 | "]") 254 | rendering = template.render(context) 255 | 256 | model_nodes.get_template.assert_called_with('model_blocks/object_list.html') 257 | self.assertEqual(rendering, expected_rendering) 258 | 259 | 260 | class DetailBlockTagTest (TestCase): 261 | fixtures = ['pepulator_factory_data.json'] 262 | 263 | def setUp(self): 264 | # Mock Django's get_template so that it doesn't load a real file; 265 | # instead just return a template that allows us to verify the context 266 | model_nodes.get_template = Mock( 267 | return_value=Template('{{ title|default_if_none:instance|safe }}:{{ model|safe }},{% for name, label, value, is_list in fields %}{{ name|safe }},{{ label|safe }},{% if not is_list %}{{ value|safe }}{% else %}[{% for item in value.all %}{{ item|safe }},{% endfor %}]{% endif %},{% endfor %}')) 268 | 269 | 270 | def test_tag_is_registered(self): 271 | """Test that the filter can be used from within a template""" 272 | 273 | template = Template(('{% load model_tags %}' 274 | '{% with pepulator_factory_pepulator_detail_template="pepulator_factory/pepulator_detail.html" %}' 275 | '{% detail_block pepulator %}' 276 | '{% endwith %}')) 277 | 278 | pepulator = Pepulator.objects.get(serial_number=1235) 279 | context = Context({'pepulator':pepulator}) 280 | 281 | expected_detail = (u"Pepulator #1235:pepulator," 282 | "serial_number,serial number,1235," 283 | "height,height,12," 284 | "width,width,15," 285 | "manufacture_date,manufacture date,2011-06-10 11:12:33," 286 | "color,color,red," 287 | "address,address,ppr://1235/," 288 | "distributed_by,distributed by,Walmart," 289 | "knuckles,knuckles,[Knuckle of hardness 2.35,Knuckle of hardness 1.10,]," 290 | "jambs,jambs,[]," 291 | ) 292 | detail = template.render(context) 293 | 294 | model_nodes.get_template.assert_called_with('pepulator_factory/pepulator_detail.html') 295 | self.assertEqual(detail, expected_detail) 296 | 297 | 298 | def test_with_specific_fields(self): 299 | """Test that the included fields spec is respected""" 300 | 301 | template = Template(('{% load model_tags %}' 302 | '{% with pepulator_factory_pepulator_detail_template="pepulator_factory/pepulator_detail.html" %}' 303 | '{% with pepulator_factory_pepulator_fields="serial_number, color, height, width" %}' 304 | '{% detail_block pepulator %}' 305 | '{% endwith %}' 306 | '{% endwith %}')) 307 | 308 | pepulator = Pepulator.objects.get(serial_number=1235) 309 | context = Context({'pepulator':pepulator}) 310 | 311 | expected_detail = (u"Pepulator #1235:pepulator," 312 | "serial_number,serial number,1235," 313 | "color,color,red," 314 | "height,height,12," 315 | "width,width,15," 316 | ) 317 | detail = template.render(context) 318 | 319 | self.assertEqual(detail, expected_detail) 320 | 321 | 322 | def test_with_excluded_fields(self): 323 | """Test that the excluded fields spec is respected""" 324 | 325 | template = Template(('{% load model_tags %}' 326 | '{% with pepulator_factory_pepulator_detail_template="pepulator_factory/pepulator_detail.html" %}' 327 | '{% with pepulator_factory_pepulator_exclude="knuckles, jambs, color, address" %}' 328 | '{% detail_block pepulator %}' 329 | '{% endwith %}' 330 | '{% endwith %}')) 331 | 332 | pepulator = Pepulator.objects.get(serial_number=1235) 333 | context = Context({'pepulator':pepulator}) 334 | 335 | expected_detail = (u"Pepulator #1235:pepulator," 336 | "serial_number,serial number,1235," 337 | "height,height,12," 338 | "width,width,15," 339 | "manufacture_date,manufacture date,2011-06-10 11:12:33," 340 | "distributed_by,distributed by,Walmart," 341 | ) 342 | detail = template.render(context) 343 | 344 | self.assertEqual(detail, expected_detail) 345 | 346 | 347 | def test_fail_on_wrong_number_of_arguments(self): 348 | self.assertRaises(TemplateSyntaxError, Template, 349 | ('{% load model_tags %}' 350 | '{% detail_block pepulator "overflow" %}')) 351 | self.assertRaises(TemplateSyntaxError, Template, 352 | ('{% load model_tags %}' 353 | '{% detail_block %}')) 354 | 355 | 356 | class TeaserBlockTagTest (TestCase): 357 | fixtures = ['pepulator_factory_data.json'] 358 | 359 | def setUp(self): 360 | # Mock Django's get_template so that it doesn't load a real file; 361 | # instead just return a template that allows us to verify the context 362 | model_nodes.get_template = Mock( 363 | return_value=Template('{{ title|default_if_none:instance|safe }}:{{ model|safe }},{% for name, label, value, is_list in fields %}{{ name|safe }},{{ label|safe }},{% if not is_list %}{{ value|safe }}{% else %}[{% for item in value.all %}{{ item|safe }},{% endfor %}]{% endif %},{% endfor %}')) 364 | 365 | 366 | def test_tag_is_registered(self): 367 | """Test that the filter can be used from within a template""" 368 | 369 | template = Template(('{% load model_tags %}' 370 | '{% with pepulator_factory_pepulator_teaser_template="pepulator_factory/pepulator_teaser.html" %}' 371 | '{% teaser_block pepulator %}' 372 | '{% endwith %}')) 373 | 374 | pepulator = Pepulator.objects.get(serial_number=1235) 375 | context = Context({'pepulator':pepulator}) 376 | 377 | expected_teaser = (u"Pepulator #1235:pepulator," 378 | "serial_number,serial number,1235," 379 | "height,height,12," 380 | "width,width,15," 381 | "manufacture_date,manufacture date,2011-06-10 11:12:33," 382 | "color,color,red," 383 | "address,address,ppr://1235/," 384 | "distributed_by,distributed by,Walmart," 385 | "knuckles,knuckles,[Knuckle of hardness 2.35,Knuckle of hardness 1.10,]," 386 | "jambs,jambs,[]," 387 | ) 388 | teaser = template.render(context) 389 | 390 | model_nodes.get_template.assert_called_with('pepulator_factory/pepulator_teaser.html') 391 | self.assertEqual(teaser, expected_teaser) 392 | 393 | def test_fail_on_wrong_number_of_arguments(self): 394 | self.assertRaises(TemplateSyntaxError, Template, 395 | ('{% load model_tags %}' 396 | '{% teaser_block pepulator "overflow" %}')) 397 | self.assertRaises(TemplateSyntaxError, Template, 398 | ('{% load model_tags %}' 399 | '{% teaser_block %}')) 400 | 401 | 402 | class ListBlockTagTest (TestCase): 403 | fixtures = ['pepulator_factory_data.json'] 404 | 405 | def setUp(self): 406 | # Mock Django's get_template so that it doesn't load a real file; 407 | # instead just return a template that allows us to verify the context 408 | model_nodes.get_template = Mock( 409 | return_value=Template('{{ title|default_if_none:model|capfirst }}{% if not title %}s{% endif %}:{{ instance_list|safe }}')) 410 | 411 | 412 | def test_filter_is_registered(self): 413 | """Test that the filter can be used from within a template""" 414 | 415 | template = Template(('{% load model_tags %}' 416 | '{% with pepulator_factory_pepulator_list_template="pepulator_factory/pepulator_list.html" %}' 417 | '{% list_block pepulators %}' 418 | '{% endwith %}')) 419 | pepulator_list = Pepulator.objects.filter(serial_number__gt=2000) 420 | context = Context({'pepulators':pepulator_list}) 421 | 422 | expected_rendering = (u"Pepulators:[, " 423 | "]") 424 | rendering = template.render(context) 425 | 426 | model_nodes.get_template.assert_called_with('pepulator_factory/pepulator_list.html') 427 | self.assertEqual(rendering, expected_rendering) 428 | 429 | def test_fail_on_wrong_number_of_arguments(self): 430 | self.assertRaises(TemplateSyntaxError, Template, 431 | ('{% load model_tags %}' 432 | '{% list_block pepulators "overflow" %}')) 433 | self.assertRaises(TemplateSyntaxError, Template, 434 | ('{% load model_tags %}' 435 | '{% list_block %}')) 436 | 437 | class ModelBlockModuleTest (TestCase): 438 | def test_all_tags_and_filters_loaded(self): 439 | template = Template(('{% load model_blocks %}' 440 | '{% detail_block pepulator %}' 441 | '{% list_block pepulators %}' 442 | '{{ pepulator|as_detail_block }}' 443 | '{{ pepulators|as_list_block }}')) 444 | 445 | # We just care that everything loaded, and we were able to get here 446 | # without incidence. 447 | self.assert_(True) 448 | 449 | 450 | class SideEffectsTest (TestCase): 451 | fixtures = ['pepulator_factory_data.json'] 452 | 453 | def setUp(self): 454 | # Mock Django's get_template so that it doesn't load a real file; 455 | # instead just return a template that allows us to verify the context 456 | model_nodes.get_template = Mock( 457 | return_value=Template('{{ title|default_if_none:model|capfirst }}{% if not title %}s{% endif %}')) 458 | 459 | 460 | def test_model_doesnt_carry_over_into_future_blocks(self): 461 | template = Template(('{% load model_tags %}' 462 | '{{ model }}' 463 | '{% list_block distributors %}' 464 | '{{ model }}')) 465 | distributor_list = Distributor.objects.all() 466 | context = Context({'model':'My String', 467 | 'distributors':distributor_list}) 468 | 469 | expected_rendering = (u"My String" 470 | "Distributors" 471 | "My String") 472 | rendering = template.render(context) 473 | 474 | self.assertEqual(rendering, expected_rendering) 475 | 476 | -------------------------------------------------------------------------------- /model_blocks/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | South 3 | mock 4 | django-nose 5 | coverage 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | model_blocks = __import__('model_blocks') 4 | 5 | readme_file = 'README.rst' 6 | try: 7 | long_description = open(readme_file).read() 8 | except IOError, err: 9 | sys.stderr.write("[ERROR] Cannot find file specified as " 10 | "``long_description`` (%s)\n" % readme_file) 11 | sys.exit(1) 12 | 13 | setup(name='django-model-blocks', 14 | version='0.8.9', 15 | description=('Simple filters and tags for generic Django ' 16 | 'model template partials'), 17 | long_description=long_description, 18 | zip_safe=False, 19 | author='Mjumbe Wawatu Ukweli', 20 | author_email='mjumbewu@kwawatu.com', 21 | url='https://github.com/mjumbewu/django-model-blocks/', 22 | download_url='https://github.com/mjumbewu/django-model-blocks/downloads', 23 | packages = find_packages(exclude=['example_project', 'example_project.*']), 24 | include_package_data=True, 25 | install_requires = [ 26 | 'Django>=1.2.1', 27 | ], 28 | obsoletes = [ 29 | 'model_filters', 30 | ], 31 | provides = [ 32 | 'model_blocks', 33 | ], 34 | classifiers = ['Development Status :: 4 - Beta', 35 | 'Environment :: Web Environment', 36 | 'Framework :: Django', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: BSD License', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Topic :: Utilities'], 42 | ) 43 | 44 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | python setup.py test 3 | 4 | """ 5 | import os 6 | import sys 7 | 8 | os.environ["DJANGO_SETTINGS_MODULE"] = 'example_project.settings' 9 | from example_project import settings 10 | 11 | settings.INSTALLED_APPS = ( 12 | 'example_project.pepulator_factory', 13 | 'model_blocks', 14 | ) 15 | 16 | def main(): 17 | from django.test.utils import get_runner 18 | test_runner = get_runner(settings)(interactive=False) 19 | failures = test_runner.run_tests(['model_blocks',]) 20 | sys.exit(failures) 21 | 22 | if __name__ == '__main__': 23 | main() 24 | --------------------------------------------------------------------------------