fields.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. from __future__ import division, unicode_literals
  2. from future.builtins import str
  3. from copy import copy
  4. from django.contrib.contenttypes.fields import GenericRelation
  5. from django.core.exceptions import ImproperlyConfigured, AppRegistryNotReady
  6. from django.db.models import IntegerField, CharField, FloatField
  7. from django.db.models.signals import post_save, post_delete
  8. class BaseGenericRelation(GenericRelation):
  9. """
  10. Extends ``GenericRelation`` to:
  11. - Add a consistent default value for ``object_id_field`` and
  12. check for a ``default_related_model`` attribute which can be
  13. defined on subclasses as a default for the ``to`` argument.
  14. - Add one or more custom fields to the model that the relation
  15. field is applied to, and then call a ``related_items_changed``
  16. method each time related items are saved or deleted, so that a
  17. calculated value can be stored against the custom fields since
  18. aggregates aren't available for GenericRelation instances.
  19. """
  20. # Mapping of field names to model fields that will be added.
  21. fields = {}
  22. def __init__(self, *args, **kwargs):
  23. """
  24. Set up some defaults and check for a ``default_related_model``
  25. attribute for the ``to`` argument.
  26. """
  27. kwargs.setdefault("object_id_field", "object_pk")
  28. to = getattr(self, "default_related_model", None)
  29. # Avoid having both a positional arg and a keyword arg for
  30. # the parameter ``to``
  31. if to and not args:
  32. kwargs.setdefault("to", to)
  33. try:
  34. # Check if ``related_model`` has been modified by a subclass
  35. self.related_model
  36. except (AppRegistryNotReady, AttributeError):
  37. # if not, all is good
  38. super(BaseGenericRelation, self).__init__(*args, **kwargs)
  39. else:
  40. # otherwise, warn the user to stick to the new (as of 4.0)
  41. # ``default_related_model`` attribute
  42. raise ImproperlyConfigured("BaseGenericRelation changed the "
  43. "way it handled a default ``related_model`` in mezzanine "
  44. "4.0. Please override ``default_related_model`` instead "
  45. "and do not tamper with django's ``related_model`` "
  46. "property anymore.")
  47. def contribute_to_class(self, cls, name):
  48. """
  49. Add each of the names and fields in the ``fields`` attribute
  50. to the model the relationship field is applied to, and set up
  51. the related item save and delete signals for calling
  52. ``related_items_changed``.
  53. """
  54. for field in cls._meta.many_to_many:
  55. if isinstance(field, self.__class__):
  56. e = "Multiple %s fields are not supported (%s.%s, %s.%s)" % (
  57. self.__class__.__name__, cls.__name__, cls.__name__,
  58. name, field.name)
  59. raise ImproperlyConfigured(e)
  60. self.related_field_name = name
  61. super(BaseGenericRelation, self).contribute_to_class(cls, name)
  62. # Not applicable to abstract classes, and in fact will break.
  63. if not cls._meta.abstract:
  64. for (name_string, field) in self.fields.items():
  65. if "%s" in name_string:
  66. name_string = name_string % name
  67. extant_fields = cls._meta._forward_fields_map
  68. if name_string in extant_fields:
  69. continue
  70. if field.verbose_name is None:
  71. field.verbose_name = self.verbose_name
  72. cls.add_to_class(name_string, copy(field))
  73. # Add a getter function to the model we can use to retrieve
  74. # the field/manager by name.
  75. getter_name = "get_%s_name" % self.__class__.__name__.lower()
  76. cls.add_to_class(getter_name, lambda self: name)
  77. sender = self.rel.to
  78. post_save.connect(self._related_items_changed, sender=sender)
  79. post_delete.connect(self._related_items_changed, sender=sender)
  80. def _related_items_changed(self, **kwargs):
  81. """
  82. Ensure that the given related item is actually for the model
  83. this field applies to, and pass the instance to the real
  84. ``related_items_changed`` handler.
  85. """
  86. for_model = kwargs["instance"].content_type.model_class()
  87. if for_model and issubclass(for_model, self.model):
  88. instance_id = kwargs["instance"].object_pk
  89. try:
  90. instance = for_model.objects.get(id=instance_id)
  91. except self.model.DoesNotExist:
  92. # Instance itself was deleted - signals are irrelevant.
  93. return
  94. if hasattr(instance, "get_content_model"):
  95. instance = instance.get_content_model()
  96. related_manager = getattr(instance, self.related_field_name)
  97. self.related_items_changed(instance, related_manager)
  98. def related_items_changed(self, instance, related_manager):
  99. """
  100. Can be implemented by subclasses - called whenever the
  101. state of related items change, eg they're saved or deleted.
  102. The instance for this field and the related manager for the
  103. field are passed as arguments.
  104. """
  105. pass
  106. def value_from_object(self, obj):
  107. """
  108. Returns the value of this field in the given model instance.
  109. See: https://code.djangoproject.com/ticket/22552
  110. """
  111. return getattr(obj, self.attname).all()
  112. class CommentsField(BaseGenericRelation):
  113. """
  114. Stores the number of comments against the
  115. ``COMMENTS_FIELD_NAME_count`` field when a comment is saved or
  116. deleted.
  117. """
  118. default_related_model = "generic.ThreadedComment"
  119. fields = {"%s_count": IntegerField(editable=False, default=0)}
  120. def related_items_changed(self, instance, related_manager):
  121. """
  122. Stores the number of comments. A custom ``count_filter``
  123. queryset gets checked for, allowing managers to implement
  124. custom count logic.
  125. """
  126. try:
  127. count = related_manager.count_queryset()
  128. except AttributeError:
  129. count = related_manager.count()
  130. count_field_name = list(self.fields.keys())[0] % \
  131. self.related_field_name
  132. setattr(instance, count_field_name, count)
  133. instance.save()
  134. class KeywordsField(BaseGenericRelation):
  135. """
  136. Stores the keywords as a single string into the
  137. ``KEYWORDS_FIELD_NAME_string`` field for convenient access when
  138. searching.
  139. """
  140. default_related_model = "generic.AssignedKeyword"
  141. fields = {"%s_string": CharField(editable=False, blank=True,
  142. max_length=500)}
  143. def __init__(self, *args, **kwargs):
  144. """
  145. Mark the field as editable so that it can be specified in
  146. admin class fieldsets and pass validation, and also so that
  147. it shows up in the admin form.
  148. """
  149. super(KeywordsField, self).__init__(*args, **kwargs)
  150. self.editable = True
  151. def formfield(self, **kwargs):
  152. """
  153. Provide the custom form widget for the admin, since there
  154. isn't a form field mapped to ``GenericRelation`` model fields.
  155. """
  156. from mezzanine.generic.forms import KeywordsWidget
  157. kwargs["widget"] = KeywordsWidget
  158. return super(KeywordsField, self).formfield(**kwargs)
  159. def save_form_data(self, instance, data):
  160. """
  161. The ``KeywordsWidget`` field will return data as a string of
  162. comma separated IDs for the ``Keyword`` model - convert these
  163. into actual ``AssignedKeyword`` instances. Also delete
  164. ``Keyword`` instances if their last related ``AssignedKeyword``
  165. instance is being removed.
  166. """
  167. from mezzanine.generic.models import Keyword
  168. related_manager = getattr(instance, self.name)
  169. # Get a list of Keyword IDs being removed.
  170. old_ids = [str(a.keyword_id) for a in related_manager.all()]
  171. new_ids = data.split(",")
  172. removed_ids = set(old_ids) - set(new_ids)
  173. # Remove current AssignedKeyword instances.
  174. related_manager.all().delete()
  175. # Convert the data into AssignedKeyword instances.
  176. if data:
  177. data = [related_manager.create(keyword_id=i) for i in new_ids]
  178. # Remove keywords that are no longer assigned to anything.
  179. Keyword.objects.delete_unused(removed_ids)
  180. super(KeywordsField, self).save_form_data(instance, data)
  181. def contribute_to_class(self, cls, name):
  182. """
  183. Swap out any reference to ``KeywordsField`` with the
  184. ``KEYWORDS_FIELD_string`` field in ``search_fields``.
  185. """
  186. super(KeywordsField, self).contribute_to_class(cls, name)
  187. string_field_name = list(self.fields.keys())[0] % \
  188. self.related_field_name
  189. if hasattr(cls, "search_fields") and name in cls.search_fields:
  190. try:
  191. weight = cls.search_fields[name]
  192. except TypeError:
  193. # search_fields is a sequence.
  194. index = cls.search_fields.index(name)
  195. search_fields_type = type(cls.search_fields)
  196. cls.search_fields = list(cls.search_fields)
  197. cls.search_fields[index] = string_field_name
  198. cls.search_fields = search_fields_type(cls.search_fields)
  199. else:
  200. del cls.search_fields[name]
  201. cls.search_fields[string_field_name] = weight
  202. def related_items_changed(self, instance, related_manager):
  203. """
  204. Stores the keywords as a single string for searching.
  205. """
  206. assigned = related_manager.select_related("keyword")
  207. keywords = " ".join([str(a.keyword) for a in assigned])
  208. string_field_name = list(self.fields.keys())[0] % \
  209. self.related_field_name
  210. if getattr(instance, string_field_name) != keywords:
  211. setattr(instance, string_field_name, keywords)
  212. instance.save()
  213. class RatingField(BaseGenericRelation):
  214. """
  215. Stores the rating count and average against the
  216. ``RATING_FIELD_NAME_count`` and ``RATING_FIELD_NAME_average``
  217. fields when a rating is saved or deleted.
  218. """
  219. default_related_model = "generic.Rating"
  220. fields = {"%s_count": IntegerField(default=0, editable=False),
  221. "%s_sum": IntegerField(default=0, editable=False),
  222. "%s_average": FloatField(default=0, editable=False)}
  223. def related_items_changed(self, instance, related_manager):
  224. """
  225. Calculates and saves the average rating.
  226. """
  227. ratings = [r.value for r in related_manager.all()]
  228. count = len(ratings)
  229. _sum = sum(ratings)
  230. average = _sum / count if count > 0 else 0
  231. setattr(instance, "%s_count" % self.related_field_name, count)
  232. setattr(instance, "%s_sum" % self.related_field_name, _sum)
  233. setattr(instance, "%s_average" % self.related_field_name, average)
  234. instance.save()