Dopracowanie aplikacji django-sponsors.
authorMarek Stępniowski <marek@stepniowski.com>
Mon, 12 Oct 2009 00:13:04 +0000 (02:13 +0200)
committerMarek Stępniowski <marek@stepniowski.com>
Mon, 12 Oct 2009 00:13:04 +0000 (02:13 +0200)
18 files changed:
apps/sponsors/admin.py
apps/sponsors/fields.py [new file with mode: 0644]
apps/sponsors/models.py
apps/sponsors/static/css/sponsors.css [deleted file]
apps/sponsors/static/js/ordered_select_multiple.js [deleted file]
apps/sponsors/static/sponsors/css/footer_admin.css [new file with mode: 0644]
apps/sponsors/static/sponsors/js/footer_admin.js [new file with mode: 0644]
apps/sponsors/static/sponsors/js/jquery.json.min.js [new file with mode: 0644]
apps/sponsors/templates/sponsors/page.html [new file with mode: 0644]
apps/sponsors/templates/sponsors/sponsors.html [deleted file]
apps/sponsors/templatetags/sponsor_tags.py
apps/sponsors/widgets.py
fabfile.py
wolnelektury/media/css/sponsors.css
wolnelektury/media/sponsors/css/footer_admin.css [new file with mode: 0644]
wolnelektury/media/sponsors/js/footer_admin.js [new file with mode: 0644]
wolnelektury/media/sponsors/js/jquery.json.min.js [new file with mode: 0644]
wolnelektury/templates/base.html

index c66a086..55af9d7 100644 (file)
@@ -1,21 +1,25 @@
-from django.db import models
 from django.contrib import admin
 from django.contrib import admin
+from django.conf import settings
 
 
-from sponsors.models import Sponsor, SponsorGroup
-from sponsors.widgets import OrderedSelectMultiple
+from sponsors import models
+from sponsors import fields
+from sponsors import widgets
 
 
-class SponsorGroupAdmin(admin.ModelAdmin):
-    formfield_overrides = {
-        models.CommaSeparatedIntegerField: {'widget': OrderedSelectMultiple},
-    }   
+
+class SponsorAdmin(admin.ModelAdmin):
     list_display = ('name',)
     search_fields = ('name',)
     ordering = ('name',)
 
     list_display = ('name',)
     search_fields = ('name',)
     ordering = ('name',)
 
-class SponsorAdmin(admin.ModelAdmin):
+
+class SponsorPageAdmin(admin.ModelAdmin):
+    formfield_overrides = {
+        fields.JSONField: {'widget': widgets.SponsorPageWidget},
+    }   
     list_display = ('name',)
     search_fields = ('name',)
     ordering = ('name',)
 
     list_display = ('name',)
     search_fields = ('name',)
     ordering = ('name',)
 
-admin.site.register(SponsorGroup, SponsorGroupAdmin)
-admin.site.register(Sponsor, SponsorAdmin)
+
+admin.site.register(models.Sponsor, SponsorAdmin)
+admin.site.register(models.SponsorPage, SponsorPageAdmin)
diff --git a/apps/sponsors/fields.py b/apps/sponsors/fields.py
new file mode 100644 (file)
index 0000000..678788e
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- encoding: utf-8 -*-
+import datetime
+
+from django.conf import settings
+from django.db import models
+from django import forms
+from django.utils import simplejson as json
+
+
+class JSONEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, datetime.datetime):
+            return obj.strftime('%Y-%m-%d %H:%M:%S')
+        elif isinstance(obj, datetime.date):
+            return obj.strftime('%Y-%m-%d')
+        elif isinstance(obj, datetime.time):
+            return obj.strftime('%H:%M:%S')
+        return json.JSONEncoder.default(self, obj)
+
+
+def dumps(data):
+    return JSONEncoder().encode(data)
+
+
+def loads(str):
+    return json.loads(str, encoding=settings.DEFAULT_CHARSET)
+
+
+class JSONFormField(forms.CharField):
+    widget = forms.Textarea
+    
+    def clean(self, value):
+        try:
+            loads(value)
+            return value
+        except ValueError, e:
+            raise forms.ValidationError('Enter a valid JSON value. Error: %s' % e)
+
+
+class JSONField(models.TextField):
+    def formfield(self, **kwargs):
+        defaults = {'form_class': JSONFormField}
+        defaults.update(kwargs)
+        return super(JSONField, self).formfield(**defaults)
+
+    def db_type(self):
+        return 'text'
+
+    def get_internal_type(self):
+        return 'TextField'
+
+    def contribute_to_class(self, cls, name):
+        super(JSONField, self).contribute_to_class(cls, name)
+        
+        def get_value(model_instance):
+            return loads(getattr(model_instance, self.attname, None))
+        setattr(cls, 'get_%s_value' % self.name, get_value)
+
+        def set_value(model_instance, json):
+            return setattr(model_instance, self.attname, dumps(json))
+        setattr(cls, 'set_%s_value' % self.name, set_value)
index ffad8e7..1df990f 100644 (file)
@@ -1,5 +1,8 @@
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
+from django.template.loader import render_to_string
+
+from sponsors.fields import JSONField
 
 
 class Sponsor(models.Model):
 
 
 class Sponsor(models.Model):
