web/hdabo/fields.py
changeset 104 28a2c02ef6c8
parent 103 f61dee6527e5
child 105 2adad7698aa5
equal deleted inserted replaced
103:f61dee6527e5 104:28a2c02ef6c8
     1 # -*- coding: utf-8 -*-
       
     2 from django.db import models, router
       
     3 from django.db.models import signals
       
     4 from django.db.models.fields.related import (create_many_related_manager, 
       
     5     ManyToManyField, ReverseManyRelatedObjectsDescriptor)
       
     6 from hdabo.forms import SortedMultipleChoiceField
       
     7 from hdabo.utils import OrderedSet
       
     8 
       
     9 
       
    10 SORT_VALUE_FIELD_NAME = 'sort_value'
       
    11 
       
    12 
       
    13 def create_sorted_many_related_manager(superclass, rel):
       
    14     RelatedManager = create_many_related_manager(superclass, rel)
       
    15 
       
    16     class SortedRelatedManager(RelatedManager):
       
    17         def get_query_set(self):
       
    18             # We use ``extra`` method here because we have no other access to
       
    19             # the extra sorting field of the intermediary model. The fields
       
    20             # are hidden for joins because we set ``auto_created`` on the
       
    21             # intermediary's meta options.
       
    22             return super(SortedRelatedManager, self).\
       
    23                 get_query_set().\
       
    24                 order_by('%s.%s' % (
       
    25                     rel.through._meta.db_table,
       
    26                     rel.through._sort_field_name,))
       
    27 
       
    28         def _add_items(self, source_field_name, target_field_name, *objs):
       
    29             # join_table: name of the m2m link table
       
    30             # source_field_name: the PK fieldname in join_table for the source object
       
    31             # target_field_name: the PK fieldname in join_table for the target object
       
    32             # *objs - objects to add. Either object instances, or primary keys of object instances.
       
    33 
       
    34             # If there aren't any objects, there is nothing to do.
       
    35             from django.db.models import Model
       
    36             if objs:
       
    37                 count = self.through._default_manager.count
       
    38                 new_ids = OrderedSet()
       
    39                 for obj in objs:
       
    40                     if isinstance(obj, self.model):
       
    41                         if not router.allow_relation(obj, self.instance):
       
    42                             raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % 
       
    43                                                (obj, self.instance._state.db, obj._state.db))
       
    44                         new_ids.add(obj.pk)
       
    45                     elif isinstance(obj, Model):
       
    46                         raise TypeError("'%s' instance expected" % self.model._meta.object_name)
       
    47                     else:
       
    48                         new_ids.add(obj)
       
    49                 db = router.db_for_write(self.through, instance=self.instance)
       
    50                 vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
       
    51                 vals = vals.filter(**{
       
    52                     source_field_name: self._pk_val,
       
    53                     '%s__in' % target_field_name: new_ids,
       
    54                 })
       
    55                 new_ids = new_ids - OrderedSet(vals)
       
    56 
       
    57                 if self.reverse or source_field_name == self.source_field_name:
       
    58                     # Don't send the signal when we are inserting the
       
    59                     # duplicate data row for symmetrical reverse entries.
       
    60                     signals.m2m_changed.send(sender=rel.through, action='pre_add',
       
    61                         instance=self.instance, reverse=self.reverse,
       
    62                         model=self.model, pk_set=new_ids, using=db)
       
    63                 # Add the ones that aren't there already
       
    64                 count = self.through._default_manager.count
       
    65                 for obj_id in new_ids:
       
    66                     self.through._default_manager.using(db).create(**{
       
    67                         '%s_id' % source_field_name: self._pk_val,
       
    68                         '%s_id' % target_field_name: obj_id,
       
    69                         self.through._sort_field_name: count(),
       
    70                     })
       
    71                 if self.reverse or source_field_name == self.source_field_name:
       
    72                     # Don't send the signal when we are inserting the
       
    73                     # duplicate data row for symmetrical reverse entries.
       
    74                     signals.m2m_changed.send(sender=rel.through, action='post_add',
       
    75                         instance=self.instance, reverse=self.reverse,
       
    76                         model=self.model, pk_set=new_ids, using=db)
       
    77 
       
    78 
       
    79     return SortedRelatedManager
       
    80 
       
    81 
       
    82 class ReverseSortedManyRelatedObjectsDescriptor(ReverseManyRelatedObjectsDescriptor):
       
    83     def __get__(self, instance, instance_type=None):
       
    84         if instance is None:
       
    85             return self
       
    86 
       
    87         # Dynamically create a class that subclasses the related
       
    88         # model's default manager.
       
    89         rel_model = self.field.rel.to
       
    90         superclass = rel_model._default_manager.__class__
       
    91         RelatedManager = create_sorted_many_related_manager(superclass, self.field.rel)
       
    92 
       
    93         manager = RelatedManager(
       
    94             model=rel_model,
       
    95             core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
       
    96             instance=instance,
       
    97             symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
       
    98             source_field_name=self.field.m2m_field_name(),
       
    99             target_field_name=self.field.m2m_reverse_field_name(),
       
   100             reverse=False
       
   101         )
       
   102 
       
   103         return manager
       
   104 
       
   105     def __set__(self, instance, value):
       
   106         if instance is None:
       
   107             raise AttributeError, "Manager must be accessed via instance"
       
   108 
       
   109         manager = self.__get__(instance)
       
   110         manager.clear()
       
   111         manager.add(*value)
       
   112 
       
   113 
       
   114 class SortedManyToManyField(ManyToManyField):
       
   115     '''
       
   116     Providing a many to many relation that remembers the order of related
       
   117     objects.
       
   118 
       
   119     Accept a boolean ``sorted`` attribute which specifies if relation is
       
   120     ordered or not. Default is set to ``True``. If ``sorted`` is set to
       
   121     ``False`` the field will behave exactly like django's ``ManyToManyField``.
       
   122     '''
       
   123     def __init__(self, to, sorted=True, **kwargs):
       
   124         self.sorted = sorted
       
   125         if self.sorted:
       
   126             # This is very hacky and should be removed if a better solution is
       
   127             # found.
       
   128             kwargs.setdefault('through', True)
       
   129         super(SortedManyToManyField, self).__init__(to, **kwargs)
       
   130         self.help_text = kwargs.get('help_text', None)
       
   131 
       
   132     def create_intermediary_model(self, cls, field_name):
       
   133         '''
       
   134         Create intermediary model that stores the relation's data.
       
   135         '''
       
   136         module = ''
       
   137 
       
   138         # make sure rel.to is a model class and not a string
       
   139         if isinstance(self.rel.to, basestring):
       
   140             bits = self.rel.to.split('.')
       
   141             if len(bits) == 1:
       
   142                 bits = cls._meta.app_label.lower(), bits[0]
       
   143             self.rel.to = models.get_model(*bits)
       
   144 
       
   145         model_name = '%s_%s_%s' % (
       
   146             cls._meta.app_label,
       
   147             cls._meta.object_name,
       
   148             field_name)
       
   149         from_ = '%s.%s' % (
       
   150             cls._meta.app_label,
       
   151             cls._meta.object_name)
       
   152 
       
   153         def default_sort_value():
       
   154             model = models.get_model(cls._meta.app_label, model_name)
       
   155             return model._default_manager.count()
       
   156 
       
   157         # Using from and to model's name as field names for relations. This is
       
   158         # also django default behaviour for m2m intermediary tables.
       
   159         fields = {
       
   160             cls._meta.object_name.lower():
       
   161                 models.ForeignKey(from_),
       
   162             # using to model's name as field name for the other relation
       
   163             self.rel.to._meta.object_name.lower():
       
   164                 models.ForeignKey(self.rel.to),
       
   165             SORT_VALUE_FIELD_NAME:
       
   166                 models.IntegerField(default=default_sort_value),
       
   167         }
       
   168 
       
   169         class Meta:
       
   170             db_table = '%s_%s_%s' % (
       
   171                 cls._meta.app_label.lower(),
       
   172                 cls._meta.object_name.lower(),
       
   173                 field_name.lower())
       
   174             app_label = cls._meta.app_label
       
   175             ordering = (SORT_VALUE_FIELD_NAME,)
       
   176             auto_created = cls
       
   177 
       
   178         attrs = {
       
   179             '__module__': module,
       
   180             'Meta': Meta,
       
   181             '_sort_field_name': SORT_VALUE_FIELD_NAME,
       
   182             '__unicode__': lambda s: 'pk=%d' % s.pk,
       
   183         }
       
   184 
       
   185         # Add in any fields that were provided
       
   186         if fields:
       
   187             attrs.update(fields)
       
   188 
       
   189         # Create the class, which automatically triggers ModelBase processing
       
   190         model = type(model_name, (models.Model,), attrs)
       
   191 
       
   192         return model
       
   193 
       
   194     def contribute_to_class(self, cls, name):
       
   195         if self.sorted:
       
   196             self.rel.through = self.create_intermediary_model(cls, name)
       
   197             super(SortedManyToManyField, self).contribute_to_class(cls, name)
       
   198             # overwrite default descriptor with reverse and sorted one
       
   199             setattr(cls, self.name, ReverseSortedManyRelatedObjectsDescriptor(self))
       
   200         else:
       
   201             super(SortedManyToManyField, self).contribute_to_class(cls, name)
       
   202 
       
   203     def formfield(self, **kwargs):
       
   204         defaults = {}
       
   205         if self.sorted:
       
   206             defaults['form_class'] = SortedMultipleChoiceField
       
   207         defaults.update(kwargs)
       
   208         return super(SortedManyToManyField, self).formfield(**defaults)