/django_mongodb_engine/fields.py

https://github.com/django-nonrel/mongodb-engine · Python · 174 lines · 113 code · 17 blank · 44 comment · 16 complexity · 54431c34b1bb98794f87258f0d7f2e48 MD5 · raw file

  1. from django.db import connections, models
  2. from gridfs import GridFS
  3. from gridfs.errors import NoFile
  4. # handle pymongo backward compatibility
  5. try:
  6. from bson.objectid import ObjectId
  7. except ImportError:
  8. from pymongo.objectid import ObjectId
  9. from django_mongodb_engine.utils import make_struct
  10. __all__ = ['GridFSField', 'GridFSString']
  11. class GridFSField(models.Field):
  12. """
  13. GridFS field to store large chunks of data (blobs) in GridFS.
  14. Model instances keep references (ObjectIds) to GridFS files
  15. (:class:`gridfs.GridOut`) which are fetched on first attribute
  16. access.
  17. :param delete:
  18. Whether to delete the data stored in the GridFS (as GridFS
  19. files) when model instances are deleted (default: :const:`True`).
  20. Note that this doesn't have any influence on what happens if
  21. you update the blob value by assigning a new file, in which
  22. case the old file is always deleted.
  23. """
  24. forbids_updates = True
  25. def __init__(self, *args, **kwargs):
  26. self._versioning = kwargs.pop('versioning', False)
  27. self._autodelete = kwargs.pop('delete', not self._versioning)
  28. if self._versioning:
  29. import warnings
  30. warnings.warn("GridFSField versioning will be deprecated on "
  31. "version 0.6. If you consider this option useful "
  32. "please add a comment on issue #65 with your use "
  33. "case.", PendingDeprecationWarning)
  34. kwargs['max_length'] = 24
  35. kwargs.setdefault('default', None)
  36. kwargs.setdefault('null', True)
  37. super(GridFSField, self).__init__(*args, **kwargs)
  38. def db_type(self, connection):
  39. return 'gridfs'
  40. def contribute_to_class(self, model, name):
  41. # GridFSFields are represented as properties in the model
  42. # class. Let 'foo' be an instance of a model that has the
  43. # GridFSField 'gridf'. 'foo.gridf' then calls '_property_get'
  44. # and 'foo.gridfs = bar' calls '_property_set(bar)'.
  45. super(GridFSField, self).contribute_to_class(model, name)
  46. setattr(model, self.attname, property(self._property_get,
  47. self._property_set))
  48. if self._autodelete:
  49. models.signals.pre_delete.connect(self._on_pre_delete,
  50. sender=model)
  51. def _property_get(self, model_instance):
  52. """
  53. Gets the file from GridFS using the id stored in the model.
  54. """
  55. meta = self._get_meta(model_instance)
  56. if meta.filelike is None and meta.oid is not None:
  57. gridfs = self._get_gridfs(model_instance)
  58. if self._versioning:
  59. try:
  60. meta.filelike = gridfs.get_last_version(filename=meta.oid)
  61. return meta.filelike
  62. except NoFile:
  63. pass
  64. meta.filelike = gridfs.get(meta.oid)
  65. return meta.filelike
  66. def _property_set(self, model_instance, value):
  67. """
  68. Sets a new value.
  69. If value is an ObjectID it must be coming from Django's ORM
  70. internals being the value fetched from the database on query.
  71. In that case just update the id stored in the model instance.
  72. Otherwise it sets the value and checks whether a save is needed
  73. or not.
  74. """
  75. meta = self._get_meta(model_instance)
  76. if isinstance(value, ObjectId) and meta.oid is None:
  77. meta.oid = value
  78. else:
  79. meta.should_save = meta.filelike != value
  80. meta.filelike = value
  81. def pre_save(self, model_instance, add):
  82. meta = self._get_meta(model_instance)
  83. if meta.should_save:
  84. gridfs = self._get_gridfs(model_instance)
  85. if not self._versioning and meta.oid is not None:
  86. # We're putting a new GridFS file, so get rid of the
  87. # old one if we weren't explicitly asked to keep it.
  88. gridfs.delete(meta.oid)
  89. meta.should_save = False
  90. if not self._versioning or meta.oid is None:
  91. meta.oid = gridfs.put(meta.filelike)
  92. else:
  93. gridfs.put(meta.filelike, filename=meta.oid)
  94. return meta.oid
  95. def _on_pre_delete(self, sender, instance, using, signal, **kwargs):
  96. """
  97. Deletes the files associated with this isntance.
  98. If versioning is enabled all versions will be deleted.
  99. """
  100. gridfs = self._get_gridfs(instance)
  101. meta = self._get_meta(instance)
  102. try:
  103. while self._versioning and meta.oid:
  104. last = gridfs.get_last_version(filename=meta.oid)
  105. gridfs.delete(last._id)
  106. except NoFile:
  107. pass
  108. gridfs.delete(meta.oid)
  109. def _get_meta(self, model_instance):
  110. meta_name = '_%s_meta' % self.attname
  111. meta = getattr(model_instance, meta_name, None)
  112. if meta is None:
  113. meta_cls = make_struct('filelike', 'oid', 'should_save')
  114. meta = meta_cls(None, None, None)
  115. setattr(model_instance, meta_name, meta)
  116. return meta
  117. def _get_gridfs(self, model_instance):
  118. model = model_instance.__class__
  119. return GridFS(connections[model.objects.db].database,
  120. model._meta.db_table)
  121. class GridFSString(GridFSField):
  122. """
  123. Similar to :class:`GridFSField`, but the data is represented as a
  124. bytestring on Python side. This implies that all data has to be
  125. copied **into memory**, so :class:`GridFSString` is for smaller
  126. chunks of data only.
  127. """
  128. def _property_get(self, model):
  129. filelike = super(GridFSString, self)._property_get(model)
  130. if filelike is None:
  131. return ''
  132. if hasattr(filelike, 'read'):
  133. return filelike.read()
  134. return filelike
  135. try:
  136. # Used to satisfy South when introspecting models that use
  137. # GridFSField/GridFSString fields. Custom rules could be added
  138. # if needed.
  139. from south.modelsinspector import add_introspection_rules
  140. add_introspection_rules(
  141. [], ['^django_mongodb_engine\.fields\.GridFSField'])
  142. add_introspection_rules(
  143. [], ['^django_mongodb_engine\.fields\.GridFSString'])
  144. except ImportError:
  145. pass