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.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',)
 
-class SponsorAdmin(admin.ModelAdmin):
+
+class SponsorPageAdmin(admin.ModelAdmin):
+    formfield_overrides = {
+        fields.JSONField: {'widget': widgets.SponsorPageWidget},
+    }   
     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.template.loader import render_to_string
+
+from sponsors.fields import JSONField
 
 
 class Sponsor(models.Model):
@@ -18,17 +21,35 @@ class Sponsor(models.Model):
             return self.name
 
 
-class SponsorGroup(models.Model):
+class SponsorPage(models.Model):
     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
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.utils.safestring import mark_safe
 
 from sponsors import models
 
@@ -6,7 +7,11 @@ from sponsors import models
 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 import forms
 from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy as _
 
 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',
-            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'$("#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))
-
index 6791864..2acd88b 100644 (file)
@@ -26,8 +26,8 @@ def production():
 # =========
 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():
     """
@@ -35,13 +35,13 @@ def deploy():
     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()
+    upload_requirements_bundle()
     install_requirements()
     symlink_current_release()
     migrate()
@@ -49,8 +49,7 @@ def deploy():
 
 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)
@@ -99,6 +98,7 @@ def rollback():
 # =====================================================================
 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)
@@ -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)
 
+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"
+    print '>>> install requirements'
     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 .')
-        run('pip install -E . %s -r requirements.txt' % pip_options)
+        run('pip install -E . requirements.pybundle')
 
 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):
@@ -130,6 +146,7 @@ def symlink_current_release():
 
 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)
@@ -137,6 +154,7 @@ def migrate():
 
 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():
index f5497c1..810e1ff 100644 (file)
@@ -1,8 +1,8 @@
-.sponsor-group {
+.sponsors-sponsor-group {
     float: left;
     overflow: hidden;
 }
 
-.sponsor-logo {
+.sponsors-sponsor-logo {
     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>
 
-                       {% sponsors %}
+                       {% sponsor_page "footer" %}
         </div>
         <div id="login-register-window">
             <div class="header"><a href="#" class="jqmClose">Zamknij</a></div>