1

I have some images inside the static directory and want to create a model that has a ImageField. I want to set the default field to any one of those images. I have tried using this -

def randomImage():
    return ImageFile('media/blog/image/' + str(random.randrange(1, 15, 1)) + '.jpg')

# ----------------------- Model for each post in the blog-------------------
class Post(models.Model):
    heading = models.CharField(max_length=150)
    author = models.ForeignKey(User)
    postBody = models.TextField()
    postDate = models.DateTimeField('posting date')
    postImage = models.ImageField(upload_to='media/blog/image/'+str(int(time.time())), default=randomImage)
user3324847
  • 25
  • 2
  • 7
  • This is related: http://stackoverflow.com/questions/1276887/default-image-for-imagefield-in-djangos-orm – abidibo Apr 09 '15 at 10:43
  • @abidibo No the above link doesn't tells anything about using random image path generating function. What I want is to use a function that returns a random image path and then to set it as the image for that record. – user3324847 Apr 13 '15 at 15:08
  • I'm putting a bounty on this as this question has no answer, and the linked answer is completely unrelated. – Routhinator Sep 27 '18 at 00:05
  • @Routhinator I've added one answer, Try it :) – JPG Sep 27 '18 at 03:50
  • @JPG Taking a look, will award bounty once I confirm it works well. :) – Routhinator Sep 27 '18 at 13:20
  • I could attach a minimal project link of the same if necessary :) – JPG Sep 27 '18 at 13:22
  • I have a number of issues with this solution which I'm trying to workout before I provide the challenges. One is that django doesn't look in static for the files but looks at media, which is not where the defaults are. Another is that the files in static do not exist when django.setup() is called, but static cannot be collected until django.setup is called. Another is that the function needs settings.BASE_DIR for your file check, but needs to return a relative path without BASE_DIR for the default – Routhinator Sep 27 '18 at 14:09
  • Sortof working through the issues, I've had to add exception handing to the function in order to allow django startup to pass, however the logic is always failing to find images that exist.... still trying to workout why. – Routhinator Sep 27 '18 at 15:36
  • I've also had to make STATIC_ROOT a subdirectory of MEDIA_ROOT – Routhinator Sep 27 '18 at 15:37

2 Answers2

7

Here I'm taking some assumptions,
1. Your default images are inside the static directory
2. Inside the static directory, all files are images

What is the major trick here ?

Django needs a valid file path only to create or update a file entry (image is also a file). So, what we're doing here is, list out all the files (assuming those are only images) and picking up one entry randomly using random.choice() and retun an absoulute path (something like static/my_img.jpg) to the default argument

import time
from random import choice
from os.path import join as path_join
from os import listdir
from os.path import isfile


def random_img():
    dir_path = 'static'
    files = [content for content in listdir(dir_path) if isfile(path_join(dir_path, content))]
    return path_join(dir_path, choice(files))


class Post(models.Model):
    # other fields
    postImage = models.ImageField(
        upload_to='media/blog/image/' + str(int(time.time())),
        default=random_img)



UPDATE-1

I've created a minimal example in Django 2.1.1 which can be found in follwing git repo
Repopsitory link -> django2X
1. clone the repository,create a virtual env and install the dependencies (provided a requirements.txt file)
2.create a new superuser or use mine (username -> admin, pass-> jerin123@)
3.run the server and login to django admin : (screenshot-1)
4. Create a new Post instance (again and again)

That's it

UPDATE-2

I've made few changes in my minimal example, which are
1. Created a simple object creation (here, the Post model object) while every django startup (checkout sample/app.py)
2. Added MEDIA_ROOT and MEDIA_URL to the settings.py file

Now, start your project (it will create 3 objects per every startup) and go to http://127.0.0.1:8000/admin/sample/post/.
Then open any instance and click on the image link (screenshot-2) and it will open the image (screenshot-3)

