Permalink
Browse files

Merge branch '1.0'

  • Loading branch information...
2 parents f6c06a2 + 5487447 commit 8642bda7f803cf23f715f852b8aef6cef50d5b0c @coordt coordt committed Feb 15, 2012
Showing with 1,938 additions and 612 deletions.
  1. +1 −1 MANIFEST.in
  2. +22 −0 README.rst
  3. +3 −3 categories/__init__.py
  4. +9 −82 categories/admin.py
  5. +156 −0 categories/base.py
  6. +27 −8 categories/editor/tree_editor.py
  7. +4 −2 categories/management/commands/import_categories.py
  8. +56 −0 categories/migrations/0009_setdefaultorder.py
  9. +78 −0 categories/migrations/0010_changed_category_relation.py
  10. +46 −75 categories/models.py
  11. +16 −1 categories/settings.py
  12. +14 −1 doc_src/_static/default.css
  13. +9 −0 doc_src/code_examples/custom_categories1.py
  14. +10 −0 doc_src/code_examples/custom_categories2.py
  15. +29 −0 doc_src/code_examples/custom_categories3.py
  16. +15 −0 doc_src/code_examples/custom_categories4.py
  17. +5 −0 doc_src/code_examples/custom_categories5.py
  18. +9 −0 doc_src/code_examples/custom_categories6.py
  19. +17 −0 doc_src/code_examples/custom_categories7.py
  20. +56 −0 doc_src/custom_categories.rst
  21. +27 −22 doc_src/getting_started.rst
  22. +30 −1 doc_src/index.rst
  23. +25 −1 doc_src/installation.rst
  24. +102 −29 doc_src/reference/models.rst
  25. +35 −1 doc_src/reference/settings.rst
  26. +56 −0 docs/_sources/custom_categories.txt
  27. +27 −22 docs/_sources/getting_started.txt
  28. +30 −1 docs/_sources/index.txt
  29. +25 −1 docs/_sources/installation.txt
  30. +102 −29 docs/_sources/reference/models.txt
  31. +35 −1 docs/_sources/reference/settings.txt
  32. +4 −17 docs/_static/basic.css
  33. +14 −1 docs/_static/default.css
  34. +6 −6 docs/_static/doctools.js
  35. +50 −92 docs/_static/searchtools.js
  36. +1 −4 docs/_static/sidebar.js
  37. +0 −7 docs/_static/underscore.js
  38. +8 −10 docs/adding_the_fields.html
  39. +339 −0 docs/custom_categories.html
  40. +96 −26 docs/genindex.html
  41. +23 −26 docs/getting_started.html
  42. +42 −13 docs/index.html
  43. +37 −12 docs/installation.html
  44. BIN docs/objects.inv
  45. +10 −10 docs/reference/index.html
  46. +6 −8 docs/reference/management_commands.html
  47. +136 −34 docs/reference/models.html
  48. +36 −18 docs/reference/settings.html
  49. +6 −8 docs/reference/templatetags.html
  50. +8 −10 docs/registering_models.html
  51. +6 −8 docs/search.html
  52. +1 −1 docs/searchindex.js
  53. +14 −16 docs/usage.html
  54. +11 −2 example/simpletext/admin.py
  55. +7 −1 example/simpletext/models.py
  56. +1 −1 requirements.txt
