diff --git a/.gitignore b/.gitignore index 15b34c3..e1c8ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ media/css/*.r*.css doc_src/_build MANIFEST dist/ +.venv +example/media/ diff --git a/categories/__init__.py b/categories/__init__.py index 1ef1f22..b626bf3 100644 --- a/categories/__init__.py +++ b/categories/__init__.py @@ -1,6 +1,6 @@ __version_info__ = { 'major': 0, - 'minor': 6, + 'minor': 7, 'micro': 0, 'releaselevel': 'final', 'serial': 1 @@ -17,7 +17,8 @@ def get_version(): __version__ = get_version() -registry = {} +field_registry = {} +model_registry = {} try: import fields @@ -30,7 +31,16 @@ try: """ pass - registry = {} + # The field registry keeps track of the individual fields created. + # {'app.model.field': Field(**extra_params)} + # Useful for doing a schema migration + field_registry = {} + + # The model registry keeps track of which models have one or more fields + # registered. + # {'app': [model1, model2]} + # Useful for admin alteration + model_registry = {} def register_m2m(model, field_name='categories', extra_params={}): return _register(model, field_name, extra_params, fields.CategoryM2MField) @@ -39,16 +49,21 @@ try: return _register(model, field_name, extra_params, fields.CategoryFKField) def _register(model, field_name, extra_params={}, field=fields.CategoryFKField): - registry_name = "%s.%s" % (model.__name__, field_name) + app_label = model._meta.app_label + registry_name = ".".join((app_label, model.__name__, field_name)).lower() - if registry_name in registry: + if registry_name in field_registry: return #raise AlreadyRegistered - registry[registry_name] = model opts = model._meta try: opts.get_field(field_name) except FieldDoesNotExist: - field(**extra_params).contribute_to_class(model, field_name) + if app_label not in model_registry: + model_registry[app_label] = [] + if model not in model_registry[app_label]: + model_registry[app_label].append(model) + field_registry[registry_name] = field(**extra_params) + field_registry[registry_name].contribute_to_class(model, field_name) from categories import settings from django.core.exceptions import ImproperlyConfigured diff --git a/categories/admin.py b/categories/admin.py index a100e1c..c3463ed 100644 --- a/categories/admin.py +++ b/categories/admin.py @@ -7,8 +7,8 @@ from mptt.forms import TreeNodeChoiceField from editor.tree_editor import TreeEditor from genericcollection import GenericCollectionTabularInline -from settings import ALLOW_SLUG_CHANGE, RELATION_MODELS -from categories import registry +from settings import ALLOW_SLUG_CHANGE, RELATION_MODELS, JAVASCRIPT_URL +from categories import model_registry from models import Category class NullTreeNodeChoiceField(forms.ModelChoiceField): @@ -85,7 +85,7 @@ class CategoryAdmin(TreeEditor, admin.ModelAdmin): 'fields': ('parent', 'name', 'thumbnail') }), ('Meta Data', { - 'fields': ('alternate_title', 'description', 'meta_keywords', 'meta_extra'), + 'fields': ('alternate_title', 'alternate_url', 'description', 'meta_keywords', 'meta_extra'), 'classes': ('collapse',), }), ('Advanced', { @@ -97,14 +97,14 @@ class CategoryAdmin(TreeEditor, admin.ModelAdmin): inlines = [InlineCategoryRelation,] class Media: - js = (settings.STATIC_URL + 'js/genericcollections.js',) + js = (JAVASCRIPT_URL + 'genericcollections.js',) admin.site.register(Category, CategoryAdmin) for model, modeladmin in admin.site._registry.items(): - if model in registry.values() and modeladmin.fieldsets: + if model in model_registry.values() and modeladmin.fieldsets: fieldsets = getattr(modeladmin, 'fieldsets', ()) - fields = [cat.split('.')[1] for cat in registry if registry[cat] == model] + fields = [cat.split('.')[2] for cat in model_registry if model_registry[cat] == model] # check each field to see if already defined for cat in fields: for k,v in fieldsets: diff --git a/categories/management/commands/__init__.py b/categories/management/commands/__init__.py index e69de29..5da0f94 100644 --- a/categories/management/commands/__init__.py +++ b/categories/management/commands/__init__.py @@ -0,0 +1,4 @@ +from django.db.models.signals import post_syncdb +from categories.migration import migrate_app + +post_syncdb.connect(migrate_app) diff --git a/categories/management/commands/add_category_fields.py b/categories/management/commands/add_category_fields.py new file mode 100644 index 0000000..0d247d6 --- /dev/null +++ b/categories/management/commands/add_category_fields.py @@ -0,0 +1,28 @@ +from django.core.management.base import BaseCommand, CommandError + +class Command(BaseCommand): + """ + Alter one or more models' tables with the registered attributes + """ + help = "Alter the tables for all registered models, or just specified models" + args = "[appname ...]" + can_import_settings = True + requires_model_validation = False + + def handle(self, *args, **options): + """ + Alter the tables + """ + try: + from south.db import db + except ImportError: + raise ImproperlyConfigured("South must be installed for this command to work") + + from categories.migration import migrate_app + from categories import model_registry + if args: + for app in args: + migrate_app(app) + else: + for app in model_registry: + migrate_app(app) diff --git a/categories/management/commands/drop_category_field.py b/categories/management/commands/drop_category_field.py new file mode 100644 index 0000000..8108e20 --- /dev/null +++ b/categories/management/commands/drop_category_field.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand, CommandError + +class Command(BaseCommand): + """ + Alter one or more models' tables with the registered attributes + """ + help = "Drop the given field from the given model's table" + args = "appname modelname fieldname" + can_import_settings = True + requires_model_validation = False + + def handle(self, *args, **options): + """ + Alter the tables + """ + try: + from south.db import db + except ImportError: + raise ImproperlyConfigured("South must be installed for this command to work") + + from categories.migration import drop_field + from categories import model_registry + if len(args) != 3: + print "You must specify an Application name, a Model name and a Field name" + + drop_field(*args) diff --git a/categories/migration.py b/categories/migration.py new file mode 100644 index 0000000..18ef603 --- /dev/null +++ b/categories/migration.py @@ -0,0 +1,90 @@ +from django.db import models +from django.db.utils import DatabaseError + +from south.db import db +from south.signals import post_migrate +from categories.fields import CategoryM2MField, CategoryFKField +from categories.models import Category +from categories import model_registry, field_registry + +def migrate_app(app, *args, **kwargs): + """ + Migrate all models of this app registered + """ + # pull the information from the registry + fields = [fld for fld in field_registry.keys() if fld.startswith(app)] + + # call the south commands to add the fields/tables + for fld in fields: + app_name, model_name, field_name = fld.split('.') + + # Table is typically appname_modelname, but it could be different + # always best to be sure. + mdl = models.get_model(app_name, model_name) + + if isinstance(field_registry[fld], CategoryFKField): + print "Adding ForeignKey %s to %s" % (field_name, model_name) + try: + db.start_transaction() + table_name = mdl._meta.db_table + field_registry[fld].default=-1 + db.add_column(table_name, field_name, field_registry[fld], keep_default=False) + db.commit_transaction() + except DatabaseError, e: + db.rollback_transaction() + if "already exists" in str(e): + print "Already exists" + else: + raise e + elif isinstance(field_registry[fld], CategoryM2MField): + print "Adding Many2Many table between %s and %s" % (model_name, 'category') + table_name = "%s_%s" % (mdl._meta.db_table, 'categories') + try: + db.start_transaction() + db.create_table(table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + (model_name, models.ForeignKey(mdl, null=False)), + ('category', models.ForeignKey(Category, null=False)) + )) + db.create_unique(table_name, ['%s_id' % model_name, 'category_id']) + db.commit_transaction() + except DatabaseError, e: + db.rollback_transaction() + if "already exists" in str(e): + print "Already exists" + else: + raise e + +def drop_field(app_name, model_name, field_name): + """ + Drop the given field from the app's model + """ + # Table is typically appname_modelname, but it could be different + # always best to be sure. + mdl = models.get_model(app_name, model_name) + + fld = "%s.%s.%s" % (app_name, model_name, field_name) + + if isinstance(field_registry[fld], CategoryFKField): + print "Dropping ForeignKey %s from %s" % (field_name, model_name) + try: + db.start_transaction() + table_name = mdl._meta.db_table + db.delete_column(table_name, field_name) + db.commit_transaction() + except DatabaseError, e: + db.rollback_transaction() + raise e + elif isinstance(field_registry[fld], CategoryM2MField): + print "Dropping Many2Many table between %s and %s" % (model_name, 'category') + table_name = "%s_%s" % (mdl._meta.db_table, 'categories') + try: + db.start_transaction() + db.delete_table(table_name, cascade=False) + db.commit_transaction() + except DatabaseError, e: + db.rollback_transaction() + raise e + + +post_migrate.connect(migrate_app) \ No newline at end of file diff --git a/categories/migration/2add_metadata.sql b/categories/migration/2add_metadata.sql deleted file mode 100644 index c677794..0000000 --- a/categories/migration/2add_metadata.sql +++ /dev/null @@ -1,6 +0,0 @@ -BEGIN; -ALTER TABLE "categories_category" ADD COLUMN "alternate_title" varchar(100); -ALTER TABLE "categories_category" ADD COLUMN "meta_keywords" varchar(255); -ALTER TABLE "categories_category" ADD COLUMN "meta_extra" text; -ALTER TABLE "categories_category" ALTER COLUMN "description" TYPE text; -COMMIT; diff --git a/categories/migration/2drop_metadata.sql b/categories/migration/2drop_metadata.sql deleted file mode 100644 index 5a4c7b0..0000000 --- a/categories/migration/2drop_metadata.sql +++ /dev/null @@ -1,6 +0,0 @@ -BEGIN; -ALTER TABLE "categories_category" DROP COLUMN "alternate_title" varchar(100); -ALTER TABLE "categories_category" DROP COLUMN "meta_keywords" varchar(255); -ALTER TABLE "categories_category" DROP COLUMN "meta_extra" text; -ALTER TABLE "categories_category" ALTER COLUMN "description" TYPE varchar(255); -COMMIT; diff --git a/categories/migration/add_description.sql b/categories/migration/add_description.sql deleted file mode 100644 index 4b8e864..0000000 --- a/categories/migration/add_description.sql +++ /dev/null @@ -1,3 +0,0 @@ -BEGIN; -ALTER TABLE "categories_category" ADD COLUMN "description" varchar(255); -COMMIT; \ No newline at end of file diff --git a/categories/migration/add_thumbnail.sql b/categories/migration/add_thumbnail.sql deleted file mode 100644 index a5aaa85..0000000 --- a/categories/migration/add_thumbnail.sql +++ /dev/null @@ -1,3 +0,0 @@ -BEGIN; -ALTER TABLE "categories_category" ADD COLUMN "thumbnail" varchar(100); -COMMIT; diff --git a/categories/migration/drop_description.sql b/categories/migration/drop_description.sql deleted file mode 100644 index 24de722..0000000 --- a/categories/migration/drop_description.sql +++ /dev/null @@ -1,3 +0,0 @@ -BEGIN; -ALTER TABLE "categories_category" DROP COLUMN "description"; -COMMIT; \ No newline at end of file diff --git a/categories/migrations/0005_auto__add_field_category_alternate_url.py b/categories/migrations/0005_auto__add_field_category_alternate_url.py new file mode 100644 index 0000000..06cb713 --- /dev/null +++ b/categories/migrations/0005_auto__add_field_category_alternate_url.py @@ -0,0 +1,44 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Category.alternate_url' + db.add_column('categories_category', 'alternate_url', self.gf('django.db.models.fields.URLField')(default='', max_length=200, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Category.alternate_url' + db.delete_column('categories_category', 'alternate_url') + + + models = { + 'categories.category': { + 'Meta': {'ordering': "('tree_id', 'lft')", 'unique_together': "(('parent', 'name'),)", 'object_name': 'Category'}, + 'alternate_title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'alternate_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'meta_extra': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'meta_keywords': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'order': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['categories.Category']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'thumbnail': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'thumbnail_height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'thumbnail_width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['categories'] diff --git a/categories/models.py b/categories/models.py index 75a2cea..d14b901 100644 --- a/categories/models.py +++ b/categories/models.py @@ -34,8 +34,11 @@ class Category(MPTTModel): blank=True, default="", max_length=100, - help_text="An alternative title to use on pages with this category." - ) + help_text="An alternative title to use on pages with this category.") + alternate_url = models.URLField( + blank=True, + verify_exists=False, + help_text="An alternative URL to use instead of the one derived from the category hierarchy.") description = models.TextField(blank=True, null=True) meta_keywords = models.CharField( blank=True, @@ -53,6 +56,8 @@ class Category(MPTTModel): def get_absolute_url(self): """Return a path""" + if self.alternate_url: + return self.alternate_url prefix = reverse('categories_tree_list') ancestors = list(self.get_ancestors()) + [self,] return prefix + '/'.join([force_unicode(i.slug) for i in ancestors]) + '/' diff --git a/categories/settings.py b/categories/settings.py index 33e7bbf..0f8678a 100644 --- a/categories/settings.py +++ b/categories/settings.py @@ -11,6 +11,7 @@ DEFAULT_SETTINGS = { 'FK_REGISTRY': {}, 'THUMBNAIL_UPLOAD_PATH': 'uploads/categories/thumbnails', 'THUMBNAIL_STORAGE': settings.DEFAULT_FILE_STORAGE, + 'JAVASCRIPT_URL': getattr(settings, 'STATIC_URL', settings.MEDIA_URL) + 'js/', } DEFAULT_SETTINGS.update(getattr(settings, 'CATEGORIES_SETTINGS', {})) diff --git a/categories/templates/admin/edit_inline/gen_coll_tabular.html b/categories/templates/admin/edit_inline/gen_coll_tabular.html index 317acc4..9d633f3 100644 --- a/categories/templates/admin/edit_inline/gen_coll_tabular.html +++ b/categories/templates/admin/edit_inline/gen_coll_tabular.html @@ -20,7 +20,7 @@ {% if inline_admin_form.original or inline_admin_form.show_url %}

- {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %} + {% if inline_admin_form.original %} {{ inline_admin_form.original.content_object }}{% endif %} {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %}

{% endif %} {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} diff --git a/categories/templatetags/category_tags.py b/categories/templatetags/category_tags.py index a72bdbf..7d016fc 100644 --- a/categories/templatetags/category_tags.py +++ b/categories/templatetags/category_tags.py @@ -1,4 +1,6 @@ from django import template +from django.template import Library, Node, TemplateSyntaxError, \ + Variable, resolve_variable, VariableDoesNotExist, Context from categories.models import Category from mptt.utils import drilldown_tree_for_node from mptt.templatetags.mptt_tags import tree_path, tree_info @@ -206,3 +208,95 @@ def get_top_level_categories(parser, token): "Usage: {%% %s as %%}" % bits[0] ) return TopLevelCategoriesNode(bits[2]) + +def resolve(var, context): + try: + return var.resolve(context) + except VariableDoesNotExist: + try: + return var.var + except AttributeError: + return var + +def get_latest_objects_by_category(category, app_label, model_name, set_name, + date_field='pub_date', num=15): + m = get_model(app_label, model_name) + if not isinstance(category, Category): + category = Category.objects.get(slug=str(category)) + children = Category.objects.filter(parent=category) + ids = [] + for cat in list(children) + [category]: + if hasattr(cat, '%s_set' % set_name): + ids.extend([x.pk for x in getattr(cat, '%s_set' % set_name).all()[:num]]) + + return m.objects.filter(pk__in=ids).order_by('-%s' % date_field)[:num] + +class LatestObjectsNode(Node): + def __init__(self, var_name, category, app_label, model_name, set_name, + date_field='pub_date', num=15): + """Get latest objects of app_label.model_name""" + self.category = Variable(category) + self.app_label = Variable(app_label) + self.model_name = Variable(model_name) + self.set_name = Variable(set_name) + self.date_field = Variable(date_field) + self.num = Variable(num) + self.var_name = var_name + + def get_cache_key(self, category, app_label, model_name, set_name, + date_field, num): + """Get the cache key""" + key = 'latest_objects.%s' % '.'.join([category, app_label, model_name, + set_name, date_field, num]) + + def render(self, context): + """Render this sucker""" + category = resolve(self.category, context) + app_label = resolve(self.app_label, context) + model_name = resolve(self.model_name, context) + set_name = resolve(self.set_name, context) + date_field = resolve(self.date_field, context) + num = resolve(self.num, context) + + cache_key = self.get_cache_key(category, app_label, model_name, set_name, + date_field, num) + result = cache.get(cache_key) + if not result: + result = get_latest_objects_by_category(category, app_label, model_name, + set_name, date_field, num) + + cache.set(key, result, 300) + context[self.var_name] = result + + return '' + +def do_get_latest_objects_by_category(parser, token): + """ + Get the latest objects by category + + {% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %} + """ + proper_form = "{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}" + bits = token.split_contents() + + if bits[-2] != 'as': + raise TemplateSyntaxError("%s tag shoud be in the form: %s" % (bits[0], proper_form)) + if len(bits) < 7: + raise TemplateSyntaxError("%s tag shoud be in the form: %s" % (bits[0], proper_form)) + if len(bits) > 9: + raise TemplateSyntaxError("%s tag shoud be in the form: %s" % (bits[0], proper_form)) + category = bits[1] + app_label = bits[2] + model_name = bits[3] + set_name = bits[4] + var_name = bits[-1] + if bits[5] != 'as': + date_field = bits[5] + else: + date_field = None + if bits[6] != 'as': + num = bits[6] + else: + num = None + return LatestObjectsNode(var_name, category, app_label, model_name, set_name, + date_field, num) \ No newline at end of file diff --git a/doc_src/Makefile b/doc_src/Makefile index 0040a51..d34da38 100644 --- a/doc_src/Makefile +++ b/doc_src/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -a SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build diff --git a/doc_src/adding_the_fields.rst b/doc_src/adding_the_fields.rst new file mode 100644 index 0000000..a6a920f --- /dev/null +++ b/doc_src/adding_the_fields.rst @@ -0,0 +1,23 @@ +.. _adding_the_fields: + +================================= +Adding the fields to the database +================================= + +While Django will create the appropriate columns and tables if you configure Django Categories first, many times that is not possible. You could also reset the table, but you would loose all data in it. There is another way. + +Enter South +*********** + +`South `_ is a Django application for managing database schema changes. South's default behavior is for managing permanent changes to a model's structure. In the case of dynamically adding a field or fields to a model, this doesn't work because you are not making the change permanent. And probably don't want to. + +Django Categories has a management command to create any missing fields. It requires South because it uses the South's API for making changes to databases. The management command is simple: ``python manage.py add_category_fields [app]``\ . If you do not include an app name, it will attempt to do all applications configured. + +Running this command several times will not hurt any data or cause any errors. + +Reconfiguring Fields +******************** + +You can make changes to the field configurations as long as they do not change the underlying database structure. For example, adding a ``related_name`` (see :ref:`registering_a_m2one_relationship`\ ) because it only affects Django code. Changing the name of the field, however, is a different matter. + +Django Categories provides a complementary management command to drop a field from the database (the field must still be in the configuration to do so): ``python manage.py drop_category_field app_name model_name field_name`` \ No newline at end of file diff --git a/doc_src/getting_started.rst b/doc_src/getting_started.rst index b5ce5e4..954a063 100644 --- a/doc_src/getting_started.rst +++ b/doc_src/getting_started.rst @@ -35,6 +35,35 @@ You can't do it as both at the same time, though. Connecting your model with Django-Categories ============================================ -Because there are a few additional methods and attributes that your model needs, you can't simply create a ``ForeignKey`` to ``Category``, even though that is eventually what happens. +There are two ways to add Category fields to an application. If you are in control of the code (it's your application) or you are willing to take control of the code (fork someone else's app) you can make a :ref:`hard_coded_connection`\ . + +For 3rd-party apps or even your own apps that you don't wish to add Django-Categories as a dependency, you can configure a :ref:`lazy_connection`\ . + +.. _hard_coded_connection: + +Hard Coded Connection +********************* + +Hard coded connections are done in the exact same way you handle any other foreign key or many-to-many relations to a model. + +.. code-block:: python + + from django.db import models + + class MyModel(models.Model): + name = models.CharField(max_length=100) + category = models.ForeignKey('categories.Category') + +Don't forget to add the field or fields to your ``ModelAdmin`` class as well. + + +.. _lazy_connection: + +Lazy Connection +*************** + +Lazy connections are done through configuring Django Categories in the project's ``settings.py`` file. When the project starts up, the configured fields are dynamically added to the configured models and admin. + +If you do this before you have created the database (before you ran ``manage.py syncdb``), the fields will also be in the tables. If you do this after you have already created all the tables, you can run ``manage.py add_category_fields`` to create the fields (this requires Django South to be installed). You add a many-to-one or many-to-many relationship with Django Categories using the ``CATEGORIES_SETTINGS['FK_REGISTRY']`` and ``CATEGORIES_SETTINGS['M2M_REGISTRY']`` settings respectively. For more information see :ref:`registering_models`\ . diff --git a/doc_src/index.rst b/doc_src/index.rst index 75142c0..d78f824 100644 --- a/doc_src/index.rst +++ b/doc_src/index.rst @@ -1,8 +1,6 @@ -=========================================== -Django Categories v|version| documentation! -=========================================== - - +============================= +Django Categories v |version| +============================= Contents: @@ -13,7 +11,7 @@ Contents: getting_started usage registering_models - templatetags + adding_the_fields reference/index Indices and tables diff --git a/doc_src/reference/index.rst b/doc_src/reference/index.rst index bc987c1..9bca020 100644 --- a/doc_src/reference/index.rst +++ b/doc_src/reference/index.rst @@ -6,4 +6,7 @@ Reference :maxdepth: 2 :glob: - settings \ No newline at end of file + management_commands + models + settings + templatetags diff --git a/doc_src/reference/management_commands.rst b/doc_src/reference/management_commands.rst new file mode 100644 index 0000000..b090695 --- /dev/null +++ b/doc_src/reference/management_commands.rst @@ -0,0 +1,38 @@ +.. _management-commands: + +=================== +Management Commands +=================== + +.. _import_categories: + +import_categories +================= + +**Usage:** ``./manage.py import_categories /path/to/file.txt [/path/to/file2.txt]`` + +Imports category tree(s) from a file. Sub categories must be indented by the same multiple of spaces or tabs. The first line in the file cannot start with a space or tab and you can't mix spaces and tabs. + + +.. _add_category_fields: + +add_category_fields +=================== + +**Usage:** ``./manage.py add_category_fields [app1 app2 ...]`` + +Add missing registered category fields to the database table of a specified application or all registered applications. + +Requires Django South. + + +.. _drop_category_field: + +drop_category_field +=================== + +**Usage:** ``./manage.py drop_category_field app_name model_name field_name`` + +Drop the ``field_name`` field from the ``app_name_model_name`` table, if the field is currently registered in ``CATEGORIES_SETTINGS``\ . + +Requires Django South. \ No newline at end of file diff --git a/doc_src/reference/models.rst b/doc_src/reference/models.rst new file mode 100644 index 0000000..2f66ba8 --- /dev/null +++ b/doc_src/reference/models.rst @@ -0,0 +1,42 @@ +====== +Models +====== + +Category +======== + +**parent** + The category's parent category. Leave this blank for an Category Tree. + +**name** + The name of the category. + +**thumbnail** + An optional thumbnail, that is uploaded to ``CATEGORY_SETTINGS['THUMBNAIL_UPLOAD_PATH']`` via ``CATEGORY_SETTINGS['THUMBNAIL_STORAGE']``\ . + +**thumbnail_width** + The thumbnail width. + +**thumbnail_height** + The thumbnail height. + +**order** + The order of this category in the listing + +**slug** + A slug created from the name field + +**alternate_title** + An alternative title to use on pages with this category. + +**alternate_url** + An alternative URL to use instead of the one derived from the category hierarchy. + +**description** + An optional longer description of the category. + +**meta_keywords** + Comma-separated keywords for search engines. + +**meta_extra** + (Advanced) Any additional HTML to be placed verbatim in the ```` diff --git a/doc_src/reference/settings.rst b/doc_src/reference/settings.rst index 1ed70f9..94f1ac3 100644 --- a/doc_src/reference/settings.rst +++ b/doc_src/reference/settings.rst @@ -67,4 +67,11 @@ THUMBNAIL_STORAGE **Default:** ``settings.DEFAULT_FILE_STORAGE`` -**Description:** How to store the thumbnails. Allows for external storage engines like S3. \ No newline at end of file +**Description:** How to store the thumbnails. Allows for external storage engines like S3. + +JAVASCRIPT_URL +============== + +**Default:** ``STATIC_URL or MEDIA_URL + 'js/'`` + +**Description:** Allows for customization of javascript placement. \ No newline at end of file diff --git a/doc_src/templatetags.rst b/doc_src/reference/templatetags.rst similarity index 100% rename from doc_src/templatetags.rst rename to doc_src/reference/templatetags.rst diff --git a/doc_src/registering_models.rst b/doc_src/registering_models.rst index d348093..e44f652 100644 --- a/doc_src/registering_models.rst +++ b/doc_src/registering_models.rst @@ -95,6 +95,8 @@ Registering one or more Many-to-Many Category fields to a Model } } +.. _registering_a_m2one_relationship: + Registering a many-to-one relationship ====================================== diff --git a/docs/.buildinfo b/docs/.buildinfo index 336f91a..0885fbe 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 0f579989dd4cbe4af0af63fb63e64641 -tags: fbb0d17656682115ca4d033fb2f83ba1 +config: +tags: diff --git a/docs/_sources/adding_the_fields.txt b/docs/_sources/adding_the_fields.txt new file mode 100644 index 0000000..a6a920f --- /dev/null +++ b/docs/_sources/adding_the_fields.txt @@ -0,0 +1,23 @@ +.. _adding_the_fields: + +================================= +Adding the fields to the database +================================= + +While Django will create the appropriate columns and tables if you configure Django Categories first, many times that is not possible. You could also reset the table, but you would loose all data in it. There is another way. + +Enter South +*********** + +`South `_ is a Django application for managing database schema changes. South's default behavior is for managing permanent changes to a model's structure. In the case of dynamically adding a field or fields to a model, this doesn't work because you are not making the change permanent. And probably don't want to. + +Django Categories has a management command to create any missing fields. It requires South because it uses the South's API for making changes to databases. The management command is simple: ``python manage.py add_category_fields [app]``\ . If you do not include an app name, it will attempt to do all applications configured. + +Running this command several times will not hurt any data or cause any errors. + +Reconfiguring Fields +******************** + +You can make changes to the field configurations as long as they do not change the underlying database structure. For example, adding a ``related_name`` (see :ref:`registering_a_m2one_relationship`\ ) because it only affects Django code. Changing the name of the field, however, is a different matter. + +Django Categories provides a complementary management command to drop a field from the database (the field must still be in the configuration to do so): ``python manage.py drop_category_field app_name model_name field_name`` \ No newline at end of file diff --git a/docs/_sources/getting_started.txt b/docs/_sources/getting_started.txt index b5ce5e4..954a063 100644 --- a/docs/_sources/getting_started.txt +++ b/docs/_sources/getting_started.txt @@ -35,6 +35,35 @@ You can't do it as both at the same time, though. Connecting your model with Django-Categories ============================================ -Because there are a few additional methods and attributes that your model needs, you can't simply create a ``ForeignKey`` to ``Category``, even though that is eventually what happens. +There are two ways to add Category fields to an application. If you are in control of the code (it's your application) or you are willing to take control of the code (fork someone else's app) you can make a :ref:`hard_coded_connection`\ . + +For 3rd-party apps or even your own apps that you don't wish to add Django-Categories as a dependency, you can configure a :ref:`lazy_connection`\ . + +.. _hard_coded_connection: + +Hard Coded Connection +********************* + +Hard coded connections are done in the exact same way you handle any other foreign key or many-to-many relations to a model. + +.. code-block:: python + + from django.db import models + + class MyModel(models.Model): + name = models.CharField(max_length=100) + category = models.ForeignKey('categories.Category') + +Don't forget to add the field or fields to your ``ModelAdmin`` class as well. + + +.. _lazy_connection: + +Lazy Connection +*************** + +Lazy connections are done through configuring Django Categories in the project's ``settings.py`` file. When the project starts up, the configured fields are dynamically added to the configured models and admin. + +If you do this before you have created the database (before you ran ``manage.py syncdb``), the fields will also be in the tables. If you do this after you have already created all the tables, you can run ``manage.py add_category_fields`` to create the fields (this requires Django South to be installed). You add a many-to-one or many-to-many relationship with Django Categories using the ``CATEGORIES_SETTINGS['FK_REGISTRY']`` and ``CATEGORIES_SETTINGS['M2M_REGISTRY']`` settings respectively. For more information see :ref:`registering_models`\ . diff --git a/docs/_sources/index.txt b/docs/_sources/index.txt index 75142c0..d78f824 100644 --- a/docs/_sources/index.txt +++ b/docs/_sources/index.txt @@ -1,8 +1,6 @@ -=========================================== -Django Categories v|version| documentation! -=========================================== - - +============================= +Django Categories v |version| +============================= Contents: @@ -13,7 +11,7 @@ Contents: getting_started usage registering_models - templatetags + adding_the_fields reference/index Indices and tables diff --git a/docs/_sources/reference/index.txt b/docs/_sources/reference/index.txt index bc987c1..9bca020 100644 --- a/docs/_sources/reference/index.txt +++ b/docs/_sources/reference/index.txt @@ -6,4 +6,7 @@ Reference :maxdepth: 2 :glob: - settings \ No newline at end of file + management_commands + models + settings + templatetags diff --git a/docs/_sources/reference/management_commands.txt b/docs/_sources/reference/management_commands.txt new file mode 100644 index 0000000..b090695 --- /dev/null +++ b/docs/_sources/reference/management_commands.txt @@ -0,0 +1,38 @@ +.. _management-commands: + +=================== +Management Commands +=================== + +.. _import_categories: + +import_categories +================= + +**Usage:** ``./manage.py import_categories /path/to/file.txt [/path/to/file2.txt]`` + +Imports category tree(s) from a file. Sub categories must be indented by the same multiple of spaces or tabs. The first line in the file cannot start with a space or tab and you can't mix spaces and tabs. + + +.. _add_category_fields: + +add_category_fields +=================== + +**Usage:** ``./manage.py add_category_fields [app1 app2 ...]`` + +Add missing registered category fields to the database table of a specified application or all registered applications. + +Requires Django South. + + +.. _drop_category_field: + +drop_category_field +=================== + +**Usage:** ``./manage.py drop_category_field app_name model_name field_name`` + +Drop the ``field_name`` field from the ``app_name_model_name`` table, if the field is currently registered in ``CATEGORIES_SETTINGS``\ . + +Requires Django South. \ No newline at end of file diff --git a/docs/_sources/reference/models.txt b/docs/_sources/reference/models.txt new file mode 100644 index 0000000..2f66ba8 --- /dev/null +++ b/docs/_sources/reference/models.txt @@ -0,0 +1,42 @@ +====== +Models +====== + +Category +======== + +**parent** + The category's parent category. Leave this blank for an Category Tree. + +**name** + The name of the category. + +**thumbnail** + An optional thumbnail, that is uploaded to ``CATEGORY_SETTINGS['THUMBNAIL_UPLOAD_PATH']`` via ``CATEGORY_SETTINGS['THUMBNAIL_STORAGE']``\ . + +**thumbnail_width** + The thumbnail width. + +**thumbnail_height** + The thumbnail height. + +**order** + The order of this category in the listing + +**slug** + A slug created from the name field + +**alternate_title** + An alternative title to use on pages with this category. + +**alternate_url** + An alternative URL to use instead of the one derived from the category hierarchy. + +**description** + An optional longer description of the category. + +**meta_keywords** + Comma-separated keywords for search engines. + +**meta_extra** + (Advanced) Any additional HTML to be placed verbatim in the ```` diff --git a/docs/_sources/reference/settings.txt b/docs/_sources/reference/settings.txt index 1ed70f9..94f1ac3 100644 --- a/docs/_sources/reference/settings.txt +++ b/docs/_sources/reference/settings.txt @@ -67,4 +67,11 @@ THUMBNAIL_STORAGE **Default:** ``settings.DEFAULT_FILE_STORAGE`` -**Description:** How to store the thumbnails. Allows for external storage engines like S3. \ No newline at end of file +**Description:** How to store the thumbnails. Allows for external storage engines like S3. + +JAVASCRIPT_URL +============== + +**Default:** ``STATIC_URL or MEDIA_URL + 'js/'`` + +**Description:** Allows for customization of javascript placement. \ No newline at end of file diff --git a/docs/_sources/reference/templatetags.txt b/docs/_sources/reference/templatetags.txt new file mode 100644 index 0000000..b5463f4 --- /dev/null +++ b/docs/_sources/reference/templatetags.txt @@ -0,0 +1,146 @@ +============= +Template Tags +============= + +get_top_level_categories +======================== + +Retrieves an alphabetical list of all the categories that have no parents. + +Syntax: + +.. code-block:: django + + {% get_top_level_categories as categories %} + +Returns an list of categories ``[, , +
  • Top + +
  • + + + +get_category_drilldown +====================== + +Retrieves the specified category, its ancestors and its immediate children +as an iterable. + +Example: + +.. code-block:: django + + {% get_category_drilldown "/Grandparent/Parent" as family %} + +or + +.. code-block:: django + + {% get_category_drilldown category_obj as family %} + +Sets ``family`` to:: + + [Grandparent, Parent, Child 1, Child 2, Child n] + + +display_drilldown_as_ul +======================= + +Render the category with ancestors and children using the +``categories/ul_tree.html`` template. + +Example: + +.. code-block:: django + + {% display_drilldown_as_ul "/Grandparent/Parent" %} + +or: + +.. code-block:: django + + {% display_drilldown_as_ul category_obj %} + +Returns: + +.. code-block:: html + + + + +breadcrumbs tag +=============== + +Render breadcrumbs, using the ``categories/breadcrumbs.html`` template, using the optional ``separator`` argument. + +Example: + +.. code-block:: django + + {% breadcrumbs "/Grandparent/Parent" %} + +or: + +.. code-block:: django + + {% breadcrumbs category_obj %} + +Returns: + +.. code-block:: html + + Grandparent / Parent + +You can alter the separator used in the template by adding a string argument to be the separator: + +.. code-block:: django + + {% breadcrumbs category_obj "::" %} + +Returns: + +.. code-block:: html + + Grandparent :: Parent diff --git a/docs/_sources/registering_models.txt b/docs/_sources/registering_models.txt index d348093..e44f652 100644 --- a/docs/_sources/registering_models.txt +++ b/docs/_sources/registering_models.txt @@ -95,6 +95,8 @@ Registering one or more Many-to-Many Category fields to a Model } } +.. _registering_a_m2one_relationship: + Registering a many-to-one relationship ====================================== diff --git a/docs/adding_the_fields.html b/docs/adding_the_fields.html new file mode 100644 index 0000000..cca1298 --- /dev/null +++ b/docs/adding_the_fields.html @@ -0,0 +1,125 @@ + + + + + + + + Adding the fields to the database — Django Categories v0.7 documentation + + + + + + + + + + + +
    +

    Django Categories v0.7 documentation

    +
    + + +
    +
    + + + +

    This Page

    + + + +
    +
    + + + +
    +
    +
    +
    + +
    +

    Adding the fields to the database

    +

    While Django will create the appropriate columns and tables if you configure Django Categories first, many times that is not possible. You could also reset the table, but you would loose all data in it. There is another way.

    +
    +

    Enter South

    +

    South is a Django application for managing database schema changes. South’s default behavior is for managing permanent changes to a model’s structure. In the case of dynamically adding a field or fields to a model, this doesn’t work because you are not making the change permanent. And probably don’t want to.

    +

    Django Categories has a management command to create any missing fields. It requires South because it uses the South’s API for making changes to databases. The management command is simple: python manage.py add_category_fields [app]. If you do not include an app name, it will attempt to do all applications configured.

    +

    Running this command several times will not hurt any data or cause any errors.

    +
    +
    +

    Reconfiguring Fields

    +

    You can make changes to the field configurations as long as they do not change the underlying database structure. For example, adding a related_name (see Registering a many-to-one relationship) because it only affects Django code. Changing the name of the field, however, is a different matter.

    +

    Django Categories provides a complementary management command to drop a field from the database (the field must still be in the configuration to do so): python manage.py drop_category_field app_name model_name field_name

    +
    +
    + + +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/genindex.html b/docs/genindex.html index 7ac93dd..0045025 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -6,13 +6,13 @@ - Index — Django Categories v0.6 documentation + Index — Django Categories v0.7 documentation - +
    -

    Django Categories v0.6 documentation

    +

    Django Categories v0.7 documentation

    diff --git a/docs/index.html b/docs/index.html index bad05dd..226639d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,13 +6,13 @@ - Django Categories v|version| documentation! — Django Categories v0.6 documentation + Django Categories v 0.7 — Django Categories v0.7 documentation - +
    -

    Django Categories v0.6 documentation

    +

    Django Categories v0.7 documentation