PageRenderTime 25ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/wagtail/wagtailimages/models.py

https://gitlab.com/hanswang2012/wagtail
Python | 253 lines | 244 code | 7 blank | 2 comment | 1 complexity | 2c78264006091010df2db7a9d4a86791 MD5 | raw file
  1. import StringIO
  2. import os.path
  3. from taggit.managers import TaggableManager
  4. from django.core.files import File
  5. from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, ValidationError
  6. from django.db import models
  7. from django.db.models.signals import pre_delete
  8. from django.dispatch.dispatcher import receiver
  9. from django.utils.safestring import mark_safe
  10. from django.utils.html import escape
  11. from django.conf import settings
  12. from django.utils.translation import ugettext_lazy as _
  13. from unidecode import unidecode
  14. from wagtail.wagtailadmin.taggable import TagSearchable
  15. from wagtail.wagtailimages.backends import get_image_backend
  16. class AbstractImage(models.Model, TagSearchable):
  17. title = models.CharField(max_length=255, verbose_name=_('Title') )
  18. def get_upload_to(self, filename):
  19. folder_name = 'original_images'
  20. filename = self.file.field.storage.get_valid_name(filename)
  21. # do a unidecode in the filename and then
  22. # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding
  23. filename = "".join((i if ord(i) < 128 else '_') for i in unidecode(filename))
  24. while len(os.path.join(folder_name, filename)) >= 95:
  25. prefix, dot, extension = filename.rpartition('.')
  26. filename = prefix[:-1] + dot + extension
  27. return os.path.join(folder_name, filename)
  28. def file_extension_validator(ffile):
  29. extension = ffile.name.split(".")[-1].lower()
  30. if extension not in ["gif", "jpg", "jpeg", "png"]:
  31. raise ValidationError(_("Not a valid image format. Please use a gif, jpeg or png file instead."))
  32. file = models.ImageField(verbose_name=_('File'), upload_to=get_upload_to, width_field='width', height_field='height', validators=[file_extension_validator])
  33. width = models.IntegerField(editable=False)
  34. height = models.IntegerField(editable=False)
  35. created_at = models.DateTimeField(auto_now_add=True)
  36. uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, editable=False)
  37. tags = TaggableManager(help_text=None, blank=True, verbose_name=_('Tags'))
  38. indexed_fields = {
  39. 'uploaded_by_user_id': {
  40. 'type': 'integer',
  41. 'store': 'yes',
  42. 'indexed': 'no',
  43. 'boost': 0,
  44. },
  45. }
  46. def __unicode__(self):
  47. return self.title
  48. def get_rendition(self, filter):
  49. if not hasattr(filter, 'process_image'):
  50. # assume we've been passed a filter spec string, rather than a Filter object
  51. # TODO: keep an in-memory cache of filters, to avoid a db lookup
  52. filter, created = Filter.objects.get_or_create(spec=filter)
  53. try:
  54. rendition = self.renditions.get(filter=filter)
  55. except ObjectDoesNotExist:
  56. file_field = self.file
  57. # If we have a backend attribute then pass it to process
  58. # image - else pass 'default'
  59. backend_name = getattr(self, 'backend', 'default')
  60. generated_image_file = filter.process_image(file_field.file, backend_name=backend_name)
  61. rendition, created = self.renditions.get_or_create(
  62. filter=filter, defaults={'file': generated_image_file})
  63. return rendition
  64. def is_portrait(self):
  65. return (self.width < self.height)
  66. def is_landscape(self):
  67. return (self.height < self.width)
  68. @property
  69. def filename(self):
  70. return os.path.basename(self.file.name)
  71. @property
  72. def default_alt_text(self):
  73. # by default the alt text field (used in rich text insertion) is populated
  74. # from the title. Subclasses might provide a separate alt field, and
  75. # override this
  76. return self.title
  77. def is_editable_by_user(self, user):
  78. if user.has_perm('wagtailimages.change_image'):
  79. # user has global permission to change images
  80. return True
  81. elif user.has_perm('wagtailimages.add_image') and self.uploaded_by_user == user:
  82. # user has image add permission, which also implicitly provides permission to edit their own images
  83. return True
  84. else:
  85. return False
  86. class Meta:
  87. abstract = True
  88. class Image(AbstractImage):
  89. pass
  90. # Receive the pre_delete signal and delete the file associated with the model instance.
  91. @receiver(pre_delete, sender=Image)
  92. def image_delete(sender, instance, **kwargs):
  93. # Pass false so FileField doesn't save the model.
  94. instance.file.delete(False)
  95. def get_image_model():
  96. from django.conf import settings
  97. from django.db.models import get_model
  98. try:
  99. app_label, model_name = settings.WAGTAILIMAGES_IMAGE_MODEL.split('.')
  100. except AttributeError:
  101. return Image
  102. except ValueError:
  103. raise ImproperlyConfigured("WAGTAILIMAGES_IMAGE_MODEL must be of the form 'app_label.model_name'")
  104. image_model = get_model(app_label, model_name)
  105. if image_model is None:
  106. raise ImproperlyConfigured("WAGTAILIMAGES_IMAGE_MODEL refers to model '%s' that has not been installed" % settings.WAGTAILIMAGES_IMAGE_MODEL)
  107. return image_model
  108. class Filter(models.Model):
  109. """
  110. Represents an operation that can be applied to an Image to produce a rendition
  111. appropriate for final display on the website. Usually this would be a resize operation,
  112. but could potentially involve colour processing, etc.
  113. """
  114. spec = models.CharField(max_length=255, db_index=True)
  115. OPERATION_NAMES = {
  116. 'max': 'resize_to_max',
  117. 'min': 'resize_to_min',
  118. 'width': 'resize_to_width',
  119. 'height': 'resize_to_height',
  120. 'fill': 'resize_to_fill',
  121. }
  122. def __init__(self, *args, **kwargs):
  123. super(Filter, self).__init__(*args, **kwargs)
  124. self.method = None # will be populated when needed, by parsing the spec string
  125. def _parse_spec_string(self):
  126. # parse the spec string, which is formatted as (method)-(arg),
  127. # and save the results to self.method_name and self.method_arg
  128. try:
  129. (method_name_simple, method_arg_string) = self.spec.split('-')
  130. self.method_name = Filter.OPERATION_NAMES[method_name_simple]
  131. if method_name_simple in ('max', 'min', 'fill'):
  132. # method_arg_string is in the form 640x480
  133. (width, height) = [int(i) for i in method_arg_string.split('x')]
  134. self.method_arg = (width, height)
  135. else:
  136. # method_arg_string is a single number
  137. self.method_arg = int(method_arg_string)
  138. except (ValueError, KeyError):
  139. raise ValueError("Invalid image filter spec: %r" % self.spec)
  140. def process_image(self, input_file, backend_name='default'):
  141. """
  142. Given an input image file as a django.core.files.File object,
  143. generate an output image with this filter applied, returning it
  144. as another django.core.files.File object
  145. """
  146. backend = get_image_backend(backend_name)
  147. if not self.method:
  148. self._parse_spec_string()
  149. # If file is closed, open it
  150. input_file.open('rb')
  151. image = backend.open_image(input_file)
  152. file_format = image.format
  153. method = getattr(backend, self.method_name)
  154. image = method(image, self.method_arg)
  155. output = StringIO.StringIO()
  156. backend.save_image(image, output, file_format)
  157. # and then close the input file
  158. input_file.close()
  159. # generate new filename derived from old one, inserting the filter spec string before the extension
  160. input_filename_parts = os.path.basename(input_file.name).split('.')
  161. filename_without_extension = '.'.join(input_filename_parts[:-1])
  162. filename_without_extension = filename_without_extension[:60] # trim filename base so that we're well under 100 chars
  163. output_filename_parts = [filename_without_extension, self.spec] + input_filename_parts[-1:]
  164. output_filename = '.'.join(output_filename_parts)
  165. output_file = File(output, name=output_filename)
  166. return output_file
  167. class AbstractRendition(models.Model):
  168. filter = models.ForeignKey('Filter', related_name='+')
  169. file = models.ImageField(upload_to='images', width_field='width', height_field='height')
  170. width = models.IntegerField(editable=False)
  171. height = models.IntegerField(editable=False)
  172. @property
  173. def url(self):
  174. return self.file.url
  175. def img_tag(self):
  176. return mark_safe(
  177. '<img src="%s" width="%d" height="%d" alt="%s">' % (escape(self.url), self.width, self.height, escape(self.image.title))
  178. )
  179. class Meta:
  180. abstract = True
  181. class Rendition(AbstractRendition):
  182. image = models.ForeignKey('Image', related_name='renditions')
  183. class Meta:
  184. unique_together = (
  185. ('image', 'filter'),
  186. )
  187. # Receive the pre_delete signal and delete the file associated with the model instance.
  188. @receiver(pre_delete, sender=Rendition)
  189. def rendition_delete(sender, instance, **kwargs):
  190. # Pass false so FileField doesn't save the model.
  191. instance.file.delete(False)