View
@@ -6,7 +6,7 @@ include LICENSE.txt
recursive-include categories *.html *.txt *.json *.html
recursive-include categories/static *.html *.gif *.png *.css *.txt *.js
-recursive-include editor *.html *.gif *.png *.css *.js
+recursive-include categories/editor *.html *.gif *.png *.css *.js
recursive-include doc_src *.rst *.txt *.png *.css *.html *.js
include doc_src/Makefile
View
@@ -2,6 +2,28 @@ Django Categories grew out of our need to provide a basic hierarchical taxonomy
As a news site, our stories, photos, and other content get divided into "sections" and we wanted all the apps to use the same set of sections. As our needs grew, the Django Categories grew in the functionality it gave to category handling within web pages.
+New in 1.0
+==========
+
+**Abstract Base Class for generic hierarchical category models**
+ When you want a multiple types of categories and don't want them all part of the same model, you can now easily create new models by subclassing ``CategoryBase``. You can also add additional metadata as necessary.
+
+ Your model's can subclass ``CategoryBaseAdminForm`` and ``CategoryBaseAdmin`` to get the hierarchical management in the admin.
+
+ See the docs for more information.
+
+**Increased the default caching time on views**
+ The default setting for ``CACHE_VIEW_LENGTH`` was ``0``, which means it would tell the browser to *never* cache the page. It is now ``600``, which is the default for `CACHE_MIDDLEWARE_SECONDS <https://docs.djangoproject.com/en/1.3/ref/settings/#cache-middleware-seconds>`_
+
+**Updated for use with Django-MPTT 0.5**
+ Just a few tweaks.
+
+**Initial compatibility with Django 1.4**
+ More is coming, but at least it works.
+
+**Slug transliteration for non-ASCII characters**
+ A new setting, ``SLUG_TRANSLITERATOR``, allows you to specify a function for converting the non-ASCII characters to ASCII characters before the slugification. Works great with `Unidecode <http://pypi.python.org/pypi/Unidecode>`_.
+
Updated in 0.8.8
================
View
@@ -1,7 +1,7 @@
__version_info__ = {
- 'major': 0,
- 'minor': 8,
- 'micro': 9,
+ 'major': 1,
+ 'minor': 0,
+ 'micro': 0,
'releaselevel': 'final',
'serial': 1
}
View
@@ -1,87 +1,48 @@
from django.contrib import admin
from django import forms
-from django.template.defaultfilters import slugify
-from genericcollection import GenericCollectionTabularInline
-
-from .settings import ALLOW_SLUG_CHANGE, RELATION_MODELS, JAVASCRIPT_URL
-from .editor.tree_editor import TreeEditor
+from .genericcollection import GenericCollectionTabularInline
+from .settings import RELATION_MODELS, JAVASCRIPT_URL
from .models import Category
+from .base import CategoryBaseAdminForm, CategoryBaseAdmin
+
from categories import model_registry
class NullTreeNodeChoiceField(forms.ModelChoiceField):
"""A ModelChoiceField for tree nodes."""
def __init__(self, level_indicator=u'---', *args, **kwargs):
self.level_indicator = level_indicator
- #kwargs['empty_label'] = None
super(NullTreeNodeChoiceField, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
"""
Creates labels which represent the tree level of each node when
generating option labels.
"""
- return u'%s %s' % (self.level_indicator * getattr(obj, obj._meta.level_attr),
- obj)
+ return u'%s %s' % (self.level_indicator * getattr(
+ obj, obj._mptt_meta.level_attr), obj)
if RELATION_MODELS:
- from models import CategoryRelation
+ from .models import CategoryRelation
class InlineCategoryRelation(GenericCollectionTabularInline):
model = CategoryRelation
-class CategoryAdminForm(forms.ModelForm):
- parent = NullTreeNodeChoiceField(queryset=Category.tree.all(),
- level_indicator=u'+-',
- empty_label='------',
- required=False)
+class CategoryAdminForm(CategoryBaseAdminForm):
class Meta:
model = Category
- def clean_slug(self):
- if self.instance is None or not ALLOW_SLUG_CHANGE:
- self.cleaned_data['slug'] = slugify(self.cleaned_data['name'])
- return self.cleaned_data['slug'][:50]
-
def clean_alternate_title(self):
if self.instance is None or not self.cleaned_data['alternate_title']:
return self.cleaned_data['name']
else:
return self.cleaned_data['alternate_title']
-
- def clean(self):
- super(CategoryAdminForm, self).clean()
-
- # Validate slug is valid in that level
- kwargs = {}
- if self.cleaned_data.get('parent', None) is None:
- kwargs['parent__isnull'] = True
- else:
- kwargs['parent__pk'] = int(self.cleaned_data['parent'].id)
- this_level_slugs = [c['slug'] for c in Category.objects.filter(**kwargs).values('id','slug') if c['id'] != self.instance.id]
- if self.cleaned_data['slug'] in this_level_slugs:
- raise forms.ValidationError("A category slug must be unique among"
- "categories at its level.")
-
- # Validate Category Parent
- # Make sure the category doesn't set itself or any of its children as its parent."
- if self.cleaned_data.get('parent', None) is None or self.instance.id is None:
- return self.cleaned_data
- elif self.cleaned_data['parent'].id == self.instance.id:
- raise forms.ValidationError("You can't set the parent of the "
- "category to itself.")
- elif self.cleaned_data['parent'].id in [i[0] for i in self.instance.get_descendants().values_list('id')]:
- raise forms.ValidationError("You can't set the parent of the "
- "category to a descendant.")
- return self.cleaned_data
-class CategoryAdmin(TreeEditor, admin.ModelAdmin):
+class CategoryAdmin(CategoryBaseAdmin):
form = CategoryAdminForm
list_display = ('name', 'alternate_title', 'active')
- search_fields = ('name', 'alternate_title', )
- prepopulated_fields = {'slug': ('name',)}
fieldsets = (
(None, {
'fields': ('parent', 'name', 'thumbnail', 'active')
@@ -97,40 +58,6 @@ class CategoryAdmin(TreeEditor, admin.ModelAdmin):
}),
)
- actions = ['activate', 'deactivate']
- def get_actions(self, request):
- actions = super(CategoryAdmin, self).get_actions(request)
- if 'delete_selected' in actions:
- del actions['delete_selected']
- return actions
-
- def deactivate(self, request, queryset):
- """
- Set active to False for selected items
- """
- selected_cats = Category.objects.filter(
- pk__in=[int(x) for x in request.POST.getlist('_selected_action')])
-
- for item in selected_cats:
- if item.active:
- item.active = False
- item.save()
- item.children.all().update(active=False)
- deactivate.short_description = "Deactivate selected categories and their children"
-
- def activate(self, request, queryset):
- """
- Set active to True for selected items
- """
- selected_cats = Category.objects.filter(
- pk__in=[int(x) for x in request.POST.getlist('_selected_action')])
-
- for item in selected_cats:
- item.active = True
- item.save()
- item.children.all().update(active=True)
- activate.short_description = "Activate selected categories and their children"
-
if RELATION_MODELS:
inlines = [InlineCategoryRelation,]
View
@@ -0,0 +1,156 @@
+"""
+This is the base class on which to build a hierarchical category-like model
+with customizable metadata and its own name space.
+"""
+
+from django.contrib import admin
+from django.db import models
+from django import forms
+from django.template.defaultfilters import slugify
+from django.utils.encoding import force_unicode
+
+from mptt.models import MPTTModel
+from mptt.fields import TreeForeignKey
+from mptt.managers import TreeManager
+
+from .editor.tree_editor import TreeEditor
+from .settings import ALLOW_SLUG_CHANGE, SLUG_TRANSLITERATOR
+
+class CategoryManager(models.Manager):
+ """
+ A manager that adds an "active()" method for all active categories
+ """
+ def active(self):
+ """
+ Only categories that are active
+ """
+ return self.get_query_set().filter(active=True)
+
+
+class CategoryBase(MPTTModel):
+ """
+ This base model includes the absolute bare bones fields and methods. One
+ could simply subclass this model and do nothing else and it should work.
+ """
+ parent = TreeForeignKey('self',
+ blank=True,
+ null=True,
+ related_name="children",
+ verbose_name='Parent')
+ name = models.CharField(max_length=100)
+ slug = models.SlugField()
+ active = models.BooleanField(default=True)
+
+ objects = CategoryManager()
+ tree = TreeManager()
+
+ def save(self, *args, **kwargs):
+ """
+ While you can activate an item without activating its descendants,
+ It doesn't make sense that you can deactivate an item and have its
+ decendants remain active.
+ """
+ if not self.slug:
+ self.slug = slugify(SLUG_TRANSLITERATOR(self.name))[:50]
+
+ super(CategoryBase, self).save(*args, **kwargs)
+
+ if not self.active:
+ for item in self.get_descendants():
+ if item.active != self.active:
+ item.active = self.active
+ item.save()
+
+ def __unicode__(self):
+ ancestors = self.get_ancestors()
+ return ' > '.join([force_unicode(i.name) for i in ancestors]+[self.name,])
+
+ class Meta:
+ abstract = True
+ unique_together = ('parent', 'name')
+ ordering = ('tree_id', 'lft')
+
+ class MPTTMeta:
+ order_insertion_by = 'name'
+
+
+class CategoryBaseAdminForm(forms.ModelForm):
+ def clean_slug(self):
+ if self.instance is None or not ALLOW_SLUG_CHANGE:
+ self.cleaned_data['slug'] = slugify(self.cleaned_data['name'])
+ return self.cleaned_data['slug'][:50]
+
+ def clean(self):
+ super(CategoryBaseAdminForm, self).clean()
+ opts = self._meta
+
+ # Validate slug is valid in that level
+ kwargs = {}
+ if self.cleaned_data.get('parent', None) is None:
+ kwargs['parent__isnull'] = True
+ else:
+ kwargs['parent__pk'] = int(self.cleaned_data['parent'].id)
+ this_level_slugs = [c['slug'] for c in opts.model.objects.filter(
+ **kwargs).values('id','slug'
+ ) if c['id'] != self.instance.id]
+ if self.cleaned_data['slug'] in this_level_slugs:
+ raise forms.ValidationError("The slug must be unique among "
+ "the items at its level.")
+
+ # Validate Category Parent
+ # Make sure the category doesn't set itself or any of its children as
+ # its parent.
+ decendant_ids = self.instance.get_descendants().values_list('id', flat=True)
+ if self.cleaned_data.get('parent', None) is None or self.instance.id is None:
+ return self.cleaned_data
+ elif self.cleaned_data['parent'].id == self.instance.id:
+ raise forms.ValidationError("You can't set the parent of the "
+ "item to itself.")
+ elif self.cleaned_data['parent'].id in decendant_ids:
+ raise forms.ValidationError("You can't set the parent of the "
+ "item to a descendant.")
+ return self.cleaned_data
+
+
+class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
+ form = CategoryBaseAdminForm
+ list_display = ('name', 'active')
+ search_fields = ('name',)
+ prepopulated_fields = {'slug': ('name',)}
+
+ actions = ['activate', 'deactivate']
+ def get_actions(self, request):
+ actions = super(CategoryBaseAdmin, self).get_actions(request)
+ if 'delete_selected' in actions:
+ del actions['delete_selected']
+ return actions
+
+ def deactivate(self, request, queryset):
+ """
+ Set active to False for selected items
+ """
+ opts = self._meta
+ selected_cats = opts.model.objects.filter(
+ pk__in=[int(x) for x in request.POST.getlist('_selected_action')])
+
+ for item in selected_cats:
+ if item.active:
+ item.active = False
+ item.save()
+ item.children.all().update(active=False)
+ deactivate.short_description = "Deactivate selected categories and their children"
+
+ def activate(self, request, queryset):
+ """
+ Set active to True for selected items
+ """
+ opts = self._meta
+
+ selected_cats = opts.model.objects.filter(
+ pk__in=[int(x) for x in request.POST.getlist('_selected_action')])
+
+ for item in selected_cats:
+ item.active = True
+ item.save()
+ item.children.all().update(active=True)
+ activate.short_description = "Activate selected categories and their children"
Oops, something went wrong.

0 comments on commit 8642bda

Please sign in to comment.