def test_select_on_queryset_with_reverse_foreign_key(self): fields = index.RelatedFields( 'categories', [index.RelatedFields('category', [index.SearchField('name')])]) queryset = fields.select_on_queryset(ManyToManyBlogPage.objects.all()) # reverse ForeignKey should be prefetch_related self.assertIn('categories', queryset._prefetch_related_lookups) self.assertFalse(queryset.query.select_related)
class Novel(Book): setting = models.CharField(max_length=255) protagonist = models.OneToOneField(Character, related_name='+', null=True) search_fields = Book.search_fields + [ index.SearchField('setting', partial_match=True), index.RelatedFields('characters', [ index.SearchField('name', boost=0.25), ]), index.RelatedFields('protagonist', [ index.SearchField('name', boost=0.5), ]), ]
class SearchTest(index.Indexed, models.Model): title = models.CharField(max_length=255) content = models.TextField() live = models.BooleanField(default=False) published_date = models.DateField(null=True) tags = TaggableManager() search_fields = [ index.SearchField('title', partial_match=True), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True), index.FilterField('slug'), ]), index.RelatedFields('subobjects', [ index.SearchField('name', partial_match=True), ]), index.SearchField('content', boost=2), index.SearchField('callable_indexed_field'), index.FilterField('title'), index.FilterField('live'), index.FilterField('published_date'), ] def callable_indexed_field(self): return "Callable" @classmethod def get_indexed_objects(cls): indexed_objects = super(SearchTest, cls).get_indexed_objects() # Exclude SearchTests that have a SearchTestChild to stop update_index creating duplicates if cls is SearchTest: indexed_objects = indexed_objects.exclude( id__in=SearchTestChild.objects.all().values_list( 'searchtest_ptr_id', flat=True)) # Exclude SearchTests that have the title "Don't index me!" indexed_objects = indexed_objects.exclude(title="Don't index me!") return indexed_objects def get_indexed_instance(self): # Check if there is a SearchTestChild that descends from this child = SearchTestChild.objects.filter( searchtest_ptr_id=self.id).first() # Return the child if there is one, otherwise return self return child or self def __str__(self): return self.title
class Book(index.Indexed, models.Model): title = models.CharField(max_length=255) authors = models.ManyToManyField(Author, related_name='books') publication_date = models.DateField() number_of_pages = models.IntegerField() tags = TaggableManager() search_fields = [ index.SearchField('title', partial_match=True, boost=2.0), index.FilterField('title'), index.RelatedFields('authors', Author.search_fields), index.FilterField('publication_date'), index.FilterField('number_of_pages'), index.RelatedFields('tags', [ index.SearchField('name'), index.FilterField('slug'), ]), ] @classmethod def get_indexed_objects(cls): indexed_objects = super(Book, cls).get_indexed_objects() # Don't index books using Book class that they have a more specific type if cls is Book: indexed_objects = indexed_objects.exclude( id__in=Novel.objects.values_list('book_ptr_id', flat=True)) indexed_objects = indexed_objects.exclude( id__in=ProgrammingGuide.objects.values_list('book_ptr_id', flat=True)) # Exclude Books that have the title "Don't index me!" indexed_objects = indexed_objects.exclude(title="Don't index me!") return indexed_objects def get_indexed_instance(self): # Check if this object is a Novel or ProgrammingGuide and return the specific object novel = Novel.objects.filter(book_ptr_id=self.id).first() programming_guide = ProgrammingGuide.objects.filter( book_ptr_id=self.id).first() # Return the novel/programming guide object if there is one, otherwise return self return novel or programming_guide or self def __str__(self): return self.title
class AbstractEmbedVideo(CollectionMember, index.Indexed, models.Model): title = models.CharField(max_length=255, verbose_name=_('Title')) url = EmbedVideoField() thumbnail = models.ForeignKey(image_model_name, verbose_name=_('Thumbnail'), null=True, blank=True, on_delete=models.SET_NULL, related_name='+') created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created')) uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, editable=False, verbose_name=_('Uploader')) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('Tags')) def get_usage(self): return get_object_usage(self) @property def usage_url(self): return reverse('wagtail_embed_videos:video_usage', args=(self.id, )) search_fields = CollectionMember.search_fields + [ index.SearchField('title', partial_match=True, boost=10), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), index.FilterField('uploaded_by_user'), ] def __str__(self): return self.title def __init__(self, *args, **kwargs): super(AbstractEmbedVideo, self).__init__(*args, **kwargs) if args: if args[3] is None: create_thumbnail(self) def save(self, *args, **kwargs): super(AbstractEmbedVideo, self).save(*args, **kwargs) if not self.thumbnail: create_thumbnail(self) @property def default_alt_text(self): return self.title def is_editable_by_user(self, user): from .permissions import permission_policy return permission_policy.user_has_permission_for_instance( user, 'change', self) class Meta: abstract = True
class RteiDocument(AbstractDocument, index.Indexed): '''A custom Document adding fields needed by RTEI Resource items.''' year = models.CharField(validators=[ RegexValidator(regex='^\d{4}$', message='Must be 4 numbers', code='nomatch') ], help_text='e.g. 1999', max_length=4, blank=True) country = models.CharField(max_length=256, blank=True) is_resource = models.BooleanField(default=True, help_text="Determines whether document " "appears on the Resources page.") description = RichTextField(blank=True) admin_form_fields = ('title', 'description', 'file', 'collection', 'country', 'year', 'is_resource', 'tags') search_fields = AbstractDocument.search_fields + [ index.SearchField('title'), index.SearchField('description'), index.SearchField('country'), index.SearchField('year'), index.RelatedFields('tags', [ index.SearchField('name'), ]) ] class Meta: get_latest_by = "created_at"
class TagSearchable(index.Indexed): """ Mixin to provide a 'search' method, searching on the 'title' field and tags, for models that provide those things. """ search_fields = ( index.SearchField('title', partial_match=True, boost=10), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), ) @classmethod def get_indexed_objects(cls): return super( TagSearchable, cls).get_indexed_objects().prefetch_related('tagged_items__tag') @classmethod def popular_tags(cls): content_type = ContentType.objects.get_for_model(cls) return Tag.objects.filter( taggit_taggeditem_items__content_type=content_type).annotate( item_count=Count('taggit_taggeditem_items')).order_by( '-item_count')[:10]
def test_select_on_queryset_with_foreign_key(self): fields = index.RelatedFields('page', [ index.SearchField('title'), ]) queryset = fields.select_on_queryset(SearchTestChild.objects.all()) # ForeignKey should be select_related self.assertFalse(queryset._prefetch_related_lookups) self.assertIn('page', queryset.query.select_related)
def test_select_on_queryset_with_one_to_one(self): fields = index.RelatedFields('searchtest_ptr', [ index.SearchField('title'), ]) queryset = fields.select_on_queryset(SearchTestChild.objects.all()) # OneToOneField should be select_related self.assertFalse(queryset._prefetch_related_lookups) self.assertIn('searchtest_ptr', queryset.query.select_related)
def test_select_on_queryset_with_foreign_key(self): fields = index.RelatedFields('protagonist', [ index.SearchField('name'), ]) queryset = fields.select_on_queryset(Novel.objects.all()) # ForeignKey should be select_related self.assertFalse(queryset._prefetch_related_lookups) self.assertIn('protagonist', queryset.query.select_related)
def test_select_on_queryset_with_many_to_many(self): fields = index.RelatedFields('adverts', [ index.SearchField('title'), ]) queryset = fields.select_on_queryset(ManyToManyBlogPage.objects.all()) # ManyToManyField should be prefetch_related self.assertIn('adverts', queryset._prefetch_related_lookups) self.assertFalse(queryset.query.select_related)
def test_select_on_queryset_with_reverse_one_to_one(self): fields = index.RelatedFields('novel', [ index.SearchField('subtitle'), ]) queryset = fields.select_on_queryset(Book.objects.all()) # reverse OneToOneField should be select_related self.assertFalse(queryset._prefetch_related_lookups) self.assertIn('novel', queryset.query.select_related)
def test_select_on_queryset_with_reverse_many_to_many(self): fields = index.RelatedFields('manytomanyblogpage', [ index.SearchField('title'), ]) queryset = fields.select_on_queryset(Advert.objects.all()) # reverse ManyToManyField should be prefetch_related self.assertIn('manytomanyblogpage', queryset._prefetch_related_lookups) self.assertFalse(queryset.query.select_related)
def test_select_on_queryset_with_taggable_manager(self): fields = index.RelatedFields('tags', [ index.SearchField('name'), ]) queryset = fields.select_on_queryset(Novel.objects.all()) # Tags should be prefetch_related self.assertIn('tags', queryset._prefetch_related_lookups) self.assertFalse(queryset.query.select_related)
class AbstractDocument(CollectionMember, index.Indexed, models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) file = models.FileField(upload_to='documents', verbose_name=_('file')) created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True) uploaded_by_user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'), null=True, blank=True, editable=False, on_delete=models.SET_NULL ) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags')) objects = DocumentQuerySet.as_manager() search_fields = CollectionMember.search_fields + [ index.SearchField('title', partial_match=True, boost=10), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), index.FilterField('uploaded_by_user'), ] def __str__(self): return self.title @property def filename(self): return os.path.basename(self.file.name) @property def file_extension(self): return os.path.splitext(self.filename)[1][1:] @property def url(self): return reverse('wagtaildocs_serve', args=[self.id, self.filename]) def get_usage(self): return get_object_usage(self) @property def usage_url(self): return reverse('wagtaildocs:document_usage', args=(self.id,)) def is_editable_by_user(self, user): from wagtail.wagtaildocs.permissions import permission_policy return permission_policy.user_has_permission_for_instance(user, 'change', self) class Meta: abstract = True verbose_name = _('document')
class TagSearchable(index.Indexed): """ Mixin to provide a 'search' method, searching on the 'title' field and tags, for models that provide those things. """ search_fields = ( index.SearchField('title', partial_match=True, boost=10), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), ) @classmethod def get_indexed_objects(cls): return super(TagSearchable, cls).get_indexed_objects().prefetch_related('tagged_items__tag') @classmethod def search(cls, q, results_per_page=None, page=1, prefetch_tags=False, filters={}): warnings.warn( "The {class_name}.search() method is deprecated. " "Please use the {class_name}.objects.search() method instead.".format(class_name=cls.__name__), RemovedInWagtail14Warning, stacklevel=2) results = cls.objects.all() if prefetch_tags: results = results.prefetch_related('tagged_items__tag') if filters: results = results.filter(**filters) results = results.search(q) # If results_per_page is set, return a paginator if results_per_page is not None: paginator = Paginator(results, results_per_page) try: return paginator.page(page) except PageNotAnInteger: return paginator.page(1) except EmptyPage: return paginator.page(paginator.num_pages) else: return results @classmethod def popular_tags(cls): content_type = ContentType.objects.get_for_model(cls) return Tag.objects.filter( taggit_taggeditem_items__content_type=content_type ).annotate( item_count=Count('taggit_taggeditem_items') ).order_by('-item_count')[:10]
class ArtworkAttributionLink(index.Indexed, models.Model): artist = ParentalKey('artists.ArtistProfilePage', related_name='artistartworklink') artwork = ParentalKey(ArtworkPage, related_name='artworkartistlink') panels = [ PageChooserPanel('artist', 'artists.ArtistProfilePage'), PageChooserPanel('artwork', 'artworks.ArtworkPage'), ] search_fields = [ index.RelatedFields('artist', [ index.SearchField('title', partial_match=True), index.SearchField('first_name', partial_match=True), index.SearchField('last_name', partial_match=True) ]), index.RelatedFields('artwork', [ index.SearchField('title', partial_match=True) ]), ] api_fields = ['artist', 'artwork']
class AbstractGiphy(CollectionMember, index.Indexed, models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) file = models.FileField( verbose_name=_('file'), upload_to=get_upload_to, ) created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True, db_index=True) uploaded_by_user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'), null=True, blank=True, editable=False, on_delete=models.SET_NULL ) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags')) file_size = models.PositiveIntegerField(null=True, editable=False) def get_file_size(self): if self.file_size is None: try: self.file_size = self.file.size except OSError: # File doesn't exist return self.save(update_fields=['file_size']) return self.file_size def get_upload_to(self, filename): folder_name = 'giphy' filename = self.file.field.storage.get_valid_name(filename) created_at = self.file.created_at return os.path.join(folder_name, '{y}/{m}/{d}/{filename}'.format(**{ 'y': created_at.year, 'm': created_at.month, 'd': created_at.day, 'filename': filename })) def __str__(self): return self.title search_fields = CollectionMember.search_fields + [ index.SearchField('title', partial_match=True, boost=10), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), index.FilterField('uploaded_by_user'), ] class Meta: abstract = True
class SearchTestChild(SearchTest): subtitle = models.CharField(max_length=191, null=True, blank=True) extra_content = models.TextField() page = models.ForeignKey('wagtailcore.Page', null=True, blank=True) search_fields = SearchTest.search_fields + [ index.SearchField('subtitle', partial_match=True), index.SearchField('extra_content'), index.RelatedFields('page', [ index.SearchField('title', partial_match=True), index.SearchField('search_description'), index.FilterField('live'), ]), ]
class RteiDocument(AbstractDocument, index.Indexed): '''A custom Document adding fields needed by RTEI Resource items.''' year = models.CharField(validators=[ RegexValidator(regex='^\d{4}$', message=_('Must be 4 numbers'), code='nomatch') ], help_text='e.g. 1999', max_length=4, blank=True) file = models.FileField( upload_to='documents', verbose_name=_('file'), blank=True, help_text="Use this to upload a file and list it as a resource") external_url = models.CharField( validators=[URLValidator(message=_('Must be a valid URL'))], blank=True, max_length=1000, verbose_name=_('External Link'), help_text="Use this to add an external website as a listed resource") country = models.CharField(max_length=256, blank=True) is_resource = models.BooleanField(default=True, help_text="Determines whether document " "appears on the Resources page.") description = RichTextField(blank=True) admin_form_fields = ('title', 'description', 'file', 'external_url', 'collection', 'country', 'year', 'is_resource', 'tags') search_fields = AbstractDocument.search_fields + [ index.SearchField('title'), index.SearchField('description'), index.SearchField('country'), index.SearchField('year'), index.RelatedFields('tags', [ index.SearchField('name'), ]) ] class Meta: get_latest_by = "created_at"
class BlogPost(Page, WithStreamField, WithFeedImage): date = models.DateField() tags = ClusterTaggableManager(through=BlogPostTag, blank=True) search_fields = Page.search_fields + [ index.SearchField('body'), index.SearchField('date'), index.RelatedFields('tags', [ index.SearchField('name'), index.SearchField('slug'), ]), ] subpage_types = [] def get_index_page(self): # Find closest ancestor which is a blog index return BlogIndexPage.objects.ancestor_of(self).last()
class NewsPost(Page, WithStreamField, WithFeedImage): image = models.ForeignKey('wagtailimages.Image', on_delete=models.PROTECT, null=True) date = models.DateField() tags = ClusterTaggableManager(through=NewsPostTag, blank=True) search_fields = Page.search_fields + [ index.SearchField('body'), index.SearchField('date'), index.RelatedFields('tags', [ index.SearchField('name'), index.SearchField('slug'), ]), ] subpage_types = [] def get_index_page(self): # Find closest ancestor which is a news index return NewsIndexPage.objects.ancestor_of(self).last()
class BlogPage(Page): intro = models.TextField(blank=True) body = RichTextField(blank=True) tags = ClusterTaggableManager(through=BlogPageTag, blank=True) date = models.DateField("Post date") feed_image = models.ForeignKey('wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') search_fields = Page.search_fields + [ index.SearchField('body'), index.RelatedFields('tags', [ index.SearchField('name'), ]) ] @property def blog_index(self): # Find closest ancestor which is a blog index return self.get_ancestors().type(BlogIndexPage).last()
class Product(Page): parent_page_types = ['longclawproducts.ProductIndex'] description = RichTextField() tags = ClusterTaggableManager(through=ProductTag, blank=True) search_fields = Page.search_fields + [ index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), ] content_panels = Page.content_panels + [ FieldPanel('description'), InlinePanel('variants', label='Product variants'), InlinePanel('images', label='Product images'), FieldPanel('tags'), ] @property def first_image(self): return self.images.first() @property def price_range(self): ''' Calculate the price range of the products variants ''' ordered = self.variants.order_by('price') if ordered: return ordered.first().price, ordered.last().price else: return None, None @property def in_stock(self): ''' Returns True if any of the product variants are in stock ''' return any(self.variants.filter(stock__gt=0))
class Event(Page, WithStreamField, WithFeedImage): image = models.ForeignKey( 'wagtailimages.Image', on_delete=models.PROTECT, ) date_from = models.DateField(verbose_name="Start Date") date_to = models.DateField(verbose_name="End Date (Leave blank if\ not required)", blank=True, null=True) time = models.TimeField(verbose_name="Time of Event") time_end = models.TimeField(verbose_name="End Time (leave blank if\ not required)", blank=True, null=True) location = models.TextField(verbose_name="Location") tags = ClusterTaggableManager(through=BlogPostTag, blank=True) search_fields = Page.search_fields + [ index.SearchField('body'), index.SearchField('date_from'), index.SearchField('date_to'), index.RelatedFields('tags', [ index.SearchField('name'), index.SearchField('slug'), ]), ] subpage_types = [] def get_index_page(self): # Find closest ancestor which is a blog index return EventIndexPage.objects.ancestor_of(self).last()
class WorkPage(Page, WithStreamField, WithFeedImage): subtitle = models.CharField(max_length=256) category = models.ForeignKey( WorkCategory, blank=True, null=True, on_delete=models.SET_NULL, ) tags = ClusterTaggableManager(through=WorkPageTag, blank=True) search_fields = Page.search_fields + [ index.SearchField('body'), index.SearchField('subtitle'), index.SearchField('category'), index.RelatedFields('tags', [ index.SearchField('name'), ]), ] subpage_types = [] def get_index_page(self): # Find closest ancestor which is a blog index return WorkIndexPage.objects.ancestor_of(self).last()
class Face(Page): image = models.ForeignKey("core.AffixImage", related_name="face_for_image", null=True, on_delete=models.SET_NULL) location = models.ForeignKey("location.Location", null=True, on_delete=models.SET_NULL, verbose_name="Place of Origin") additional_info = RichTextField(blank=True) language = models.CharField(max_length=7, choices=settings.LANGUAGES) occupation = models.CharField( max_length=100, null=True, blank=True, help_text="Enter the occupation of the person") occupation_of_parent = models.CharField(max_length=100, null=True, blank=True) adivasi = models.CharField(max_length=100, null=True, blank=True) quote = RichTextField(blank=True) child = models.BooleanField(default=False) age = models.IntegerField(null=True, blank=True) GENDER_CHOICES = ( ('F', 'Female'), ('M', 'Male'), ('T', 'Transgender'), ) gender = models.CharField(max_length=1, choices=GENDER_CHOICES, null=True, blank=True) def __str__(self): return "{0} {1}".format(self.title, self.location.district) @property def featured_image(self): return self.image @property def title_to_share(self): title = "Meet " + self.title title += ", " + self.occupation if self.occupation else "" title += " from " + self.location.district title += ", " + self.location.state return title @property def locations(self): return [self.location] @property def photographers(self): return self.image.photographers.all() def get_authors_or_photographers(self): return [photographer.name for photographer in self.photographers] search_fields = Page.search_fields + [ index.SearchField('title', partial_match=True, boost=SearchBoost.TITLE), index.FilterField('image'), index.SearchField( 'additional_info', partial_match=True, boost=SearchBoost.CONTENT), index.FilterField('location'), index.RelatedFields('location', [ index.SearchField('name'), index.SearchField('block'), index.SearchField('district'), index.SearchField('state'), index.SearchField('panchayat'), ]), index.SearchField('occupation'), index.SearchField('occupation_of_parent'), index.SearchField('quote'), index.SearchField('get_locations_index', partial_match=True, boost=SearchBoost.LOCATION), index.SearchField('get_photographers_index', partial_match=True, boost=SearchBoost.AUTHOR), index.SearchField('adivasi'), index.SearchField('language'), index.FilterField('get_search_type'), index.FilterField('language'), index.FilterField('get_minimal_locations'), index.FilterField('get_authors_or_photographers') ] def get_locations_index(self): return self.image.get_locations_index() def get_minimal_locations(self): return [self.location.minimal_address] def get_photographers_index(self): return self.image.get_all_photographers() def get_search_type(self): return self.__class__.__name__.lower() content_panels = Page.content_panels + [ ImageChooserPanel('image'), M2MFieldPanel('location'), FieldPanel('adivasi'), MultiFieldPanel([ FieldPanel('child'), FieldPanel('occupation'), FieldPanel('occupation_of_parent'), FieldPanel('age'), FieldPanel('gender'), ], heading="Personal details", classname="collapsible "), MultiFieldPanel([ FieldPanel('additional_info'), FieldPanel('language'), FieldPanel('quote'), ], heading="Additional details", classname="collapsible"), ] def get_absolute_url(self): name = "face-detail-single" return reverse(name, kwargs={ "alphabet": self.location.district[0].lower(), "slug": self.slug }) def get_context(self, request, *args, **kwargs): return { 'faces': [self], 'alphabet': self.location.district[0].lower(), 'request': request }
class AbstractImage(CollectionMember, index.Indexed, models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) file = models.ImageField(verbose_name=_('file'), upload_to=get_upload_to, width_field='width', height_field='height') width = models.IntegerField(verbose_name=_('width'), editable=False) height = models.IntegerField(verbose_name=_('height'), editable=False) created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True, db_index=True) uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'), null=True, blank=True, editable=False, on_delete=models.SET_NULL) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags')) focal_point_x = models.PositiveIntegerField(null=True, blank=True) focal_point_y = models.PositiveIntegerField(null=True, blank=True) focal_point_width = models.PositiveIntegerField(null=True, blank=True) focal_point_height = models.PositiveIntegerField(null=True, blank=True) file_size = models.PositiveIntegerField(null=True, editable=False) objects = ImageQuerySet.as_manager() def is_stored_locally(self): """ Returns True if the image is hosted on the local filesystem """ try: self.file.path return True except NotImplementedError: return False def get_file_size(self): if self.file_size is None: try: self.file_size = self.file.size except AttributeError: # self.file is NoneType return except OSError: # File doesn't exist return self.save(update_fields=['file_size']) return self.file_size def get_upload_to(self, filename): folder_name = 'original_images' filename = self.file.field.storage.get_valid_name(filename) # do a unidecode in the filename and then # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding filename = "".join( (i if ord(i) < 128 else '_') for i in unidecode(filename)) # Truncate filename so it fits in the 100 character limit # https://code.djangoproject.com/ticket/9893 full_path = os.path.join(folder_name, filename) if len(full_path) >= 95: chars_to_trim = len(full_path) - 94 prefix, extension = os.path.splitext(filename) filename = prefix[:-chars_to_trim] + extension full_path = os.path.join(folder_name, filename) return full_path def get_usage(self): return get_object_usage(self) @property def usage_url(self): return reverse('wagtailimages:image_usage', args=(self.id, )) search_fields = CollectionMember.search_fields + [ index.SearchField('title', partial_match=True, boost=10), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), index.FilterField('uploaded_by_user'), ] def __str__(self): return self.title @contextmanager def get_willow_image(self): # Open file if it is closed close_file = False try: image_file = self.file if self.file.closed: # Reopen the file if self.is_stored_locally(): self.file.open('rb') else: # Some external storage backends don't allow reopening # the file. Get a fresh file instance. #1397 storage = self._meta.get_field('file').storage image_file = storage.open(self.file.name, 'rb') close_file = True except IOError as e: # re-throw this as a SourceImageIOError so that calling code can distinguish # these from IOErrors elsewhere in the process raise SourceImageIOError(text_type(e)) # Seek to beginning image_file.seek(0) try: yield WillowImage.open(image_file) finally: if close_file: image_file.close() def get_rect(self): return Rect(0, 0, self.width, self.height) def get_focal_point(self): if self.focal_point_x is not None and \ self.focal_point_y is not None and \ self.focal_point_width is not None and \ self.focal_point_height is not None: return Rect.from_point( self.focal_point_x, self.focal_point_y, self.focal_point_width, self.focal_point_height, ) def has_focal_point(self): return self.get_focal_point() is not None def set_focal_point(self, rect): if rect is not None: self.focal_point_x = rect.centroid_x self.focal_point_y = rect.centroid_y self.focal_point_width = rect.width self.focal_point_height = rect.height else: self.focal_point_x = None self.focal_point_y = None self.focal_point_width = None self.focal_point_height = None def get_suggested_focal_point(self): with self.get_willow_image() as willow: faces = willow.detect_faces() if faces: # Create a bounding box around all faces left = min(face[0] for face in faces) top = min(face[1] for face in faces) right = max(face[2] for face in faces) bottom = max(face[3] for face in faces) focal_point = Rect(left, top, right, bottom) else: features = willow.detect_features() if features: # Create a bounding box around all features left = min(feature[0] for feature in features) top = min(feature[1] for feature in features) right = max(feature[0] for feature in features) bottom = max(feature[1] for feature in features) focal_point = Rect(left, top, right, bottom) else: return None # Add 20% to width and height and give it a minimum size x, y = focal_point.centroid width, height = focal_point.size width *= 1.20 height *= 1.20 width = max(width, 100) height = max(height, 100) return Rect.from_point(x, y, width, height) @classmethod def get_rendition_model(cls): """ Get the Rendition model for this Image model """ if django.VERSION >= (1, 9): return cls.renditions.rel.related_model else: return cls.renditions.related.related_model def get_rendition(self, filter): if isinstance(filter, string_types): filter = Filter(spec=filter) cache_key = filter.get_cache_key(self) Rendition = self.get_rendition_model() try: rendition = self.renditions.get( filter_spec=filter.spec, focal_point_key=cache_key, ) except Rendition.DoesNotExist: # Generate the rendition image generated_image = filter.run(self, BytesIO()) # Generate filename input_filename = os.path.basename(self.file.name) input_filename_without_extension, input_extension = os.path.splitext( input_filename) # A mapping of image formats to extensions FORMAT_EXTENSIONS = { 'jpeg': '.jpg', 'png': '.png', 'gif': '.gif', } output_extension = filter.spec.replace( '|', '.') + FORMAT_EXTENSIONS[generated_image.format_name] if cache_key: output_extension = cache_key + '.' + output_extension # Truncate filename to prevent it going over 60 chars output_filename_without_extension = input_filename_without_extension[:( 59 - len(output_extension))] output_filename = output_filename_without_extension + '.' + output_extension rendition, created = self.renditions.get_or_create( filter_spec=filter.spec, focal_point_key=cache_key, defaults={ 'file': File(generated_image.f, name=output_filename) }) return rendition def is_portrait(self): return (self.width < self.height) def is_landscape(self): return (self.height < self.width) @property def filename(self): return os.path.basename(self.file.name) @property def default_alt_text(self): # by default the alt text field (used in rich text insertion) is populated # from the title. Subclasses might provide a separate alt field, and # override this return self.title def is_editable_by_user(self, user): from wagtail.wagtailimages.permissions import permission_policy return permission_policy.user_has_permission_for_instance( user, 'change', self) class Meta: abstract = True
class AbstractMedia(CollectionMember, index.Indexed, models.Model): MEDIA_TYPES = ( ('audio', _('Audio file')), ('video', _('Video file')), ) title = models.CharField(max_length=255, verbose_name=_('title')) file = models.FileField(upload_to='media', verbose_name=_('file')) type = models.CharField(choices=MEDIA_TYPES, max_length=255, blank=False, null=False) duration = models.PositiveIntegerField(verbose_name=_('duration'), help_text=_('Duration in seconds')) width = models.PositiveIntegerField(null=True, blank=True, verbose_name=_('width')) height = models.PositiveIntegerField(null=True, blank=True, verbose_name=_('height')) thumbnail = models.FileField(upload_to='media_thumbnails', blank=True, verbose_name=_('thumbnail')) created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True) uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'), null=True, blank=True, editable=False, on_delete=models.SET_NULL) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags')) objects = MediaQuerySet.as_manager() search_fields = CollectionMember.search_fields + [ index.SearchField('title', partial_match=True, boost=10), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), index.FilterField('uploaded_by_user'), ] def __str__(self): return self.title @property def filename(self): return os.path.basename(self.file.name) @property def thumbnail_filename(self): return os.path.basename(self.thumbnail.name) @property def file_extension(self): return os.path.splitext(self.filename)[1][1:] @property def url(self): return self.file.url def get_usage(self): return get_object_usage(self) @property def usage_url(self): return reverse('wagtailmedia:media_usage', args=(self.id, )) def is_editable_by_user(self, user): from wagtailmedia.permissions import permission_policy return permission_policy.user_has_permission_for_instance( user, 'change', self) class Meta: abstract = True verbose_name = _('media')
class Video(CollectionMember, index.Indexed, models.Model): ''' Video model ''' block_id = models.CharField(max_length=128, unique=True) display_name = models.CharField(max_length=255) view_url = models.URLField(max_length=255) transcript_url = models.URLField(max_length=255, null=True) source_course_run = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags')) objects = VideoQuerySet.as_manager() search_fields = CollectionMember.search_fields + [ index.SearchField('display_name', partial_match=True), index.SearchField('transcript', partial_match=False, es_extra=LARGE_TEXT_FIELD_SEARCH_PROPS), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True, boost=10), ]), index.FilterField('id'), index.FilterField('source_course_run'), ] def get_action_url_name(self, action): return '%s_%s_modeladmin_%s' % (self._meta.app_label, self._meta.object_name.lower(), action) def get_usage(self): return JournalPage.objects.filter(videos=self) def transcript(self): ''' Read the transcript from the transcript url to provide to elasticsearch ''' if not self.transcript_url: return None try: response = requests.get( self.transcript_url) # No auth needed for transcripts contents = response.content return contents.decode( 'utf-8')[:settings. MAX_ELASTICSEARCH_UPLOAD_SIZE] if contents else None except Exception as err: # pylint: disable=broad-except logger.error( 'Exception trying to read transcript url={url} for Video err={err}' .format(url=self.transcript_url, err=err)) return None @property def view_access_url(self): ''' Return the url to access the video on LMS based on the Journal that the video is found in. ''' journal_page = JournalPage.objects.filter( videos=self).live().distinct().first() journal_uuid = journal_page.get_journal().uuid if journal_page else 0 url = self.view_url.replace("xblock", "journals/render_journal_block") return "{url}?journal_uuid={journal_uuid}".format( url=url, journal_uuid=journal_uuid) def __str__(self): return self.display_name