Commits

Maciej Wiśniowski committed 8809e84

initial commit

  • Participants

Comments (0)

Files changed (22)

+syntax: glob
+*.pyc
+*.pyo
+*.DS_Store 
+*.installed.cfg
+*build/
+*dist/
+*.egg-info/
+Copyright (c) 2009 - 2011, Kai Diefenbach - IQ++
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    
+    * Redistributions in binary form must reproduce the above copyright 
+      notice, this list of conditions and the following disclaimer in the 
+      documentation and/or other materials provided with the distribution.
+      
+    * Neither the name of the author nor the names of other contributors may 
+      be used to endorse or promote products derived from this software 
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include README.txt
+recursive-include lfs_carousel/locale *
+recursive-include lfs_carousel/templates *
+What is it?
+===========
+
+LFS Carousel is pluggable carousel/slider application for use with LFS (Lighting Fast Shop)
+
+
+Basic usage
+===========
+
+* add lfs_carousel to INSTALLED_APPS in your settings.py
+* add lfs_carousel urls to your site urls.py like:
+
+  from lfs_carousel import carousel
+  urlpatterns += patterns("",
+    (...)
+    (r'^carousel/', include(carousel.urls)),
+    (...)
+)
+  
+* add carousel to management panel to 'Shop' -> settings page as new tab:
+  copy lfs/templates/manage/shop/shop.html to your theme and modify it by adding:
+  {% load lfs_carousel_tags %}
+  ( ... )
+  <div id="manage-tabs">
+    <ul>
+        <li class="ui-tabs-nav-item"><a href="#data">{% trans 'Shop' %}</a></li>
+        <li class="ui-tabs-nav-item"><a href="#default-values">{% trans 'Default Values' %}</a></li>
+        <li class="ui-tabs-nav-item"><a href="#portlets">{% trans 'Portlets' %}</a></li>
+        **<li class="ui-tabs-nav-item"><a href="#carousel-items">{% trans 'Carousel' %}</a></li>**
+    </ul>
+  (...)
+  <div id="portlets">
+    {{ portlets|safe }}
+  </div>
+  **{% carousel_management shop %}**
+  
+* add carousel to your shop's start page
+  By default lfs_carousel uses coin-slider but you can use anything you want. 
+  First add necessary JavaScript and CSS files either to base.html or to shop.html:
+  
+  <link rel="stylesheet" href="{{ STATIC_URL }}coin-slider/coin-slider-styles.css" type="text/css" />
+  <script type="text/javascript" src="{{ STATIC_URL }}coin-slider/coin-slider.min.js"></script>
+  <script type="text/javascript">
+        $(document).ready(function() {
+            $('#coin-slider').coinslider({ width: 400, navigation: true, delay: 10000, hoverPause: true });
+        });
+  </script>
+  
+  Second: copy lfstheme/templates/lfs/shop/shop.html to your theme and add:
+    {% load lfs_carousel_tags %}
+    (...)
+    {% carousel_show shop %}
+  
+* run syncdb (!)

File lfs_carousel/__init__.py

+from lfs_carousel.views import LFCCarouselView, carousel

File lfs_carousel/admin.py

Empty file added.

File lfs_carousel/images.py

+# django imports
+from django.contrib.auth.decorators import permission_required
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.template import RequestContext
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+from django.utils import simplejson
+
+# lfs.imports
+import lfs.core.utils
+from lfs.caching.utils import lfs_get_object_or_404
+from lfs.catalog.models import Image
+from lfs.catalog.models import Product
+from lfs.core.signals import product_changed
+from lfs.core.utils import LazyEncoder
+
+# Load logger
+import logging
+logger = logging.getLogger("default")
+
+@permission_required("core.manage_shop", login_url="/login/")
+def manage_images(request, product_id, as_string=False, template_name="lfc_carousel/images.html"):
+    """
+    """
+    product = lfs_get_object_or_404(Product, pk=product_id)
+
+    result = render_to_string(template_name, RequestContext(request, {
+        "product": product,
+    }))
+
+    if as_string:
+        return result
+    else:
+        result = simplejson.dumps({
+            "images": result,
+            "message": _(u"Images has been added."),
+        }, cls=LazyEncoder)
+
+        return HttpResponse(result)
+
+
+# Actions
+# @permission_required("core.manage_shop", login_url="/login/")
+def add_image(request, product_id):
+    """Adds an image to product with passed product_id.
+    """
+    product = lfs_get_object_or_404(Product, pk=product_id)
+    if request.method == "POST":
+        for file_content in request.FILES.getlist("file"):
+            image = Image(content=product, title=file_content.name)
+            try:
+                image.image.save(file_content.name, file_content, save=True)
+            except Exception, e:
+                logger.info("Upload image: %s %s" % (file_content.name, e))
+                continue
+
+    # Refresh positions
+    for i, image in enumerate(product.images.all()):
+        image.position = (i + 1) * 10
+        image.save()
+
+    product_changed.send(product, request=request)
+
+    result = simplejson.dumps({"name": file_content.name, "type": "image/jpeg", "size": "123456789"})
+    return HttpResponse(result)
+
+
+@permission_required("core.manage_shop", login_url="/login/")
+def update_images(request, product_id):
+    """Saves/deletes images with given ids (passed by request body).
+    """
+    product = lfs_get_object_or_404(Product, pk=product_id)
+
+    action = request.POST.get("action")
+    if action == "delete":
+        message = _(u"Images has been deleted.")
+        for key in request.POST.keys():
+            if key.startswith("delete-"):
+                try:
+                    id = key.split("-")[1]
+                    image = Image.objects.get(pk=id).delete()
+                except (IndexError, ObjectDoesNotExist):
+                    pass
+
+    elif action == "update":
+        message = _(u"Images has been updated.")
+        for key, value in request.POST.items():
+            if key.startswith("title-"):
+                id = key.split("-")[1]
+                try:
+                    image = Image.objects.get(pk=id)
+                except ObjectDoesNotExist:
+                    pass
+                else:
+                    image.title = value
+                    image.save()
+
+            elif key.startswith("position-"):
+                try:
+                    id = key.split("-")[1]
+                    image = Image.objects.get(pk=id)
+                except (IndexError, ObjectDoesNotExist):
+                    pass
+                else:
+                    image.position = value
+                    image.save()
+
+    # Refresh positions
+    for i, image in enumerate(product.images.all()):
+        image.position = (i + 1) * 10
+        image.save()
+
+    product_changed.send(product, request=request)
+
+    html = [["#images", manage_images(request, product_id, as_string=True)]]
+    result = simplejson.dumps({
+        "html": html,
+        "message": message,
+    }, cls=LazyEncoder)
+
+    return HttpResponse(result)
+
+
+@permission_required("core.manage_shop", login_url="/login/")
+def move_image(request, id):
+    """Moves the image with passed id up or down.
+
+    **Parameters:**
+
+        id
+            The id of the image which should be edited.
+
+    **Query String:**
+
+        direction
+            The direction in which the image should be moved. One of 0 (up)
+            or 1 (down).
+
+    **Permission:**
+
+        edit (of the belonging content object)
+    """
+    image = Image.objects.get(pk=id)
+    product = image.content
+
+    direction = request.GET.get("direction", 0)
+
+    if direction == "1":
+        image.position += 15
+    else:
+        image.position -= 15
+        if image.position < 0:
+            image.position = 10
+
+    image.save()
+
+    # Refresh positions
+    for i, image in enumerate(product.images.all()):
+        image.position = (i + 1) * 10
+        image.save()
+
+    html = [["#images", manage_images(request, product.id, as_string=True)]]
+
+    result = simplejson.dumps({
+         "html": html,
+    }, cls=LazyEncoder)
+
+    return HttpResponse(result)
+
+
+@permission_required("core.manage_shop", login_url="/login/")
+def update_active_images(request, product_id):
+    """Updates the images activity state for product variants.
+    """
+    product = Product.objects.get(pk=product_id)
+    if request.POST.get("active_images"):
+        product.active_images = True
+    else:
+        product.active_images = False
+    product.save()
+
+    return lfs.core.utils.set_message_cookie(
+        url=reverse("lfs_manage_product", kwargs={"product_id": product.id}),
+        msg=_(u"Active images has been updated."),
+    )