JPG
  • 82,442
  • 19
  • 127
  • 206
  • Looks like this won't work. Django executes the random function when django.start() is called, which is before static is collected. It gets a default response I added for if the files do not exist, and then it uses that response for the default setting statically. It does not call the function on model.save(), rendering the function useless. – Routhinator Sep 27 '18 at 15:45
  • Maybe I spoke too soon. It works for new users being registered after application start, but not for the users being save from the initial import on startup.. Trying to track down why. – Routhinator Sep 27 '18 at 15:57
  • 1
    What you mean by ***initial import***. I don't get that point – JPG Sep 27 '18 at 16:02
  • I'm migrating an exisiting site into Django. After I run staticfile collection and migrations in my startup.py script, I run a custom management command called "installlegacy" which checks to see if users and data have been imported. This uses a legacy db connection to grab the user objects from the old DB, and then maps their properties to the new user model, and then calls user.save() - For some reason during this process, the function fails to find any images, even though staticfiles have already been collected. – Routhinator Sep 27 '18 at 20:07
  • in order to give you a complete picture, I've created snippets of the startup script, import process, modified version of your example and the models.py for the members. The random_img() function is set on the default for the background image: https://gitlab.routh.io/users/Routhinator/snippets – Routhinator Sep 27 '18 at 23:16
  • I think what's actually happening here is the startup script is a single execution of the django code, thus the result of random_image is only called once and then used on every save in that execution, whereas the registration scenario where this works is an execution per request, so it works. – Routhinator Sep 28 '18 at 01:15
  • @Routhinator TLDR, When I look into [this line](https://gitlab.routh.io/snippets/5#L122) I found that you are provided a ***value*** rather than a ***callable function*** – JPG Sep 28 '18 at 03:35
  • to get the ***dynamic result of `random_img` fucntion***, you should provide the function as as ***callable*** (without *round brackets* ) – JPG Sep 28 '18 at 03:38
  • AFAIK, thus you can't pass any arguments to the function from a default argument of model – JPG Sep 28 '18 at 03:39
  • It does in fact work for requests once the app is loaded, with the callable and all, and if I collect static before the startup script is called, it loads one background, but then uses it for ALL the users on the import. Thus, I think the callable method works most of the time, however it will not work on multiple model saves in a single request/execution, as the model is only loaded once, and the value for default is only set on model load. So this has to be moved to the save function to work in 100% of situations – Routhinator Sep 28 '18 at 03:50
  • *"however it will not work on multiple model saves in a single request/execution"*, What you mean by this? and *"the value for default is only set on model load"* this isn't true. If you set a callable function, Django will call that function on **every object creation** – JPG Sep 28 '18 at 03:57
  • Can you make a ***minimal django project***, that reflects the current situation of yours? – JPG Sep 28 '18 at 03:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/180913/discussion-between-routhinator-and-jpg). – Routhinator Sep 28 '18 at 03:59
1

My solution is to override the model save method and check if the model is being created for the first time and also check if the postImage image field is empty. If so populate the postImage field with contents of a Radom image. Django will handle the rest

If we use the path of the Radom image directly in our model we will end up like serving some of the post model files from the media folder and some other from the static directory which is not recommended. Instead, we feed the image file content to the postImage field and Django will save the image to media folder and thus we can serve all our model images from media folder itself. Wola

Code

The following code is tested in Python 3.6 Please add the code to your models.py

from pathlib import Path
from random import randint
import time
from django.core.files import File
from django.db import models

allowed_image_extensions = ['.jpg', '.png', '.jpeg']


def get_file_extension(file_path):
    return Path(file_path).suffix


def get_files_in_directory(directory, absolute_path=False):
    from os import listdir
    from os.path import isfile
    only_files = [f for f in listdir(directory) if isfile("{}/{}".format(directory, f))]

    if not absolute_path:
        return only_files

    else:
        return ["{}/{}".format(directory, file_) for file_ in only_files]


def get_random_image_from_directory(directory_path, image_extension=None):
    files_in_directory_path = get_files_in_directory(directory_path, absolute_path=True)

    if image_extension:
        allowed_file_types = [image_extension]
    else:
        allowed_file_types = allowed_image_extensions

    # filter out files of type other than required file types
    filtered_files_list = [_file for _file in files_in_directory_path if
                       get_file_extension(_file).lower() in allowed_file_types]

   random_index = randint(0, len(filtered_files_list) - 1)
   random_file_path = filtered_files_list[random_index]
   random_file_content = File(open(random_file_path, 'rb'))

   return random_file_content


def get_post_image_path(instance, filename):
    path_first_component = 'posts'
    ext = get_file_extension(filename)
    current_time_stamp = int(time.time())
    file_name = '{}/posts_{}_{}{}'.format(path_first_component, instance.id, current_time_stamp, ext)
    full_path = path_first_component + file_name
    return full_path

class Post(models.Model):
    heading = models.CharField(max_length=150)
    author = models.ForeignKey(User)
    postBody = models.TextField()
    postDate = models.DateTimeField('posting date')
    postImage = models.ImageField(blank=True, null=True, upload_to=get_post_image_path)

    # override model save method
    def save(self, *args, **kwargs):

        # check model is new and postImage is empty
        if self.pk is None and not self.postImage:
            random_image = get_random_image_from_directory(settings.STATIC_ROOT)
            self.postImage = random_image
            random_image.close()


    super(Post, self).save(*args, **kwargs)

Also no need to set ‘/media’ in upload_to path. Django will read media path from settings variable

The best practice is to move those set of default images out of static directory to another folder probably another folder with name resources or any another meaningful name since the static directory contents will change frequently as the project grows

Iyvin Jose
  • 707
  • 5
  • 17
  • Will test this, I think it has promise in that this will force the random image to actually be called on save. I think what happens with @JPG 's method is it's called once per execution when the model is loaded by django, so my import only executes it once, before staticfiles are collected, and then uses the failure result for every user. Whereas this will be forcibly called on each call to save, and will work all the time. – Routhinator Sep 28 '18 at 03:42
  • Ah, this code is not immune to `django.core.exceptions.SuspiciousFileOperation: The joined path (/usr/src/app/static/images/avatars/wolf_avatar1.jpg) is located outside of the base path component (/usr/src/app/media)` – Routhinator Sep 28 '18 at 13:27
  • Managed to get it to work with a small change: .`self.postImage.save( str(self.postDate) + get_file_extension(random_image.name), random_image, save=True)` – Routhinator Sep 28 '18 at 13:56
  • Bounty awarded. This seems to be the most robust solution that works on every model save. Please update your answer with the small modification that was needed. – Routhinator Sep 28 '18 at 13:58
  • Two more small modifications. On this line you are creating a list of lists, which causes the extension filter to filter everything out, even if valid. It should be `allowed_file_types = allowed_image_extensions` (no [] around the var), and the absolute path is needed, so in model save we should actually call it with the django settings.STATIC_ROOT var: `get_random_image_from_directory(settings.STATIC_ROOT)` – Routhinator Sep 28 '18 at 15:09
  • One final modification `random_image.close()` needs to be called after save() or else performance dies. – Routhinator Sep 28 '18 at 15:52
  • @Routhinator Thanks for pointing out the mistakes. I have updated the code – Iyvin Jose Oct 01 '18 at 04:17