I built an image upload API with Python Django. There are a fixed number of 6 slots, which can either be null or filled with something. There are two entities, Profile, which contains the six photos, and an overridden save method. The other entity involved is Upload, which contains the timestamp when it was uploaded at, and the file which is an ImageField.
The magic happens in views.py
where the /users/profile/
request is processed. This request takes in optional image parameters photo1
, photo2
, photo3
, ..., photo6
. If one of these photo slots in the request are already filled, it calls the swap_photos()
function which deletes the current photo from both the mediafiles/
directory and the Upload entity in the SQLite database, then creates a new photo in place of the deleted photo. The save process is quite complicated to avoid any ForeignKey constraint errors which I spent a considerable amount of time troubleshooting. Here is my code:
views.py:
def swap_photos(photo_obj, new_photo_val, user_profile, photo_id):
"""Deletes the photo in the old reference and adds the photo in the request"""
Upload.delete(photo_obj)
if photo_id == 1:
user_profile.photo1 = Upload.objects.create(file=new_photo_val)
elif photo_id == 2:
user_profile.photo2 = Upload.objects.create(file=new_photo_val)
elif photo_id == 3:
user_profile.photo3 = Upload.objects.create(file=new_photo_val)
elif photo_id == 4:
user_profile.photo4 = Upload.objects.create(file=new_photo_val)
elif photo_id == 5:
user_profile.photo5 = Upload.objects.create(file=new_photo_val)
elif photo_id == 6:
user_profile.photo6 = Upload.objects.create(file=new_photo_val)
@api_view(['POST', 'PUT'])
def profile_create_or_update(request):
"""Creates or updates a profile"""
user_profile = Profile.objects.filter(user=request.user).first()
# Photo logic
if 'photo1' in request.data:
if user_profile.photo1 is not None:
swap_photos(user_profile.photo1, request.data['photo1'], user_profile, 1)
else:
photo1 = Upload.objects.create(file=request.data['photo1'])
user_profile.photo1 = photo1
if 'photo2' in request.data:
if user_profile.photo2 is not None:
swap_photos(user_profile.photo2, request.data['photo2'], user_profile, 2)
else:
photo2 = Upload.objects.create(file=request.data['photo2'])
user_profile.photo2 = photo2
if 'photo3' in request.data:
if user_profile.photo3 is not None:
swap_photos(user_profile.photo3, request.data['photo3'], user_profile, 3)
else:
photo3 = Upload.objects.create(file=request.data['photo3'])
user_profile.photo3 = photo3
if 'photo4' in request.data:
if user_profile.photo4 is not None:
swap_photos(user_profile.photo4, request.data['photo4'], user_profile, 4)
else:
photo4 = Upload.objects.create(file=request.data['photo4'])
user_profile.photo4 = photo4
if 'photo5' in request.data:
if user_profile.photo5 is not None:
swap_photos(user_profile.photo5, request.data['photo5'], user_profile, 5)
else:
photo5 = Upload.objects.create(file=request.data['photo5'])
user_profile.photo5 = photo5
if 'photo6' in request.data:
if user_profile.photo6 is not None:
swap_photos(user_profile.photo6, request.data['photo6'], user_profile, 6)
else:
photo6 = Upload.objects.create(file=request.data['photo6'])
user_profile.photo6 = photo6
user_profile.save()
return Response(status=status.HTTP_200_OK)
photos/models.py:
class Upload(models.Model):
uploaded_at = models.DateTimeField(auto_now_add=True)
file = models.ImageField()
@property
def is_valid(self):
"""
A photo is valid if the datetime flag has value
"""
return bool(self.uploaded_at)
def delete(self, *args, **kwargs):
super(Upload, self).delete(*args, **kwargs)
os.remove(os.path.join(settings.MEDIA_ROOT, self.file.name))
def save(self, *args, **kwargs):
if not self.pk:
super(Upload, self).save(*args, **kwargs)
def __str__(self):
return self.file.name
users/models.py:
import datetime
from django.contrib.auth.models import AbstractUser, BaseUserManager, PermissionsMixin
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.forms import ValidationError
from photos.models import Upload
from .enums import get_choices, ReligionConstants, PoliticsConstants, \
ChildrenStatusConstants, EthnicityConstants, SeekingConstants, PetsConstants, \
AlcoholConstants, SmokingConstants, MarijuanaConstants, EducationLevelConstants
def age_from_birthday(birthday: datetime.date, /) -> int:
return (datetime.date.today() - birthday).days // 365
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
"""Create and save a User with the given email and password."""
if not email:
raise ValueError('The given email was not set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
profile = Profile.objects.create(user=user)
profile.save()
return user
def create_user(self, email, password=None, **extra_fields):
"""Create and save a regular User with the given email and password."""
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
"""Create and save a SuperUser with the given email and password."""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('location', {"town": "Jackson", "state": "New Jersey",
"country": "United States of America", "latitude": 40.09188, "longitude": -74.35875})
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields)
class User(AbstractUser, PermissionsMixin):
"""User characteristics that aren't expected to change often"""
id = models.BigAutoField(primary_key=True)
username = None
email = models.EmailField(_('email address'), unique=True)
first_name = models.CharField(max_length=16, default="")
class Gender(models.TextChoices):
MALE = "MALE", 'Male'
FEMALE = "FEMALE", 'Female'
gender = models.CharField(max_length=6, choices=Gender.choices)
@property
def age(self) -> int:
return age_from_birthday(self.birthday)
def age_validator(birthday):
if not age_from_birthday(birthday) >= 18:
raise ValidationError(_("User must be over 18"))
birthday = models.DateField(validators=[age_validator])
likes = models.ManyToManyField("self", blank=True)
# LocationField() # Town, State, Country, Latitude, Longitude
location = models.JSONField(default=dict)
# {"town": "Jackson", "state": "New Jersey", "country": "United States of America", "latitude": 40.09188, "longitude": -74.35875}
date_joined = models.DateField(auto_now_add=True)
force_reset_password = models.BooleanField(
default=False, verbose_name="Force reset password on next login")
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'birthday', 'gender']
objects = UserManager()
class Profile(models.Model):
"""User characteristics that are expected to change often"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
photo1 = models.ForeignKey(Upload, related_name='photo1', null=True, blank=True, on_delete=models.SET_NULL)
photo2 = models.ForeignKey(Upload, related_name='photo2', null=True, blank=True, on_delete=models.SET_NULL)
photo3 = models.ForeignKey(Upload, related_name='photo3', null=True, blank=True, on_delete=models.SET_NULL)
photo4 = models.ForeignKey(Upload, related_name='photo4', null=True, blank=True, on_delete=models.SET_NULL)
photo5 = models.ForeignKey(Upload, related_name='photo5', null=True, blank=True, on_delete=models.SET_NULL)
photo6 = models.ForeignKey(Upload, related_name='photo6', null=True, blank=True, on_delete=models.SET_NULL)
seeking_choice = models.CharField(choices=get_choices(SeekingConstants), default=SeekingConstants.BOTH_MEN_AND_WOMEN, null=True, blank=True, max_length=18)
ethnicity = models.CharField(choices=get_choices(EthnicityConstants), null=True, blank=True, max_length=27)
children_status = models.CharField(choices=get_choices(ChildrenStatusConstants), null=True, blank=True, max_length=25)
religion = models.CharField(choices=get_choices(ReligionConstants), null=True, blank=True, max_length=20)
politics = models.CharField(choices=get_choices(PoliticsConstants), null=True, blank=True, max_length=12)
pets = models.CharField(choices=get_choices(PetsConstants), null=True, blank=True, max_length=7)
alcohol = models.CharField(choices=get_choices(AlcoholConstants), null=True, blank=True, max_length=9)
smoking = models.CharField(choices=get_choices(SmokingConstants), null=True, blank=True, max_length=9)
marijuana = models.CharField(choices=get_choices(MarijuanaConstants), null=True, blank=True, max_length=9)
education_level = models.CharField(choices=get_choices(EducationLevelConstants), null=True, blank=True, max_length=28)
def save(self, *args, **kwargs):
if self.photo1 is not None:
self.photo1.save()
if self.photo2 is not None:
self.photo2.save()
if self.photo3 is not None:
self.photo3.save()
if self.photo4 is not None:
self.photo4.save()
if self.photo5 is not None:
self.photo5.save()
if self.photo6 is not None:
self.photo6.save()
super(Profile, self).save(*args, **kwargs)
photos/models.py:
import os
from django.db import models
from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage
# Create your models here.
class PublicMediaStorage(S3Boto3Storage):
location = 'media'
default_acl = 'public-read'
file_overwrite = False
class Upload(models.Model):
uploaded_at = models.DateTimeField(auto_now_add=True)
file = models.ImageField()
@property
def is_valid(self):
"""
A photo is valid if the datetime flag has value
"""
return bool(self.uploaded_at)
def delete(self, *args, **kwargs):
if os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.local':
super(Upload, self).delete(*args, **kwargs)
os.remove(os.path.join(settings.MEDIA_ROOT, self.file.name))
elif os.environ.get('DJANGO_SETTINGS_MODULE') == 'config.settings.dev':
super(Upload, self).delete(*args, **kwargs)
# os.remove(os.path.join(settings.MEDIA_ROOT, self.file.name))
def save(self, *args, **kwargs):
if not self.pk:
super(Upload, self).save(*args, **kwargs)
def __str__(self):
return self.file.name
What are some more efficient ways to write this code out for both increased readability, maintainability, performance, and security if necessary?