django-sortedm2m-0.7.0/ 0000775 0001750 0001750 00000000000 12300742665 015475 5 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/sortedm2m/ 0000775 0001750 0001750 00000000000 12300742665 017411 5 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/sortedm2m/forms.py 0000664 0001750 0001750 00000007370 12266027344 021121 0 ustar gregor gregor 0000000 0000000 # -*- 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]
django-sortedm2m-0.7.0/sortedm2m/templates/ 0000775 0001750 0001750 00000000000 12300742665 021407 5 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/sortedm2m/templates/sortedm2m/ 0000775 0001750 0001750 00000000000 12300742665 023323 5 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/sortedm2m/templates/sortedm2m/sorted_checkbox_select_multiple_widget.html 0000664 0001750 0001750 00000001323 12266026175 034175 0 ustar gregor gregor 0000000 0000000 {% load i18n static %}
{% for row in selected %}
{% endfor %}
{% for row in unselected %}
{% endfor %}
{% trans "Choose items and order by drag & drop." %}
django-sortedm2m-0.7.0/sortedm2m/__init__.py 0000664 0001750 0001750 00000000060 12300740105 021501 0 ustar gregor gregor 0000000 0000000 # -*- coding: utf-8 -*-
__version__ = '0.7.0'
django-sortedm2m-0.7.0/sortedm2m/static/ 0000775 0001750 0001750 00000000000 12300742665 020700 5 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/sortedm2m/static/sortedm2m/ 0000775 0001750 0001750 00000000000 12300742665 022614 5 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/sortedm2m/static/sortedm2m/widget.css 0000664 0001750 0001750 00000001551 12171044714 024607 0 ustar gregor gregor 0000000 0000000 .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;
}
django-sortedm2m-0.7.0/sortedm2m/static/sortedm2m/widget.js 0000664 0001750 0001750 00000007250 12171037064 024435 0 ustar gregor gregor 0000000 0000000 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(
$('').attr('for', id).append(
$('').attr('id', id).val(newId)
).append($('').text(' ' + newRepr))
);
sortedm2m.append(new_li);
win.close();
};
}
});
})(jQuery);
django-sortedm2m-0.7.0/sortedm2m/static/sortedm2m/selector-search.gif 0000664 0001750 0001750 00000001050 12171037064 026356 0 ustar gregor gregor 0000000 0000000 GIF89a <<>>jjj666ӫXXXGGG```CCCEEE}}}ccc[[[lll___乹SSSԪNNNfffŚUUU{{{ZZZ~~~???|||iii^^^ႂdddAAA ! , !4- ><+
,A1"%:?0&.*'@DE3B92;#7C= F $
8
(5/6)
; django-sortedm2m-0.7.0/sortedm2m/models.py 0000664 0001750 0001750 00000000000 12113216106 021220 0 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/sortedm2m/fields.py 0000664 0001750 0001750 00000037375 12300737603 021244 0 ustar gregor gregor 0000000 0000000 # -*- 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
django-sortedm2m-0.7.0/MANIFEST.in 0000664 0001750 0001750 00000000260 12171045725 017230 0 ustar gregor gregor 0000000 0000000 include CHANGES.rst
include LICENSE.txt
include MANIFEST.in
include README.rst
include AUTHORS.rst
recursive-include sortedm2m/static *
recursive-include sortedm2m/templates *
django-sortedm2m-0.7.0/django_sortedm2m.egg-info/ 0000775 0001750 0001750 00000000000 12300742665 022425 5 ustar gregor gregor 0000000 0000000 django-sortedm2m-0.7.0/django_sortedm2m.egg-info/SOURCES.txt 0000664 0001750 0001750 00000001107 12300742661 024304 0 ustar gregor gregor 0000000 0000000 AUTHORS.rst
CHANGES.rst
LICENSE.txt
MANIFEST.in
README.rst
setup.py
django_sortedm2m.egg-info/PKG-INFO
django_sortedm2m.egg-info/SOURCES.txt
django_sortedm2m.egg-info/dependency_links.txt
django_sortedm2m.egg-info/not-zip-safe
django_sortedm2m.egg-info/top_level.txt
sortedm2m/__init__.py
sortedm2m/fields.py
sortedm2m/forms.py
sortedm2m/models.py
sortedm2m/static/sortedm2m/jquery-ui.js
sortedm2m/static/sortedm2m/selector-search.gif
sortedm2m/static/sortedm2m/widget.css
sortedm2m/static/sortedm2m/widget.js
sortedm2m/templates/sortedm2m/sorted_checkbox_select_multiple_widget.html django-sortedm2m-0.7.0/django_sortedm2m.egg-info/not-zip-safe 0000664 0001750 0001750 00000000001 12113226133 024640 0 ustar gregor gregor 0000000 0000000
django-sortedm2m-0.7.0/django_sortedm2m.egg-info/top_level.txt 0000664 0001750 0001750 00000000012 12300742655 025147 0 ustar gregor gregor 0000000 0000000 sortedm2m
django-sortedm2m-0.7.0/django_sortedm2m.egg-info/PKG-INFO 0000664 0001750 0001750 00000021715 12300742655 023527 0 ustar gregor gregor 0000000 0000000 Metadata-Version: 1.1
Name: django-sortedm2m
Version: 0.7.0
Summary: Drop-in replacement for django's many to many field with sorted relations.
Home-page: http://github.com/gregmuellegger/django-sortedm2m
Author: Gregor Müllegger
Author-email: gregor@muellegger.de
License: BSD
Description: ================
django-sortedm2m
================
.. image:: https://travis-ci.org/gregmuellegger/django-sortedm2m.png
:alt: Build Status
:target: https://travis-ci.org/gregmuellegger/django-sortedm2m
``sortedm2m`` is a drop-in replacement for django's own ``ManyToManyField``.
The provided ``SortedManyToManyField`` behaves like the original one but
remembers the order of added relations.
Usecases
========
Imagine that you have a gallery model and a photo model. Usually you want a
relation between these models so you can add multiple photos to one gallery
but also want to be able to have the same photo on many galleries.
This is where you usually can use many to many relation. The downside is that
django's default implementation doesn't provide a way to order the photos in
the gallery. So you only have a random ordering which is not suitable in most
cases.
You can work around this limitation by using the ``SortedManyToManyField``
provided by this package as drop in replacement for django's
``ManyToManyField``.
Usage
=====
Use ``SortedManyToManyField`` like ``ManyToManyField`` in your models::
from django.db import models
from sortedm2m.fields import SortedManyToManyField
class Photo(models.Model):
name = models.CharField(max_length=50)
image = models.ImageField(upload_to='...')
class Gallery(models.Model):
name = models.CharField(max_length=50)
photos = SortedManyToManyField(Photo)
If you use the relation in your code like the following, it will remember the
order in which you have added photos to the gallery. ::
gallery = Gallery.objects.create(name='Photos ordered by name')
for photo in Photo.objects.order_by('name'):
gallery.photos.add(photo)
``SortedManyToManyField``
-------------------------
You can use the following arguments to modify the default behavior:
``sorted``
~~~~~~~~~~
**Default:** ``True``
You can set the ``sorted`` to ``False`` which will force the
``SortedManyToManyField`` in behaving like Django's original
``ManyToManyField``. No ordering will be performed on relation nor will the
intermediate table have a database field for storing ordering information.
``sort_value_field_name``
~~~~~~~~~~~~~~~~~~~~~~~~~
**Default:** ``'sort_value'``
Specifies how the field is called in the intermediate database table by which
the relationship is ordered. You can change its name if you have a legacy
database that you need to integrate into your application.
Admin
=====
``SortedManyToManyField`` provides a custom widget which can be used to sort
the selected items. It renders a list of checkboxes that can be sorted by
drag'n'drop.
To use the widget in the admin you need to add ``sortedm2m`` to your
INSTALLED_APPS settings, like::
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'sortedm2m',
'...',
)
Otherwise it will not find the css and js files needed to sort by drag'n'drop.
Finally, make sure *not* to have the model listed in any ``filter_horizontal``
or ``filter_vertical`` tuples inside of your ``ModelAdmin`` definitions.
If you did it right, you'll wind up with something like this:
.. image:: http://i.imgur.com/HjIW7MI.jpg
It's also possible to use the ``SortedManyToManyField`` with admin's
``raw_id_fields`` option in the ``ModelAdmin`` definition. Add the name of the
``SortedManyToManyField`` to this list to get a simple text input field. The
order in which the ids are entered into the input box is used to sort the
items of the sorted m2m relation.
Example::
from django.contrib import admin
class GalleryAdmin(admin.ModelAdmin):
raw_id_fields = ('photos',)
Contribute
==========
You can find the latest development version on github_. Get there and fork it,
file bugs or send me nice wishes.
Feel free to drop me a message about critique or feature requests. You can get
in touch with me by mail_ or twitter_.
.. _github: http://github.com/gregmuellegger/django-sortedm2m
.. _mail: mailto:gregor@muellegger.de
.. _twitter: http://twitter.com/gregmuellegger
Changelog
=========
0.7.0
-----
* Adding support for ``prefetch_related()``. Thanks to Marcin Ossowski for
the idea and patch.
0.6.1
-----
* Correct escaping of *for* attribute in label for the sortedm2m widget. Thanks
to Mystic-Mirage for the report and fix.
0.6.0
-----
* Python 3 support!
* Better widget. Thanks to Mike Knoop for the initial patch.
0.5.0
-----
* Django 1.5 support. Thanks to Antti Kaihola for the patches.
* Dropping Django 1.3 support. Please use django-sortedm2m<0.5 if you need to
use Django 1.3.
* Adding support for a ``sort_value_field_name`` argument in
``SortedManyToManyField``. Thanks to Trey Hunner for the idea.
0.4.0
-----
* Django 1.4 support. Thanks to Flavio Curella for the patch.
* south support is only enabled if south is actually in your INSTALLED_APPS
setting. Thanks to tcmb for the report and Florian Ilgenfritz for the patch.
0.3.3
-----
* South support (via monkeypatching, but anyway... it's there!). Thanks to
Chris Church for the patch. South migrations won't pick up a changed
``sorted`` argument though.
0.3.2
-----
* Use already included jQuery version in global scope and don't override with
django's version. Thank you to Hendrik van der Linde for reporting this
issue.
0.3.1
-----
* Fixed packaging error.
0.3.0
-----
* Heavy internal refactorings. These were necessary to solve a problem with
``SortedManyToManyField`` and a reference to ``'self'``.
0.2.5
-----
* Forgot to exclude debug print/console.log statements from code. Sorry.
0.2.4
-----
* Fixing problems with ``SortedCheckboxSelectMultiple`` widget, especially in
admin where a "create and add another item" popup is available.
0.2.3
-----
* Fixing issue with primary keys instead of model instances for ``.add()`` and
``.remove()`` methods in ``SortedRelatedManager``.
0.2.2
-----
* Fixing validation error for ``SortedCheckboxSelectMultiple``. It caused
errors if only one value was passed.
0.2.1
-----
* Removed unnecessary reference of jquery ui css file in
``SortedCheckboxSelectMultiple``. Thanks to Klaas van Schelven and Yuwei Yu
for the hint.
0.2.0
-----
* Added a widget for use in admin.
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
django-sortedm2m-0.7.0/django_sortedm2m.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 12300742655 026472 0 ustar gregor gregor 0000000 0000000
django-sortedm2m-0.7.0/setup.cfg 0000664 0001750 0001750 00000000073 12300742665 017316 0 ustar gregor gregor 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
django-sortedm2m-0.7.0/CHANGES.rst 0000664 0001750 0001750 00000004203 12300740130 017257 0 ustar gregor gregor 0000000 0000000 Changelog
=========
0.7.0
-----
* Adding support for ``prefetch_related()``. Thanks to Marcin Ossowski for
the idea and patch.
0.6.1
-----
* Correct escaping of *for* attribute in label for the sortedm2m widget. Thanks
to Mystic-Mirage for the report and fix.
0.6.0
-----
* Python 3 support!
* Better widget. Thanks to Mike Knoop for the initial patch.
0.5.0
-----
* Django 1.5 support. Thanks to Antti Kaihola for the patches.
* Dropping Django 1.3 support. Please use django-sortedm2m<0.5 if you need to
use Django 1.3.
* Adding support for a ``sort_value_field_name`` argument in
``SortedManyToManyField``. Thanks to Trey Hunner for the idea.
0.4.0
-----
* Django 1.4 support. Thanks to Flavio Curella for the patch.
* south support is only enabled if south is actually in your INSTALLED_APPS
setting. Thanks to tcmb for the report and Florian Ilgenfritz for the patch.
0.3.3
-----
* South support (via monkeypatching, but anyway... it's there!). Thanks to
Chris Church for the patch. South migrations won't pick up a changed
``sorted`` argument though.
0.3.2
-----
* Use already included jQuery version in global scope and don't override with
django's version. Thank you to Hendrik van der Linde for reporting this
issue.
0.3.1
-----
* Fixed packaging error.
0.3.0
-----
* Heavy internal refactorings. These were necessary to solve a problem with
``SortedManyToManyField`` and a reference to ``'self'``.
0.2.5
-----
* Forgot to exclude debug print/console.log statements from code. Sorry.
0.2.4
-----
* Fixing problems with ``SortedCheckboxSelectMultiple`` widget, especially in
admin where a "create and add another item" popup is available.
0.2.3
-----
* Fixing issue with primary keys instead of model instances for ``.add()`` and
``.remove()`` methods in ``SortedRelatedManager``.
0.2.2
-----
* Fixing validation error for ``SortedCheckboxSelectMultiple``. It caused
errors if only one value was passed.
0.2.1
-----
* Removed unnecessary reference of jquery ui css file in
``SortedCheckboxSelectMultiple``. Thanks to Klaas van Schelven and Yuwei Yu
for the hint.
0.2.0
-----
* Added a widget for use in admin.
django-sortedm2m-0.7.0/LICENSE.txt 0000664 0001750 0001750 00000002764 11453164213 017324 0 ustar gregor gregor 0000000 0000000 Copyright (c) 2010, Gregor Müllegger
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
django-sortedm2m-0.7.0/AUTHORS.rst 0000664 0001750 0001750 00000000326 12300740126 017343 0 ustar gregor gregor 0000000 0000000 Author
------
* Gregor Müllegger
Contributors
------------
* Chris Church
* jonny5532
* Sean O'Connor
* Flavio Curella
* Florian Ilgenfritz
* Antti Kaihola
* Mike Knoop
* Marcin Ossowski
django-sortedm2m-0.7.0/setup.py 0000775 0001750 0001750 00000004532 12171045415 017211 0 ustar gregor gregor 0000000 0000000 #!/usr/bin/env python
# -*- coding: utf-8 -*-
import codecs
import os
import re
import sys
from setuptools import setup
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
if sys.version_info[0] < 3:
def read(*parts):
return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read()
else:
def read(*parts):
return open(os.path.join(os.path.dirname(__file__), *parts), 'r').read()
class UltraMagicString(object):
'''
Taken from
http://stackoverflow.com/questions/1162338/whats-the-right-way-to-use-unicode-metadata-in-setup-py
'''
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
def __unicode__(self):
return self.value.decode('UTF-8')
def __add__(self, other):
return UltraMagicString(self.value + str(other))
def split(self, *args, **kw):
return self.value.split(*args, **kw)
long_description = UltraMagicString('\n\n'.join((
read('README.rst'),
read('CHANGES.rst'),
)))
setup(
name = 'django-sortedm2m',
version = find_version('sortedm2m', '__init__.py'),
url = 'http://github.com/gregmuellegger/django-sortedm2m',
license = 'BSD',
description =
'Drop-in replacement for django\'s many to many field with '
'sorted relations.',
long_description = long_description,
author = UltraMagicString('Gregor Müllegger'),
author_email = 'gregor@muellegger.de',
packages = ['sortedm2m'],
include_package_data = True,
zip_safe = False,
classifiers = [
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
],
install_requires = [],
)
django-sortedm2m-0.7.0/README.rst 0000664 0001750 0001750 00000010272 12171046511 017157 0 ustar gregor gregor 0000000 0000000 ================
django-sortedm2m
================
.. image:: https://travis-ci.org/gregmuellegger/django-sortedm2m.png
:alt: Build Status
:target: https://travis-ci.org/gregmuellegger/django-sortedm2m
``sortedm2m`` is a drop-in replacement for django's own ``ManyToManyField``.
The provided ``SortedManyToManyField`` behaves like the original one but
remembers the order of added relations.
Usecases
========
Imagine that you have a gallery model and a photo model. Usually you want a
relation between these models so you can add multiple photos to one gallery
but also want to be able to have the same photo on many galleries.
This is where you usually can use many to many relation. The downside is that
django's default implementation doesn't provide a way to order the photos in
the gallery. So you only have a random ordering which is not suitable in most
cases.
You can work around this limitation by using the ``SortedManyToManyField``
provided by this package as drop in replacement for django's
``ManyToManyField``.
Usage
=====
Use ``SortedManyToManyField`` like ``ManyToManyField`` in your models::
from django.db import models
from sortedm2m.fields import SortedManyToManyField
class Photo(models.Model):
name = models.CharField(max_length=50)
image = models.ImageField(upload_to='...')
class Gallery(models.Model):
name = models.CharField(max_length=50)
photos = SortedManyToManyField(Photo)
If you use the relation in your code like the following, it will remember the
order in which you have added photos to the gallery. ::
gallery = Gallery.objects.create(name='Photos ordered by name')
for photo in Photo.objects.order_by('name'):
gallery.photos.add(photo)
``SortedManyToManyField``
-------------------------
You can use the following arguments to modify the default behavior:
``sorted``
~~~~~~~~~~
**Default:** ``True``
You can set the ``sorted`` to ``False`` which will force the
``SortedManyToManyField`` in behaving like Django's original
``ManyToManyField``. No ordering will be performed on relation nor will the
intermediate table have a database field for storing ordering information.
``sort_value_field_name``
~~~~~~~~~~~~~~~~~~~~~~~~~
**Default:** ``'sort_value'``
Specifies how the field is called in the intermediate database table by which
the relationship is ordered. You can change its name if you have a legacy
database that you need to integrate into your application.
Admin
=====
``SortedManyToManyField`` provides a custom widget which can be used to sort
the selected items. It renders a list of checkboxes that can be sorted by
drag'n'drop.
To use the widget in the admin you need to add ``sortedm2m`` to your
INSTALLED_APPS settings, like::
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'sortedm2m',
'...',
)
Otherwise it will not find the css and js files needed to sort by drag'n'drop.
Finally, make sure *not* to have the model listed in any ``filter_horizontal``
or ``filter_vertical`` tuples inside of your ``ModelAdmin`` definitions.
If you did it right, you'll wind up with something like this:
.. image:: http://i.imgur.com/HjIW7MI.jpg
It's also possible to use the ``SortedManyToManyField`` with admin's
``raw_id_fields`` option in the ``ModelAdmin`` definition. Add the name of the
``SortedManyToManyField`` to this list to get a simple text input field. The
order in which the ids are entered into the input box is used to sort the
items of the sorted m2m relation.
Example::
from django.contrib import admin
class GalleryAdmin(admin.ModelAdmin):
raw_id_fields = ('photos',)
Contribute
==========
You can find the latest development version on github_. Get there and fork it,
file bugs or send me nice wishes.
Feel free to drop me a message about critique or feature requests. You can get
in touch with me by mail_ or twitter_.
.. _github: http://github.com/gregmuellegger/django-sortedm2m
.. _mail: mailto:gregor@muellegger.de
.. _twitter: http://twitter.com/gregmuellegger
django-sortedm2m-0.7.0/PKG-INFO 0000664 0001750 0001750 00000021715 12300742665 016600 0 ustar gregor gregor 0000000 0000000 Metadata-Version: 1.1
Name: django-sortedm2m
Version: 0.7.0
Summary: Drop-in replacement for django's many to many field with sorted relations.
Home-page: http://github.com/gregmuellegger/django-sortedm2m
Author: Gregor Müllegger
Author-email: gregor@muellegger.de
License: BSD
Description: ================
django-sortedm2m
================
.. image:: https://travis-ci.org/gregmuellegger/django-sortedm2m.png
:alt: Build Status
:target: https://travis-ci.org/gregmuellegger/django-sortedm2m
``sortedm2m`` is a drop-in replacement for django's own ``ManyToManyField``.
The provided ``SortedManyToManyField`` behaves like the original one but
remembers the order of added relations.
Usecases
========
Imagine that you have a gallery model and a photo model. Usually you want a
relation between these models so you can add multiple photos to one gallery
but also want to be able to have the same photo on many galleries.
This is where you usually can use many to many relation. The downside is that
django's default implementation doesn't provide a way to order the photos in
the gallery. So you only have a random ordering which is not suitable in most
cases.
You can work around this limitation by using the ``SortedManyToManyField``
provided by this package as drop in replacement for django's
``ManyToManyField``.
Usage
=====
Use ``SortedManyToManyField`` like ``ManyToManyField`` in your models::
from django.db import models
from sortedm2m.fields import SortedManyToManyField
class Photo(models.Model):
name = models.CharField(max_length=50)
image = models.ImageField(upload_to='...')
class Gallery(models.Model):
name = models.CharField(max_length=50)
photos = SortedManyToManyField(Photo)
If you use the relation in your code like the following, it will remember the
order in which you have added photos to the gallery. ::
gallery = Gallery.objects.create(name='Photos ordered by name')
for photo in Photo.objects.order_by('name'):
gallery.photos.add(photo)
``SortedManyToManyField``
-------------------------
You can use the following arguments to modify the default behavior:
``sorted``
~~~~~~~~~~
**Default:** ``True``
You can set the ``sorted`` to ``False`` which will force the
``SortedManyToManyField`` in behaving like Django's original
``ManyToManyField``. No ordering will be performed on relation nor will the
intermediate table have a database field for storing ordering information.
``sort_value_field_name``
~~~~~~~~~~~~~~~~~~~~~~~~~
**Default:** ``'sort_value'``
Specifies how the field is called in the intermediate database table by which
the relationship is ordered. You can change its name if you have a legacy
database that you need to integrate into your application.
Admin
=====
``SortedManyToManyField`` provides a custom widget which can be used to sort
the selected items. It renders a list of checkboxes that can be sorted by
drag'n'drop.
To use the widget in the admin you need to add ``sortedm2m`` to your
INSTALLED_APPS settings, like::
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'sortedm2m',
'...',
)
Otherwise it will not find the css and js files needed to sort by drag'n'drop.
Finally, make sure *not* to have the model listed in any ``filter_horizontal``
or ``filter_vertical`` tuples inside of your ``ModelAdmin`` definitions.
If you did it right, you'll wind up with something like this:
.. image:: http://i.imgur.com/HjIW7MI.jpg
It's also possible to use the ``SortedManyToManyField`` with admin's
``raw_id_fields`` option in the ``ModelAdmin`` definition. Add the name of the
``SortedManyToManyField`` to this list to get a simple text input field. The
order in which the ids are entered into the input box is used to sort the
items of the sorted m2m relation.
Example::
from django.contrib import admin
class GalleryAdmin(admin.ModelAdmin):
raw_id_fields = ('photos',)
Contribute
==========
You can find the latest development version on github_. Get there and fork it,
file bugs or send me nice wishes.
Feel free to drop me a message about critique or feature requests. You can get
in touch with me by mail_ or twitter_.
.. _github: http://github.com/gregmuellegger/django-sortedm2m
.. _mail: mailto:gregor@muellegger.de
.. _twitter: http://twitter.com/gregmuellegger
Changelog
=========
0.7.0
-----
* Adding support for ``prefetch_related()``. Thanks to Marcin Ossowski for
the idea and patch.
0.6.1
-----
* Correct escaping of *for* attribute in label for the sortedm2m widget. Thanks
to Mystic-Mirage for the report and fix.
0.6.0
-----
* Python 3 support!
* Better widget. Thanks to Mike Knoop for the initial patch.
0.5.0
-----
* Django 1.5 support. Thanks to Antti Kaihola for the patches.
* Dropping Django 1.3 support. Please use django-sortedm2m<0.5 if you need to
use Django 1.3.
* Adding support for a ``sort_value_field_name`` argument in
``SortedManyToManyField``. Thanks to Trey Hunner for the idea.
0.4.0
-----
* Django 1.4 support. Thanks to Flavio Curella for the patch.
* south support is only enabled if south is actually in your INSTALLED_APPS
setting. Thanks to tcmb for the report and Florian Ilgenfritz for the patch.
0.3.3
-----
* South support (via monkeypatching, but anyway... it's there!). Thanks to
Chris Church for the patch. South migrations won't pick up a changed
``sorted`` argument though.
0.3.2
-----
* Use already included jQuery version in global scope and don't override with
django's version. Thank you to Hendrik van der Linde for reporting this
issue.
0.3.1
-----
* Fixed packaging error.
0.3.0
-----
* Heavy internal refactorings. These were necessary to solve a problem with
``SortedManyToManyField`` and a reference to ``'self'``.
0.2.5
-----
* Forgot to exclude debug print/console.log statements from code. Sorry.
0.2.4
-----
* Fixing problems with ``SortedCheckboxSelectMultiple`` widget, especially in
admin where a "create and add another item" popup is available.
0.2.3
-----
* Fixing issue with primary keys instead of model instances for ``.add()`` and
``.remove()`` methods in ``SortedRelatedManager``.
0.2.2
-----
* Fixing validation error for ``SortedCheckboxSelectMultiple``. It caused
errors if only one value was passed.
0.2.1
-----
* Removed unnecessary reference of jquery ui css file in
``SortedCheckboxSelectMultiple``. Thanks to Klaas van Schelven and Yuwei Yu
for the hint.
0.2.0
-----
* Added a widget for use in admin.
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3