Photologue : Nice Image Management for Django with DRF Integration
Tags: django, python, webdev, opensource
Published: 2021-03-20 10:58:43

## Concepts

A powerful image management and gallery application for the Django web framework. Upload photos, group them into galleries, apply effects such as watermarks.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/80ogkcayq9iyamvb1bnc.png)

## Dependencies

* Django.
* Pillow.
* Django-sortedm2m. (Automatically installed with django-photologue)

## Installation

The easiest way to install Photologue is with pip; this will give you the latest version available on PyPi:

```bash
pip install django-photologue
```

## Getting Started

### Prerequisites

Ensure that you have set *MEDIA_URL* and *MEDIA_ROOT* in *setting.py* :

```python
#settings.py

MEDIA_URL = /media/
MEDIA_ROOT = /data/media/
```

### Configuration


1) Add *photologue*, *sortedm2m*, *django.contrib.sites* in *INSTALLED_APPS* and *SITE_ID=1*.

```python
#settings.py
SITE_ID = 1

INSTALLED_APPS = [
	 ...
	 
    'django.contrib.sites',

    # Django Image Library - Photologue
    'photologue',
    'sortedm2m',
]
```

2) Apply photologue migrations

```bash
python manage.py migrate photologue
```

3) Register photologue urls in *urls.py*

```python
urlpatterns = [
	...
	# Photologue Urls
	path('photologue/', include('photologue.urls', namespace='photologue')),
]
```

### Models

Add the photologue Photo field in your model : 

```python
image = models.ForeignKey(
    'photologue.Photo',
    null=True,
    blank=True,
    on_delete=models.SET_NULL,
)
```

Then apply migrations.

```bash
python manage.py makemigrations
python manage.py migrate
```


Example of model : 

```python
class BlogPost(models.Model):
    uuid = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        help_text="The uuid4 primary key of the post",
    )
    title = models.CharField(
        max_length=255,
        help_text="The title of the post",
    )
    content = models.TextField(
        blank=True,
        help_text="The content of the post",
    )
    publishers = models.ManyToManyField(
        'auth.User',
        related_name='posts',
        help_text="The maintainers of the pods",
    )
    preview_image = models.ForeignKey(
        'photologue.Photo',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        help_text="The preview image of the appx",
    )
    
    def __str__(self):
        return f'Appx: {self.name}'
```

### Gallery

You can add a Gallery field which is a M2M of Photo field.

```python
gallery = models.ForeignKey(
    'photologue.Gallery',
    null=True,
    blank=True,
    on_delete=models.SET_NULL,
)
```

### Photo Sizes

Go to the Django Admin Interface, then you can create new PhotoSize instances.
When a Photo instance will be created, an image will be created for each photo sizes with automatic crop and quality compression.

Here is examples of photo sizes :

* thumbnail (100 x 100)
* small (320 x 200)
* medium (640 x 400)
* large (960 x 600)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cb89iy6iavblc5d5t0b.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/630hocamctfj30u51hwk.png)

### Accessors

The base of Photologue is the Photo model. When an instance is created, methods will be automatically added in order to retrieve photos at various photosizes. E.g. if you have an instance of Photo called image, then the following methods will have been added automatically:

```python
preview_image.get_small_url()
preview_image.get_medium_url()
preview_image.get_large_url()
preview_image.get_raw_url()
```

These can be used in a custom template to display a thumbnail, e.g.:

```html
<a href="{{ preview_image.image.url }}">
    <img src="{{ preview_image.get_raw_url }}" alt="{{ preview_image.title }}">
</a>
```
### Utils

Here is an function that take an image and produce a Photo instance :

```python
from datetime import datetime
from django.utils.text import slugify
from photologue.models import Photo


def create_photo(image):
    title = f'{datetime.now()}'
    slug = slugify(title)
    photo = Photo.objects.create(
        title=title,
        slug=slug,
        image=image,
    )
    return photo

```

## DRF Integration

### Mixins

You can define this method *create_mixin_image_serializer()* which produces a MixinSerializer based on a image field name.

