wagtail set auto focal-point during upload - django

I notice we can set custom image model on wagtail here: https://docs.wagtail.io/en/v2.9/advanced_topics/images/custom_image_model.html
I am trying to add an automated focal point during upload max max 200*220px.
I tried a lot following its above documentation.
from django.db import models
from wagtail.images.models import Image, AbstractImage, AbstractRendition
class CustomImage(AbstractImage):
# Add any extra fields to image here
# eg. To add a caption field:
# caption = models.CharField(max_length=255, blank=True)
admin_form_fields = Image.admin_form_fields + (
# Then add the field names here to make them appear in the form:
# 'caption',
)
class CustomRendition(AbstractRendition):
image = models.ForeignKey(CustomImage, on_delete=models.CASCADE, related_name='renditions')
class Meta:
unique_together = (
('image', 'filter_spec', 'focal_point_key'),
Can anyone please help me get it done setting up custom focal point?
Thanks
Anamul

You probably do not need a custom image model to achieve this goal, Django has a built in system called signals. This lets you listen to creation & editing (plus others) of any existing Django model and modify the data before it is saved to the DB.
A good example of this in use already in Wagtail is the feature detection system, which will automatically add a focal point on save if faces are detected.
You can see how this has been implemented in the source code, wagtail/images/signal_handlers.py.
You may need to understand how to build up a focal point, depending on how you want to calculate it but basically you need to call set_focal_point on your image instance. This method must be supplied an instance of a Rect which can be found in the source at images/rect.py.
It is important to understand how to call your signal handlers registration function, I found this Stack Overflow answer to be helpful. However, it might be simpler to just add it to your wagtail_hooks.py file as you know it will be run at the right time (when the app is ready & models are loaded.
You can read more at the Django docs for app.ready() if you would prefer not to rely on the wagtail_hooks.py approach.
Example Implementation
myapp/signal_handlers.py
from django.db.models.signals import pre_save
from wagtail.images import get_image_model
from wagtail.images.rect import Rect
def pre_save_image_add_auto_focal_point(instance, **kwargs):
# Make sure the image doesn't already have a focal point
# add any other logic here based on the image about to be saved
if not instance.has_focal_point():
# this will run on update and creation, check instance.pk to see if this is new
# generate a focal_point - via Rect(left, top, right, bottom)
focal_point = Rect(15, 15, 150, 150)
# Set the focal point
instance.set_focal_point(focal_point)
def register_signal_handlers():
# important: this function must be called at the app ready
Image = get_image_model()
pre_save.connect(pre_save_image_add_auto_focal_point, sender=Image)
myapp/wagtail_hooks.py
from .signal_handlers import register_signal_handlers
register_signal_handlers()

Related

Django - adding gamification features

I have a medium size Django REST app that I'm looking to add gamification features to.
The application in question is a school webapp where students can create mockup quizzes, participate in exams that teachers publish, and write didactical content that gets voted by other students and teachers.
I want to add some gamification features to make the app more interesting and to incentivize participation and usage of the various features: for example, each student will have a personal "reputation" score, and gain points upon completing certain actions--a student may gain points when completing a quiz with a high score, when submitting some content, or when receiving upvotes to such content.
The tricky part is I want to be able to have this logic be as separate as possible from the existing codebase, for various reasons: separation of concerns, ability to plug the engine in/out if needed, ability to easily deactivate features for certain groups of users, etc.
What I'm looking for here is some software engineering advice that's also Django-specific. Here's a high level description of what I'm thinking of doing--I'd like some advice on the approach.
create a new gamification app. Here I will have models that describe a change in reputation for a user and possibly other related events. The app should also send notifications when gamification-related events occur
from the gamification app, expose a callback-based interface, which the other primary app can call into to dispatch events
use the django-lifecycle package to call the callbacks from gamification when triggers occur.
This way, my existing models would only get touched to register the triggers from django-lifecycle (similar to signals). For example, let's say I want to give students points when they turn in an assignment. Let's say I have an AssignmentSubmission model to handle assignment submissions. With the added lifecycle hook, it'd look like this:
class AssignmentSubmission(models.Model):
NOT_TURNED_IN = 0
TURNED_IN = 1
STATES = ((NOT_TURNED_IN, 'NOT_TURNED_IN'), (TURNED_IN, 'TURNED_IN'))
user = models.ForeignKey(user)
assignment = models.ForeignKey(assignment)
state = models.PositiveSmallIntegerField(choices=STATES, default=NOT_TURNED_IN)
#hook(AFTER_UPDATE, when="state", was=NOT_TURNED_IN, is_now=TURNED_IN)
def on_turn_in(self):
get_gamification_interface().on_assignment_turn_in(self.user)
The on_assignment_turn_in method might look something like:
def on_assignment_turn_in(user):
ReputationIncrease.objects.create(user, points=50)
notifications.notify(user, "You gained 50 points")
This is pretty much just a sketch to give an idea.
I am unsure how get_gamification_interface() would work. Should it return a singleton? Maybe instantiate an object? Or return a class with static methods? I think it'd be best to have a getter like this as opposed to manually importing methods from the gamification app, but maybe it could also create too much overhead.
What's a good way to handle adding "pluggable" features to a project that are inherently entangled with existing models and business logic while also touching those as little as possible?
The foreign key approach is fine. You can easily chain and link queries using information from existing tables and you could even avoid touching the original code by importing your models to the new app. You can use Django signals in your new app and ditch the django-lifecycle extension to avoid adding lines to your core models. I used the following approach to keep track of modified records in a table; take a TrackedModel with fields field_one, field_two, field_n... which will be tracked by one of your new app's model, namely RecordTrackingModel:
from parent_app.models import TrackedModel # The model you want to track from a parent app.
from django.db.models.signals import post_save # I'm choosing post_save just to illustrate.
from django.dispatch import receiver
from datetime import datetime
class RecordTrackingModel(models.Model):
record = models.ForeignKey(TrackedModel, verbose_name=("Tracked Model"), on_delete=models.CASCADE)
field_one = models.TextField(verbose_name=("Tracked Field One"), null=True, blank=True) # Use same field type as original
field_two = models.TextField(("Tracked Field Two"))
field_n = ...
notes = models.TextField(verbose_name=("notes"), null=True, blank=True)
created = models.DateTimeField(verbose_name=("Record creation date"), auto_now=False, auto_now_add=True)
#receiver(post_save, sender=TrackedModel) # Here, listen for the save signal coming from a saved or updated TrackedModel record.
def modified_records(instance, **kwargs):
record = instance
tracked_field_one = instance.field_one
tracked_field_two = instance.field_two
tracked_field_n = another_function(instance.field_n) #an external function that could take a field content as input.
...
note = 'Timestamp: ' + str(datetime.now().isoformat(timespec='seconds'))
track_record = RecordTrackingModel.objects.create(record=record, field_one=tracked_field_one, field_two=tracked_field_two, field_n=tracked_field_n, ..., notes=note)
return track_record
There's no need to add functions to your pre-existing models as the signal dispatcher triggers whenever a save or delete signal appears at TrackedModel. Then you could place "if" statements for wether or not to perform actions based on field values, i.e.: just pass if an AssignmentSubmission record has a "Not Turned In" status.
Check Django signals reference for more information about when they trigger.
Additionally, I would suggest to change the "state" field to boolean type and rename it to something more explicit like "is_turned_in" for ease of use. It will simplify your forms and code. Edit: For more than 2 choices (non-boolean), I prefer using ForeignKey instead. It will let you modify choices over time easily from the admin site.
Edit:
Another approach could be mirroring the original models in your gamification app and call for a mirror record update when a save method is used in the original model.
gamification_app/models.py:
from parent_app.models import OriginalModel # The model you want to track from a parent app.
from django.db.models.signals import post_save # I'm choosing post_save just to illustrate.
from django.dispatch import receiver
from datetime import datetime
def gamification_function(input, parameters):
output = *your gamification logic*
return output
class MirrorModel(models.Model):
original_model = (OriginalModel, verbose_name=("Original Model"), on_delete=models.CASCADE)
field_one = ... #Same type as the original
field_two = ...
field_n = ...
#hook(AFTER_UPDATE, when="field_n", was=OPTION_1, is_now=OPTION_2)
def on_turn_in(self):
gamification_function(self.field, self.other_field)
#receiver(post_save, sender=OriginalModel) # Here, listen for the save signal coming from the original app change record.
def mirror_model_update(instance, **kwargs):
pk = instance.pk
mirror_model = []
if MirrorModel.objects.get(original_model.pk=pk).exists():
mirror_model = MirrorModel.objects.get(original_model.pk=pk)
mirror_model.field_one = instance.field_one # Map field values
*other logic ...*
mirror_model.save() # This should trigger your hook
else:
mirror_model = MirrorModel(original_model = instance, field_one = instance.field_one, ...)
mirror_model.save() #This should trigger your hooks as well, but for a new record
This way you can decouple the gamification app and even choose not to keep a record of all or the same fields as the original model, but fields specific to your functionality needs.
Your idea was good.
In the gamification app add your views, protect it with LoginRequiredMixin, and extend it with a check if a related record in the AssignmentSubmission table exists and is turned on.
In this way you have a 100% separated gamification views, models, logic, ecc...
In the existing views you can add a link to a gamification view.

set default image to ImageField for existing objects in django

I have a model which has an ImageField.
class A(model.Model):
blah = CharField(max_length=10)
profile = ImageField(upload_to='uploads/', null=True, blank=True)
I want to set a default image for existing data, so I tried saving the file in shell like the following:
>>> myapp.models import A
>>> from django.core.files import File
>>> for a in A.objects.all():
>>> a.profile.save('default.jpg', File(open('/Users/myusername/Pictures/default.jpg', 'rb')))
I was quite happy, until I found out that in myprojectroot/media/uploads/ there are as many defaultblablah.jpg files as the number of A objects.
I'd like to know if there's any way to set the same image file with all objects as you do with update() method.
Could someone shed some lights on it?
Thank you in advance.
Yes, you can store a file default.jpg in the uploads/ directory, and then update the objects with:
A.objects.filter(profile=None).update(profile='uploads/default.jpg')
I would also advise to make the field non-nullable, and use as default an value:
class A(model.Model):
blah = CharField(max_length=10)
profile = ImageField(
upload_to='uploads/',
default='uploads/default.jpg',
blank=True
)
Behind the curtains an ImageField is a varchar field in the database, that stores a path to the image you use. The Django ImageField has extra logic such that it presents it as a FieldFile [Django-doc] with some logic, but it is stored in the database as a string.
The reason why it will generate multiple images, is because you can later decide to edit one of the images. If that is the case, all of the other images would see these edits. So if the filename "clashes", it will make copies to prevent this. If you however make use of a default image, the idea is to not modify that image. Therefore it might also be a good idea to mark the file as readonly.

Manually trigger image save method in Django model

I have a Django model with a customized image field. The image field creates some thumbnail sizes on upload. Code may look like this:
from django.db import models
from utils import CustomImageField
class Photo(models.Model):
image = CustomImageField()
Now I modify the original image, let's say I rotate it. And now I want to trigger the save method of my image field again, in order to overwrite the thumbnails and create rotated versions. So, I don't need to rotate the thumbnails elsewhere in my code (DRY).
Any thoughts? Something along those lines - but how exactly?
p = Photo.objects.get(pk=1)
p.image.save(...)
I have full control over the CustomImageField widget. The save() method is defined as:
def save(self, name, path, save=True):
Question is, what do I use for the methods parameters?
This question looks like a duplicate of Programmatically saving image to Django ImageField
The parameters of the ImageField.save() method are documented for FileField.save() (of which ImageField is a subclass):
https://docs.djangoproject.com/en/1.9/ref/models/fields/#django.db.models.fields.files.FieldFile.save
Takes two required arguments: name which is the name of the file, and
content which is an object containing the file’s contents. The
optional save argument controls whether or not the model instance is
saved after the file associated with this field has been altered.
Defaults to True.
Here is what is working for us:
class CustomImage(models.Model):
image = models.ImageField(upload_to=get_file_path, max_length=500)
orig_name = models.TextField()
This is the method that adds an image file to the ImageField from an http resource:
from django.core.files.base import ContentFile
def download_photo(amazon_id, url):
img_data = requests.get(url)
img = CustomImage(orig_name=img_data.url)
img.image.save(slugify(img.orig_name), ContentFile(img_data.content), save=True)
It also works without ContentFile:
new_img = File(open(different_obj.image.path), 'r')
img.image.save(different_obj.image.url, new_img, save=True)
See also:
- https://docs.djangoproject.com/en/1.9/topics/files/
- https://djangosnippets.org/snippets/2587/
An option is dirty field checking, either manually (see this SO question) or using a pypi package
Alternatively, if you want to conserve memory, the resizing can be triggered from the field's property setter (assuming you inherit from FileField
class CustomImageField(FileField):
def _set_file(self, file):
has_file_changed = file != self._file
super(CustomImageField, self)._set_file(file)
if has_file_changed:
self.handle_resizing_etc()
# need to redeclare property so it points to the right _set_file
file = property(FileField._get_file, _set_file, FileField._del_file)
disclaimer: I haven't used this approach in production code and I haven't written a proof of concept before posting this answer, so it might not work as desired

How to resize source with sorl-thumbnail?

I was searching the web for my question and couldn't find a clear answeror any example.
Basically, I want to use sorl and want to resize the source image during the Model save time to resize it down to 640x480 size, so that I don't end-up storing user's original 2.5 MB files on the disk. I will then use templatetags to create regular thumbnails out of my source as documented in sorl.
I came across couple of sources refer to use ThumbnailField model field that is supposed to be available in sorl.thumbnail.fields. See the link here. However, in my up-to-date sorl copy from the trunk I don't see any ThumbnailField or ImageWithThumbnailsField. My attempt to import it in the model fails accordingly. I see these references are old though and wondering whether I can achieve the same with up-to-date sorl.
On the other hand sorl documentation indicates only ImageField from sorl.thumbnail (see here) that does not have any size argument to control the source resizing.
BTW, I see this functionality is available with easy_thumbnail that takes an input parameter source_resize.
Any help will be appreciated!
SUMMARY
I accepted the below answer, however I feel natural sorl support for this use case can be very useful - i.e. adding resize_source param to sorl's ImageField to allow resizing the source image. Below are two factors why this can be useful in the field:
Not to store user's huge original images if your app has no need for it. Saving disk space.
Not to spend extra CPU for resizing thumbnails from that huge source images if you don't have specific extreme high quality reasons. To avoid this one may write nested tags in templates to thumbnail from smaller size images but it can become annoying very soon.
I found a flaw in the code above, got “str has no method chunck()”, if somebody want to use it. Here is my fix:
from sorl.thumbnail import get_thumbnail
from django.core.files.base import ContentFile
class Foo(models.Model):
image = models.ImageField(upload_to...)
def save(self, *args, **kwargs):
if not self.id:
super(Foo, self).save(*args, **kwargs)
resized = get_thumbnail(self.image, "100x100" ...)
self.image.save(resized.name, ContentFile(resized.read()), True)
super(Foo, self).save(*args, **kwargs)
Sorl's ImageField that you mention, is just a normal Django ImageField with the added benefit of managing the deletion of cached thumbnails. There's no resizing done on the initial upload - that's something you have to implement yourself manually via the view you are using to upload. The docs show how to go about this. You can use sorl in that view to do the actual resize operation itself, using the low level API examlpes
EDIT
A quicker alternative is to just resize the image when the model is being saved using sorl. You can do something like the following (completely untested though!)
from sorl.thumbnail import get_thumbnail
class Foo(models.Model):
image = models.ImageField(upload_to...)
def save(self, *args, **kwargs):
if not self.id:
# Have to save the image (and imagefield) first
super(Foo, self).save(*args, **kwargs)
# obj is being created for the first time - resize
resized = get_thumbnail(self.image, "100x100" ...)
# Manually reassign the resized image to the image field
self.image.save(resized.name, resized.read(), True)
super(Foo, self).save(*args, **kwargs)
this will mean that you will have 2 versions of the same image on disk - one where the django image field decides to save it (upload_to path) and one where sorl thumbnail has saved it's resized thumbnail. This, along with the fact the image is uploaded and saved twice, are the disadvantages of this approach. It's quicker to implement though
I was looking for solution for some time and eventually wrote app django-resized.
The following code uses the PIL Engine (part of sorl-thumbnail) to crop an image called picture.jpg (tested using Python 3.8 and sorl-thumbnail==12.6.3):
#
# Change this import to get the Engine of your underlying libraries.
# Options are: convert_engine, pgmagick_engine, pil_engine, vipsthumbnail_engine or wand_engine.
#
from sorl.thumbnail.engines.pil_engine import Engine
# This object has all we need
engine = Engine()
#
# When receiving data from a request
# you probably have a BytesIO instance ready to use like:
#
# im = engine.get_image(my_bytes_io)
#
with open("picture.jpg", "rb") as f:
im = engine.get_image(f)
im_crop = engine.crop(im, (535, 535), options={'crop': 'smart'})
im_crop.save("picture-thumb.jpg")
Instead of modifying the save method, I would have a helper function for reducing the image size (with the lines above) and call it from the Django view or form before updating the image field. Though it would work doing it on the save itself.
On the other hand, the Engine API has more useful features that could be useful! This API has been there since the first commit so, in my opinion, it's unlikely to change in the future: create, cropbox, orientation, flip_dimensions, colorspace, remove_border, calculate_scaling_factor, scale, crop, rounded, blur, padding, write, cleanup, get_image_ratio, get_image_info, get_image, get_image_size, is_valid_image.

How to force order of pictures in a gallery

I'm building a django app that has an image gallery, and the client insists the images be displayed in specific order. I use the admin interface to upload the images and edit their properties, and I have an ImageFile class in my model that basically looks like this:
class ImageFile(models.Model):
"""represents an image file"""
# the image description
description = models.CharField(max_length=45)
# the actual image
image = models.ImageFile(upload_to='images')
# running number representing the order on the page
order = models.IntegerField()
def __unicode__(self):
return "%s" % (self.description)
class Meta:
db_table = 'images'
I'm using the IntegerField 'order' to have running number that'll control the sorting. I figured there has to be a smarter/better way to do this (another model?) and also be able to easily control it through the admin interface.
I suppouse you would like give possibility to sort images to user, (anyway if you want sort it via time add, best way is order it by id), so, if there is model like Gallery (of images) maybe you should store tuple of ids of images from the galery (in DB as a text object). After read cast it to tuple, and you have expected order. Hope I help.
if the order of the images is the order that they are uploaded you could use a timestamp to order them,.
I used the same method (with integer "order" field in the model) to define the ordering. However, I customized the admin to allow drag and drop the images belong to an album to define the order. When the admin hits "save" button, the order will be calculated automatically based on the current order after drag-and-drop. All data will be submitted to the server for saving to DB.