diff --git a/categories/admin.py b/categories/admin.py index 495fcf0..a34e314 100644 --- a/categories/admin.py +++ b/categories/admin.py @@ -1,99 +1,48 @@ -from ellington.categories.models import Hierarchy, Category, Alias +from models import Category from django.contrib import admin from django import forms -from ellington.core.forms import RequiredModelForm +from django.template.defaultfilters import slugify +from mptt.forms import TreeNodeChoiceField +from editor import TreeEditorMixin -class CategoryAdminForm(RequiredModelForm): - required_if_other_not_given = { - 'hierarchy': 'parent', - 'parent': 'hierarchy', - } - +class CategoryAdminForm(forms.ModelForm): + parent = TreeNodeChoiceField(queryset=Category.tree.all(), level_indicator=u'+-', required=False) class Meta: model = Category - - def clean_name(self): - if '/' in self.cleaned_data['name']: - raise forms.ValidationError, "A category name can't contain slashes." - return self.cleaned_data['name'] - + + def clean_slug(self): + self.cleaned_data['slug'] = slugify(self.cleaned_data['name']) + return self.cleaned_data['slug'] + def clean(self): super(CategoryAdminForm, self).clean() - if 'slug' in self.cleaned_data and 'parent' in self.cleaned_data and 'hierarchy' in self.cleaned_data: - if self.cleaned_data['parent'] is not None: - # inherit from parent - self.cleaned_data['hierarchy'] = self.cleaned_data['parent'].hierarchy - - #Validate slug - kwargs = {} - if self.cleaned_data.get('hierarchy', False): - kwargs['hierarchy__pk'] = int(self.cleaned_data['hierarchy'].id) - 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) if c.id != self.cleaned_data.get("id", None)] - 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 - "Makes sure the category doesn't set itself or any of its children as its parent." - if not self.cleaned_data['parent']: - return self.cleaned_data - - p_data = int(self.cleaned_data['parent'].pk) - h_data = self.cleaned_data.get('hierarchy', False) - if h_data: - h_data = int(h_data.pk) - if p_data and h_data: - p = Category.objects.get(pk=p_data) - if p.hierarchy_id != h_data: - raise forms.ValidationError("This parent is not within the selected hierarchy.") - - # Check that the parent isn't a child of this category - # Requires that we look up "this" object; if it doesn't exist - # we can assume we're at the add stage and return - this_id = self.cleaned_data.get("id", None) - if not this_id: - return self.cleaned_data - - try: - selected_parent = Category.objects.get(pk=p_data) - except Category.DoesNotExist: - return self.cleaned_data - - if selected_parent.id == this_id: - raise forms.ValidationError("A category can't be its own parent.") - - try: - this_category = Category.objects.get(pk=p_data) - except Category.DoesNotExist: - return self.cleaned_data - - for c in this_category.get_all_children(): - if c.id == this_id: - raise forms.ValidationError("A category can't set a child category to be its own parent.") - return self.cleaned_data + # Validate slug is valid in that level + kwargs = {} + if self.cleaned_data.get('parent', None) is None: + kwargs['parent__isnull'] = True else: - raise forms.ValidationError("Cannot clean data") + 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(admin.ModelAdmin): +class CategoryAdmin(TreeEditorMixin, admin.ModelAdmin): form=CategoryAdminForm - fields = ('hierarchy', 'parent', 'name', 'slug') list_display = ('__unicode__',) - list_filter = ('hierarchy',) - search_fields = ('name', 'path') + search_fields = (('name',)) prepopulated_fields = {'slug': ('name',)} -class HierarchyAdmin(admin.ModelAdmin): - prepopulated_fields = {'slug': ('name',)} - -class AliasAdmin(admin.ModelAdmin): - list_display = ['category', 'parent'] - search_fields = ['category__name', 'parent__name'] - admin.site.register(Category, CategoryAdmin) -admin.site.register(Hierarchy, HierarchyAdmin) -admin.site.register(Alias, AliasAdmin) diff --git a/categories/media/css/images/title.gif b/categories/media/css/images/title.gif new file mode 100644 index 0000000..f92b596 Binary files /dev/null and b/categories/media/css/images/title.gif differ diff --git a/categories/media/css/jquery.alerts.css b/categories/media/css/jquery.alerts.css new file mode 100644 index 0000000..0b5e1e8 --- /dev/null +++ b/categories/media/css/jquery.alerts.css @@ -0,0 +1,54 @@ +#popup_container { + font-family: Arial, sans-serif; + font-size: 12px; + min-width: 300px; /* Dialog will be no smaller than this */ + max-width: 600px; /* Dialog will wrap after this width */ + background: #FFF; + border: solid 1px #666; + color: #000; +} + +#popup_title { + font-size: 14px; + font-weight: bold; + text-align: center; + line-height: 1.75em; + color: #666; + background: #eee url(images/title.gif) top repeat-x; + border: solid 1px #FFF; + border-bottom: solid 1px #666; + cursor: default; + padding: 0em; + margin: 0em; +} + +#popup_content { + background: 16px 16px no-repeat url(../img/info.gif); + padding: 1em 1.75em; + margin: 0em; +} + +#popup_content.alert { + background-image: url(../img/info.gif); +} + +#popup_content.confirm { + background-image: url(../img/important.gif); +} + +#popup_content.prompt { + background-image: url(../img/help.gif); +} + +#popup_message { + padding-left: 48px; +} + +#popup_panel { + text-align: center; + margin: 1em 0em 0em 1em; +} + +#popup_prompt { + margin: .5em 0em; +} \ No newline at end of file diff --git a/categories/media/css/jquery.treeTable.css b/categories/media/css/jquery.treeTable.css new file mode 100644 index 0000000..5305653 --- /dev/null +++ b/categories/media/css/jquery.treeTable.css @@ -0,0 +1,47 @@ +/* jQuery TreeTable Core 2.0 stylesheet + * + * This file contains styles that are used to display the tree table. Each tree + * table is assigned the +treeTable+ class. + * ========================================================================= */ + +/* jquery.treeTable.collapsible + * ------------------------------------------------------------------------- */ +.treeTable tr td .expander { + background-position: left center; + background-repeat: no-repeat; + cursor: pointer; + padding: 0; + zoom: 1; /* IE7 Hack */ +} + +.treeTable tr.collapsed td .expander { + background-image: url(../img/toggle-expand-dark.png); +} + +.treeTable tr.expanded td .expander { + background-image: url(../img/toggle-collapse-dark.png); +} + +/* jquery.treeTable.sortable + * ------------------------------------------------------------------------- */ +.treeTable tr.selected, .treeTable tr.accept { + /*background-color: #7799ff; + color: #fff;*/ +} + +.treeTable tr.append { + border-bottom: 2px solid #7799ff; +} + +.treeTable tr.collapsed.selected td .expander, .treeTable tr.collapsed.accept td .expander { + /*background-image: url(../img/toggle-expand-light.png);*/ +} + +.treeTable tr.expanded.selected td .expander, .treeTable tr.expanded.accept td .expander { + /*background-image: url(../img/toggle-collapse-light.png);*/ +} + +.treeTable .ui-draggable-dragging { + color: #000; + z-index: 1; +} \ No newline at end of file diff --git a/categories/media/css/layout.css b/categories/media/css/layout.css new file mode 100644 index 0000000..116f9f7 --- /dev/null +++ b/categories/media/css/layout.css @@ -0,0 +1,217 @@ +#content.colM { + width:745px; +} + +.navi_tab { + float:left; + padding: 3px 10px 3px 10px; + cursor:pointer; + border: 1px solid #ccc; + border-bottom:none; + min-width:100px; + margin-top:3px; + height:13px; + font-weight: normal; + background-image:none; + color: black; +} +.tab_active { + margin-top: 0; + height:16px; + font-weight: bold; + background-image:url('../img/default-bg.gif'); + color: white; +} + +#main { + clear:both; + padding: 10px 10px 10px 10px; + border: 1px solid #ccc; + margin: 0 0 10px 0; +} +.panel { + display:none; + position:relative; + padding-bottom:50px; +} + +.order-machine { + margin-bottom:10px; +} + +.order-item { + margin: 0 0 10px 0; + position:relative; +} + +.order-item .handle { + display: block; + height: 100%; + width: 650px; + cursor: move; +} + +.item-delete { + cursor:pointer; + float:right; + margin:3px 0px 0px 5px; +} +.active-item { + background-color:#eee; +} + +.highlight { + height: 26px; + background-color:#def; + margin: 0 0 10px 0; +} + +.button { + margin:5px; padding:5px; + font-weight: bold; + background-image:url('../img/nav-bg.gif'); + cursor:pointer; + border: 1px solid #678; + width:30px; +} +th { vertical-align: middle; } + +#test-log { padding: 5px; border: 1px solid #ccc; } + +#overview span { margin-right:15px; margin-left:5px;} + +.richtextcontent {display:none;} + +select { + max-width: 580px; +} + +textarea { + width: 580px; + margin-top:5px; + margin-bottom:5px; +} + +.item-minimize { + width: 15px; + height:15px; + float: left; + cursor: pointer; + margin-left:-17px; +} + +.item-minimize-disabled { + width: 15px; + height:15px; + float: left; + margin-left:-17px; +} + +.machine-control { + height:50px; + padding: 5px 0px 5px 0px; + border: 1px solid #ccc; + background-color:#fff; + width:743px; + position:absolute; + left:-11px; + bottom:-11px; +} + +.control-unit { + float:left; + padding-left:15px; + padding-right:10px; + border-right: 1px solid #eee; +} + +.control-unit span { + font-weight:bold; +} + +.empty-machine-msg { + margin:10px 0px 5px 300px; + font-style: italic; + font-size:14px; + color:#999; +} + +td span select { + width:600px; +} + +.popup_bg { + width:100%; height:100%; + background-color:white; + opacity: 0.5; + filter:alpha(opacity=50); + position:absolute; + top:0px; left:0px; +} + +#sitetree-wrapper { + margin: 0; + padding: 20px 1px 0 1px; +} + +#sitetree { + width: 100%; +} + +#sitetree tr td { + overflow: hidden; + white-space: nowrap; +} + +.title-col { + position:relative; + z-index:2; + background-color:none; +} +td div.wrap { + border: 0px solid #000; + margin: -5px; + padding: 4px; + position:relative; +} + +.insert-as-child { + position:absolute; + bottom:-3px; + left: 15px; + height:15px; + width:100%; + border: 0px solid #0f0; +} +.insert-as-sibling { + position:absolute; + top:0px; + left: 15px; + height:10px; + width:100%; + border: 0px solid #f00; +} + +.nohover { + background-color: #eef; +} + +.flash { + background-color: #afa; +} + +.hover-as-child { + background-color:#cdf; +} + +.hover-as-sibling { + border: 1px solid #aee; +} + +.move-node { + cursor: move; + margin-left:5px; +} +.del-page { + cursor: pointer; +} diff --git a/categories/media/helper.js b/categories/media/helper.js new file mode 100644 index 0000000..9029fdc --- /dev/null +++ b/categories/media/helper.js @@ -0,0 +1,85 @@ +function region_append(region, obj, modname) { + var wrp = []; + wrp.push('
'); + + $("#"+REGIONS[region]+"_body").children("div.order-machine").append(wrp.join("")) + .children("fieldset.order-item:last").children(".item-content").append(obj); +} + +function create_new_spare_form(form, modvar, last_id) { + // create new spare form + var new_form = form.html().replace( + new RegExp(modvar+'-'+last_id, 'g'), + modvar+'-'+(last_id+1)); + new_form = '| {{ header.text|capfirst }} + {% endfor %} + | {% trans "Delete" %} | +
|---|