diff --git a/.gitignore b/.gitignore index abde105..c3b195d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.swp *.pyc *.egg-info +.project +.pydevproject project/ ve/ build/ diff --git a/AUTHORS.rst b/AUTHORS.rst index 2525632..5c126a2 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -7,4 +7,4 @@ Praekelt Foundation * Shaun Sephton * Jonathan Bydendyk * Euan Jonker - +* Max Naude diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1862fed..f00806c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +0.1.5.unomena.1 +----- +#. Added GenericObjectFilterOrderList and GenericSearchForm views + +0.1.5.unomena +----- +#. Merged in unomena changes + 0.1.5 ----- #. Use photologue 2.6.praekelt diff --git a/panya/admin.py b/panya/admin.py index 3c2f3c3..88ac3c0 100644 --- a/panya/admin.py +++ b/panya/admin.py @@ -10,6 +10,8 @@ from publisher.models import Publisher from photologue.admin import ImageOverrideInline +from reversion.admin import VersionAdmin + class ModelBaseAdmin(admin.ModelAdmin): inlines = [ImageOverrideInline,] list_display = ('title', 'state', 'admin_thumbnail', 'owner', 'created') @@ -73,3 +75,52 @@ def save_model(self, request, obj, form, change): obj.owner = request.user return super(ModelBaseAdmin, self).save_model(request, obj, form, change) + +#============================================================================== +class BaseAdmin(admin.ModelAdmin): + + #-------------------------------------------------------------------------- + def __init__(self, model, admin_site): + super(BaseAdmin, self).__init__(model, admin_site) + + if not self.fieldsets: + self.fieldsets = tuple() + +#============================================================================== +class PublisherModelAdmin(BaseAdmin, VersionAdmin): + + publisher_fieldsets = (('Publishing', {'fields': ('state', + 'publish_on', + 'retract_on', + 'sites', + ), + 'classes': ('collapse',), + }, + ), + ) + + #-------------------------------------------------------------------------- + def __init__(self, model, admin_site): + + super(PublisherModelAdmin, self).__init__(model, admin_site) + self.fieldsets += self.publisher_fieldsets + +#============================================================================== +class ImageModelAdmin(BaseAdmin, VersionAdmin): + + image_fieldsets = (('Images', {'fields': ('image', + 'crop_from', + 'effect', + ), + 'classes': ('collapse',), + }, + ), + ) + + #-------------------------------------------------------------------------- + def __init__(self, model, admin_site): + + super(ImageModelAdmin, self).__init__(model, admin_site) + self.fieldsets += self.image_fieldsets + + diff --git a/panya/generic/views.py b/panya/generic/views.py index f0017bd..34ccb8d 100755 --- a/panya/generic/views.py +++ b/panya/generic/views.py @@ -1,14 +1,14 @@ +import sys import copy -import traceback from django.db.models import Q -from django.db.models.fields import related from django.contrib import messages from django.shortcuts import render_to_response from django.template import loader from django.template import RequestContext from django.utils.translation import ugettext from django.views.generic import list_detail +from django.http import HttpResponse class DefaultURL(object): def __call__(self, obj=None): @@ -131,6 +131,161 @@ def __call__(self, request, *args, **kwargs): generic_object_filter_list = GenericObjectFilterList() +class GenericObjectFilterOrderList(GenericObjectList): + + def __call__(self, request, *args, **kwargs): + # generate our view via genericbase + view = super(GenericObjectList, self).__call__(request, *args, **kwargs) + + # setup object_list params + queryset=view.params['queryset'] + del view.params['queryset'] + + if view.params['extra_context'].has_key('filter'): + # Filter + for field in view.params['extra_context']['filter'].keys(): + + if view.params['extra_context']['filter'][field].has_key('operator'): + queryset = queryset.filter(Q(**{"%s__%s" % (field, view.params['extra_context']['filter'][field]['operator']) : view.params['extra_context']['filter'][field]['value']})) + else: + queryset = queryset.filter(Q(**{"%s" % field : view.params['extra_context']['filter'][field]['value']})) + + order_by = '-id' + + if request.GET.has_key('order_by'): + order_by = request.GET['order_by'] + request.session['order_by_%s' % view.params['extra_context']['unique_session_key']] = order_by + elif request.session.has_key('order_by_%s' % view.params['extra_context']['unique_session_key']): + order_by = request.session['order_by_%s' % view.params['extra_context']['unique_session_key']] + + view.params['extra_context']['order_by'] = order_by + + queryset = queryset.order_by(order_by,'-id') + + request.session['full_queryset'] = queryset + + try: + view.params['paginate_by'] = int(request.POST.get('page_limit', request.session['page_limit'])) + request.session['page_limit'] = view.params['paginate_by'] + except: + request.session['page_limit'] = kwargs['paginate_by'] + + view.params['extra_context']['page_limit'] = request.session['page_limit'] + + # return object list generic view + return list_detail.object_list(request, queryset=queryset, **view.params) + +generic_object_filter_order_list = GenericObjectFilterOrderList() + +#============================================================================== +class GenericSearchForm(GenericBase): + defaults = { + 'form_class': None, + 'form_args': None, + 'initial': None, + 'extra_context': None, + 'template_name': None, + 'success_message': None, + 'filter_name': None, + } + + def handle_valid(self, form=None, *args, **kwargs): + """ + Called after the form has validated. + """ + # Try and call handle_valid method of the form itself. + if hasattr(form, 'handle_valid'): + filter = form.handle_valid(*args, **kwargs) + + return filter + + def redirect(self, request, *args, **kwargs): + """ + Redirect after successful form submission. + Default behaviour is to not redirect and hence return the original view. + """ + return None + + #-------------------------------------------------------------------------- + def get_form_args(self, *args, **kwargs): + return {} + + #-------------------------------------------------------------------------- + def get_initial(self, request, *args, **kwargs): + return {} + + def __call__(self, request, *args, **kwargs): + # generate our view via genericbase + view = super(GenericSearchForm, self).__call__(request, *args, **kwargs) + + self.form_class = view.params['form_class'] + self.form_args = view.params['form_args'] + self.template_name = view.params['template_name'] + self.success_message = view.params['success_message'] + self.filter_name = view.params['filter_name'] + + kwargs['filter_on'] = False + kwargs['filter_form'] = view.params['form_class'] + + if self.form_class: + if request.method == 'POST': + + if request.POST.has_key('clearFilter') and request.POST['clearFilter'] == 'True': + form = self.form_class(initial=self.get_initial(request=request, *args, **kwargs), **self.form_args) + kwargs['filter_on'] = False + request.session['%s_filter_on' % self.filter_name] = False + request.session[self.filter_name] = {} + + else: + if request.POST.has_key('clearFilter'): + form = self.form_class(data=request.POST, files=request.FILES, **self.form_args) + request.session['%s_data' % self.filter_name] = request.POST + else: + if request.session.has_key('%s_filter_on' % self.filter_name) and request.session['%s_filter_on' % self.filter_name]: + data = request.session['%s_data' % self.filter_name] + form = self.form_class(data=data, files=request.FILES, **self.form_args) + else: + form = self.form_class(initial=self.get_initial(request=request, *args, **kwargs), **self.form_args) + + if form.is_valid(): + request.session[self.filter_name] = self.handle_valid(form=form, request=request, *args, **kwargs) + if request.session[self.filter_name]: + kwargs['filter_on'] = True + else: + kwargs['filter_on'] = False + + request.session['%s_filter_on' % self.filter_name] = kwargs['filter_on'] + + else: + if request.session.has_key('%s_filter_on' % self.filter_name) and request.session['%s_filter_on' % self.filter_name]: + data = request.session['%s_data' % self.filter_name] + form = self.form_class(data=data, files=request.FILES, **self.form_args) + + if form.is_valid(): + request.session[self.filter_name] = self.handle_valid(form=form, request=request, *args, **kwargs) + if request.session[self.filter_name]: + kwargs['filter_on'] = True + else: + kwargs['filter_on'] = False + + request.session['%s_filter_on' % self.filter_name] = kwargs['filter_on'] + else: + form = self.form_class(initial=self.get_initial(request=request, *args, **kwargs), **self.form_args) + + kwargs['filter_form'] = form + else: + kwargs['filter_form'] = None + request.session[self.filter_name] = {} + + try: + kwargs['filter'] = request.session[self.filter_name] + except: + kwargs['filter'] = {} + + return generic_object_filter_order_list(request, *args, **kwargs) + +generic_search_form = GenericSearchForm() + class GenericObjectDetail(GenericBase): defaults = { 'queryset': None, @@ -162,7 +317,7 @@ def __call__(self, request, *args, **kwargs): class GenericForm(GenericBase): defaults = { 'form_class': None, - 'form_args': {}, + 'form_args': None, 'initial': None, 'extra_context': None, 'template_name': None, @@ -192,7 +347,7 @@ def __call__(self, request, *args, **kwargs): view = super(GenericForm, self).__call__(request, *args, **kwargs) self.form_class = view.params['form_class'] - self.form_args = view.params['form_args'] + self.form_args = view.params['form_args'] or {} self.initial = view.params['initial'] self.template_name = view.params['template_name'] self.success_message = view.params['success_message'] @@ -218,3 +373,129 @@ def __call__(self, request, *args, **kwargs): return render_to_response(self.template_name, context) generic_form_view = GenericForm() + +class GenericList(GenericBase): + defaults = { + 'callback': None, + 'callback_kwargs': None, + 'template_name': None, + 'template_name_field': None, + 'template_loader': loader, + 'template_object_name': 'object_list', + 'context_processors': None, + 'extra_context': None, + 'mimetype': None, + } + + def __call__(self, request, *args, **kwargs): + # generate our view via genericbase + view = super(GenericList, self).__call__(request, *args, **kwargs) + + # return generic detail generic view + return self.generic_list(request, view.params.pop('callback'), **view.params) + + def generic_list(self, request, callback, **kwargs): + """ + Generic list of a non-QuerySet objects, yielded by calling the klass + method and passing method_kwargs + """ + template_name = kwargs['template_name'] + template_name_field = kwargs['template_name_field'] + template_loader = kwargs['template_loader'] + template_object_name = kwargs['template_object_name'] + context_processors = kwargs['context_processors'] + mimetype = kwargs['mimetype'] + + extra_context = kwargs.get('extra_context', {}) or {} + callback_args = kwargs.get('callback_args', ()) or () + callback_kwargs = kwargs.get('callback_kwargs', {}) or {} + + if not callable(callback): + mod_name = callback.split('.')[:-1] + __import__(mod_name) + callback = getattr(sys.modules[mod_name], callback.split('.')[-1]) + + object_list = callback(*callback_args, **callback_kwargs) + + if hasattr(self, 'callback_results_processor') and \ + callable(getattr(self, 'callback_results_processor')): + getattr(self, 'callback_results_processor')(request, object_list, **kwargs) + + if template_name_field: + template_name_list = [getattr(object_list, template_name_field), template_name] + t = template_loader.select_template(template_name_list) + else: + t = template_loader.get_template(template_name) + c = RequestContext(request, {template_object_name: object_list}, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + response = HttpResponse(t.render(c), mimetype=mimetype) + return response + +generic_list = GenericList() + +class GenericDetail(GenericBase): + defaults = { + 'callback': None, + 'callback_kwargs': None, + 'template_name': None, + 'template_name_field': None, + 'template_loader': loader, + 'template_object_name': 'object', + 'context_processors': None, + 'extra_context': None, + 'mimetype': None, + } + + def __call__(self, request, *args, **kwargs): + # generate our view via genericbase + view = super(GenericDetail, self).__call__(request, *args, **kwargs) + + # return generic detail generic view + return self.generic_detail(request, view.params.pop('callback'), **view.params) + + def generic_detail(self, request, callback, **kwargs): + """ + Generic detail of a non-QuerySet object, yielded by calling the klass + method and passing method_kwargs + """ + template_name = kwargs['template_name'] + template_name_field = kwargs['template_name_field'] + template_loader = kwargs['template_loader'] + template_object_name = kwargs['template_object_name'] + context_processors = kwargs['context_processors'] + mimetype = kwargs['mimetype'] + + extra_context = kwargs.get('extra_context', {}) or {} + callback_args = kwargs.get('callback_args', ()) or () + callback_kwargs = kwargs.get('callback_kwargs', {}) or {} + + if not callable(callback): + mod_name = callback.split('.')[:-1] + __import__(mod_name) + callback = getattr(sys.modules[mod_name], callback.split('.')[-1]) + + obj = callback(*callback_args, **callback_kwargs) + + if hasattr(self, 'callback_results_processor') and \ + callable(getattr(self, 'callback_results_processor')): + getattr(self, 'callback_results_processor')(request, obj, **kwargs) + + if template_name_field: + template_name_list = [getattr(obj, template_name_field), template_name] + t = template_loader.select_template(template_name_list) + else: + t = template_loader.get_template(template_name) + c = RequestContext(request, {template_object_name: obj}, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + response = HttpResponse(t.render(c), mimetype=mimetype) + return response + +generic_detail = GenericDetail() diff --git a/panya/managers.py b/panya/managers.py index fa41cc5..754cb12 100644 --- a/panya/managers.py +++ b/panya/managers.py @@ -1,6 +1,11 @@ +import datetime +import logging + from django.conf import settings from django.db import models +import caching.base + class PermittedManager(models.Manager): def get_query_set(self): # get base queryset and exclude based on state @@ -13,3 +18,16 @@ def get_query_set(self): # filter objects for current site queryset = queryset.filter(sites__id__exact=settings.SITE_ID) return queryset + +#============================================================================== +class PublisherManager(caching.base.CachingManager): + def get_query_set(self): + + today = datetime.date.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:00') + + if getattr(settings, 'STAGING', False): + queryset = super(PublisherManager, self).get_query_set().exclude(state='unpublished') + else: + queryset = super(PublisherManager, self).get_query_set().filter(state='published').exclude(publish_on__gt=today).exclude(retract_on__lte=today) + + return queryset.filter(sites__id__exact=settings.SITE_ID) \ No newline at end of file diff --git a/panya/models.py b/panya/models.py index 1de1b53..bb4882d 100644 --- a/panya/models.py +++ b/panya/models.py @@ -12,13 +12,15 @@ from django.utils.encoding import smart_unicode import secretballot -from panya.managers import PermittedManager +from panya.managers import PublisherManager, PermittedManager from panya.utils import generate_slug from photologue.models import ImageModel from secretballot.models import Vote -class ModelBase(ImageModel): +import caching.base + +class ModelBase(caching.base.CachingMixin, ImageModel): objects = models.Manager() permitted = PermittedManager() @@ -301,3 +303,105 @@ def set_managers(sender, **kwargs): # enable voting for ModelBase, but specify a different total name # so ModelBase's vote_total method is not overwritten secretballot.enable_voting_on(ModelBase, total_name="secretballot_added_vote_total") + + +#============================================================================== +class PublisherBase(caching.base.CachingMixin, models.Model): + """ + A publiser-only model + """ + objects = caching.base.CachingManager() + published = PublisherManager() + + state = models.CharField( + max_length=32, + choices=( + ('unpublished', 'Unpublished'), + ('published', 'Published'), + ('staging', 'Staging'), + ), + default='unpublished', + help_text="Set the item state. The 'Published' state makes the item visible to the public, 'Unpublished' retracts it and 'Staging' makes the item visible to staff users.", + db_index=True, + blank=True, + null=True, + ) + publish_on = models.DateTimeField( + blank=True, + null=True, + help_text="Date and time on which to publish this item (state will change to 'published').", + ) + retract_on = models.DateTimeField( + blank=True, + null=True, + help_text="Date and time on which to retract this item (state will change to 'unpublished').", + ) + content_type = models.ForeignKey( + ContentType, + editable=False, + null=True + ) + class_name = models.CharField( + max_length=32, + editable=False, + null=True + ) + sites = models.ManyToManyField( + 'sites.Site', + blank=True, + null=True, + help_text='Makes item eligible to be published on selected sites.', + ) + + def as_leaf_class(self): + """ + Returns the leaf class no matter where the calling instance is in the inheritance hierarchy. + Inspired by http://www.djangosnippets.org/snippets/1031/ + """ + try: + return self.__getattribute__(self.class_name.lower()) + except AttributeError: + content_type = self.content_type + model = content_type.model_class() + if(model == PublisherBase): + return self + return model.objects.get(id=self.id) + + def save(self, *args, **kwargs): + + # set leaf class content type + if not self.content_type: + self.content_type = ContentType.objects.get_for_model(self.__class__) + + # set leaf class class name + if not self.class_name: + self.class_name = self.__class__.__name__ + + super(PublisherBase, self).save(*args, **kwargs) + + @property + def is_permitted(self): + def for_site(): + if self.sites.filter(id__exact=settings.SITE_ID): + return True + else: + return False + + if self.state == 'unpublished': + return False + elif self.state == 'published': + return for_site() + elif self.state == 'staging': + if getattr(settings, 'STAGING', False): + return for_site() + + return False + + @property + def base_obj(self): + if self.__class__ == PublisherBase: + return self + else: + return self.publisherbase_ptr + + \ No newline at end of file diff --git a/setup.py b/setup.py index c5019cc..127a743 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='panya', - version='0.1.5', + version='0.1.5.unomena.1', description='Panya base app.', long_description = open('README.rst', 'r').read() + open('AUTHORS.rst', 'r').read() + open('CHANGELOG.rst', 'r').read(), author='Praekelt Foundation', @@ -13,6 +13,7 @@ dependency_links = [ 'http://dist.plone.org/thirdparty/', 'http://github.com/praekelt/django-photologue/tarball/2.6.praekelt#egg=django-photologue-2.6.praekelt', + 'http://github.com/unomena/django-cache-machine/tarball/master#egg=django-cache-machine-0.4.1.unomena', ], install_requires = [ 'PIL', @@ -20,6 +21,9 @@ 'django-photologue==2.6.praekelt', 'django-publisher', 'django-secretballot', + 'python-memcached==1.47', + 'django-cache-machine==0.4.1.unomena', + 'django-reversion==1.3.2', ], include_package_data=True, classifiers = [