Get datetime now, not at server initialisation

48 views
Skip to first unread message

Clive Bruton

unread,
Oct 27, 2020, 6:21:19 PM10/27/20
to django...@googlegroups.com
I have a function that uses the current date to set up a file path
for uploaded images:

********************

def upload_path():
dtnow = datetime.now(timezone.utc)
dtstr = '{:%Y/%m/%d/%H/%M/%S}'.format(dtnow)
print('dtstr: ')
print(dtstr)
dtstr = 'images/items/' + dtstr
return dtstr

class Image(models.Model):
item = models.ForeignKey(Item, on_delete=models.CASCADE)
#file = ImageField(_('image'), upload_to='images')
file = ImageField(_('image'), upload_to=upload_path())

********************

The problem is that when the Image class calls 'upload_path' the
datetime portion of this is always the runserver initialisation time,
rather than the time when 'upload_path' is called/used.

How to fix this?

Thanks


-- Clive

Carles Pina i Estany

unread,
Oct 28, 2020, 5:00:35 AM10/28/20
to django...@googlegroups.com

Hi,

On Oct/27/2020, Clive Bruton wrote:

> I have a function that uses the current date to set up a file path for
> uploaded images:
>
> ********************
>
> def upload_path():

[...]

> class Image(models.Model):
> item = models.ForeignKey(Item, on_delete=models.CASCADE)
> #file = ImageField(_('image'), upload_to='images')
> file = ImageField(_('image'), upload_to=upload_path())


> The problem is that when the Image class calls 'upload_path' the
> datetime portion of this is always the runserver initialisation time,
> rather than the time when 'upload_path' is called/used.
>
> How to fix this?

For what I remeber (I use FileField usually but this is noto relevant I
hope) you should do:

def upload_path(instance, filename):
# you have access to the instance (object model Image in your case)
# and the filename that the user used to upload it
# and this is executed on the upload time
return your_new_file_name

In the model:
file = ImageField(_('image'), upload_to=upload_path)

note that upload_path is a callable: it gets called on the upload time.
In your code it was called on initialization time.

Some time ago I had a similar code that caused a bug: in a ListView the
"queryset = " is executed on startup time. For a dynamic one I should
have used get_queryset() method.

--
Carles Pina i Estany
https://carles.pina.cat

Clive Bruton

unread,
Oct 28, 2020, 5:40:56 PM10/28/20
to django...@googlegroups.com
Thanks, that was very helpful. I had another pointer on this and
found out that the easiest way to do this is just to change the
```file``` line in the class to:

```
file = ImageField(_('image'), upload_to='images/items/%Y/%m/%d/%
H/%M/%S')
````

There are a few things that I do not understand, however, after
playing around with some code for a while.

If I just pass a string to ```file``` it understands that this is a
partial path, and it has to stick ```filename``` on the end. eg:

```file = ImageField(_('image'), upload_to='images/items')```

Likewise with the version above with the date formatting.

This is also the case when I used my initial ```upload_path```
function, calling it with ```upload_to=upload_path()``` – but this
did not suit the task, because ```upload_path``` loaded at
initialisation of the server.

So, given that ```upload_path``` returns a string, I don't understand
why I also have to concatenate the ```filename```:

```
def upload_path(instance, filename):
dtnow = datetime.now(timezone.utc)
dtstr = '{:%Y/%m/%d}'.format(dtnow)
dtstr = 'images/items/' + dtstr + '/' + filename
return dtstr
```
In this case, if I do not concatenate ```filename``` then the
uploaded file gets named with the last element of the date formatting
and without a file extension, ie potentially like this:

```images/items/2020/10/28/20/59/55```

rather than:

```images/items/2020/10/28/20/59/55/image.jpg```

The other thing I don't understand is how ```instance``` and
```filename``` are passed to the function ```upload_path```. I would
expect to do something like:

``` file = ImageField(_('image'), upload_to=upload_path(instance,
filename))```

But if I do that I get:

```NameError: name 'instance' is not defined```

Sorry for so many more questions!


-- Clive
> --
> You received this message because you are subscribed to the Google
> Groups "Django users" group.
> To unsubscribe from this group and stop receiving emails from it,
> send an email to django-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/
> d/msgid/django-users/20201028085843.GA20091%40pina.cat.
>

Carles Pina i Estany

unread,
Oct 30, 2020, 7:02:45 AM10/30/20
to django...@googlegroups.com

Hi Clive,

On Oct/28/2020, Clive Bruton wrote:
> Thanks, that was very helpful. I had another pointer on this and found out
> that the easiest way to do this is just to change the ```file``` line in the
> class to:
>
> ```
> file = ImageField(_('image'), upload_to='images/items/%Y/%m/%d/%H/%M/%S')
> ````

