From 61a86e9a1d44ed8656a7fac870ffd23071e060d0 Mon Sep 17 00:00:00 2001 From: Alexander Burdeinyi Date: Fri, 2 Dec 2016 15:44:56 +0200 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/admin.py | 6 + article/models.py | 6 +- conference/views.py | 5 +- emencia/django/newsletter/forms.py | 7 + emencia/django/newsletter/models.py | 4 +- exposition/views.py | 5 +- functions/model_mixin.py | 44 ++- functions/models_methods.py | 17 +- functions/pipeline.py | 15 + proj/middleware.py | 11 +- requirements.txt | 1 + sortedm2m/__init__.py | 4 + sortedm2m/fields.py | 353 ++++++++++++++++++ sortedm2m/forms.py | 104 ++++++ sortedm2m/models.py | 0 sortedm2m/static/sortedm2m/jquery-ui.js | 7 + .../static/sortedm2m/selector-search.gif | Bin 0 -> 552 bytes sortedm2m/static/sortedm2m/widget.css | 50 +++ sortedm2m/static/sortedm2m/widget.js | 91 +++++ ...orted_checkbox_select_multiple_widget.html | 23 ++ static/js/select2-4.0.3/js/select2.full.js | 7 + support/d_dev/settings.py | 12 +- support/dev/settings.py | 8 +- support/prod/settings.py | 8 +- templates/c_admin/conference/conference.html | 23 +- templates/c_admin/forms/multilang.html | 13 +- templates/client/article/news.html | 41 +- .../conference/conference_partner.html | 31 +- templates/registration/acquire_email.html | 11 +- 29 files changed, 817 insertions(+), 90 deletions(-) create mode 100644 sortedm2m/__init__.py create mode 100644 sortedm2m/fields.py create mode 100644 sortedm2m/forms.py create mode 100644 sortedm2m/models.py create mode 100644 sortedm2m/static/sortedm2m/jquery-ui.js create mode 100644 sortedm2m/static/sortedm2m/selector-search.gif create mode 100644 sortedm2m/static/sortedm2m/widget.css create mode 100644 sortedm2m/static/sortedm2m/widget.js create mode 100644 sortedm2m/templates/sortedm2m/sorted_checkbox_select_multiple_widget.html diff --git a/accounts/admin.py b/accounts/admin.py index 7353249b..da600922 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -9,6 +9,7 @@ from hashlib import md5 from company.models import Company from django.conf import settings from django.contrib.auth.decorators import login_required +# from django.contrib import admin from django.core.context_processors import csrf from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse, HttpResponseRedirect @@ -30,6 +31,11 @@ from functions.admin_views import AdminListView, AdminView, paginate_results # models and forms from .models import User +# +# class UserAdmin(admin.ModelAdmin): +# search_fields = ['email', ] +# admin.site.register(User, UserAdmin) + class DeleteAccount(DeleteView): model = User diff --git a/article/models.py b/article/models.py index 1d80a49f..9f7191d1 100644 --- a/article/models.py +++ b/article/models.py @@ -269,14 +269,14 @@ class Article(TranslatableModel): tags = self.tag.all().values_list('pk', flat=True) themes = self.theme.all().values_list('pk', flat=True) exclude_pks = set([self.pk]) - qs = model.objects.language() \ - .filter(type=self.type, publish_date__isnull=False).order_by('-publish_date') + qs = model.objects.language()\ + .filter(type=self.type, publish_date__isnull=False).distinct().order_by('-publish_date') tags_sim = qs.filter(tag__in=tags).exclude(pk__in=exclude_pks)[:4] exclude_pks.update([x.pk for x in tags_sim]) themes_sim = qs.filter(theme__in=themes).exclude(pk__in=exclude_pks)[:4] exclude_pks.update([x.pk for x in themes_sim]) last_sim = qs.exclude(pk__in=exclude_pks)[:4] - result = list(chain(tags_sim, themes_sim, last_sim)[:4]) + result = list(chain(tags_sim, themes_sim, last_sim))[:4] self._similar = result return self._similar diff --git a/conference/views.py b/conference/views.py index 2bfbad33..fefa2e84 100644 --- a/conference/views.py +++ b/conference/views.py @@ -33,7 +33,7 @@ from models import Conference from note.models import Note from service.models import Service from service.order_forms import AdvertiseForm -from service.views import order_forms +from service.views import order_forms, get_userlog_data from stats_collector.mixin import ( ConfSectionKindMixin, ConfSectionMixin, @@ -406,7 +406,8 @@ class ConferenceServiceView(FormMixin, DetailView): return context def form_valid(self, form): - order = form.save(commit=False) + userlog = get_userlog_data(self.request) + order = form.save(commit=False, userlog=userlog) order.conference = self.object order.save() messages.success(self.request, _(u'Ваш запрос был успешно отправлен')) diff --git a/emencia/django/newsletter/forms.py b/emencia/django/newsletter/forms.py index 4224d91a..71ca7b09 100644 --- a/emencia/django/newsletter/forms.py +++ b/emencia/django/newsletter/forms.py @@ -12,6 +12,7 @@ from django.utils import translation from django.utils.translation import ugettext_lazy as _ from django.utils.translation import get_language from django.utils.encoding import smart_text, force_text +from django.conf import settings from conference.models import Conference from exposition.models import Exposition @@ -163,6 +164,12 @@ class MailingSettingsForm(forms.ModelForm): obj.f_countries = self.cleaned_data.get('co') obj.area = areas obj.r_cities = self.cleaned_data.get('r_cities') or [] + + if obj.moscow and not settings.MOSCOW_PK in obj.r_cities: + obj.r_cities.add(settings.MOSCOW_PK) + if not obj.moscow and settings.MOSCOW_PK in obj.r_cities: + obj.moscow = True + obj.tags = self.cleaned_data.get('tg') or [] obj.themes = self.cleaned_data.get('th') or [] obj.from_users = False diff --git a/emencia/django/newsletter/models.py b/emencia/django/newsletter/models.py index 9a24977d..6a3899af 100644 --- a/emencia/django/newsletter/models.py +++ b/emencia/django/newsletter/models.py @@ -164,8 +164,8 @@ class Contact(models.Model): periodic_day = models.PositiveSmallIntegerField(_(u'День отправки'), choices=PERIODIC_DAY_CHOICES, default=PERIODIC_DAY_CHOICES.WED) content_news = models.BooleanField(_(u'Новости событий'), blank=True, default=True) - content_overview = models.BooleanField(_(u'Обзоры'), blank=True, default=True) - content_articles = models.BooleanField(_(u'Статьи из блога по участию в вывставках'), blank=True, default=True) + content_overview = models.BooleanField(_(u'Статьи для экспонентов'), blank=True, default=True) + content_articles = models.BooleanField(_(u'Статьи для организаторов событий'), blank=True, default=True) tags = models.ManyToManyField('theme.Tag', verbose_name=_(u'Теги'), blank=True, null=True) themes = models.ManyToManyField('theme.Theme', blank=True, null=True, verbose_name=_(u'Тематики')) diff --git a/exposition/views.py b/exposition/views.py index 83caae27..e775f48f 100644 --- a/exposition/views.py +++ b/exposition/views.py @@ -32,7 +32,7 @@ from models import Exposition from note.models import Note from service.models import Service from service.order_forms import AdvertiseForm -from service.views import order_forms +from service.views import order_forms, get_userlog_data # from stats_collector.models import SectionStats from stats_collector.mixin import ( ExpoSectionKindMixin, @@ -327,7 +327,8 @@ class ExpositionServiceView(JitterCacheMixin, MetadataMixin, FormMixin, DetailVi return context def form_valid(self, form): - order = form.save(commit=False) + userlog = get_userlog_data(self.request) + order = form.save(commit=False, userlog=userlog) order.exposition = self.object order.save() return HttpResponseRedirect(self.object.get_permanent_url()+'service/thanks/') diff --git a/functions/model_mixin.py b/functions/model_mixin.py index a92cc9f7..f603deac 100644 --- a/functions/model_mixin.py +++ b/functions/model_mixin.py @@ -114,29 +114,39 @@ class EventMixin(object): } return ctx + def get_nearest_events_incl(self, _qs, tags, themes, exclude=None): + now = datetime.datetime.now() + month = now + relativedelta(months=1) + month2 = now + relativedelta(months=2) + exclude_pks = exclude or set([self.pk]) + events_m2 = [] + events_m = [] + events = [] + for params in [{'tag__in': tags}, {'theme__in': themes}]: + qs = _qs.filter(**params).order_by('data_begin') + events_m2.extend(qs.filter(data_begin__gte=month2).exclude(pk__in=exclude_pks)[:3]) + exclude_pks.update([x.pk for x in events_m2]) + events_m.extend(qs.filter(data_begin__gte=month).exclude(pk__in=exclude_pks)[:3]) + exclude_pks.update([x.pk for x in events_m]) + events.extend(qs.filter(data_begin__gte=now).exclude(pk__in=exclude_pks)[:3]) + exclude_pks.update([x.pk for x in events]) + return (list( + sorted((chain(zip(*[x for x in [events_m2, events_m, events] if x]))[:3]), key=lambda x: x.data_begin)), + exclude_pks) + def get_nearest_events(self): if getattr(self, '_nearest_events', None) is None: model = self.__class__ result = [] tags = self.tag.all().values_list('pk', flat=True) themes = self.theme.all().values_list('pk', flat=True) - now = datetime.datetime.now() - month = now + relativedelta(months=1) - month2 = now + relativedelta(months=2) - exclude_pks = set([self.pk]) - events_m2 = [] - events_m = [] - events = [] - for params in [{'tag__in': tags}, {'theme__in': themes}]: - qs = model.objects.language().filter(**params).order_by('data_begin') - events_m2.extend(qs.filter(data_begin__gte=month2).exclude(pk__in=exclude_pks)[:3]) - exclude_pks.update([x.pk for x in events_m2]) - events_m.extend(qs.filter(data_begin__gte=month).exclude(pk__in=exclude_pks)[:3]) - exclude_pks.update([x.pk for x in events_m]) - events.extend(qs.filter(data_begin__gte=now).exclude(pk__in=exclude_pks)[:3]) - exclude_pks.update([x.pk for x in events]) - result = list(sorted((chain(zip(*[x for x in [events_m2, events_m, events] if x]))[:3]), key=lambda x: x.data_begin)) - self._nearest_events = result + qs = model.objects.language().filter(country_id=self.country_id) + result, exclude = self.get_nearest_events_incl(qs, tags, themes) + if len(result) > 3: + qs = model.objects.language() + r, e = self.get_nearest_events_incl(qs, tags, themes, exclude) + result += r + self._nearest_events = result[:3] return self._nearest_events def get_nearest_events_url(self): diff --git a/functions/models_methods.py b/functions/models_methods.py index 9f94e54b..58dff11b 100644 --- a/functions/models_methods.py +++ b/functions/models_methods.py @@ -192,12 +192,21 @@ class CityManager(TranslationManager): if cached and key in cache: cities = cache.get(key) else: - cities = list( - self.language(lang).filter( + cities = [] + popular_cities = [] + _cities = self.language(lang).filter( country__url='russia', exposition_city__isnull=False, conference_city__isnull=False - ).exclude(pk=settings.MOSCOW_PK).distinct().order_by('name').values_list('pk', 'name') - ) + ).distinct().order_by('name').values_list('pk', 'name') + for city in _cities: + if city[0] == settings.MOSCOW_PK: + popular_cities.insert(0, city) + elif city[0] == settings.SPB_PK: + popular_cities.append(city) + else: + cities.append(city) + # .exclude(pk=settings.MOSCOW_PK) + cities = popular_cities + cities cache.set(key, cities, self.cache_time) return cities diff --git a/functions/pipeline.py b/functions/pipeline.py index 389bcff4..f0ab6dff 100644 --- a/functions/pipeline.py +++ b/functions/pipeline.py @@ -78,6 +78,21 @@ def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): return +def social_user(backend, uid, user=None, *args, **kwargs): + provider = backend.name + social = backend.strategy.storage.user.get_social_auth(provider, uid) + if social: + # if user and social.user != user: + # msg = 'This {0} account is already in use.'.format(provider) + # raise AuthAlreadyAssociated(backend, msg) + # elif not user: + user = social.user + return {'social': social, + 'user': user, + 'is_new': user is None, + 'new_association': social is None} + + def SendVerificationEmail(strategy, backend, code): """ Send an email with an embedded verification code and the necessary details to restore the required session diff --git a/proj/middleware.py b/proj/middleware.py index 0b67c5d1..62b2d743 100644 --- a/proj/middleware.py +++ b/proj/middleware.py @@ -54,12 +54,17 @@ class Referer(object): for key, val in data_exists.iteritems(): if val: if key == 'search': + print(request.session.get('search', [])) + print(request.session.get('search', []))[-2:] search = request.session.get('search', [])[-2:] search.append(val) request.session['search'] = search - if userlog is not None: - for i, d in enumerate(search, start=1): - setattr(userlog, key + str(i), d) + try: + if userlog is not None: + for i, d in enumerate(search, start=1): + setattr(userlog, key + str(i), d) + except Exception as e: + print(e) else: attr1 = key + '1' attr2 = key + '2' diff --git a/requirements.txt b/requirements.txt index 3512c3a6..7739abbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ django-redis==4.0.0 django-redis-cache==0.13.1 django-sortedm2m==1.2.0 django-tinymce==2.0.5 +django-widget-tweaks==1.4.1 djutils==0.3.2 docutils==0.12 fpconst==0.7.2 diff --git a/sortedm2m/__init__.py b/sortedm2m/__init__.py new file mode 100644 index 00000000..2530024d --- /dev/null +++ b/sortedm2m/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + + +__version__ = '0.7.0' diff --git a/sortedm2m/fields.py b/sortedm2m/fields.py new file mode 100644 index 00000000..c7432a15 --- /dev/null +++ b/sortedm2m/fields.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +from operator import attrgetter +import sys +from django.db import connections +from django.db import router +from django.db.models import signals +from django.db.models.fields.related import add_lazy_relation, create_many_related_manager +from django.db.models.fields.related import ManyToManyField, ReverseManyRelatedObjectsDescriptor +from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT +from django.conf import settings +from django.utils.functional import curry +from sortedm2m.forms import SortedMultipleChoiceField + + +if sys.version_info[0] < 3: + string_types = basestring +else: + string_types = str + + +SORT_VALUE_FIELD_NAME = 'sort_value' + + +def create_sorted_many_to_many_intermediate_model(field, klass): + from django.db import models + managed = True + if isinstance(field.rel.to, string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: + to_model = field.rel.to + to = to_model.split('.')[-1] + def set_managed(field, model, cls): + field.rel.through._meta.managed = model._meta.managed or cls._meta.managed + add_lazy_relation(klass, field, to_model, set_managed) + elif isinstance(field.rel.to, string_types): + to = klass._meta.object_name + to_model = klass + managed = klass._meta.managed + else: + to = field.rel.to._meta.object_name + to_model = field.rel.to + managed = klass._meta.managed or to_model._meta.managed + name = '%s_%s' % (klass._meta.object_name, field.name) + if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or to == klass._meta.object_name: + from_ = 'from_%s' % to.lower() + to = 'to_%s' % to.lower() + else: + from_ = klass._meta.object_name.lower() + to = to.lower() + meta = type(str('Meta'), (object,), { + 'db_table': field._get_m2m_db_table(klass._meta), + 'managed': managed, + 'auto_created': klass, + 'app_label': klass._meta.app_label, + 'unique_together': (from_, to), + 'ordering': (field.sort_value_field_name,), + 'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to}, + 'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to}, + }) + # Construct and return the new class. + def default_sort_value(name): + model = models.get_model(klass._meta.app_label, name) + return model._default_manager.count() + + default_sort_value = curry(default_sort_value, name) + + return type(str(name), (models.Model,), { + 'Meta': meta, + '__module__': klass.__module__, + from_: models.ForeignKey(klass, related_name='%s+' % name), + to: models.ForeignKey(to_model, related_name='%s+' % name), + field.sort_value_field_name: models.IntegerField(default=default_sort_value), + '_sort_field_name': field.sort_value_field_name, + '_from_field_name': from_, + '_to_field_name': to, + }) + + +def create_sorted_many_related_manager(superclass, rel): + RelatedManager = create_many_related_manager(superclass, rel) + + class SortedRelatedManager(RelatedManager): + def get_query_set(self): + # We use ``extra`` method here because we have no other access to + # the extra sorting field of the intermediary model. The fields + # are hidden for joins because we set ``auto_created`` on the + # intermediary's meta options. + try: + return self.instance._prefetched_objects_cache[self.prefetch_cache_name] + except (AttributeError, KeyError): + return super(SortedRelatedManager, self).\ + get_query_set().\ + extra(order_by=['%s.%s' % ( + rel.through._meta.db_table, + rel.through._sort_field_name, + )]) + + if not hasattr(RelatedManager, '_get_fk_val'): + @property + def _fk_val(self): + return self._pk_val + + def get_prefetch_query_set(self, instances): + # mostly a copy of get_prefetch_query_set from ManyRelatedManager + # but with addition of proper ordering + db = self._db or router.db_for_read(instances[0].__class__, instance=instances[0]) + query = {'%s__pk__in' % self.query_field_name: + set(obj._get_pk_val() for obj in instances)} + qs = super(RelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**query) + + # M2M: need to annotate the query in order to get the primary model + # that the secondary model was actually related to. We know that + # there will already be a join on the join table, so we can just add + # the select. + + # For non-autocreated 'through' models, can't assume we are + # dealing with PK values. + fk = self.through._meta.get_field(self.source_field_name) + source_col = fk.column + join_table = self.through._meta.db_table + connection = connections[db] + qn = connection.ops.quote_name + qs = qs.extra(select={'_prefetch_related_val': + '%s.%s' % (qn(join_table), qn(source_col))}, + order_by=['%s.%s' % ( + rel.through._meta.db_table, + rel.through._sort_field_name, + )]) + select_attname = fk.rel.get_related_field().get_attname() + return (qs, + attrgetter('_prefetch_related_val'), + attrgetter(select_attname), + False, + self.prefetch_cache_name) + + def _add_items(self, source_field_name, target_field_name, *objs): + # source_field_name: the PK fieldname in join_table for the source object + # target_field_name: the PK fieldname in join_table for the target object + # *objs - objects to add. Either object instances, or primary keys of object instances. + + # If there aren't any objects, there is nothing to do. + from django.db.models import Model + if objs: + new_ids = [] + for obj in objs: + if isinstance(obj, self.model): + if not router.allow_relation(obj, self.instance): + raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % + (obj, self.instance._state.db, obj._state.db)) + if hasattr(self, '_get_fk_val'): # Django>=1.5 + fk_val = self._get_fk_val(obj, target_field_name) + if fk_val is None: + raise ValueError('Cannot add "%r": the value for field "%s" is None' % + (obj, target_field_name)) + new_ids.append(self._get_fk_val(obj, target_field_name)) + else: # Django<1.5 + new_ids.append(obj.pk) + elif isinstance(obj, Model): + raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) + else: + new_ids.append(obj) + db = router.db_for_write(self.through, instance=self.instance) + vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True) + vals = vals.filter(**{ + source_field_name: self._fk_val, + '%s__in' % target_field_name: new_ids, + }) + for val in vals: + if val in new_ids: + new_ids.remove(val) + _new_ids = [] + for pk in new_ids: + if pk not in _new_ids: + _new_ids.append(pk) + new_ids = _new_ids + new_ids_set = set(new_ids) + + if self.reverse or source_field_name == self.source_field_name: + # Don't send the signal when we are inserting the + # duplicate data row for symmetrical reverse entries. + signals.m2m_changed.send(sender=rel.through, action='pre_add', + instance=self.instance, reverse=self.reverse, + model=self.model, pk_set=new_ids_set, using=db) + # Add the ones that aren't there already + sort_field_name = self.through._sort_field_name + sort_field = self.through._meta.get_field_by_name(sort_field_name)[0] + for obj_id in new_ids: + self.through._default_manager.using(db).create(**{ + '%s_id' % source_field_name: self._fk_val, # Django 1.5 compatibility + '%s_id' % target_field_name: obj_id, + sort_field_name: sort_field.get_default(), + }) + if self.reverse or source_field_name == self.source_field_name: + # Don't send the signal when we are inserting the + # duplicate data row for symmetrical reverse entries. + signals.m2m_changed.send(sender=rel.through, action='post_add', + instance=self.instance, reverse=self.reverse, + model=self.model, pk_set=new_ids_set, using=db) + + return SortedRelatedManager + + +class ReverseSortedManyRelatedObjectsDescriptor(ReverseManyRelatedObjectsDescriptor): + @property + def related_manager_cls(self): + return create_sorted_many_related_manager( + self.field.rel.to._default_manager.__class__, + self.field.rel + ) + + +class SortedManyToManyField(ManyToManyField): + ''' + Providing a many to many relation that remembers the order of related + objects. + + Accept a boolean ``sorted`` attribute which specifies if relation is + ordered or not. Default is set to ``True``. If ``sorted`` is set to + ``False`` the field will behave exactly like django's ``ManyToManyField``. + ''' + def __init__(self, to, sorted=True, **kwargs): + self.sorted = sorted + self.sort_value_field_name = kwargs.pop( + 'sort_value_field_name', + SORT_VALUE_FIELD_NAME) + super(SortedManyToManyField, self).__init__(to, **kwargs) + if self.sorted: + self.help_text = kwargs.get('help_text', None) + + def contribute_to_class(self, cls, name): + if not self.sorted: + return super(SortedManyToManyField, self).contribute_to_class(cls, name) + + # To support multiple relations to self, it's useful to have a non-None + # related name on symmetrical relations for internal reasons. The + # concept doesn't make a lot of sense externally ("you want me to + # specify *what* on my non-reversible relation?!"), so we set it up + # automatically. The funky name reduces the chance of an accidental + # clash. + if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name): + self.rel.related_name = "%s_rel_+" % name + + super(ManyToManyField, self).contribute_to_class(cls, name) + + # The intermediate m2m model is not auto created if: + # 1) There is a manually specified intermediate, or + # 2) The class owning the m2m field is abstract. + if not self.rel.through and not cls._meta.abstract: + self.rel.through = create_sorted_many_to_many_intermediate_model(self, cls) + + # Add the descriptor for the m2m relation + setattr(cls, self.name, ReverseSortedManyRelatedObjectsDescriptor(self)) + + # Set up the accessor for the m2m table name for the relation + self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) + + # Populate some necessary rel arguments so that cross-app relations + # work correctly. + if isinstance(self.rel.through, string_types): + def resolve_through_model(field, model, cls): + field.rel.through = model + add_lazy_relation(cls, self, self.rel.through, resolve_through_model) + + if hasattr(cls._meta, 'duplicate_targets'): # Django<1.5 + if isinstance(self.rel.to, string_types): + target = self.rel.to + else: + target = self.rel.to._meta.db_table + cls._meta.duplicate_targets[self.column] = (target, "m2m") + + def formfield(self, **kwargs): + defaults = {} + if self.sorted: + defaults['form_class'] = SortedMultipleChoiceField + defaults.update(kwargs) + return super(SortedManyToManyField, self).formfield(**defaults) + + +# Add introspection rules for South database migrations +# See http://south.aeracode.org/docs/customfields.html +try: + import south +except ImportError: + south = None + +if south is not None and 'south' in settings.INSTALLED_APPS: + from south.modelsinspector import add_introspection_rules + add_introspection_rules( + [( + (SortedManyToManyField,), + [], + {"sorted": ["sorted", {"default": True}]}, + )], + [r'^sortedm2m\.fields\.SortedManyToManyField'] + ) + + # Monkeypatch South M2M actions to create the sorted through model. + # FIXME: This doesn't detect if you changed the sorted argument to the field. + import south.creator.actions + from south.creator.freezer import model_key + + class AddM2M(south.creator.actions.AddM2M): + SORTED_FORWARDS_TEMPLATE = ''' + # Adding SortedM2M table for field %(field_name)s on '%(model_name)s' + db.create_table(%(table_name)r, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + (%(left_field)r, models.ForeignKey(orm[%(left_model_key)r], null=False)), + (%(right_field)r, models.ForeignKey(orm[%(right_model_key)r], null=False)), + (%(sort_field)r, models.IntegerField()) + )) + db.create_unique(%(table_name)r, [%(left_column)r, %(right_column)r])''' + + def console_line(self): + if isinstance(self.field, SortedManyToManyField) and self.field.sorted: + return " + Added SortedM2M table for %s on %s.%s" % ( + self.field.name, + self.model._meta.app_label, + self.model._meta.object_name, + ) + else: + return super(AddM2M, self).console_line() + + def forwards_code(self): + if isinstance(self.field, SortedManyToManyField) and self.field.sorted: + return self.SORTED_FORWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "field_name": self.field.name, + "table_name": self.field.m2m_db_table(), + "left_field": self.field.m2m_column_name()[:-3], # Remove the _id part + "left_column": self.field.m2m_column_name(), + "left_model_key": model_key(self.model), + "right_field": self.field.m2m_reverse_name()[:-3], # Remove the _id part + "right_column": self.field.m2m_reverse_name(), + "right_model_key": model_key(self.field.rel.to), + "sort_field": self.field.sort_value_field_name, + } + else: + return super(AddM2M, self).forwards_code() + + class DeleteM2M(AddM2M): + def console_line(self): + return " - Deleted M2M table for %s on %s.%s" % ( + self.field.name, + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + return AddM2M.backwards_code(self) + + def backwards_code(self): + return AddM2M.forwards_code(self) + + south.creator.actions.AddM2M = AddM2M + south.creator.actions.DeleteM2M = DeleteM2M diff --git a/sortedm2m/forms.py b/sortedm2m/forms.py new file mode 100644 index 00000000..2588007e --- /dev/null +++ b/sortedm2m/forms.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +import sys +from itertools import chain +from django import forms +from django.conf import settings +from django.db.models.query import QuerySet +from django.template.loader import render_to_string +from django.utils.encoding import force_text +from django.utils.html import conditional_escape, escape +from django.utils.safestring import mark_safe + + +if sys.version_info[0] < 3: + iteritems = lambda d: iter(d.iteritems()) + string_types = basestring, + str_ = unicode +else: + iteritems = lambda d: iter(d.items()) + string_types = str, + str_ = str + + +STATIC_URL = getattr(settings, 'STATIC_URL', settings.MEDIA_URL) + + +class SortedCheckboxSelectMultiple(forms.CheckboxSelectMultiple): + class Media: + js = ( + STATIC_URL + 'sortedm2m/widget.js', + STATIC_URL + 'sortedm2m/jquery-ui.js', + ) + css = {'screen': ( + STATIC_URL + 'sortedm2m/widget.css', + )} + + def build_attrs(self, attrs=None, **kwargs): + attrs = super(SortedCheckboxSelectMultiple, self).\ + build_attrs(attrs, **kwargs) + classes = attrs.setdefault('class', '').split() + classes.append('sortedm2m') + attrs['class'] = ' '.join(classes) + return attrs + + def render(self, name, value, attrs=None, choices=()): + if value is None: value = [] + has_id = attrs and 'id' in attrs + final_attrs = self.build_attrs(attrs, name=name) + + # Normalize to strings + str_values = [force_text(v) for v in value] + + selected = [] + unselected = [] + + for i, (option_value, option_label) in enumerate(chain(self.choices, choices)): + # If an ID attribute was given, add a numeric index as a suffix, + # so that the checkboxes don't all have the same ID attribute. + if has_id: + final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i)) + label_for = ' for="%s"' % conditional_escape(final_attrs['id']) + else: + label_for = '' + + cb = forms.CheckboxInput(final_attrs, check_test=lambda value: value in str_values) + option_value = force_text(option_value) + rendered_cb = cb.render(name, option_value) + option_label = conditional_escape(force_text(option_label)) + item = {'label_for': label_for, 'rendered_cb': rendered_cb, 'option_label': option_label, 'option_value': option_value} + if option_value in str_values: + selected.append(item) + else: + unselected.append(item) + + # re-order `selected` array according str_values which is a set of `option_value`s in the order they should be shown on screen + ordered = [] + for value in str_values: + for select in selected: + if value == select['option_value']: + ordered.append(select) + selected = ordered + + html = render_to_string( + 'sortedm2m/sorted_checkbox_select_multiple_widget.html', + {'selected': selected, 'unselected': unselected}) + return mark_safe(html) + + def value_from_datadict(self, data, files, name): + value = data.get(name, None) + if isinstance(value, string_types): + return [v for v in value.split(',') if v] + return value + + +class SortedMultipleChoiceField(forms.ModelMultipleChoiceField): + widget = SortedCheckboxSelectMultiple + + def clean(self, value): + queryset = super(SortedMultipleChoiceField, self).clean(value) + if value is None or not isinstance(queryset, QuerySet): + return queryset + object_list = dict(( + (str_(key), value) + for key, value in iteritems(queryset.in_bulk(value)))) + return [object_list[str_(pk)] for pk in value] diff --git a/sortedm2m/models.py b/sortedm2m/models.py new file mode 100644 index 00000000..e69de29b diff --git a/sortedm2m/static/sortedm2m/jquery-ui.js b/sortedm2m/static/sortedm2m/jquery-ui.js new file mode 100644 index 00000000..7e499dcb --- /dev/null +++ b/sortedm2m/static/sortedm2m/jquery-ui.js @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.10.3 - 2013-07-15 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.sortable.js +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +(function(e,t){function i(t,i){var a,n,r,o=t.nodeName.toLowerCase();return"area"===o?(a=t.parentNode,n=a.name,t.href&&n&&"map"===a.nodeName.toLowerCase()?(r=e("img[usemap=#"+n+"]")[0],!!r&&s(r)):!1):(/input|select|textarea|button|object/.test(o)?!t.disabled:"a"===o?t.href||i:i)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var a=0,n=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.3",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,s){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),s&&s.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var s,a,n=e(this[0]);n.length&&n[0]!==document;){if(s=n.css("position"),("absolute"===s||"relative"===s||"fixed"===s)&&(a=parseInt(n.css("zIndex"),10),!isNaN(a)&&0!==a))return a;n=n.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++a)})},removeUniqueId:function(){return this.each(function(){n.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,s){return!!e.data(t,s[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var s=e.attr(t,"tabindex"),a=isNaN(s);return(a||s>=0)&&i(t,!a)}}),e("").outerWidth(1).jquery||e.each(["Width","Height"],function(i,s){function a(t,i,s,a){return e.each(n,function(){i-=parseFloat(e.css(t,"padding"+this))||0,s&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),a&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var n="Width"===s?["Left","Right"]:["Top","Bottom"],r=s.toLowerCase(),o={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+s]=function(i){return i===t?o["inner"+s].call(this):this.each(function(){e(this).css(r,a(this,i)+"px")})},e.fn["outer"+s]=function(t,i){return"number"!=typeof t?o["outer"+s].call(this,t):this.each(function(){e(this).css(r,a(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,s){var a,n=e.ui[t].prototype;for(a in s)n.plugins[a]=n.plugins[a]||[],n.plugins[a].push([i,s[a]])},call:function(e,t,i){var s,a=e.plugins[t];if(a&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(s=0;a.length>s;s++)e.options[a[s][0]]&&a[s][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",a=!1;return t[s]>0?!0:(t[s]=1,a=t[s]>0,t[s]=0,a)}})})(jQuery);(function(e,t){var i=0,s=Array.prototype.slice,n=e.cleanData;e.cleanData=function(t){for(var i,s=0;null!=(i=t[s]);s++)try{e(i).triggerHandler("remove")}catch(a){}n(t)},e.widget=function(i,s,n){var a,r,o,h,l={},u=i.split(".")[0];i=i.split(".")[1],a=u+"-"+i,n||(n=s,s=e.Widget),e.expr[":"][a.toLowerCase()]=function(t){return!!e.data(t,a)},e[u]=e[u]||{},r=e[u][i],o=e[u][i]=function(e,i){return this._createWidget?(arguments.length&&this._createWidget(e,i),t):new o(e,i)},e.extend(o,r,{version:n.version,_proto:e.extend({},n),_childConstructors:[]}),h=new s,h.options=e.widget.extend({},h.options),e.each(n,function(i,n){return e.isFunction(n)?(l[i]=function(){var e=function(){return s.prototype[i].apply(this,arguments)},t=function(e){return s.prototype[i].apply(this,e)};return function(){var i,s=this._super,a=this._superApply;return this._super=e,this._superApply=t,i=n.apply(this,arguments),this._super=s,this._superApply=a,i}}(),t):(l[i]=n,t)}),o.prototype=e.widget.extend(h,{widgetEventPrefix:r?h.widgetEventPrefix:i},l,{constructor:o,namespace:u,widgetName:i,widgetFullName:a}),r?(e.each(r._childConstructors,function(t,i){var s=i.prototype;e.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete r._childConstructors):s._childConstructors.push(o),e.widget.bridge(i,o)},e.widget.extend=function(i){for(var n,a,r=s.call(arguments,1),o=0,h=r.length;h>o;o++)for(n in r[o])a=r[o][n],r[o].hasOwnProperty(n)&&a!==t&&(i[n]=e.isPlainObject(a)?e.isPlainObject(i[n])?e.widget.extend({},i[n],a):e.widget.extend({},a):a);return i},e.widget.bridge=function(i,n){var a=n.prototype.widgetFullName||i;e.fn[i]=function(r){var o="string"==typeof r,h=s.call(arguments,1),l=this;return r=!o&&h.length?e.widget.extend.apply(null,[r].concat(h)):r,o?this.each(function(){var s,n=e.data(this,a);return n?e.isFunction(n[r])&&"_"!==r.charAt(0)?(s=n[r].apply(n,h),s!==n&&s!==t?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):t):e.error("no such method '"+r+"' for "+i+" widget instance"):e.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+r+"'")}):this.each(function(){var t=e.data(this,a);t?t.option(r||{})._init():e.data(this,a,new n(r,this))}),l}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(t,s){s=e(s||this.defaultElement||this)[0],this.element=e(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),s!==this&&(e.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===s&&this.destroy()}}),this.document=e(s.style?s.ownerDocument:s.document||s),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(i,s){var n,a,r,o=i;if(0===arguments.length)return e.widget.extend({},this.options);if("string"==typeof i)if(o={},n=i.split("."),i=n.shift(),n.length){for(a=o[i]=e.widget.extend({},this.options[i]),r=0;n.length-1>r;r++)a[n[r]]=a[n[r]]||{},a=a[n[r]];if(i=n.pop(),s===t)return a[i]===t?null:a[i];a[i]=s}else{if(s===t)return this.options[i]===t?null:this.options[i];o[i]=s}return this._setOptions(o),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,"disabled"===e&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!t).attr("aria-disabled",t),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var a,r=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=a=e(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,a=this.widget()),e.each(n,function(n,o){function h(){return i||r.options.disabled!==!0&&!e(this).hasClass("ui-state-disabled")?("string"==typeof o?r[o]:o).apply(r,arguments):t}"string"!=typeof o&&(h.guid=o.guid=o.guid||h.guid||e.guid++);var l=n.match(/^(\w+)\s*(.*)$/),u=l[1]+r.eventNamespace,c=l[2];c?a.delegate(c,u,h):s.bind(u,h)})},_off:function(e,t){t=(t||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(t).undelegate(t)},_delay:function(e,t){function i(){return("string"==typeof e?s[e]:e).apply(s,arguments)}var s=this;return setTimeout(i,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,i,s){var n,a,r=this.options[t];if(s=s||{},i=e.Event(i),i.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(e.isFunction(r)&&r.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,i){e.Widget.prototype["_"+t]=function(s,n,a){"string"==typeof n&&(n={effect:n});var r,o=n?n===!0||"number"==typeof n?i:n.effect||i:t;n=n||{},"number"==typeof n&&(n={duration:n}),r=!e.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),r&&e.effects&&e.effects.effect[o]?s[t](n):o!==t&&s[o]?s[o](n.duration,n.easing,a):s.queue(function(i){e(this)[t](),a&&a.call(s[0]),i()})}})})(jQuery);(function(e){var t=!1;e(document).mouseup(function(){t=!1}),e.widget("ui.mouse",{version:"1.10.3",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var t=this;this.element.bind("mousedown."+this.widgetName,function(e){return t._mouseDown(e)}).bind("click."+this.widgetName,function(i){return!0===e.data(i.target,t.widgetName+".preventClickEvent")?(e.removeData(i.target,t.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!t){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?e(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===e.data(i.target,this.widgetName+".preventClickEvent")&&e.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(e){return s._mouseMove(e)},this._mouseUpDelegate=function(e){return s._mouseUp(e)},e(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),t=!0,!0)):!0}},_mouseMove:function(t){return e.ui.ie&&(!document.documentMode||9>document.documentMode)&&!t.button?this._mouseUp(t):this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,t)!==!1,this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted)},_mouseUp:function(t){return e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&e.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),!1},_mouseDistanceMet:function(e){return Math.max(Math.abs(this._mouseDownEvent.pageX-e.pageX),Math.abs(this._mouseDownEvent.pageY-e.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}function i(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))}t.widget("ui.sortable",t.ui.mouse,{version:"1.10.3",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var t=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?"x"===t.axis||i(this.items[0].item):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_setOption:function(e,i){"disabled"===e?(this.options[e]=i,this.widget().toggleClass("ui-sortable-disabled",!!i)):t.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(e,i){var s=null,n=!1,a=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,a.widgetName+"-item")===a?(s=t(this),!1):undefined}),t.data(e.target,a.widgetName+"-item")===a&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,a,o=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,o.cursorAt&&this._adjustOffsetFromHelper(o.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),o.containment&&this._setContainment(),o.cursor&&"auto"!==o.cursor&&(a=this.document.find("body"),this.storedCursor=a.css("cursor"),a.css("cursor",o.cursor),this.storedStylesheet=t("").appendTo(a)),o.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",o.opacity)),o.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",o.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!o.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,a,o=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY=0;i--)if(s=this.items[i],n=s.item[0],a=this._intersectsWithPointer(s),a&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===a?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===a?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),a=this.options.axis,o={};a&&"x"!==a||(o.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),a&&"y"!==a||(o.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(o,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,a=t.left,o=a+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>a&&o>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>a&&o>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,a=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return n?this.floating?o&&"right"===o||"down"===a?2:1:a&&("down"===a?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return this.floating&&a?"right"===a&&s||"left"===a&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){var i,s,n,a,o=[],r=[],h=this._connectWith();if(h&&e)for(i=h.length-1;i>=0;i--)for(n=t(h[i]),s=n.length-1;s>=0;s--)a=t.data(n[s],this.widgetFullName),a&&a!==this&&!a.options.disabled&&r.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(r.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),i=r.length-1;i>=0;i--)r[i][0].each(function(){o.push(this)});return t(o)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,a,o,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)a=t.data(n[s],this.widgetFullName),a&&a!==this&&!a.options.disabled&&(u.push([t.isFunction(a.options.items)?a.options.items.call(a.element[0],e,{item:this.currentItem}):t(a.options.items,a.element),a]),this.containers.push(a));for(i=u.length-1;i>=0;i--)for(o=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",o),c.push({item:h,instance:o,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,a;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),a=n.offset(),s.left=a.left,s.top=a.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)a=this.containers[i].element.offset(),this.containers[i].containerCache.left=a.left,this.containers[i].containerCache.top=a.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t(" ",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,a,o,r,h,l,c,u,d,p,f=null,m=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],m=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[m].containerCache.over||(this.containers[m]._trigger("over",s,this._uiHash(this)),this.containers[m].containerCache.over=1);else{for(o=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],a=this.items.length-1;a>=0;a--)t.contains(this.containers[m].element[0],this.items[a].item[0])&&this.items[a].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[a].top,this.items[a].height))&&(u=this.items[a].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[a][l]-c)&&(d=!0,u+=this.items[a][l]),o>Math.abs(u-c)&&(o=Math.abs(u-c),r=this.items[a],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[m])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[m].element,!0),this._trigger("change",s,this._uiHash()),this.containers[m]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[m],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[m]._trigger("over",s,this._uiHash(this)),this.containers[m].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,a=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():a?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():a?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,a=e.pageX,o=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(a=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(o=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((o-this.originalPageY)/n.grid[1])*n.grid[1],o=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((a-this.originalPageX)/n.grid[0])*n.grid[0],a=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:o-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:a-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){this.reverting=!1;var i,s=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(i in this._storedCSS)("auto"===this._storedCSS[i]||"static"===this._storedCSS[i])&&(this._storedCSS[i]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&s.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||s.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(s.push(function(t){this._trigger("remove",t,this._uiHash())}),s.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),s.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),i=this.containers.length-1;i>=0;i--)e||s.push(function(t){return function(e){t._trigger("deactivate",e,this._uiHash(this))}}.call(this,this.containers[i])),this.containers[i].containerCache.over&&(s.push(function(t){return function(e){t._trigger("out",e,this._uiHash(this))}}.call(this,this.containers[i])),this.containers[i].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),i=0;s.length>i;i++)s[i].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(i=0;s.length>i;i++)s[i].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery); + diff --git a/sortedm2m/static/sortedm2m/selector-search.gif b/sortedm2m/static/sortedm2m/selector-search.gif new file mode 100644 index 0000000000000000000000000000000000000000..6d5f4c74923af2daba13baa484364380fb1614b9 GIT binary patch literal 552 zcmZ?wbhEHb6krfwc*ekR;r}fg8=Jp>|9X0Qe){yu!NI}X+uO&-$I8koH8s`3!otqZ z?(W^YSy@?TW@e{PpT2zg^6J&ABO)T)-Q5!s60Tjl=Ire3>gqaW%9PsL+PQP*Ha9mX zCnrZoN9W|^#K*^f{rYv@ym@clym|8E$=&V_@LPJBVtE(?uxDXW;RaaLxbLLEYd;7Mww%N01*VNRUKY#wgg9n+J znUg0^j*E-y=;*j{51X2rQc_ZG-@bkS{(VPBM~0yT6o0ZXf?TTuB0zD%z~0)x z(A3=0+M*~cF2*k1)@;Hotjo#FYS$;lCc@9cB5K1e*(T4%&$OJ0N1L&YotI^~f{vr% zn$~qnssdI5qW0Tb4Ak_r)E!)0jSnm~~hECZvY5o%BVqvfb0K*)eZU6uP literal 0 HcmV?d00001 diff --git a/sortedm2m/static/sortedm2m/widget.css b/sortedm2m/static/sortedm2m/widget.css new file mode 100644 index 00000000..b19e2d63 --- /dev/null +++ b/sortedm2m/static/sortedm2m/widget.css @@ -0,0 +1,50 @@ +.sortedm2m-container { + margin-right: 10px; + width: 570px; +} + +.sortedm2m-container p.selector-filter { + width: 570px; + padding: 0; + margin: 0; +} + +.sortedm2m-container p.selector-filter input { + width: 532px; + margin: 5px 4px; +} + +ul.sortedm2m { + display: block; + width: 554px; + min-height: 200px; + max-height: 400px; + overflow-x: hidden; + overflow-y: auto; + margin: 0; + padding: 6px 8px; + list-style-type: none; + text-align: left; +} + +ul.sortedm2m li { + list-style-type: none; + text-align: left; + width: 550px; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre; +} + +ul.sortedm2m li, ul.sortedm2m label { + cursor: move; +} + +/* required to work properly in django admin */ +body.change-form .sortedm2m-container { + float: left; +} +.module ul.sortedm2m { + margin: 0; + padding: 6px 8px; +} diff --git a/sortedm2m/static/sortedm2m/widget.js b/sortedm2m/static/sortedm2m/widget.js new file mode 100644 index 00000000..ecb3daf6 --- /dev/null +++ b/sortedm2m/static/sortedm2m/widget.js @@ -0,0 +1,91 @@ +if (jQuery === undefined) { + jQuery = django.jQuery; +} + +(function ($) { + $(function () { + $('.sortedm2m').parents('ul').each(function () { + $(this).addClass('sortedm2m'); + var checkboxes = $(this).find('input[type=checkbox]'); + var id = checkboxes.first().attr('id').match(/^(.*)_\d+$/)[1]; + var name = checkboxes.first().attr('name'); + checkboxes.removeAttr('name'); + $(this).before(''); + var that = this; + var recalculate_value = function () { + var values = []; + $(that).find(':checked').each(function () { + values.push($(this).val()); + }); + $('#' + id).val(values.join(',')); + } + recalculate_value(); + checkboxes.change(recalculate_value); + $(this).sortable({ + axis: 'y', + //containment: 'parent', + update: recalculate_value + }); + }); + + $('.sortedm2m-container .selector-filter input').each(function () { + $(this).bind('input', function() { + var search = $(this).val().toLowerCase(); + var $el = $(this).closest('.selector-filter'); + var $container = $el.siblings('ul').each(function() { + // walk over each child list el and do name comparisons + $(this).children().each(function() { + var curr = $(this).find('label').text().toLowerCase(); + if (curr.indexOf(search) === -1) { + $(this).css('display', 'none'); + } else { + $(this).css('display', 'inherit'); + }; + }); + }); + }); + }); + + if (window.showAddAnotherPopup) { + var django_dismissAddAnotherPopup = window.dismissAddAnotherPopup; + window.dismissAddAnotherPopup = function (win, newId, newRepr) { + // newId and newRepr are expected to have previously been escaped by + // django.utils.html.escape. + newId = html_unescape(newId); + newRepr = html_unescape(newRepr); + var name = windowname_to_id(win.name); + var elem = $('#' + name); + var sortedm2m = elem.siblings('ul.sortedm2m'); + if (sortedm2m.length == 0) { + // no sortedm2m widget, fall back to django's default + // behaviour + return django_dismissAddAnotherPopup.apply(this, arguments); + } + + if (elem.val().length > 0) { + elem.val(elem.val() + ','); + } + elem.val(elem.val() + newId); + + var id_template = ''; + var maxid = 0; + sortedm2m.find('li input').each(function () { + var match = this.id.match(/^(.+)_(\d+)$/); + id_template = match[1]; + id = parseInt(match[2]); + if (id > maxid) maxid = id; + }); + + var id = id_template + '_' + (maxid + 1); + var new_li = $('
  • ').append( + $('
  • - + {# audience #} +
    + +
    {{ form.audience }} + {{ form.audience.errors }} +
    +
    {# country #}
    @@ -201,13 +213,6 @@ {{ form.speakers.errors }}
    - {# audience #} -
    - -
    {{ form.audience }} - {{ form.audience.errors }} -
    -
    {# logo #}
    diff --git a/templates/c_admin/forms/multilang.html b/templates/c_admin/forms/multilang.html index 161182fd..ced00c1d 100644 --- a/templates/c_admin/forms/multilang.html +++ b/templates/c_admin/forms/multilang.html @@ -1,4 +1,6 @@ +{% load widget_tweaks %} {% comment %} + Usage: used for creating multilanguage tabs Uses form, languages dictionary and field which will be in tabs @@ -29,11 +31,14 @@ {% ifequal f.name lang_field %}
    -
    {{f}}{# style for making content border #} +
    + {% if f.field.required %}{{ f|attr:"required" }}{% else %}{{ f }}{% endif %} {# style for making content border #} {{f.errors}}
    {% endifequal %} diff --git a/templates/client/article/news.html b/templates/client/article/news.html index c638478a..bc1438d3 100644 --- a/templates/client/article/news.html +++ b/templates/client/article/news.html @@ -53,26 +53,29 @@
    -
    -
    - -
    - {% for news in object.similar %} -
    -
    - - - {% include 'includes/show_logo.html' with obj=news %} - - -

    {{ news.main_title }}

    -

    {{ news.preview|safe }}

    - {{ news.publish_date|date:"d E Y" }}{% if news.get_event %}{{ news.get_event.name }}<{% endif %} + {% if object.get_similar %} +
    +
    + +
    + {% for news in object.get_similar %} +
    +
    + + + {% include 'includes/show_logo.html' with obj=news %} + + +

    {{ news.main_title }}

    +

    {{ news.preview|safe }}

    + {{ news.publish_date|date:"d E Y" }}{% if news.get_event %}{{ news.get_event.name }}<{% endif %} +
    +
    +
    + {% endfor %}
    -
    - {% endfor %}
    -
    -
    + {% endif %} + {% endblock %} diff --git a/templates/client/includes/conference/conference_partner.html b/templates/client/includes/conference/conference_partner.html index b97bbf44..1ddd444d 100644 --- a/templates/client/includes/conference/conference_partner.html +++ b/templates/client/includes/conference/conference_partner.html @@ -132,19 +132,24 @@
    -
    -

    {% trans 'Кому будет интересно' %}

    -
      - {% for audience in object.audience.all %} -
    • {{ audience.title }}
    • - {% endfor %} -
    -
    -
    -

    {% trans 'Стоимость участия' %}

    - -
    {{ object.min_price }} - {{ object.max_price }} {{ object.get_currency_html }}
    -
    + {% with object.audience.all as audiences %} + {% if audiences %} +
    +

    {% trans 'Кому будет интересно' %}

    +
      + {% for audience in audiences %} +
    • {{ audience.title }}
    • + {% endfor %} +
    +
    + {% endif %} + {% endwith %} + {% if object.min_price and object.max_price %} +
    +

    {% trans 'Стоимость участия' %}

    +
    {{ object.min_price }} - {{ object.max_price }} {{ object.get_currency_html }}
    +
    + {% endif %}

    diff --git a/templates/registration/acquire_email.html b/templates/registration/acquire_email.html index f65ac56b..65fb145f 100644 --- a/templates/registration/acquire_email.html +++ b/templates/registration/acquire_email.html @@ -1,16 +1,16 @@ {% extends "client/base_catalog.html" %} {% load i18n %} -{% block title %}{% trans "Email confirmation" %}{% endblock %} +{% block title %}{% trans "Подтверждение email" %}{% endblock %} {% block page_body %} -

    {% trans "Enter your email" %}

    +

    {% trans "Введите ваш email" %}

    {% if form.errors %} -

    {% trans "Please correct the errors below:" %}

    +

    {% trans "Пожалуйста исправьте ошибки:" %}

    {% endif %} {# {% url "social:complete" backend=backend %}#}
    {% csrf_token %} @@ -22,11 +22,12 @@
    {% endfor %} - + {% endblock %} {% block content-related %} -

    {% trans "Please enter your valid email, to activate account we will send you an email on this e-mail address" %}

    +

    {% trans "Пожалуйста введите ваш email. Мы вышлем на него письмо чтобы активировать аккаунт." %}

    +{#

    {% trans "Please enter your valid email, to activate account we will send you an email on this e-mail address" %}

    #} {% endblock %}