class EventPage(Page): date_from = models.DateField("Start date", null=True) date_to = models.DateField( "End date", null=True, blank=True, help_text="Not required if event is on a single day") time_from = models.TimeField("Start time", null=True, blank=True) time_to = models.TimeField("End time", null=True, blank=True) audience = models.CharField(max_length=255, choices=EVENT_AUDIENCE_CHOICES) location = models.CharField(max_length=255) body = RichTextField(blank=True) cost = models.CharField(max_length=255) signup_link = models.URLField(blank=True) feed_image = models.ForeignKey('tuiuiuimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') categories = ParentalManyToManyField(EventCategory, blank=True) search_fields = [ index.SearchField('get_audience_display'), index.SearchField('location'), index.SearchField('body'), ] password_required_template = 'tests/event_page_password_required.html' base_form_class = EventPageForm
def test_overriding(self): # If there are two fields with the same type and name # the last one should override all the previous ones. This ensures that the # standard convention of: # # class SpecificPageType(Page): # search_fields = Page.search_fields + [some_other_definitions] # # ...causes the definitions in some_other_definitions to override Page.search_fields # as intended. cls = self.make_dummy_type([ index.SearchField('test', boost=100, partial_match=False), index.SearchField('test', partial_match=True), ]) self.assertEqual(len(cls.get_search_fields()), 1) self.assertEqual(len(cls.get_searchable_search_fields()), 1) self.assertEqual(len(cls.get_filterable_search_fields()), 0) field = cls.get_search_fields()[0] self.assertIsInstance(field, index.SearchField) # Boost should be reset to the default if it's not specified by the override self.assertIsNone(field.boost) # Check that the partial match was overridden self.assertTrue(field.partial_match)
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('tuiuiudocs_serve', args=[self.id, self.filename]) def get_usage(self): return get_object_usage(self) @property def usage_url(self): return reverse('tuiuiudocs:document_usage', args=(self.id,)) def is_editable_by_user(self, user): from tuiuiu.tuiuiudocs.permissions import permission_policy return permission_policy.user_has_permission_for_instance(user, 'change', self) class Meta: abstract = True verbose_name = _('document')
class EventPage(Page): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) AUDIENCE_CHOICES = ( ('public', "Public"), ('private', "Private"), ) date_from = models.DateField("Start date") date_to = models.DateField( "End date", null=True, blank=True, help_text="Not required if event is on a single day" ) time_from = models.TimeField("Start time", null=True, blank=True) time_to = models.TimeField("End time", null=True, blank=True) audience = models.CharField(max_length=255, choices=AUDIENCE_CHOICES) location = models.CharField(max_length=255) body = RichTextField(blank=True) cost = models.CharField(max_length=255) signup_link = models.URLField(blank=True) feed_image = models.ForeignKey( 'tuiuiuimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) api_fields = ( 'date_from', 'date_to', 'time_from', 'time_to', 'audience', 'location', 'body', 'cost', 'signup_link', 'feed_image', 'carousel_items', 'related_links', 'speakers', ) search_fields = Page.search_fields + [ index.SearchField('get_audience_display'), index.SearchField('location'), index.SearchField('body'), ] def get_event_index(self): # Find closest ancestor which is an event index return EventIndexPage.objects.ancester_of(self).last()
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'), 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 EventIndexPage(Page): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) intro = RichTextField(blank=True) api_fields = ( 'intro', 'related_links', ) search_fields = Page.search_fields + [ index.SearchField('intro'), ] def get_events(self): # Get list of live event pages that are descendants of this page events = EventPage.objects.descendant_of(self).live() # Filter events list to get ones that are either # running now or start in the future events = events.filter(date_from__gte=date.today()) # Order by date events = events.order_by('date_from') return events
class BlogEntryPage(Page): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) body = RichTextField() tags = ClusterTaggableManager(through='BlogEntryPageTag', blank=True) date = models.DateField("Post date") feed_image = models.ForeignKey( 'tuiuiuimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) api_fields = ( APIField('body'), APIField('tags'), APIField('date'), APIField('feed_image'), APIField('feed_image_thumbnail', serializer=ImageRenditionField('fill-300x300', source='feed_image')), APIField('carousel_items'), APIField('related_links'), ) search_fields = Page.search_fields + [ index.SearchField('body'), ] def get_blog_index(self): # Find closest ancestor which is a blog index return BlogIndexPage.ancestor_of(self).last()
class SearchTestChild(SearchTest): subtitle = models.CharField(max_length=255, null=True, blank=True) extra_content = models.TextField() page = models.ForeignKey('tuiuiucore.Page', null=True, blank=True, on_delete=models.SET_NULL) 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 AnotherSearchTestChild(SearchTest): # Checks that having the same field name in two child models with different # search configuration doesn't give an error subtitle = models.CharField(max_length=255, null=True, blank=True) search_fields = SearchTest.search_fields + [ index.SearchField('subtitle', boost=10), ]
class SearchableSnippet(index.Indexed, models.Model): text = models.CharField(max_length=255) search_fields = [ index.SearchField('text'), ] def __str__(self): return self.text
def test_basic(self): cls = self.make_dummy_type([ index.SearchField('test', boost=100, partial_match=False), index.FilterField('filter_test'), ]) self.assertEqual(len(cls.get_search_fields()), 2) self.assertEqual(len(cls.get_searchable_search_fields()), 1) self.assertEqual(len(cls.get_filterable_search_fields()), 1)
def test_select_on_queryset_with_taggable_manager(self): fields = index.RelatedFields('tags', [ index.SearchField('name'), ]) queryset = fields.select_on_queryset(SearchTestChild.objects.all()) # Tags should be prefetch_related self.assertIn('tags', queryset._prefetch_related_lookups) self.assertFalse(queryset.query.select_related)
def test_select_on_queryset_with_reverse_one_to_one(self): fields = index.RelatedFields('searchtestchild', [ index.SearchField('subtitle'), ]) queryset = fields.select_on_queryset(SearchTest.objects.all()) # reverse OneToOneField should be select_related self.assertFalse(queryset._prefetch_related_lookups) self.assertIn('searchtestchild', queryset.query.select_related)
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)
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_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_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_different_field_types_dont_override(self): # A search and filter field with the same name should be able to coexist cls = self.make_dummy_type([ index.SearchField('test', boost=100, partial_match=False), index.FilterField('test'), ]) self.assertEqual(len(cls.get_search_fields()), 2) self.assertEqual(len(cls.get_searchable_search_fields()), 1) self.assertEqual(len(cls.get_filterable_search_fields()), 1)
def test_checking_search_fields(self): with patch_search_fields( models.SearchTest, models.SearchTest.search_fields + [index.SearchField('foo')]): expected_errors = [ checks.Warning( "SearchTest.search_fields contains field 'foo' but it doesn't exist", obj=models.SearchTest) ] errors = models.SearchTest.check() self.assertEqual(errors, expected_errors)
class PersonPage(Page, ContactFieldsMixin): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) intro = RichTextField(blank=True) biography = RichTextField(blank=True) image = models.ForeignKey( 'tuiuiuimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) feed_image = models.ForeignKey( 'tuiuiuimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) api_fields = ( 'first_name', 'last_name', 'intro', 'biography', 'image', 'feed_image', 'related_links', ) + ContactFieldsMixin.api_fields search_fields = Page.search_fields + [ index.SearchField('first_name'), index.SearchField('last_name'), index.SearchField('intro'), index.SearchField('biography'), ]
class StandardPage(Page): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) intro = RichTextField(blank=True) body = RichTextField(blank=True) feed_image = models.ForeignKey( 'tuiuiuimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) api_fields = ( 'intro', 'body', 'feed_image', 'carousel_items', 'related_links', ) search_fields = Page.search_fields + [ index.SearchField('intro'), index.SearchField('body'), ]
class HomePage(Page): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) body = RichTextField(blank=True) api_fields = ( 'body', 'carousel_items', 'related_links', ) search_fields = Page.search_fields + [ index.SearchField('body'), ] class Meta: verbose_name = "homepage"
class ContactPage(Page, ContactFieldsMixin): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) body = RichTextField(blank=True) feed_image = models.ForeignKey( 'tuiuiuimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) api_fields = ( 'body', 'feed_image', ) + ContactFieldsMixin.api_fields search_fields = Page.search_fields + [ index.SearchField('body'), ]
class BlogIndexPage(Page): page_ptr = models.OneToOneField(Page, parent_link=True, related_name='+', on_delete=models.CASCADE) intro = RichTextField(blank=True) api_fields = ( 'intro', 'related_links', ) search_fields = Page.search_fields + [ index.SearchField('intro'), ] def get_blog_entries(self): # Get list of live blog pages that are descendants of this page entries = BlogEntryPage.objects.descendant_of(self).live() # Order by most recent date first entries = entries.order_by('-date') return entries def get_context(self, request): # Get blog entries entries = self.get_blog_entries() # Filter by tag tag = request.GET.get('tag') if tag: entries = entries.filter(tags__name=tag) paginator, entries = paginate(request, entries, page_key='page', per_page=10) # Update template context context = super(BlogIndexPage, self).get_context(request) context['entries'] = entries return context
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 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('tuiuiuimages: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 tuiuiu.tuiuiuimages.permissions import permission_policy return permission_policy.user_has_permission_for_instance( user, 'change', self) class Meta: abstract = True