I haven't used this method. I always pass a callable (passing a
function).

(I'm deleting a part of your message...)

> So, given that ```upload_path``` returns a string, I don't understand why I
> also have to concatenate the ```filename```:
>
> ```
> def upload_path(instance, filename):
> dtnow = datetime.now(timezone.utc)
> dtstr = '{:%Y/%m/%d}'.format(dtnow)
> dtstr = 'images/items/' + dtstr + '/' + filename
> return dtstr
> ```
> In this case, if I do not concatenate ```filename``` then the uploaded file
> gets named with the last element of the date formatting and without a file
> extension, ie potentially like this:
>
> ```images/items/2020/10/28/20/59/55```
>
> rather than:
>
> ```images/items/2020/10/28/20/59/55/image.jpg```

I see...

> The other thing I don't understand is how ```instance``` and ```filename```
> are passed to the function ```upload_path```. I would expect to do something
> like:
>
> ``` file = ImageField(_('image'), upload_to=upload_path(instance,
> filename))```
>
> But if I do that I get:
>
> ```NameError: name 'instance' is not defined```
>
> Sorry for so many more questions!

I see...

I'll explain two things that put together might help you.

What Django is doing (for now believe me, but keep reading to see it
yourself :-) ) is similar to this:
------------
def print_name(name):
print(f'The name is {name}')

def print_short_name(name):
print(f'Short name: {name[0:2]}')

def call_with_upper_parameter(function_to_call, param):
param = param.upper()
function_to_call(param)

for name in ['Clive', 'Jannett']:
call_with_upper_parameter(print_name, name)
call_with_upper_parameter(print_short_name, name)
------------

The output is:

The name is CLIVE
Short name: CL
The name is JANNETT
Short name: JA

Note that "call_with_upper_parameter" gets two parameters: a callable
and a parameter. It converts the parameter to upper case and
then calls the function_to_call with this parameter.

If you would do "print(function_to_call)" you would see "<function ... at
0x......>"

There is the Python function "callable" to know if a variable can be
called (so if it can be executed: pass parameter -or not- and with () )

Or in another way:
------------------
In [1]: def f():
...: pass
...:

In [2]: name = 'Clive'

In [3]: callable(f)
Out[3]: True

In [4]: callable(name)
Out[4]: False

In [5]: f()

In [6]: name()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-6-8afa4fbf817e> in <module>()
----> 1 name()

TypeError: 'str' object is not callable

In [7]:
------------------

In my opinion: it takes a bit of effort the first times to understand
them. After this it stays for you for any language.

And now the Django part:
You are using ImageField. ImageField is a FileField: https://github.com/django/django/blob/master/django/db/models/fields/files.py#L370

FileField in the init saves the upload_to into self.upload_to: https://github.com/django/django/blob/966b5b49b6521483f1c90b4499c4c80e80136de3/django/db/models/fields/files.py#L240

Side note: if self.upload_to is a str at some point forces to be relative: https://github.com/django/django/blob/966b5b49b6521483f1c90b4499c4c80e80136de3/django/db/models/fields/files.py#L265

But here the interesting code: https://github.com/django/django/blob/966b5b49b6521483f1c90b4499c4c80e80136de3/django/db/models/fields/files.py#L308
In "generate_filename" is doing:
if it's a callable: it calls self.upload_to with the instance and the
filename
If it's not a callable (a str in your case): it executes
datetime.datetime.now().strftime(str(self.upload_to)) : so it's
interpolating the %Y, %m, etc. that you had used and uses it as a base
directory and then adds the filename (posixpath.join(dirname, filename))

Does this explain how it works? I think that your questions might be
answered with the callable code and the Django code.

(I think that you had read the documentation but here it is:
https://docs.djangoproject.com/en/3.1/ref/models/fields/#django.db.models.FileField.upload_to
, it doesn't explain how it is implemented internally).

It might help you to use a Python debugger and see the steps as they
happen, variables, etc. (adding a breakpoint in the Django code and
inspect variables).

Let me know if it's not clear or if I explained something that you
didn't ask!

Cheers,

Clive Bruton

unread,
Nov 1, 2020, 4:25:11 PM11/1/20
to django...@googlegroups.com

On 30 Oct 2020, at 11:00, Carles Pina i Estany wrote:

> Let me know if it's not clear or if I explained something that you
> didn't ask!

Thanks you very much for spending the time on this, to explain it to
me - it was very helpful!


-- Clive
Reply all
Reply to author
Forward
0 new messages