@@ -18,17 +21,35 @@ class Sponsor(models.Model):
             return self.name
 
 
             return self.name
 
 
-class SponsorGroup(models.Model):
+class SponsorPage(models.Model):
     name = models.CharField(_('name'), max_length=120)
     name = models.CharField(_('name'), max_length=120)
-    order = models.IntegerField(_('order'), default=0)
-    column_width = models.PositiveIntegerField(_('column width'))
-    sponsor_ids = models.CommaSeparatedIntegerField(_('sponsors'), max_length=255)
+    column_width = models.PositiveIntegerField(_('column width'), default=200)
+    sponsors = JSONField(_('sponsors'), default={})
+    _html = models.TextField(blank=True, editable=False)
+    
+    def populated_sponsors(self):
+        result = []
+        for column in self.get_sponsors_value():
+            result_group = {'name': column['name'], 'sponsors': []}
+            sponsor_objects = Sponsor.objects.in_bulk(column['sponsors'])
+            for sponsor_pk in column['sponsors']:
+                try:
+                    result_group['sponsors'].append(sponsor_objects[sponsor_pk])
+                except KeyError:
+                    pass
+            result.append(result_group)
+        return result
     
     
-    def sponsors(self):
-        ids = [int(pk) for pk in self.sponsor_ids.split(',')]
-        result = Sponsor.objects.in_bulk(ids)
-        return [result[pk] for pk in ids]
-    sponsors.changes_data = False
+    def html(self):
+        return self._html
+    html = property(fget=html)
+
+    def save(self, *args, **kwargs):
+        self._html = render_to_string('sponsors/page.html', {
+            'column_width': self.column_width,
+            'sponsors': self.populated_sponsors(),
+        })
+        return super(SponsorPage, self).save(*args, **kwargs)
     
     def __unicode__(self):
         return self.name
     
     def __unicode__(self):
         return self.name