File lfs_carousel/migrations/0001_initial.py

+# 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 model 'CarouselItem'
+        db.create_table('lfs_carousel_carouselitem', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='carousel_item', null=True, to=orm['contenttypes.ContentType'])),
+            ('content_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)),
+            ('image', self.gf('lfs.core.fields.thumbs.ImageWithThumbsField')(blank=True, max_length=100, null=True, sizes=((60, 60), (100, 100), (200, 200), (300, 300), (400, 400)))),
+            ('link', self.gf('django.db.models.fields.URLField')(default='', max_length=200, null=True, blank=True)),
+            ('position', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=999)),
+        ))
+        db.send_create_signal('lfs_carousel', ['CarouselItem'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'CarouselItem'
+        db.delete_table('lfs_carousel_carouselitem')
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'lfs_carousel.carouselitem': {
+            'Meta': {'ordering': "('position',)", 'object_name': 'CarouselItem'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'carousel_item'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'image': ('lfs.core.fields.thumbs.ImageWithThumbsField', [], {'blank': 'True', 'max_length': '100', 'null': 'True', 'sizes': '((60, 60), (100, 100), (200, 200), (300, 300), (400, 400))'}),
+            'link': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '999'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['lfs_carousel']

File lfs_carousel/migrations/0002_auto__add_field_carouselitem_text.py

+# 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 'CarouselItem.text'
+        db.add_column('lfs_carousel_carouselitem', 'text', self.gf('django.db.models.fields.TextField')(default='', blank=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'CarouselItem.text'
+        db.delete_column('lfs_carousel_carouselitem', 'text')
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'lfs_carousel.carouselitem': {
+            'Meta': {'ordering': "('position',)", 'object_name': 'CarouselItem'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'carousel_item'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'image': ('lfs.core.fields.thumbs.ImageWithThumbsField', [], {'blank': 'True', 'max_length': '100', 'null': 'True', 'sizes': '((60, 60), (100, 100), (200, 200), (300, 300), (400, 400))'}),
+            'link': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '999'}),
+            'text': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['lfs_carousel']

File lfs_carousel/migrations/__init__.py

Empty file added.

File lfs_carousel/models.py

+# -*- coding: utf-8 -*-
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import ugettext_lazy as _
+from django.db import models
+from django.conf import settings
+
+from lfs.catalog.settings import THUMBNAIL_SIZES
+from lfs.core.fields.thumbs import ImageWithThumbsField
+
+
+class CarouselItem(models.Model):
+    """An carousel item with a title, image with several sizes and url.
+
+    Attributes:
+        - content
+          The content object it belongs to.
+        - title
+          The title of the image.
+        - image
+          The image file.
+        - link
+          The link that can be used 'under' image
+        - position
+          The position of the image within the content object it belongs to.
+    """
+    content_type = models.ForeignKey(ContentType, verbose_name=_(u"Content type"), related_name="carousel_item", blank=True, null=True)
+    content_id = models.PositiveIntegerField(_(u"Content id"), blank=True, null=True)
+    content = generic.GenericForeignKey(ct_field="content_type", fk_field="content_id")
+
+    title = models.CharField(_(u"Title"), blank=True, max_length=100)
+    image = ImageWithThumbsField(_(u"Image"), upload_to="images", blank=True, null=True, sizes=THUMBNAIL_SIZES)
+    link = models.URLField(_(u"URL"), null=True, blank=True, default='')
+    text = models.TextField(_('Text'), null=False, blank=True, default='')
+    position = models.PositiveSmallIntegerField(_(u"Position"), default=999)
+
+    class Meta:
+        ordering = ("position", )
+
+    def __unicode__(self):
+        return self.title
+
+
+if 'south' in settings.INSTALLED_APPS:
+    # south rules
+    rules = [
+      (
+        (ImageWithThumbsField,),
+        [],
+        {
+            "sizes": ["sizes", {"default": None}]
+        },
+      )
+    ]
+    from south.modelsinspector import add_introspection_rules
+    add_introspection_rules(rules, ["^lfs\.core\.fields\.thumbs"])

File lfs_carousel/signals.py

+# django imports
+import django.dispatch
+
+carousel_changed = django.dispatch.Signal()
+
+

File lfs_carousel/static/coin-slider/coin-slider-styles.css

+/*
+	Coin Slider jQuery plugin CSS styles
+	http://workshop.rs/projects/coin-slider
+*/
+
+
+.coin-slider, #coin-slider { overflow: hidden; zoom: 1; position: relative; width:400px; height:300px}
+.coin-slider a{ text-decoration: none; outline: none; border: none; }
+
+.cs-buttons { font-size: 0px; padding: 10px; float: left; }
+.cs-buttons a { margin-left: 5px; height: 10px; width: 10px; float: left; border: 1px solid #B8C4CF; color: #B8C4CF; text-indent: -1000px; }
+.cs-active { background-color: #B8C4CF; color: #FFFFFF; }
+
+.cs-title { width: 380px; padding: 10px; background-color: #000000; color: #FFFFFF; }
+
+.cs-prev, 
+.cs-next { background-color: #000000; color: #FFFFFF; padding: 0px 10px; }

File lfs_carousel/static/coin-slider/coin-slider.js

+/**
+ * Coin Slider - Unique jQuery Image Slider
+ * @version: 1.0 - (2010/04/04)
+ * @requires jQuery v1.2.2 or later 
+ * @author Ivan Lazarevic
+ * Examples and documentation at: http://workshop.rs/projects/coin-slider/
+ 
+ * Licensed under MIT licence:
+ *   http://www.opensource.org/licenses/mit-license.php
+**/
+
+(function($) {
+
+	var params 		= new Array;
+	var order		= new Array;
+	var images		= new Array;
+	var links		= new Array;
+	var linksTarget = new Array;
+	var titles		= new Array;
+	var interval	= new Array;
+	var imagePos	= new Array;
+	var appInterval = new Array;	
+	var squarePos	= new Array;	
+	var reverse		= new Array;
+	
+	$.fn.coinslider= $.fn.CoinSlider = function(options){
+		
+		init = function(el){
+				
+			order[el.id] 		= new Array();	// order of square appereance
+			images[el.id]		= new Array();
+			links[el.id]		= new Array();
+			linksTarget[el.id]	= new Array();
+			titles[el.id]		= new Array();
+			imagePos[el.id]		= 0;
+			squarePos[el.id]	= 0;
+			reverse[el.id]		= 1;						
+				
+			params[el.id] = $.extend({}, $.fn.coinslider.defaults, options);
+						
+			// create images, links and titles arrays
+			$.each($('#'+el.id+' img'), function(i,item){
+				images[el.id][i] 		= $(item).attr('src');
+				links[el.id][i] 		= $(item).parent().is('a') ? $(item).parent().attr('href') : '';
+				linksTarget[el.id][i] 	= $(item).parent().is('a') ? $(item).parent().attr('target') : '';
+				titles[el.id][i] 		= $(item).next().is('span') ? $(item).next().html() : '';
+				$(item).hide();
+				$(item).next().hide();
+			});			
+			
+
+			// set panel
+			$(el).css({
+				'background-image':'url('+images[el.id][0]+')',
+				'width': params[el.id].width,
+				'height': params[el.id].height,
+				'position': 'relative',
+				'background-position': 'top left'
+			}).wrap("<div class='coin-slider' id='coin-slider-"+el.id+"' />");	
+			
+				
+			// create title bar
+			$('#'+el.id).append("<div class='cs-title' id='cs-title-"+el.id+"' style='position: absolute; bottom:0; left: 0; z-index: 1000;'></div>");
+						
+			$.setFields(el);
+			
+			if(params[el.id].navigation)
+				$.setNavigation(el);
+			
+			$.transition(el,0);
+			$.transitionCall(el);
+				
+		}
+		
+		// squares positions
+		$.setFields = function(el){
+			
+			tWidth = sWidth = parseInt(params[el.id].width/params[el.id].spw);
+			tHeight = sHeight = parseInt(params[el.id].height/params[el.id].sph);
+			
+			counter = sLeft = sTop = 0;
+			tgapx = gapx = params[el.id].width - params[el.id].spw*sWidth;
+			tgapy = gapy = params[el.id].height - params[el.id].sph*sHeight;
+			
+			for(i=1;i <= params[el.id].sph;i++){
+				gapx = tgapx;
+				
+					if(gapy > 0){
+						gapy--;
+						sHeight = tHeight+1;
+					} else {
+						sHeight = tHeight;
+					}
+				
+				for(j=1; j <= params[el.id].spw; j++){	
+
+					if(gapx > 0){
+						gapx--;
+						sWidth = tWidth+1;
+					} else {
+						sWidth = tWidth;
+					}
+
+					order[el.id][counter] = i+''+j;
+					counter++;
+					
+					if(params[el.id].links)
+						$('#'+el.id).append("<a href='"+links[el.id][0]+"' class='cs-"+el.id+"' id='cs-"+el.id+i+j+"' style='width:"+sWidth+"px; height:"+sHeight+"px; float: left; position: absolute;'></a>");
+					else
+						$('#'+el.id).append("<div class='cs-"+el.id+"' id='cs-"+el.id+i+j+"' style='width:"+sWidth+"px; height:"+sHeight+"px; float: left; position: absolute;'></div>");
+								
+					// positioning squares
+					$("#cs-"+el.id+i+j).css({ 
+						'background-position': -sLeft +'px '+(-sTop+'px'),
+						'left' : sLeft ,
+						'top': sTop
+					});
+				
+					sLeft += sWidth;
+				}
+
+				sTop += sHeight;
+				sLeft = 0;					
+					
+			}
+			
+			
+			$('.cs-'+el.id).mouseover(function(){
+				$('#cs-navigation-'+el.id).show();
+			});
+		
+			$('.cs-'+el.id).mouseout(function(){
+				$('#cs-navigation-'+el.id).hide();
+			});	
+			
+			$('#cs-title-'+el.id).mouseover(function(){
+				$('#cs-navigation-'+el.id).show();
+			});
+		
+			$('#cs-title-'+el.id).mouseout(function(){
+				$('#cs-navigation-'+el.id).hide();
+			});	
+			
+			if(params[el.id].hoverPause){	
+				$('.cs-'+el.id).mouseover(function(){
+					params[el.id].pause = true;
+				});
+			
+				$('.cs-'+el.id).mouseout(function(){
+					params[el.id].pause = false;
+				});	
+				
+				$('#cs-title-'+el.id).mouseover(function(){
+					params[el.id].pause = true;
+				});
+			
+				$('#cs-title-'+el.id).mouseout(function(){
+					params[el.id].pause = false;
+				});	
+			}
+					
+			
+		};
+				
+		
+		$.transitionCall = function(el){
+		
+			clearInterval(interval[el.id]);	
+			delay = params[el.id].delay + params[el.id].spw*params[el.id].sph*params[el.id].sDelay;
+			interval[el.id] = setInterval(function() { $.transition(el)  }, delay);
+			
+		}
+		
+		// transitions
+		$.transition = function(el,direction){
+			
+			if(params[el.id].pause == true) return;
+			
+			$.effect(el);
+			
+			squarePos[el.id] = 0;
+			appInterval[el.id] = setInterval(function() { $.appereance(el,order[el.id][squarePos[el.id]])  },params[el.id].sDelay);
+					
+			$(el).css({ 'background-image': 'url('+images[el.id][imagePos[el.id]]+')' });
+			
+			if(typeof(direction) == "undefined")
+				imagePos[el.id]++;
+			else
+				if(direction == 'prev')
+					imagePos[el.id]--;
+				else
+					imagePos[el.id] = direction;
+		
+			if  (imagePos[el.id] == images[el.id].length) {
+				imagePos[el.id] = 0;
+			}
+			
+			if (imagePos[el.id] == -1){
+				imagePos[el.id] = images[el.id].length-1;
+			}
+	
+			$('.cs-button-'+el.id).removeClass('cs-active');
+			$('#cs-button-'+el.id+"-"+(imagePos[el.id]+1)).addClass('cs-active');
+			
+			if(titles[el.id][imagePos[el.id]]){
+				$('#cs-title-'+el.id).css({ 'opacity' : 0 }).animate({ 'opacity' : params[el.id].opacity }, params[el.id].titleSpeed);
+				$('#cs-title-'+el.id).html(titles[el.id][imagePos[el.id]]);
+			} else {
+				$('#cs-title-'+el.id).css('opacity',0);
+			}				
+				
+		};
+		
+		$.appereance = function(el,sid){
+
+			$('.cs-'+el.id).attr('href',links[el.id][imagePos[el.id]]).attr('target',linksTarget[el.id][imagePos[el.id]]);
+
+			if (squarePos[el.id] == params[el.id].spw*params[el.id].sph) {
+				clearInterval(appInterval[el.id]);
+				return;
+			}
+
+			$('#cs-'+el.id+sid).css({ opacity: 0, 'background-image': 'url('+images[el.id][imagePos[el.id]]+')' });
+			$('#cs-'+el.id+sid).animate({ opacity: 1 }, 300);
+			squarePos[el.id]++;
+			
+		};
+		
+		// navigation
+		$.setNavigation = function(el){
+			// create prev and next 
+			$(el).append("<div id='cs-navigation-"+el.id+"'></div>");
+			$('#cs-navigation-'+el.id).hide();
+			
+			$('#cs-navigation-'+el.id).append("<a href='#' id='cs-prev-"+el.id+"' class='cs-prev'>prev</a>");
+			$('#cs-navigation-'+el.id).append("<a href='#' id='cs-next-"+el.id+"' class='cs-next'>next</a>");
+			$('#cs-prev-'+el.id).css({
+				'position' 	: 'absolute',
+				'top'		: params[el.id].height/2 - 15,
+				'left'		: 0,
+				'z-index' 	: 1001,
+				'line-height': '30px',
+				'opacity'	: params[el.id].opacity
+			}).click( function(e){
+				e.preventDefault();
+				$.transition(el,'prev');
+				$.transitionCall(el);		
+			}).mouseover( function(){ $('#cs-navigation-'+el.id).show() });
+	
+			$('#cs-next-'+el.id).css({
+				'position' 	: 'absolute',
+				'top'		: params[el.id].height/2 - 15,
+				'right'		: 0,
+				'z-index' 	: 1001,
+				'line-height': '30px',
+				'opacity'	: params[el.id].opacity
+			}).click( function(e){
+				e.preventDefault();
+				$.transition(el);
+				$.transitionCall(el);
+			}).mouseover( function(){ $('#cs-navigation-'+el.id).show() });
+		
+			// image buttons
+			$("<div id='cs-buttons-"+el.id+"' class='cs-buttons'></div>").appendTo($('#coin-slider-'+el.id));
+
+			
+			for(k=1;k<images[el.id].length+1;k++){
+				$('#cs-buttons-'+el.id).append("<a href='#' class='cs-button-"+el.id+"' id='cs-button-"+el.id+"-"+k+"'>"+k+"</a>");
+			}
+			
+			$.each($('.cs-button-'+el.id), function(i,item){
+				$(item).click( function(e){
+					$('.cs-button-'+el.id).removeClass('cs-active');
+					$(this).addClass('cs-active');
+					e.preventDefault();
+					$.transition(el,i);
+					$.transitionCall(el);				
+				})
+			});	
+			
+			$('#cs-navigation-'+el.id+' a').mouseout(function(){
+				$('#cs-navigation-'+el.id).hide();
+				params[el.id].pause = false;
+			});						
+
+			$("#cs-buttons-"+el.id).css({
+				'left'			: '50%',
+				'margin-left' 	: -images[el.id].length*15/2-5,
+				'position'		: 'relative'
+				
+			});
+			
+				
+		}
+
+
+
+
+		// effects
+		$.effect = function(el){
+			
+			effA = ['random','swirl','rain','straight'];
+			if(params[el.id].effect == '')
+				eff = effA[Math.floor(Math.random()*(effA.length))];
+			else
+				eff = params[el.id].effect;
+
+			order[el.id] = new Array();
+
+			if(eff == 'random'){
+				counter = 0;
+				  for(i=1;i <= params[el.id].sph;i++){
+				  	for(j=1; j <= params[el.id].spw; j++){	
+				  		order[el.id][counter] = i+''+j;
+						counter++;
+				  	}
+				  }	
+				$.random(order[el.id]);
+			}
+			
+			if(eff == 'rain')	{
+				$.rain(el);
+			}
+			
+			if(eff == 'swirl')
+				$.swirl(el);
+				
+			if(eff == 'straight')
+				$.straight(el);
+				
+			reverse[el.id] *= -1;
+			if(reverse[el.id] > 0){
+				order[el.id].reverse();
+			}
+
+		}
+
+			
+		// shuffle array function
+		$.random = function(arr) {
+						
+		  var i = arr.length;
+		  if ( i == 0 ) return false;
+		  while ( --i ) {
+		     var j = Math.floor( Math.random() * ( i + 1 ) );
+		     var tempi = arr[i];
+		     var tempj = arr[j];
+		     arr[i] = tempj;
+		     arr[j] = tempi;
+		   }
+		}	
+		
+		//swirl effect by milos popovic
+		$.swirl = function(el){
+
+			var n = params[el.id].sph;
+			var m = params[el.id].spw;
+
+			var x = 1;
+			var y = 1;
+			var going = 0;
+			var num = 0;
+			var c = 0;
+			
+			var dowhile = true;
+						
+			while(dowhile) {
+				
+				num = (going==0 || going==2) ? m : n;
+				
+				for (i=1;i<=num;i++){
+					
+					order[el.id][c] = x+''+y;
+					c++;
+
+					if(i!=num){
+						switch(going){
+							case 0 : y++; break;
+							case 1 : x++; break;
+							case 2 : y--; break;
+							case 3 : x--; break;
+						
+						}
+					}
+				}
+				
+				going = (going+1)%4;
+
+				switch(going){
+					case 0 : m--; y++; break;
+					case 1 : n--; x++; break;
+					case 2 : m--; y--; break;
+					case 3 : n--; x--; break;		
+				}
+				
+				check = $.max(n,m) - $.min(n,m);			
+				if(m<=check && n<=check)
+					dowhile = false;
+									
+			}
+		}
+
+		// rain effect
+		$.rain = function(el){
+			var n = params[el.id].sph;
+			var m = params[el.id].spw;
+
+			var c = 0;
+			var to = to2 = from = 1;
+			var dowhile = true;
+
+
+			while(dowhile){
+				
+				for(i=from;i<=to;i++){
+					order[el.id][c] = i+''+parseInt(to2-i+1);
+					c++;
+				}
+				
+				to2++;
+				
+				if(to < n && to2 < m && n<m){
+					to++;	
+				}
+				
+				if(to < n && n>=m){
+					to++;	
+				}
+				
+				if(to2 > m){
+					from++;
+				}
+				
+				if(from > to) dowhile= false;
+				
+			}			
+
+		}
+
+		// straight effect
+		$.straight = function(el){
+			counter = 0;
+			for(i=1;i <= params[el.id].sph;i++){
+				for(j=1; j <= params[el.id].spw; j++){	
+					order[el.id][counter] = i+''+j;
+					counter++;
+				}
+				
+			}
+		}
+
+		$.min = function(n,m){
+			if (n>m) return m;
+			else return n;
+		}
+		
+		$.max = function(n,m){
+			if (n<m) return m;
+			else return n;
+		}		
+	
+	this.each (
+		function(){ init(this); }
+	);
+	
+
+	};
+	
+	
+	// default values
+	$.fn.coinslider.defaults = {	
+		width: 565, // width of slider panel
+		height: 290, // height of slider panel
+		spw: 7, // squares per width
+		sph: 5, // squares per height
+		delay: 3000, // delay between images in ms
+		sDelay: 30, // delay beetwen squares in ms
+		opacity: 0.7, // opacity of title and navigation
+		titleSpeed: 500, // speed of title appereance in ms
+		effect: '', // random, swirl, rain, straight
+		navigation: true, // prev next and buttons
+		links : true, // show images as links 
+		hoverPause: true // pause on hover		
+	};	
+	
+})(jQuery);
+	

File lfs_carousel/static/coin-slider/coin-slider.min.js

+/**
+ * Coin Slider - Unique jQuery Image Slider
+ * @version: 1.0 - (2010/04/04)
+ * @requires jQuery v1.2.2 or later 
+ * @author Ivan Lazarevic
+ * Examples and documentation at: http://workshop.rs/projects/coin-slider/
+ 
+ * Licensed under MIT licence:
+ *   http://www.opensource.org/licenses/mit-license.php
+**/
+
+(function($){var params=new Array;var order=new Array;var images=new Array;var links=new Array;var linksTarget=new Array;var titles=new Array;var interval=new Array;var imagePos=new Array;var appInterval=new Array;var squarePos=new Array;var reverse=new Array;$.fn.coinslider=$.fn.CoinSlider=function(options){init=function(el){order[el.id]=new Array();images[el.id]=new Array();links[el.id]=new Array();linksTarget[el.id]=new Array();titles[el.id]=new Array();imagePos[el.id]=0;squarePos[el.id]=0;reverse[el.id]=1;params[el.id]=$.extend({},$.fn.coinslider.defaults,options);$.each($('#'+el.id+' img'),function(i,item){images[el.id][i]=$(item).attr('src');links[el.id][i]=$(item).parent().is('a')?$(item).parent().attr('href'):'';linksTarget[el.id][i]=$(item).parent().is('a')?$(item).parent().attr('target'):'';titles[el.id][i]=$(item).next().is('span')?$(item).next().html():'';$(item).hide();$(item).next().hide();});$(el).css({'background-image':'url('+images[el.id][0]+')','width':params[el.id].width,'height':params[el.id].height,'position':'relative','background-position':'top left'}).wrap("<div class='coin-slider' id='coin-slider-"+el.id+"' />");$('#'+el.id).append("<div class='cs-title' id='cs-title-"+el.id+"' style='position: absolute; bottom:0; left: 0; z-index: 1000;'></div>");$.setFields(el);if(params[el.id].navigation)
+$.setNavigation(el);$.transition(el,0);$.transitionCall(el);}
+$.setFields=function(el){tWidth=sWidth=parseInt(params[el.id].width/params[el.id].spw);tHeight=sHeight=parseInt(params[el.id].height/params[el.id].sph);counter=sLeft=sTop=0;tgapx=gapx=params[el.id].width-params[el.id].spw*sWidth;tgapy=gapy=params[el.id].height-params[el.id].sph*sHeight;for(i=1;i<=params[el.id].sph;i++){gapx=tgapx;if(gapy>0){gapy--;sHeight=tHeight+1;}else{sHeight=tHeight;}
+for(j=1;j<=params[el.id].spw;j++){if(gapx>0){gapx--;sWidth=tWidth+1;}else{sWidth=tWidth;}
+order[el.id][counter]=i+''+j;counter++;if(params[el.id].links)
+$('#'+el.id).append("<a href='"+links[el.id][0]+"' class='cs-"+el.id+"' id='cs-"+el.id+i+j+"' style='width:"+sWidth+"px; height:"+sHeight+"px; float: left; position: absolute;'></a>");else
+$('#'+el.id).append("<div class='cs-"+el.id+"' id='cs-"+el.id+i+j+"' style='width:"+sWidth+"px; height:"+sHeight+"px; float: left; position: absolute;'></div>");$("#cs-"+el.id+i+j).css({'background-position':-sLeft+'px '+(-sTop+'px'),'left':sLeft,'top':sTop});sLeft+=sWidth;}
+sTop+=sHeight;sLeft=0;}
+$('.cs-'+el.id).mouseover(function(){$('#cs-navigation-'+el.id).show();});$('.cs-'+el.id).mouseout(function(){$('#cs-navigation-'+el.id).hide();});$('#cs-title-'+el.id).mouseover(function(){$('#cs-navigation-'+el.id).show();});$('#cs-title-'+el.id).mouseout(function(){$('#cs-navigation-'+el.id).hide();});if(params[el.id].hoverPause){$('.cs-'+el.id).mouseover(function(){params[el.id].pause=true;});$('.cs-'+el.id).mouseout(function(){params[el.id].pause=false;});$('#cs-title-'+el.id).mouseover(function(){params[el.id].pause=true;});$('#cs-title-'+el.id).mouseout(function(){params[el.id].pause=false;});}};$.transitionCall=function(el){clearInterval(interval[el.id]);delay=params[el.id].delay+params[el.id].spw*params[el.id].sph*params[el.id].sDelay;interval[el.id]=setInterval(function(){$.transition(el)},delay);}
+$.transition=function(el,direction){if(params[el.id].pause==true)return;$.effect(el);squarePos[el.id]=0;appInterval[el.id]=setInterval(function(){$.appereance(el,order[el.id][squarePos[el.id]])},params[el.id].sDelay);$(el).css({'background-image':'url('+images[el.id][imagePos[el.id]]+')'});if(typeof(direction)=="undefined")
+imagePos[el.id]++;else
+if(direction=='prev')
+imagePos[el.id]--;else
+imagePos[el.id]=direction;if(imagePos[el.id]==images[el.id].length){imagePos[el.id]=0;}
+if(imagePos[el.id]==-1){imagePos[el.id]=images[el.id].length-1;}
+$('.cs-button-'+el.id).removeClass('cs-active');$('#cs-button-'+el.id+"-"+(imagePos[el.id]+1)).addClass('cs-active');if(titles[el.id][imagePos[el.id]]){$('#cs-title-'+el.id).css({'opacity':0}).animate({'opacity':params[el.id].opacity},params[el.id].titleSpeed);$('#cs-title-'+el.id).html(titles[el.id][imagePos[el.id]]);}else{$('#cs-title-'+el.id).css('opacity',0);}};$.appereance=function(el,sid){$('.cs-'+el.id).attr('href',links[el.id][imagePos[el.id]]).attr('target',linksTarget[el.id][imagePos[el.id]]);if(squarePos[el.id]==params[el.id].spw*params[el.id].sph){clearInterval(appInterval[el.id]);return;}
+$('#cs-'+el.id+sid).css({opacity:0,'background-image':'url('+images[el.id][imagePos[el.id]]+')'});$('#cs-'+el.id+sid).animate({opacity:1},300);squarePos[el.id]++;};$.setNavigation=function(el){$(el).append("<div id='cs-navigation-"+el.id+"'></div>");$('#cs-navigation-'+el.id).hide();$('#cs-navigation-'+el.id).append("<a href='#' id='cs-prev-"+el.id+"' class='cs-prev'>prev</a>");$('#cs-navigation-'+el.id).append("<a href='#' id='cs-next-"+el.id+"' class='cs-next'>next</a>");$('#cs-prev-'+el.id).css({'position':'absolute','top':params[el.id].height/2-15,'left':0,'z-index':1001,'line-height':'30px','opacity':params[el.id].opacity}).click(function(e){e.preventDefault();$.transition(el,'prev');$.transitionCall(el);}).mouseover(function(){$('#cs-navigation-'+el.id).show()});$('#cs-next-'+el.id).css({'position':'absolute','top':params[el.id].height/2-15,'right':0,'z-index':1001,'line-height':'30px','opacity':params[el.id].opacity}).click(function(e){e.preventDefault();$.transition(el);$.transitionCall(el);}).mouseover(function(){$('#cs-navigation-'+el.id).show()});$("<div id='cs-buttons-"+el.id+"' class='cs-buttons'></div>").appendTo($('#coin-slider-'+el.id));for(k=1;k<images[el.id].length+1;k++){$('#cs-buttons-'+el.id).append("<a href='#' class='cs-button-"+el.id+"' id='cs-button-"+el.id+"-"+k+"'>"+k+"</a>");}
+$.each($('.cs-button-'+el.id),function(i,item){$(item).click(function(e){$('.cs-button-'+el.id).removeClass('cs-active');$(this).addClass('cs-active');e.preventDefault();$.transition(el,i);$.transitionCall(el);})});$('#cs-navigation-'+el.id+' a').mouseout(function(){$('#cs-navigation-'+el.id).hide();params[el.id].pause=false;});$("#cs-buttons-"+el.id).css({'left':'50%','margin-left':-images[el.id].length*15/2-5,'position':'relative'});}
+$.effect=function(el){effA=['random','swirl','rain','straight'];if(params[el.id].effect=='')
+eff=effA[Math.floor(Math.random()*(effA.length))];else
+eff=params[el.id].effect;order[el.id]=new Array();if(eff=='random'){counter=0;for(i=1;i<=params[el.id].sph;i++){for(j=1;j<=params[el.id].spw;j++){order[el.id][counter]=i+''+j;counter++;}}
+$.random(order[el.id]);}
+if(eff=='rain'){$.rain(el);}
+if(eff=='swirl')
+$.swirl(el);if(eff=='straight')
+$.straight(el);reverse[el.id]*=-1;if(reverse[el.id]>0){order[el.id].reverse();}}
+$.random=function(arr){var i=arr.length;if(i==0)return false;while(--i){var j=Math.floor(Math.random()*(i+1));var tempi=arr[i];var tempj=arr[j];arr[i]=tempj;arr[j]=tempi;}}
+$.swirl=function(el){var n=params[el.id].sph;var m=params[el.id].spw;var x=1;var y=1;var going=0;var num=0;var c=0;var dowhile=true;while(dowhile){num=(going==0||going==2)?m:n;for(i=1;i<=num;i++){order[el.id][c]=x+''+y;c++;if(i!=num){switch(going){case 0:y++;break;case 1:x++;break;case 2:y--;break;case 3:x--;break;}}}
+going=(going+1)%4;switch(going){case 0:m--;y++;break;case 1:n--;x++;break;case 2:m--;y--;break;case 3:n--;x--;break;}
+check=$.max(n,m)-$.min(n,m);if(m<=check&&n<=check)
+dowhile=false;}}
+$.rain=function(el){var n=params[el.id].sph;var m=params[el.id].spw;var c=0;var to=to2=from=1;var dowhile=true;while(dowhile){for(i=from;i<=to;i++){order[el.id][c]=i+''+parseInt(to2-i+1);c++;}
+to2++;if(to<n&&to2<m&&n<m){to++;}
+if(to<n&&n>=m){to++;}
+if(to2>m){from++;}
+if(from>to)dowhile=false;}}
+$.straight=function(el){counter=0;for(i=1;i<=params[el.id].sph;i++){for(j=1;j<=params[el.id].spw;j++){order[el.id][counter]=i+''+j;counter++;}}}
+$.min=function(n,m){if(n>m)return m;else return n;}
+$.max=function(n,m){if(n<m)return m;else return n;}
+this.each(function(){init(this);});};$.fn.coinslider.defaults={width:565,height:290,spw:7,sph:5,delay:3000,sDelay:30,opacity:0.7,titleSpeed:500,effect:'',navigation:true,links:true,hoverPause:true};})(jQuery);

File lfs_carousel/templates/lfs_carousel/carousel.html

+<div id='coin-slider'>
+    {% for item in items %}
+        <a href="{{ item.link }}">
+            <img src="{{ item.image.url_400x400 }}" alt="{{ item.title }}" />
+            {% if item.title %}
+                <span>
+                    {{ item.title }}
+                </span>
+            {% endif %}
+        </a>
+    {% endfor %}
+</div>

File lfs_carousel/templates/lfs_carousel/items.html

+{% load i18n %}
+<div id="carousel-items">
+<h2 class="heading-first">{% trans 'Carousel items' %}</h2>
+
+{% if items %}
+    <form id="carousel-items-update-form"
+          action="{% url lfs_carousel_update_items ct.pk obj.pk %}"
+          method="post">
+
+        <table class="lfs-manage-table carousel-items">
+            <tr>
+                <th class="tiny">
+                    <input type="checkbox"
+                           class="select-all"
+                           value="delete-images" />
+                </th>
+                <th class="tiny" style="padding: 0 10px">
+                    {% trans 'Image' %}
+                </th>
+                <th class="small">
+                    {% trans 'Title' %}
+                </th>
+                <th class="small">
+                    {% trans 'URL' %}
+                </th>
+                <th class="small">
+                    {% trans 'Text' %}
+                </th>
+                <th class="right-padding">
+                    {% trans 'Position' %}
+                </th>
+                <th class="small right-padding">
+                    {% trans 'Manage' %}
+                </th>
+            </tr>
+            {% for item in items %}
+                <tr>
+                    <td>
+                        <input type="checkbox"
+                               class="select-delete-images"
+                               name="delete-{{ item.id }}"
+                               style="float:left" />
+                    </td>
+                    <td style="padding: 3px 10px">
+                        <img src="{{ item.image.url_60x60 }}"
+                             alt="{{ item.title }}"
+                             title="{{ item.title }}"
+                             style="float:left"/>
+                    </td>
+                    <td class="small">
+                        <input type="text" name="title-{{ item.id }}" value="{{ item.title }}" style="width:300px" />
+                    </td>
+                    <td class="small">
+                        <input type="text" name="link-{{ item.id }}" value="{{ item.link }}" style="width:300px" />
+                    </td>
+                    <td class="small">
+                        <input type="text" name="text-{{ item.id }}" value="{{ item.text }}" style="width:300px" />
+                    </td>
+                    <td class="right-padding">
+                        <input type="text" name="position-{{ item.id }}" value="{{ item.position }}" size="3" />
+                    </td>
+                    <td class="right-padding">
+                        {% if not forloop.first %}
+                            <a class="up ajax-link"
+                               href="{% url lfc_carousel_move_item item.id %}?direction=0"
+                               title='{% trans "Move Up" %}'></a>
+                        {% endif %}
+                        {% if not forloop.last %}
+                            <a class="down ajax-link"
+                               href="{% url lfc_carousel_move_item item.id %}?direction=1"
+                               title='{% trans "Move Down" %}'></a>
+                        {% else %}
+                            <span class="blank"></span>
+                        {% endif %}
+                    </td>
+
+                </tr>
+            {% endfor %}
+        </table>
+        <div class="buttons">
+            <input class="ajax-save-button button"
+                   type="submit" name="update" value="{% trans 'Update items' %}" />
+            <input class="ajax-save-button button"
+                   type="submit" name="delete" value="{% trans 'Delete items' %}" />
+        </div>
+    </form>
+{% else %}
+    <span>{% trans 'There are no items' %}</span>
+{% endif %}
+
+<h2 class="heading-middle">{% trans 'Add items' %}</h2>
+
+<div id="content">
+    <form id="file_upload" action="{% url lfs_carousel_add_item ct.pk obj.pk %}" method="POST" enctype="multipart/form-data">
+        {% csrf_token %}
+        <input class="button" type="file" name="file" multiple>
+    </form>
+    <table id="files" data="{% url lfs_carousel_manage_items ct.pk obj.pk %}" msg='{% trans "Uploading images:" %}'></table>
+</div>
+<script>
+    $(function () {
+        $('#file_upload').fileUploadUI({
+            uploadTable: $('#files'),
+            multiFileRequest : true,
+            buildUploadRow: function (files) {
+                var fileNames = '';
+                for (i = 0; i < files.length; i += 1) {
+                    fileNames = fileNames + files[i].name + '<br>';
+                }
+                var msg = $("#files").attr("msg");
+                return $(
+                    '<tr>' +
+                    '<td><div style="font-weight:bold; padding-bottom:10px">' + msg + '<img src="{{ STATIC_URL }}img/ajax-loader.gif" style="padding:8px 0 0 10px" /></div>' + fileNames + '<\/td>' +
+                    '<\/tr>'
+                );
+            },
+            onLoadAll: function(files) {
+                var url = $("#files").attr("data");
+                $.get(url, function(data) {
+                    data = $.parseJSON(data);
+                    $("#carousel-items").html(data["items"]);
+                    $.jGrowl(data["message"]);
+                });
+            }
+        });
+    });
+</script>
+</div>

File lfs_carousel/templatetags/__init__.py

Empty file added.

File lfs_carousel/urls.py

+from django.conf.urls.defaults import *
+
+
+# Carousel items
+urlpatterns = patterns('lfs_carousel.views',
+    url(r'^add-item/(?P<content_type_id>\d*)/(?P<object_id>\d*)/$', "add_item", name="lfs_carousel_add_item"),
+    url(r'^update-items/(?P<content_type_id>\d*)/(?P<object_id>\d*)/$', "update_items", name="lfs_carousel_update_items"),
+    url(r'^manage-items/(?P<content_type_id>\d*)/(?P<object_id>\d*)/$', "manage_items", name="lfs_carousel_manage_items"),
+    #url(r'^update-active-items/(?P<product_id>\d*)$', "update_active_items", name="lfs_manage_update_active_images"),
+    url(r'^move-item/(?P<id>\d+)$', "move_item", name="lfc_carousel_move_item"),
+)

File lfs_carousel/views.py

+# -*- coding: utf-8 -*-
+# django imports
+from django.contrib.auth.decorators import permission_required
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.template import RequestContext
+from django.template.loader import render_to_string
+from django.utils.functional import update_wrapper
+from django.utils.translation import ugettext_lazy as _
+from django.utils import simplejson
+from django.contrib.contenttypes.models import ContentType
+
+# lfs.imports
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_protect
+from lfs.caching.utils import lfs_get_object_or_404
+from lfs.core.utils import LazyEncoder
+
+# lfs_carousel imports
+from lfs_carousel.models import CarouselItem
+from lfs_carousel.signals import carousel_changed
+
+# Load logger
+import logging
+logger = logging.getLogger("default")
+
+
+class LFCCarouselView(object):
+
+    def refresh_positions(self, ct, object_id):
+        """ order items
+        """
+        items = self.get_item_cls().objects.filter(content_type=ct,
+                                            content_id=object_id)
+        for i, item in enumerate(items):
+            item.position = (i + 1) * 10
+            item.save()
+
+    def get_item_cls(self):
+        """ return model Class used by Carousel
+        """
+        return CarouselItem
+
+    def manage_items(self, request, content_type_id, object_id, as_string=False,
+                      template_name="lfs_carousel/items.html"):
+        """
+        """
+        ct = lfs_get_object_or_404(ContentType, pk=content_type_id)
+        obj = ct.get_object_for_this_type(pk=object_id)
+
+        items = self.get_item_cls().objects.filter(content_type=ct,
+                                              content_id=object_id)
+
+        result = render_to_string(template_name, RequestContext(request, {
+            "obj": obj,
+            "ct": ct,
+            "items": items
+        }))
+
+        if as_string:
+            return result
+        else:
+            result = simplejson.dumps({
+                "items": result,
+                "message": _(u"Carousel items have been added."),
+            }, cls=LazyEncoder)
+
+            return HttpResponse(result)
+
+    def add_item(self, request, content_type_id, object_id):
+        """Adds an image/carousel item to object
+        """
+        ct = lfs_get_object_or_404(ContentType, pk=content_type_id)
+        obj = ct.get_object_for_this_type(pk=object_id)
+
+        if request.method == "POST":
+            for file_content in request.FILES.getlist("file"):
+                item = self.get_item_cls()(content=obj)
+                try:
+                    item.image.save(file_content.name, file_content, save=True)
+                except Exception, e:
+                    logger.info("Upload item: %s %s" % (file_content.name, e))
+                    continue
+
+        self.refresh_positions(ct, object_id)
+
+        carousel_changed.send(obj, request=request)
+
+        result = simplejson.dumps({"name": file_content.name, "type": "image/jpeg", "size": "123456789"})
+        return HttpResponse(result)
+
+    def update_items(self, request, content_type_id, object_id):
+        """Saves/deletes items with given ids (passed by request body).
+        """
+        ct = lfs_get_object_or_404(ContentType, pk=content_type_id)
+        obj = ct.get_object_for_this_type(pk=object_id)
+
+        action = request.POST.get("action")
+        if action == "delete":
+            message = _(u"Carousel items have been deleted.")
+            for key in request.POST.keys():
+                if key.startswith("delete-"):
+                    try:
+                        id = key.split("-")[1]
+                        item = self.get_item_cls().objects.get(pk=id).delete()
+                    except (IndexError, ObjectDoesNotExist):
+                        pass
+
+        elif action == "update":
+            message = _(u"Carousel items have been updated.")
+            for key, value in request.POST.items():
+                if not '-' in key:
+                    continue
+                id = key.split("-")[1]
+                try:
+                    item = self.get_item_cls().objects.get(pk=id)
+                except ObjectDoesNotExist:
+                    pass
+                else:
+                    if key.startswith("title-"):
+                        item.title = value
+                        item.save()
+                    elif key.startswith("position-"):
+                        item.position = value
+                        item.save()
+                    elif key.startswith("link-"):
+                        item.link = value
+                        item.save()
+                    elif key.startswith("text-"):
+                        item.text = value
+                        item.save()
+
+        self.refresh_positions(ct, object_id)
+
+        carousel_changed.send(obj, request=request)
+
+        html = [["#carousel-items", self.manage_items(request, content_type_id, object_id, as_string=True)]]
+        result = simplejson.dumps({
+            "html": html,
+            "message": message,
+        }, cls=LazyEncoder)
+
+        return HttpResponse(result)
+
+    #@permission_required("core.manage_shop", login_url="/login/")
+    def move_item(self, request, id):
+        """Moves the items with passed id up or down.
+
+        **Parameters:**
+
+            id
+                The id of the item which should be edited.
+
+        **Query String:**
+
+            direction
+                The direction in which the item should be moved. One of 0 (up)
+                or 1 (down).
+
+        **Permission:**
+
+            edit (of the belonging content object)
+        """
+        item = self.get_item_cls().objects.get(pk=id)
+        obj = item.content
+
+        direction = request.GET.get("direction", 0)
+
+        if direction == "1":
+            item.position += 15
+        else:
+            item.position -= 15
+            if item.position < 0:
+                item.position = 10
+
+        item.save()
+
+        ct = ContentType.objects.get_for_model(obj)
+
+        self.refresh_positions(ct, obj.pk)
+
+        html = [["#carousel-items", self.manage_items(request, ct.pk, obj.pk, as_string=True)]]
+
+        result = simplejson.dumps({
+             "html": html,
+        }, cls=LazyEncoder)
+
+        return HttpResponse(result)
+
+    # copied from contrib.admin.sites
+    def has_permission(self, request):
+            """
+            Returns True if the given HttpRequest has permission to view
+            *at least one* page in the admin site.
+            """
+            return request.user.is_active and request.user.has_perm('core.manage_shop')
+
+    def carousel_view(self, view, cacheable=False):
+        """
+        Decorator to create an carousel view attached to this ``LFSCarousel``. This
+        wraps the view and provides permission checking by calling
+        ``self.has_permission``.
+
+        """
+        def inner(request, *args, **kwargs):
+            if not self.has_permission(request):
+                return HttpResponseRedirect('%s?next=%s' % (reverse('django.contrib.auth.views.login'), request.path))
+            return view(request, *args, **kwargs)
+        if not cacheable:
+            inner = never_cache(inner)
+        # We add csrf_protect here so this function can be used as a utility
+        # function for any view, without having to repeat 'csrf_protect'.
+        if not getattr(view, 'csrf_exempt', False):
+            inner = csrf_protect(inner)
+        return update_wrapper(inner, view)
+
+    def get_urls(self):
+        from django.conf.urls.defaults import patterns, url, include
+
+        def wrap(view, cacheable=False):
+            def wrapper(*args, **kwargs):
+                return self.carousel_view(view, cacheable)(*args, **kwargs)
+            return update_wrapper(wrapper, view)
+
+        urlpatterns = patterns('',
+            url(r'^add-item/(?P<content_type_id>\d*)/(?P<object_id>\d*)/$', wrap(self.add_item), name="lfs_carousel_add_item"),
+            url(r'^update-items/(?P<content_type_id>\d*)/(?P<object_id>\d*)/$', wrap(self.update_items), name="lfs_carousel_update_items"),
+            url(r'^manage-items/(?P<content_type_id>\d*)/(?P<object_id>\d*)/$', wrap(self.manage_items), name="lfs_carousel_manage_items"),
+            url(r'^move-item/(?P<id>\d+)$', self.move_item, name="lfc_carousel_move_item"),
+        )
+
+        return urlpatterns
+
+    @property
+    def urls(self):
+        return self.get_urls()
+
+carousel = LFCCarouselView()
+from setuptools import setup, find_packages
+import os
+
+version = '1.0'
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+
+setup(name='lfs-carousel',
+      version=version,
+      description='A pluggable carousel/slider for LFS',
+      long_description=README,
+      classifiers=[
+          'Environment :: Web Environment',
+          'Framework :: Django',
+          'License :: OSI Approved :: BSD License',
+          'Operating System :: OS Independent',
+          'Programming Language :: Python',
+      ],
+      keywords='django e-commerce online-shop',
+      author='Maciej Wisniowski',
+      author_email='maciej.wisniowski@natcam.pl',
+      license='BSD',
+      packages=find_packages(exclude=['ez_setup']),
+      include_package_data=True,
+      zip_safe=False,
+      dependency_links=[],
+      install_requires=[
+        'setuptools',
+      ],
+      )