11import base64
22import os
33import uuid
4+ from string import Formatter
45
56try : # use unicode-slugify library if installed
67 from slugify import slugify
7-
8- SLUGIFY_KWARGS = dict (only_ascii = True )
98except 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
1544class 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