diff --git a/apps/sponsors/static/css/sponsors.css b/apps/sponsors/static/css/sponsors.css
deleted file mode 100644 (file)
index f3d7ed9..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-.sponsor-group {
-    float: left;
-    overflow: hidden;
-}
diff --git a/apps/sponsors/static/js/ordered_select_multiple.js b/apps/sponsors/static/js/ordered_select_multiple.js
deleted file mode 100644 (file)
index e4fd74d..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-(function($) {
-  $.fn.orderedSelectMultiple = function(options) {
-    var settings = {
-      choices: []
-    };
-    $.extend(settings, options);
-    
-    var input = $(this).hide();
-    var values = input.val().split(',');
-    
-    var container = $('<div></div>').insertAfter($(this));
-    var choicesList = $('<ol class="choices connectedSortable"></ol>').appendTo(container).css({
-      width: 200, float: 'left', minHeight: 200, backgroundColor: '#eee', margin: 0, padding: 0
-    });
-    var valuesList = $('<ol class="values connectedSortable"></ol>').appendTo(container).css({
-      width: 200, float: 'left', minHeight: 200, backgroundColor: '#eee', margin: 0, padding: 0
-    });
-    var choiceIds = [];
-    $.each(settings.choices, function() {
-      choiceIds.push('' + this.id);
-    });
-    
-    function createItem(hash) {
-      return $('<li>' + hash.name + '</li>').css({
-        backgroundColor: '#cff',
-        display: 'block',
-        border: '1px solid #cdd',
-        padding: 2,
-        margin: 0
-      }).data('obj-id', hash.id);
-    }
-    
-    $.each(settings.choices, function() {
-      if ($.inArray('' + this.id, values) == -1) {
-        choicesList.append(createItem(this));
-      }
-    });
-    
-    $.each(values, function() {
-      var index = $.inArray('' + this, choiceIds); // Why this[0]?
-      if (index != -1) {
-        valuesList.append(createItem(settings.choices[index]));
-      }
-    });
-    
-    choicesList.sortable({
-               connectWith: '.connectedSortable'
-       }).disableSelection();
-       
-       valuesList.sortable({
-               connectWith: '.connectedSortable',
-               update: function() {
-                 values = [];
-                 $('li', valuesList).each(function(index) {
-          values.push($(this).data('obj-id'));
-          console.log($(this).data('obj-id'));
-                 });
-                 console.log('update', values.join(','), input);
-                 input.val(values.join(','));
-               }
-       }).disableSelection();
-  };
-})(jQuery);
diff --git a/apps/sponsors/static/sponsors/css/footer_admin.css b/apps/sponsors/static/sponsors/css/footer_admin.css
new file mode 100644 (file)
index 0000000..ba56771
--- /dev/null
@@ -0,0 +1,67 @@
+.sponsors {
+    display: block;
+    clear: both;
+    margin-top: 5px;
+}
+
+.sponsors .sponsors-sponsor-group {
+    float: left;
+    width: 200px;
+    border: 1px solid #CCC;
+    margin: 2px 2px 0 0;
+}
+
+.sponsors .sponsors-sponsor-group-name {
+    border-bottom: 1px solid #CCC;
+    padding: 2px 2px 2px 4px;
+    margin: 0;
+    color: #FFF;
+    background-color: #7CA0C7;
+    font-weight: bold;
+    height: 15px;
+}
+
+.sponsors .sponsors-sponsor-group-name input {
+    margin: -2px -2px -2px -4px;
+    padding: 0;
+    height: 15px;
+    width: 180px;
+}
+
+.sponsors .sponsors-remove-sponsor-group {
+    float: right;
+    background-color: #CC3434;
+    color: #FFF;
+    width: 10px;
+    height: 15px;
+    padding: 2px;
+    text-align: center;
+    font-weight: bold;
+    display: block;
+    cursor: default;
+}
+
+.sponsors .sponsors-remove-sponsor-group:hover {
+    color: #CC3434;
+    background-color: white;
+}
+
+.sponsors .sponsors-unused-sponsor-group-name {
+    background-color: #FFF;
+    color: #666;
+}
+
+.sponsors .sponsors-sponsor-group-list {
+    margin: 0;
+    padding: 2px;
+    list-style: none;
+    min-height: 200px;
+}
+
+.sponsors-sponsor {
+    margin: 0 0 2px 0;
+    padding: 2px;
+    border: 1px solid #CCC;
+    background-color: #EEE;
+    cursor: default;
+}
diff --git a/apps/sponsors/static/sponsors/js/footer_admin.js b/apps/sponsors/static/sponsors/js/footer_admin.js
new file mode 100644 (file)
index 0000000..2f2cd93
--- /dev/null
@@ -0,0 +1,131 @@
+(function($) {
+  $.fn.sponsorsFooter = function(options) {
+    var settings = {
+      sponsors: []
+    };
+    $.extend(settings, options);
+    
+    var input = $(this).hide();
+    
+    var container = $('<div class="sponsors"></div>').appendTo(input.parent());
+    var groups = $.evalJSON(input.val());
+    
+    var unusedDiv = $('<div class="sponsors-sponsor-group sponsors-unused-sponsor-group"></div>')
+      .appendTo(container)
+      .append('<p class="sponsors-sponsor-group-name sponsors-unused-sponsor-group-name">dostępni sponsorzy</p>');
+    var unusedList = $('<ol class="sponsors-sponsor-group-list sponsors-unused-group-list"></ol>')
+        .appendTo(unusedDiv)
+        .sortable({
+          connectWith: '.sponsors-sponsor-group-list'
+               });
+    
+    // Edit group name inline
+    function editNameInline(name) {
+      name.unbind('click.sponsorsFooter');
+      var inlineInput = $('<input></input>').val(name.html());
+      name.html('');
+      
+      function endEditing() {
+        name.html(inlineInput.val());
+        inlineInput.remove();
+        name.bind('click.sponsorsFooter', function() {
+          editNameInline($(this));
+        });
+        input.parents('form').unbind('submit.sponsorsFooter', endEditing);
+        return false;
+      }
+      
+      inlineInput.appendTo(name).focus().blur(endEditing);
+      input.parents('form').bind('submit.sponsorsFooter', endEditing);
+    }
+    
+    // Remove sponsor with passed id from sponsors array and return it
+    function popSponsor(id) {
+      for (var i=0; i < settings.sponsors.length; i++) {
+        if (settings.sponsors[i].id == id) {
+          var s = settings.sponsors[i];
+          settings.sponsors.splice(i, 1);
+          return s;
+        }
+      }
+      return null;
+    }
+    
+    // Create sponsor group and bind events
+    function createGroup(name, sponsors) {
+      if (!sponsors) {
+        sponsors = [];
+      }
+      
+      var groupDiv = $('<div class="sponsors-sponsor-group"></div>');
+      
+      $('<a class="sponsors-remove-sponsor-group">X</a>')
+        .click(function() {
+          groupDiv.fadeOut('slow', function() {
+            $('.sponsors-sponsor', groupDiv).hide().appendTo(unusedList).fadeIn();
+            groupDiv.remove();
+          });
+        }).appendTo(groupDiv);
+      
+      $('<p class="sponsors-sponsor-group-name">' + name + '</p>')
+        .bind('click.sponsorsFooter', function() {
+          editNameInline($(this));
+        }).appendTo(groupDiv);
+      
+      var groupList = $('<ol class="sponsors-sponsor-group-list"></ol>')
+        .appendTo(groupDiv)
+        .sortable({
+          connectWith: '.sponsors-sponsor-group-list'
+               });
+      
+      
+      for (var i = 0; i < sponsors.length; i++) {
+        $('<li class="sponsors-sponsor">' + sponsors[i].name + '</li>')
+          .data('obj_id', sponsors[i].id)
+          .appendTo(groupList);
+      }
+      return groupDiv;
+    }
+    
+    // Create groups from data in input value
+    for (var i = 0; i < groups.length; i++) {
+      var group = groups[i];
+      var sponsors = [];
+      
+      for (var j = 0; j < group.sponsors.length; j++) {
+        var s = popSponsor(group.sponsors[j]);
+        if (s) {
+          sponsors.push(s);
+        }
+      }
+      createGroup(group.name, sponsors).appendTo(container);
+    }
+    
+    // Serialize input value before submiting form
+    input.parents('form').submit(function(event) {
+      var groups = [];
+      $('.sponsors-sponsor-group', container).not('.sponsors-unused-sponsor-group').each(function() {
+        var group = {name: $('.sponsors-sponsor-group-name', this).html(), sponsors: []};
+        $('.sponsors-sponsor', this).each(function() {
+          group.sponsors.push($(this).data('obj_id'));
+        });
+        groups.push(group);
+      });
+      input.val($.toJSON(groups));
+    });
+    
+    for (i = 0; i < settings.sponsors.length; i++) {
+      $('<li class="sponsors-sponsor">' + settings.sponsors[i].name + '</li>')
+        .data('obj_id', settings.sponsors[i].id)
+        .appendTo(unusedList);
+    }
+    
+    $('<button type="button">Dodaj nową grupę</button>')
+      .click(function() {
+        var newGroup = createGroup('').appendTo(container);
+        editNameInline($('.sponsors-sponsor-group-name', newGroup));
+      }).prependTo(input.parent());
+    
+    input.parent().append('<div style="clear: both"></div>');
+  };
+})(jQuery);
diff --git a/apps/sponsors/static/sponsors/js/jquery.json.min.js b/apps/sponsors/static/sponsors/js/jquery.json.min.js
new file mode 100644 (file)
index 0000000..bad4a0a
--- /dev/null
@@ -0,0 +1,31 @@
+
+(function($){$.toJSON=function(o)
+{if(typeof(JSON)=='object'&&JSON.stringify)
+return JSON.stringify(o);var type=typeof(o);if(o===null)
+return"null";if(type=="undefined")
+return undefined;if(type=="number"||type=="boolean")
+return o+"";if(type=="string")
+return $.quoteString(o);if(type=='object')
+{if(typeof o.toJSON=="function")
+return $.toJSON(o.toJSON());if(o.constructor===Date)
+{var month=o.getUTCMonth()+1;if(month<10)month='0'+month;var day=o.getUTCDate();if(day<10)day='0'+day;var year=o.getUTCFullYear();var hours=o.getUTCHours();if(hours<10)hours='0'+hours;var minutes=o.getUTCMinutes();if(minutes<10)minutes='0'+minutes;var seconds=o.getUTCSeconds();if(seconds<10)seconds='0'+seconds;var milli=o.getUTCMilliseconds();if(milli<100)milli='0'+milli;if(milli<10)milli='0'+milli;return'"'+year+'-'+month+'-'+day+'T'+
+hours+':'+minutes+':'+seconds+'.'+milli+'Z"';}
+if(o.constructor===Array)
+{var ret=[];for(var i=0;i<o.length;i++)
+ret.push($.toJSON(o[i])||"null");return"["+ret.join(",")+"]";}
+var pairs=[];for(var k in o){var name;var type=typeof k;if(type=="number")
+name='"'+k+'"';else if(type=="string")
+name=$.quoteString(k);else
+continue;if(typeof o[k]=="function")
+continue;var val=$.toJSON(o[k]);pairs.push(name+":"+val);}
+return"{"+pairs.join(", ")+"}";}};$.evalJSON=function(src)
+{if(typeof(JSON)=='object'&&JSON.parse)
+return JSON.parse(src);return eval("("+src+")");};$.secureEvalJSON=function(src)
+{if(typeof(JSON)=='object'&&JSON.parse)
+return JSON.parse(src);var filtered=src;filtered=filtered.replace(/\\["\\\/bfnrtu]/g,'@');filtered=filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']');filtered=filtered.replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered))
+return eval("("+src+")");else
+throw new SyntaxError("Error parsing JSON, source is not valid.");};$.quoteString=function(string)
+{if(string.match(_escapeable))
+{return'"'+string.replace(_escapeable,function(a)
+{var c=_meta[a];if(typeof c==='string')return c;c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';}
+return'"'+string+'"';};var _escapeable=/["\\\x00-\x1f\x7f-\x9f]/g;var _meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};})(jQuery);
\ No newline at end of file
diff --git a/apps/sponsors/templates/sponsors/page.html b/apps/sponsors/templates/sponsors/page.html
new file mode 100644 (file)
index 0000000..ad4fbaa
--- /dev/null
@@ -0,0 +1,11 @@
+<div class="sponsors-sponsor-group">
+{% for column in sponsors %}
+       <div class="sponsors-sponsor-column" style="width: {{ column_width }}px">
+               <p class="sponsors-sponsor-column-name">{{ column.name }}</p>
+               {% for sponsor in column.sponsors %}
+                       <div class="sponsors-sponsor">{% if sponsor.url %}<a style="sponsors-sponsor-link" href="{{ sponsor.url }}" >{% endif %}<img class="sponsors-sponsor-logo" src="{{ sponsor.logo.url }}" alt="{{ sponsor.description }}"/>{% if sponsor.url %}</a>{% endif %}</div>
+               {% endfor %}
+       </div>
+{% endfor %}
+<div style="clear: both" />
+</div>
diff --git a/apps/sponsors/templates/sponsors/sponsors.html b/apps/sponsors/templates/sponsors/sponsors.html
deleted file mode 100644 (file)
index d5decae..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="sponsors">
-{% for group in sponsor_groups %}
-       <div class="sponsor-group" style="width: {{ group.column_width }}px">
-               <p class="sponsor-group-name">{{ group.name }}</p>
-               {% for sponsor in group.sponsors %}
-                       <div class="sponsor">{% if sponsor.url %}<a style="sponsor-link" href="{{ sponsor.url }}" >{% endif %}<img class="sponsor-logo" src="{{ sponsor.logo.url }}" alt="{{ sponsor.description }}"/>{% if sponsor.url %}</a>{% endif %}</div>
-               {% endfor %}
-       </div>
-{% endfor %}
-<div style="clear: both" />
-</div>
index 87289e8..c1d18d1 100644 (file)
@@ -1,4 +1,5 @@
 from django import template
 from django import template
+from django.utils.safestring import mark_safe
 
 from sponsors import models
 
 
 from sponsors import models
 
@@ -6,7 +7,11 @@ from sponsors import models
 register = template.Library()
 
 
 register = template.Library()
 
 
-def sponsors():
-    return {'sponsor_groups': models.SponsorGroup.objects.all()}
+def sponsor_page(name):
+    try:
+        page = models.SponsorPage.objects.get(name=name)
+    except:
+        return u''
+    return mark_safe(page.html)
     
     
-compressed_js = register.inclusion_tag('sponsors/sponsors.html')(sponsors)
+sponsor_page = register.simple_tag(sponsor_page)
index 2ed5793..3bb586b 100644 (file)
@@ -1,30 +1,29 @@
-from django import forms
 from django.conf import settings
 from django.conf import settings
+from django import forms
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy as _
 
 from sponsors import models
 
 
 
 from sponsors import models
 
 
-class OrderedSelectMultiple(forms.TextInput):
-    """
-    A SelectMultiple with a JavaScript interface.
-    """
+class SponsorPageWidget(forms.Textarea):
     class Media:
         js = (
             'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js',
             'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/jquery-ui.min.js',
     class Media:
         js = (
             'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js',
             'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/jquery-ui.min.js',
-            settings.MEDIA_URL + 'js/ordered_select_multiple.js',
+            settings.MEDIA_URL + 'sponsors/js/jquery.json.min.js',
+            settings.MEDIA_URL + 'sponsors/js/footer_admin.js',
         )
         )
+        css = {
+            'all': (settings.MEDIA_URL + 'sponsors/css/footer_admin.css',),
+        }
 
 
-    def render(self, name, value, attrs=None, choices=()):
-        output = [super(OrderedSelectMultiple, self).render(name, value, attrs)]
-        choices = [(unicode(obj), obj.pk) for obj in models.Sponsor.objects.all()]
-        choices_js = ', '.join('{name: "%s", id: %d}' % choice for choice in choices)
+    def render(self, name, value, attrs=None):
+        output = [super(SponsorPageWidget, self).render(name, value, attrs)]
+        sponsors = [(unicode(obj), obj.pk) for obj in models.Sponsor.objects.all()]
+        sponsors_js = ', '.join('{name: "%s", id: %d}' % sponsor for sponsor in sponsors)
         output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
         # TODO: "id_" is hard-coded here. This should instead use the correct
         # API to determine the ID dynamically.
         output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
         # TODO: "id_" is hard-coded here. This should instead use the correct
         # API to determine the ID dynamically.
-        output.append(u'$("#id_%s").orderedSelectMultiple({choices: [%s]}); });</script>\n' % 
-            (name, choices_js))
+        output.append(u'$("#id_%s").sponsorsFooter({sponsors: [%s]}); });</script>\n' % 
+            (name, sponsors_js))
         return mark_safe(u''.join(output))
         return mark_safe(u''.join(output))
-
index 6791864..2acd88b 100644 (file)
@@ -26,8 +26,8 @@ def production():
 # =========
 def test():
     "Run the test suite and bail out if it fails"
 # =========
 def test():
     "Run the test suite and bail out if it fails"
-    require('project_dir', provided_by=[staging, production])
-    result = local('cd %(path)s; python manage.py test' % env)
+    require('hosts', 'path', provided_by=[staging, production])
+    result = run('cd %(path)s/%(project_name)s; python manage.py test' % env)
 
 def deploy():
     """
 
 def deploy():
     """
@@ -35,13 +35,13 @@ def deploy():
     install any required third party modules, 
     install the virtual host and then restart the webserver
     """
     install any required third party modules, 
     install the virtual host and then restart the webserver
     """
-    require('hosts', provided_by=[staging, production])
-    require('path')
+    require('hosts', 'path', provided_by=[staging, production])
     
     import time
     env.release = time.strftime('%Y-%m-%dT%H%M')
     
     upload_tar_from_git()
     
     import time
     env.release = time.strftime('%Y-%m-%dT%H%M')
     
     upload_tar_from_git()
+    upload_requirements_bundle()
     install_requirements()
     symlink_current_release()
     migrate()
     install_requirements()
     symlink_current_release()
     migrate()
@@ -49,8 +49,7 @@ def deploy():
 
 def deploy_version(version):
     "Specify a specific version to be made live"
 
 def deploy_version(version):
     "Specify a specific version to be made live"
-    require('hosts', provided_by=[localhost,webserver])
-    require('path')
+    require('hosts', 'path', provided_by=[localhost,webserver])
     env.version = version
     with cd(env.path):
         run('rm releases/previous; mv releases/current releases/previous;', pty=True)
     env.version = version
     with cd(env.path):
         run('rm releases/previous; mv releases/current releases/previous;', pty=True)
@@ -99,6 +98,7 @@ def rollback():
 # =====================================================================
 def upload_tar_from_git():
     "Create an archive from the current Git master branch and upload it"
 # =====================================================================
 def upload_tar_from_git():
     "Create an archive from the current Git master branch and upload it"
+    print '>>> upload tar from git'
     require('release', provided_by=[deploy])
     local('git archive --format=tar master | gzip > %(release)s.tar.gz' % env)
     run('mkdir -p %(path)s/releases/%(release)s' % env, pty=True)
     require('release', provided_by=[deploy])
     local('git archive --format=tar master | gzip > %(release)s.tar.gz' % env)
     run('mkdir -p %(path)s/releases/%(release)s' % env, pty=True)
@@ -107,16 +107,32 @@ def upload_tar_from_git():
     run('cd %(path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
     local('rm %(release)s.tar.gz' % env)
 
     run('cd %(path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
     local('rm %(release)s.tar.gz' % env)
 
+def upload_requirements_bundle():
+    "Create a pybundle from requirements.txt file and upload it"
+    print '>>> upload requirements bundle'
+    require('release', provided_by=[deploy])
+    requirements_mtime = os.path.getmtime('requirements.txt')
+    pybundle_mtime = 0
+    try:
+        pybundle_mtime = os.path.getmtime('requirements.pybundle')
+    except os.error:
+        pass
+    if pybundle_mtime < requirements_mtime:
+        pip_options = file('pip-options.txt').read().strip()
+        local('pip bundle %s -r requirements.txt requirements.pybundle' % pip_options)
+    put('requirements.pybundle', '%(path)s/releases/%(release)s' % env)
+
 def install_requirements():
     "Install the required packages from the requirements file using pip"
 def install_requirements():
     "Install the required packages from the requirements file using pip"
+    print '>>> install requirements'
     require('release', provided_by=[deploy])
     require('release', provided_by=[deploy])
-    pip_options = file('pip-options.txt').read().strip()
     with cd('%(path)s/releases/%(release)s' % env):
         run('virtualenv --no-site-packages .')
     with cd('%(path)s/releases/%(release)s' % env):
         run('virtualenv --no-site-packages .')
-        run('pip install -E . %s -r requirements.txt' % pip_options)
+        run('pip install -E . requirements.pybundle')
 
 def symlink_current_release():
     "Symlink our current release"
 
 def symlink_current_release():
     "Symlink our current release"
+    print '>>> symlink current release'
     require('release', provided_by=[deploy])
     require('path', provided_by=[staging, production])
     with cd(env.path):
     require('release', provided_by=[deploy])
     require('path', provided_by=[staging, production])
     with cd(env.path):
@@ -130,6 +146,7 @@ def symlink_current_release():
 
 def migrate():
     "Update the database"
 
 def migrate():
     "Update the database"
+    print '>>> migrate'
     require('project_name', provided_by=[staging, production])
     with cd('%(path)s/releases/current/%(project_name)s' % env):
         run('../bin/python manage.py syncdb --noinput' % env, pty=True)
     require('project_name', provided_by=[staging, production])
     with cd('%(path)s/releases/current/%(project_name)s' % env):
         run('../bin/python manage.py syncdb --noinput' % env, pty=True)
@@ -137,6 +154,7 @@ def migrate():
 
 def restart_webserver():
     "Restart the web server"
 
 def restart_webserver():
     "Restart the web server"
+    print '>>> restart webserver'
     run('touch %(path)s/releases/current/%(project_name)s/%(project_name)s.wsgi' % env)
 
 # def install_site():
     run('touch %(path)s/releases/current/%(project_name)s/%(project_name)s.wsgi' % env)
 
 # def install_site():
index f5497c1..810e1ff 100644 (file)
@@ -1,8 +1,8 @@
-.sponsor-group {
+.sponsors-sponsor-group {
     float: left;
     overflow: hidden;
 }
 
     float: left;
     overflow: hidden;
 }
 
-.sponsor-logo {
+.sponsors-sponsor-logo {
     float: left;
 }
\ No newline at end of file
     float: left;
 }
\ No newline at end of file
diff --git a/wolnelektury/media/sponsors/css/footer_admin.css b/wolnelektury/media/sponsors/css/footer_admin.css
new file mode 100644 (file)
index 0000000..ba56771
--- /dev/null
@@ -0,0 +1,67 @@
+.sponsors {
+    display: block;
+    clear: both;
+    margin-top: 5px;
+}
+
+.sponsors .sponsors-sponsor-group {
+    float: left;
+    width: 200px;
+    border: 1px solid #CCC;
+    margin: 2px 2px 0 0;
+}
+
+.sponsors .sponsors-sponsor-group-name {
+    border-bottom: 1px solid #CCC;
+    padding: 2px 2px 2px 4px;
+    margin: 0;
+    color: #FFF;
+    background-color: #7CA0C7;
+    font-weight: bold;
+    height: 15px;
+}
+
+.sponsors .sponsors-sponsor-group-name input {
+    margin: -2px -2px -2px -4px;
+    padding: 0;
+    height: 15px;
+    width: 180px;
+}
+
+.sponsors .sponsors-remove-sponsor-group {
+    float: right;
+    background-color: #CC3434;
+    color: #FFF;
+    width: 10px;
+    height: 15px;
+    padding: 2px;
+    text-align: center;
+    font-weight: bold;
+    display: block;
+    cursor: default;
+}
+
+.sponsors .sponsors-remove-sponsor-group:hover {
+    color: #CC3434;
+    background-color: white;
+}
+
+.sponsors .sponsors-unused-sponsor-group-name {
+    background-color: #FFF;
+    color: #666;
+}
+
+.sponsors .sponsors-sponsor-group-list {
+    margin: 0;
+    padding: 2px;
+    list-style: none;
+    min-height: 200px;
+}
+
+.sponsors-sponsor {
+    margin: 0 0 2px 0;
+    padding: 2px;
+    border: 1px solid #CCC;
+    background-color: #EEE;
+    cursor: default;
+}
diff --git a/wolnelektury/media/sponsors/js/footer_admin.js b/wolnelektury/media/sponsors/js/footer_admin.js
new file mode 100644 (file)
index 0000000..2f2cd93
--- /dev/null
@@ -0,0 +1,131 @@
+(function($) {
+  $.fn.sponsorsFooter = function(options) {
+    var settings = {
+      sponsors: []
+    };
+    $.extend(settings, options);
+    
+    var input = $(this).hide();
+    
+    var container = $('<div class="sponsors"></div>').appendTo(input.parent());
+    var groups = $.evalJSON(input.val());
+    
+    var unusedDiv = $('<div class="sponsors-sponsor-group sponsors-unused-sponsor-group"></div>')
+      .appendTo(container)
+      .append('<p class="sponsors-sponsor-group-name sponsors-unused-sponsor-group-name">dostępni sponsorzy</p>');
+    var unusedList = $('<ol class="sponsors-sponsor-group-list sponsors-unused-group-list"></ol>')
+        .appendTo(unusedDiv)
+        .sortable({
+          connectWith: '.sponsors-sponsor-group-list'
+               });
+    
+    // Edit group name inline
+    function editNameInline(name) {
+      name.unbind('click.sponsorsFooter');
+      var inlineInput = $('<input></input>').val(name.html());
+      name.html('');
+      
+      function endEditing() {
+        name.html(inlineInput.val());
+        inlineInput.remove();
+        name.bind('click.sponsorsFooter', function() {
+          editNameInline($(this));
+        });
+        input.parents('form').unbind('submit.sponsorsFooter', endEditing);
+        return false;
+      }
+      
+      inlineInput.appendTo(name).focus().blur(endEditing);
+      input.parents('form').bind('submit.sponsorsFooter', endEditing);
+    }
+    
+    // Remove sponsor with passed id from sponsors array and return it
+    function popSponsor(id) {
+      for (var i=0; i < settings.sponsors.length; i++) {
+        if (settings.sponsors[i].id == id) {
+          var s = settings.sponsors[i];
+          settings.sponsors.splice(i, 1);
+          return s;
+        }
+      }
+      return null;
+    }
+    
+    // Create sponsor group and bind events
+    function createGroup(name, sponsors) {
+      if (!sponsors) {
+        sponsors = [];
+      }
+      
+      var groupDiv = $('<div class="sponsors-sponsor-group"></div>');
+      
+      $('<a class="sponsors-remove-sponsor-group">X</a>')
+        .click(function() {
+          groupDiv.fadeOut('slow', function() {
+            $('.sponsors-sponsor', groupDiv).hide().appendTo(unusedList).fadeIn();
+            groupDiv.remove();
+          });
+        }).appendTo(groupDiv);
+      
+      $('<p class="sponsors-sponsor-group-name">' + name + '</p>')
+        .bind('click.sponsorsFooter', function() {
+          editNameInline($(this));
+        }).appendTo(groupDiv);
+      
+      var groupList = $('<ol class="sponsors-sponsor-group-list"></ol>')
+        .appendTo(groupDiv)
+        .sortable({
+          connectWith: '.sponsors-sponsor-group-list'
+               });
+      
+      
+      for (var i = 0; i < sponsors.length; i++) {
+        $('<li class="sponsors-sponsor">' + sponsors[i].name + '</li>')
+          .data('obj_id', sponsors[i].id)
+          .appendTo(groupList);
+      }
+      return groupDiv;
+    }
+    
+    // Create groups from data in input value
+    for (var i = 0; i < groups.length; i++) {
+      var group = groups[i];
+      var sponsors = [];
+      
+      for (var j = 0; j < group.sponsors.length; j++) {
+        var s = popSponsor(group.sponsors[j]);
+        if (s) {
+          sponsors.push(s);
+        }
+      }
+      createGroup(group.name, sponsors).appendTo(container);
+    }
+    
+    // Serialize input value before submiting form
+    input.parents('form').submit(function(event) {
+      var groups = [];
+      $('.sponsors-sponsor-group', container).not('.sponsors-unused-sponsor-group').each(function() {
+        var group = {name: $('.sponsors-sponsor-group-name', this).html(), sponsors: []};
+        $('.sponsors-sponsor', this).each(function() {
+          group.sponsors.push($(this).data('obj_id'));
+        });
+        groups.push(group);
+      });
+      input.val($.toJSON(groups));
+    });
+    
+    for (i = 0; i < settings.sponsors.length; i++) {
+      $('<li class="sponsors-sponsor">' + settings.sponsors[i].name + '</li>')
+        .data('obj_id', settings.sponsors[i].id)
+        .appendTo(unusedList);
+    }
+    
+    $('<button type="button">Dodaj nową grupę</button>')
+      .click(function() {
+        var newGroup = createGroup('').appendTo(container);
+        editNameInline($('.sponsors-sponsor-group-name', newGroup));
+      }).prependTo(input.parent());
+    
+    input.parent().append('<div style="clear: both"></div>');
+  };
+})(jQuery);
diff --git a/wolnelektury/media/sponsors/js/jquery.json.min.js b/wolnelektury/media/sponsors/js/jquery.json.min.js
new file mode 100644 (file)
index 0000000..bad4a0a
--- /dev/null
@@ -0,0 +1,31 @@
+
+(function($){$.toJSON=function(o)
+{if(typeof(JSON)=='object'&&JSON.stringify)
+return JSON.stringify(o);var type=typeof(o);if(o===null)
+return"null";if(type=="undefined")
+return undefined;if(type=="number"||type=="boolean")
+return o+"";if(type=="string")
+return $.quoteString(o);if(type=='object')
+{if(typeof o.toJSON=="function")
+return $.toJSON(o.toJSON());if(o.constructor===Date)
+{var month=o.getUTCMonth()+1;if(month<10)month='0'+month;var day=o.getUTCDate();if(day<10)day='0'+day;var year=o.getUTCFullYear();var hours=o.getUTCHours();if(hours<10)hours='0'+hours;var minutes=o.getUTCMinutes();if(minutes<10)minutes='0'+minutes;var seconds=o.getUTCSeconds();if(seconds<10)seconds='0'+seconds;var milli=o.getUTCMilliseconds();if(milli<100)milli='0'+milli;if(milli<10)milli='0'+milli;return'"'+year+'-'+month+'-'+day+'T'+
+hours+':'+minutes+':'+seconds+'.'+milli+'Z"';}
+if(o.constructor===Array)
+{var ret=[];for(var i=0;i<o.length;i++)
+ret.push($.toJSON(o[i])||"null");return"["+ret.join(",")+"]";}
+var pairs=[];for(var k in o){var name;var type=typeof k;if(type=="number")
+name='"'+k+'"';else if(type=="string")
+name=$.quoteString(k);else
+continue;if(typeof o[k]=="function")
+continue;var val=$.toJSON(o[k]);pairs.push(name+":"+val);}
+return"{"+pairs.join(", ")+"}";}};$.evalJSON=function(src)
+{if(typeof(JSON)=='object'&&JSON.parse)
+return JSON.parse(src);return eval("("+src+")");};$.secureEvalJSON=function(src)
+{if(typeof(JSON)=='object'&&JSON.parse)
+return JSON.parse(src);var filtered=src;filtered=filtered.replace(/\\["\\\/bfnrtu]/g,'@');filtered=filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']');filtered=filtered.replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered))
+return eval("("+src+")");else
+throw new SyntaxError("Error parsing JSON, source is not valid.");};$.quoteString=function(string)
+{if(string.match(_escapeable))
+{return'"'+string.replace(_escapeable,function(a)
+{var c=_meta[a];if(typeof c==='string')return c;c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';}
+return'"'+string+'"';};var _escapeable=/["\\\x00-\x1f\x7f-\x9f]/g;var _meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};})(jQuery);
\ No newline at end of file
index ec75d63..0f30a9e 100644 (file)
@@ -56,7 +56,7 @@
                 e-mail: <a href="mailto:fundacja@nowoczesnapolska.org.pl">fundacja@nowoczesnapolska.org.pl</a>
             </p>
 
                 e-mail: <a href="mailto:fundacja@nowoczesnapolska.org.pl">fundacja@nowoczesnapolska.org.pl</a>
             </p>
 
-                       {% sponsors %}
+                       {% sponsor_page "footer" %}
         </div>
         <div id="login-register-window">
             <div class="header"><a href="#" class="jqmClose">Zamknij</a></div>
         </div>
         <div id="login-register-window">
             <div class="header"><a href="#" class="jqmClose">Zamknij</a></div>