```python
from rest_framework import serializers
from photologue.models import PhotoSize
from collections import OrderedDict


def _get_image_factory(image_field_name, photo_size):
    def get_image(self, obj):
        request = self.context['request']
        if request and hasattr(obj, image_field_name):
            image = getattr(obj, image_field_name)
            if image is not None:
                get_url_method = getattr(image, f'get_{photo_size}_url')
                url = get_url_method()
                return request.build_absolute_uri(url)
        return None
    return get_image


def create_mixin_image_serializer(image_field_name):
    photo_sizes = PhotoSize.objects\
        .filter(id__gte=3)\
        .values_list('name', flat=True)

    class ImageSerializerMixin(serializers.ModelSerializer):
        pass

    ImageSerializerMixin._declared_fields = OrderedDict([
        (f'{image_field_name}_{photo_size}', serializers.SerializerMethodField())
        for photo_size in photo_sizes
    ])

    for photo_size in photo_sizes:
        setattr(
            ImageSerializerMixin,
            f'get_{image_field_name}_{photo_size}',
            _get_image_factory(image_field_name, photo_size),
        )
    return ImageSerializerMixin

```

### Serializers

1) You can instanciate a mixin image serializer with the field *preview_image* :

```python
PreviewImageSerializerMixin = create_mixin_image_serializer('preview_image')
```

Otherwise you can manually create the mixin image serializer :

```python
class PreviewImageSerializerMixin(serializers.ModelSerializer):
	preview_image_small = serializers.SerializerMethodField()
	preview_image_medium = serializers.SerializerMethodField()
	preview_image_large = serializers.SerializerMethodField()
	preview_image_raw = serializers.SerializerMethodField()

   def get_preview_image_small(self, obj):
       request = self.context['request']
       if request and hasattr(obj, 'preview_image'):
           image = getattr(obj, 'preview_image')
           if image is not None:
               url = image.get_small_url()
               return request.build_absolute_uri(url)
       return None

   def get_preview_image_medium(self, obj):
       request = self.context['request']
       if request and hasattr(obj, 'preview_image'):
           image = getattr(obj, 'preview_image')
           if image is not None:
               url = image.get_medium_url()
               return request.build_absolute_uri(url)
       return None

   def get_preview_image_large(self, obj):
       request = self.context['request']
       if request and hasattr(obj, 'preview_image'):
           image = getattr(obj, 'preview_image')
           if image is not None:
               url = image.get_large_url()
               return request.build_absolute_uri(url)
       return None

   def get_preview_image_raw(self, obj):
       request = self.context['request']
       if request and hasattr(obj, 'preview_image'):
           image = getattr(obj, 'preview_image')
           if image is not None:
               url = image.get_raw_url()
               return request.build_absolute_uri(url)
       return None

```

2) Here is an exemple of serializer : 

```python
class BlogPostSerializer(PreviewImageSerializerMixin):
    """
    Serializer for BlogPost instance
    """
    class Meta:
        model = BlogPost
        fields = ('uuid', 'title', 'content',
                  'preview_image_small', 'preview_image_medium',
                  'preview_image_large', 'preview_image_raw')

```

3) Define *create()* and *update()* in case of uploading image : 

```python
    def create(self, validated_data):
        preview_image = validated_data.pop('preview_image', None)
        instance = super(BlogPostSerializer, self).create(validated_data)

        # Save image field
        if preview_image:
            instance.preview_image = create_photo(preview_image)

        instance.save()
        return instance

    def update(self, instance, validated_data):
		  preview_image = validated_data.pop('preview_image', None)
        instance = super(BlogPostSerializer, self).update(instance, validated_data)

        # Update image field
        if preview_image:
            instance.preview_image = create_photo(preview_image)
 
        instance.save()
        return instance
```

## Notes for Production

You should integrate the photologue library step by step.

1) Install the package.

```bash
pip install django-photologue
```

2) Add *photologue*, *sortedm2m*, *django.contrib.sites* in *INSTALLED_APPS*

```python
INSTALLED_APPS = [
	 ...
	 
    'django.contrib.sites',

    # Django Image Library - Photologue
    'photologue',
    'sortedm2m',
]
```

```bash
python manage.py migrate photologue
```

3) Apply photologue migrations

```bash
python manage.py migrate photologue
```

4) If you would like to replace an ImageField with a Photo in your models.py, you have to follow theses steps :

* Remove ImageField in your models.py
* Apply migrations 
* Add Photo in your models.py
* Apply migrations 

If you would like to keep your images, you should keep two fields:

* Add Photo in your models.py
* Apply migrations 
* Instanciates Photo
* Remove ImageField in your models.py
* Apply migrations 

## Links

* [https://django-photologue.readthedocs.io/](https://django-photologue.readthedocs.io/)

* [https://www.django-photologue.net/](https://www.django-photologue.net/)

* [https://github.com/richardbarran/django-photologue/wiki/Examples-and-forks](https://github.com/richardbarran/django-photologue/wiki/Examples-and-forks)