import pickle from django.db.models import Field, BLANK_CHOICE_DASH from django.utils.translation import ugettext_lazy, string_concat from django.utils.encoding import smart_unicode from django.utils.functional import curry class PickledManyToManyField(Field): def __init__(self, to, to_field=None, **kwargs): to_field = to_field or to._meta.pk.name kwargs['verbose_name'] = kwargs.get('verbose_name', None) self.my_rel = PickledManyToManyRel(to, to_field, num_in_admin=kwargs.pop('num_in_admin', 0), filter_interface=kwargs.pop('filter_interface', None), limit_choices_to=kwargs.pop('limit_choices_to', None), raw_id_admin=kwargs.pop('raw_id_admin', False)) if self.my_rel.raw_id_admin: kwargs.setdefault('validator_list', []).append(self.isValidIDList) super(PickledManyToManyField, self).__init__(**kwargs) if self.my_rel.raw_id_admin: msg = ugettext_lazy('Separate multiple IDs with commas.') else: msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') self.help_text = string_concat(self.help_text, ' ', msg) def get_attname(self): return '%s_%ss' % (self.name, self.my_rel.to._meta.pk.name) def set_attributes_from_rel(self): self.name = self.name or self.get_attname() self.verbose_name = self.verbose_name or self.my_rel.to._meta.verbose_name self.my_rel.field_name = self.my_rel.field_name or self.my_rel.to._meta.pk.name def contribute_to_class(self, cls, name): super(PickledManyToManyField, self).contribute_to_class(cls, name) self.set_attributes_from_rel() self.related_query_name = lambda self: opts.object_name.lower() setattr(cls, self.name, PickledManyRelatedObjectDescriptor(self)) standard_init = cls.__init__ def unpickling_init(slf, *args, **kwargs): standard_init(slf, *args, **kwargs) value = getattr(slf, self.attname) if not isinstance(value, set): value = pickle.loads(value) setattr(slf, self.attname, value) cls.__init__ = unpickling_init def db_type(self): return 'text' def to_python(self, value): return pickle.loads(value) def get_db_prep_save(self, value): return pickle.dumps(value) ############################################### # this is just copy-pasted from ManyToManyField def get_manipulator_field_objs(self): from django import oldforms if self.my_rel.raw_id_admin: return [oldforms.RawIdAdminField] else: choices = self.get_choices_default() return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] def get_choices_default(self): return self.get_choices(include_blank=False) def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): "Returns a list of tuples used as SelectField choices for this field." first_choice = include_blank and blank_choice or [] if self.choices: return first_choice + list(self.choices) rel_model = self.my_rel.to if hasattr(self.my_rel, 'get_related_field'): lst = [(getattr(x, self.my_rel.get_related_field().attname), smart_unicode(x)) for x in rel_model._default_manager.complex_filter(self.my_rel.limit_choices_to)] else: lst = [(x._get_pk_val(), smart_unicode(x)) for x in rel_model._default_manager.complex_filter(self.my_rel.limit_choices_to)] return first_choice + lst def isValidIDList(self, field_data, all_data): "Validates that the value is a valid list of foreign keys" mod = self.my_rel.to try: pks = map(int, field_data.split(',')) except ValueError: # the CommaSeparatedIntegerField validator will catch this error return objects = mod._default_manager.in_bulk(pks) if len(objects) != len(pks): badkeys = [k for k in pks if k not in objects] raise validators.ValidationError, ungettext("Please enter valid %(self)s IDs. The value %(value)r is invalid.", "Please enter valid %(self)s IDs. The values %(value)r are invalid.", len(badkeys)) % { 'self': self.verbose_name, 'value': len(badkeys) == 1 and badkeys[0] or tuple(badkeys), } # end of copy-past ################## class PickledManyRelatedObjectDescriptor(object): def __init__(self, field_with_rel): self.field = field_with_rel def __get__(self, instance, instance_type=None): if instance is None: raise AttributeError, "%s must be accessed via instance" % self.field.name self.field.renew_attfield(instance) val = getattr(instance, self.field.attname) rel_model = self.field.my_rel.to superclass = rel_model._default_manager.__class__ RelatedManager = create_pickled_many_related_manager(superclass) manager = RelatedManager( model=rel_model, core_filters={'%s__in' % self.field.my_rel.field_name: val}, instance=instance, attname=self.field.attname, ) return manager def __set__(self, instance, value): if instance is None: raise AttributeError, "%s must be accessed via instance" % self._field.name # Set the value of the related field val = set() for obj in value: try: val.add(getattr(obj, self.field.my_rel.get_related_field().attname)) except AttributeError: pass setattr(instance, self.field.attname, val) # Clear the cache, if it exists try: delattr(instance, self.field.get_cache_name()) except AttributeError: pass class PickledManyToManyRel(object): def __init__(self, to, field_name, num_in_admin=0, filter_interface=None, limit_choices_to=None, raw_id_admin=False): self.to = to self.field_name = field_name self.num_in_admin = num_in_admin self.filter_interface = filter_interface if limit_choices_to is None: limit_choices_to = {} self.limit_choices_to = limit_choices_to self.edit_inline = False self.raw_id_admin = raw_id_admin self.multiple = True self.related_name = '%s_%s_set' % (to._meta.pk.name, field_name) assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" # get_related_field = ManyToOneRel.get_related_field def get_related_field(self): "Returns the Field in the 'to' object to which this relationship is tied." return self.to._meta.get_field(self.field_name) def create_pickled_many_related_manager(superclass): class RelatedManager(superclass): def __init__(self, model=None, core_filters=None, instance=None, attname=None): super(RelatedManager, self).__init__() self.model = model self.core_filters = core_filters self.instance = instance self.attname = attname def get_query_set(self): return superclass.get_query_set(self).filter(**(self.core_filters)) def add(self, *objs): obj_pks = (obj.pk for obj in objs) setattr(self.instance, self.attname, getattr(self.instance, self.attname).union(obj_pks)) add.alters_data = True def remove(self, *objs): obj_pks = (obj.pk for obj in objs) setattr(self.instance, self.attname, getattr(self.instance, self.attname).difference(obj_pks)) remove.alters_data = True def clear(self): setattr(self.instance, self.attname, set()) clear.alters_data = True def create(self, **kwargs): new_obj = self.model(**kwargs) new_obj.save() self.add(new_obj) return new_obj create.alters_data = True return RelatedManager