Skip to content

Commit d869f62

Browse files
authored
Add instance to context and extend python format language (#2)
1 parent e1098c3 commit d869f62

File tree

4 files changed

+171
-116
lines changed

4 files changed

+171
-116
lines changed

README.rst

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,22 @@ Basic example:
2626
from django.db import models
2727
from dynamic_names import FilePattern
2828
29-
upload_to_pattern = FilePattern('{app_name:.25}/{model_name:.30}/{uuid_base32}{ext}')
29+
upload_to_pattern = FilePattern(
30+
filename_pattern='{app_name:.25}/{model_name:.30}/{uuid:base32}{ext}'
31+
)
3032
3133
class FileModel(models.Model):
3234
my_file = models.FileField(upload_to=upload_to_pattern)
3335
3436
3537
Auto slug example:
3638

37-
.. code-block:: python
38-
39-
from django.db import models
40-
from dynamic_names import FilePattern
4139

42-
class SlugPattern(FilePattern):
43-
filename_pattern = '{app_name:.25}/{model_name:.30}/{slug}{ext}'
44-
45-
class FileModel(models.Model):
46-
title = models.CharField(max_length=100)
47-
my_file = models.FileField(upload_to=SlugPattern(populate_slug_from='title'))
40+
Features
41+
--------
4842

49-
Supported Attributes
50-
--------------------
43+
Field names
44+
~~~~~~~~~~~
5145

5246
``ext``
5347
File extension including the dot.
@@ -61,19 +55,58 @@ Supported Attributes
6155
``app_label``
6256
App label of the Django model.
6357

64-
``uuid_base16``
65-
Base16 (hex) representation of a UUID.
58+
``instance``
59+
Instance of the model before it has been saved. You may not have a primary
60+
key at this point.
61+
62+
``uuid``
63+
UUID version 4 that supports multiple type specifiers. The UUID will be
64+
the same should you use it twice in the same string, but different on each
65+
invocation of the ``upload_to`` callable.
66+
67+
The type specifiers allow you to format the UUID in different ways, e.g.
68+
``{uuid:x}`` will give you a with a hexadecimal UUID.
69+
70+
The supported type specifiers are:
71+
72+
``s``
73+
String representation of a UUID including dashes.
74+
75+
``i``
76+
Integer representation of a UUID. Like to ``UUID.int``.
77+
78+
``x``
79+
Hexadecimal (Base16) representation of a UUID. Like to ``UUID.hex``.
6680

67-
``uuid_base32``
68-
Base32 representation of a UUID.
81+
``X``
82+
Upper case hexadecimal representation of a UUID. Like to
83+
``UUID.hex``.
6984

70-
``uuid_base64``
71-
Base64 representation of a UUID. Not supported by all file systems.
85+
``base32``
86+
Base32 representation of a UUID without padding.
7287

73-
``slug``
74-
Auto created slug based on another field on the model instance.
88+
``base64``
89+
Base64 representation of a UUID without padding.
7590

76-
``slug_from``
77-
Name of the field the slug should be populated from.
91+
.. warning:: Not all file systems support Base64 file names.
92+
93+
Type specifiers
94+
~~~~~~~~~~~~~~~
95+
96+
You can also use a special slug type specifier, that slugifies strings.
97+
98+
Example:
99+
100+
.. code-block:: python
101+
102+
from django.db import models
103+
from dynamic_names import FilePattern
104+
105+
upload_to_pattern = FilePattern(
106+
filename_pattern='{app_name:.25}/{model_name:.30}/{instance.title:slug}{ext}'
107+
)
108+
109+
class FileModel(models.Model):
110+
title = models.CharField(max_length=100)
111+
my_file = models.FileField(upload_to=upload_to_pattern)
78112
79-
.. note:: The field name itself is not part of the pattern.

dynamic_filenames.py

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
11
import base64
22
import os
33
import uuid
4+
from string import Formatter
45

56
try: # use unicode-slugify library if installed
67
from slugify import slugify
7-
8-
SLUGIFY_KWARGS = dict(only_ascii=True)
98
except ImportError:
109
from django.utils.text import slugify
1110

12-
SLUGIFY_KWARGS = dict(allow_unicode=False)
11+
12+
class SlugFormatter(Formatter):
13+
14+
def format_field(self, value, format_spec):
15+
if format_spec == 'slug':
16+
return slugify(value)
17+
return super().format_field(value=value, format_spec=format_spec)
18+
19+
20+
class ExtendedUUID(uuid.UUID):
21+
22+
def __format__(self, format_spec):
23+
if format_spec == '':
24+
return str(self)
25+
if format_spec == 's':
26+
return str(self)
27+
if format_spec == 'i':
28+
return str(self.int)
29+
if format_spec == 'x':
30+
return self.hex.lower()
31+
if format_spec == 'X':
32+
return self.hex.upper()
33+
if format_spec == 'base32':
34+
return base64.b32encode(
35+
self.bytes
36+
).decode('utf-8').rstrip('=\n')
37+
if format_spec == 'base64':
38+
return base64.urlsafe_b64encode(
39+
self.bytes
40+
).decode('utf-8').rstrip('=\n')
41+
return super().__format__(format_spec)
1342

1443

1544
class FilePattern:
@@ -23,7 +52,7 @@ class FilePattern:
2352
from django.db import models
2453
from dynamic_names import FilePattern
2554
26-
upload_to_pattern = FilePattern('{app_name:.25}/{model_name:.30}/{uuid_base32}{ext}')
55+
upload_to_pattern = FilePattern('{app_name:.25}/{model_name:.30}/{uuid:base32}{ext}')
2756
2857
class FileModel(models.Model):
2958
my_file = models.FileField(upload_to=upload_to_pattern)
@@ -34,11 +63,13 @@ class FileModel(models.Model):
3463
name: Filename excluding the folders.
3564
model_name: Name of the Django model.
3665
app_label: App label of the Django model.
37-
uuid_base16: Base16 (hex) representation of a UUID.
38-
uuid_base32: Base32 representation of a UUID.
39-
uuid_base64: Base64 representation of a UUID. Not supported by all file systems.
40-
slug: Auto created slug based on another field on the model instance.
41-
slug_from: Name of the field the slug should be populated from.
66+
uuid:
67+
UUID version 4 that supports multiple type specifiers. The UUID will be
68+
the same should you use it twice in the same string, but different on each
69+
invocation of the ``upload_to`` callable.
70+
instance:
71+
Instance of the model before it has been saved.
72+
You may not have a primary key at this point.
4273
4374
4475
Auto slug example:
@@ -48,45 +79,38 @@ class FileModel(models.Model):
4879
from django.db import models
4980
from dynamic_names import FilePattern
5081
51-
class SlugPattern(FilePattern):
52-
filename_pattern = '{app_name:.25}/{model_name:.30}/{slug}{ext}'
82+
class TitleSlugPattern(FilePattern):
83+
filename_pattern = '{app_name:.25}/{model_name:.30}/{instance.title:slug}{ext}'
5384
5485
class FileModel(models.Model):
5586
title = models.CharField(max_length=100)
56-
my_file = models.FileField(upload_to=SlugPattern(slug_from='title'))
87+
my_file = models.FileField(upload_to=TitleSlugPattern())
5788
5889
"""
5990

60-
slug_from = None
91+
formatter = SlugFormatter()
6192

6293
filename_pattern = '{name}{ext}'
6394

6495
def __call__(self, instance, filename):
6596
"""Return filename based for given instance and filename."""
6697
# UUID needs to be set on call, not per instance to avoid state leakage.
67-
guid = self.get_uuid()
6898
path, ext = os.path.splitext(filename)
6999
path, name = os.path.split(path)
70100
defaults = {
71101
'ext': ext,
72102
'name': name,
73103
'model_name': instance._meta.model_name,
74104
'app_label': instance._meta.app_label,
75-
'uuid_base10': self.uuid_2_base10(guid),
76-
'uuid_base16': self.uuid_2_base16(guid),
77-
'uuid_base32': self.uuid_2_base32(guid),
78-
'uuid_base64': self.uuid_2_base64(guid),
105+
'uuid': self.get_uuid(),
106+
'instance': instance,
79107
}
80108
defaults.update(self.override_values)
81-
if self.slug_from is not None:
82-
field_value = getattr(instance, self.slug_from)
83-
defaults['slug'] = slugify(field_value, **SLUGIFY_KWARGS)
84-
return self.filename_pattern.format(**defaults)
109+
return self.formatter.format(self.filename_pattern, **defaults)
85110

86111
def __init__(self, **kwargs):
87112
self.kwargs = kwargs
88113
override_values = kwargs.copy()
89-
self.slug_from = override_values.pop('slug_from', self.slug_from)
90114
self.filename_pattern = override_values.pop('filename_pattern', self.filename_pattern)
91115
self.override_values = override_values
92116

@@ -98,34 +122,4 @@ def deconstruct(self):
98122
@staticmethod
99123
def get_uuid():
100124
"""Return UUID version 4."""
101-
return uuid.uuid4()
102-
103-
@staticmethod
104-
def uuid_2_base10(uuid):
105-
"""Return 39 digits long integer UUID as Base10."""
106-
return uuid.int
107-
108-
@staticmethod
109-
def uuid_2_base16(uuid):
110-
"""Return 32 char long UUID as Base16 (hex)."""
111-
return uuid.hex
112-
113-
@staticmethod
114-
def uuid_2_base32(uuid):
115-
"""Return 27 char long UUIDv4 as Base32."""
116-
return base64.b32encode(
117-
uuid.bytes
118-
).decode('utf-8').rstrip('=\n')
119-
120-
@staticmethod
121-
def uuid_2_base64(uuid):
122-
"""
123-
Return 23 char long UUIDv4 as Base64.
124-
125-
.. warning:: Not all file systems support Base64 file names.
126-
e.g. The Apple File System (APFS) is case insensitive by default.
127-
128-
"""
129-
return base64.urlsafe_b64encode(
130-
uuid.bytes
131-
).decode('utf-8').rstrip('=\n')
125+
return ExtendedUUID(bytes=os.urandom(16), version=4)

0 commit comments

Comments
 (0)