22
22
import string
23
23
from operator import truediv
24
24
25
+ from django .core .validators import RegexValidator
25
26
from django .db import models
26
- from django .utils .encoding import force_str
27
+ from django .utils .translation import gettext_lazy as _
27
28
from slugify import slugify as unicode_slugify
28
29
29
30
30
- def get_fields_with_model (cls ):
31
- """
32
- Replace deprecated function of the same name in Model._meta.
33
-
34
- This replaces deprecated function (as of Django 1.10) in Model._meta as
35
- prescrived in the Django docs.
36
- https://docs.djangoproject.com/en/1.11/ref/models/meta/#migrating-from-the-old-api
37
- """
38
- return [
39
- (f , f .model if f .model != cls else None )
40
- for f in cls ._meta .get_fields ()
41
- if not f .is_relation or f .one_to_one or (f .many_to_one and f .related_model )
42
- ]
31
+ class VersionSlugField (models .CharField ):
32
+ """Just for backwards compatibility with old migrations."""
43
33
44
34
45
35
# Regex breakdown:
@@ -50,164 +40,89 @@ def get_fields_with_model(cls):
50
40
# regexes.
51
41
VERSION_SLUG_REGEX = "(?:[a-z0-9A-Z][-._a-z0-9A-Z]*?)"
52
42
43
+ version_slug_validator = RegexValidator (
44
+ # NOTE: we use the lower case version of the regex here,
45
+ # since slugs are always lower case,
46
+ # maybe we can change the VERSION_SLUG_REGEX itself,
47
+ # but that may be a breaking change somewhere else...
48
+ regex = f"^{ VERSION_SLUG_REGEX .lower ()} $" ,
49
+ message = _ (
50
+ "Enter a valid slug consisting of lowercase letters, numbers, dots, dashes or underscores. It must start with a letter or a number."
51
+ ),
52
+ )
53
+
54
+
55
+ def generate_unique_version_slug (source , version ):
56
+ slug = generate_version_slug (source ) or "unknown"
57
+ queryset = version .project .versions .all ()
58
+ if version .pk :
59
+ queryset = queryset .exclude (pk = version .pk )
60
+ base_slug = slug
61
+ iteration = 0
62
+ while queryset .filter (slug = slug ).exists ():
63
+ suffix = _uniquifying_suffix (iteration )
64
+ slug = f"{ base_slug } _{ suffix } "
65
+ iteration += 1
66
+ return slug
67
+
68
+
69
+ def generate_version_slug (source ):
70
+ normalized = _normalize (source )
71
+ ok_chars = "-._" # dash, dot, underscore
72
+ slugified = unicode_slugify (
73
+ normalized ,
74
+ only_ascii = True ,
75
+ spaces = False ,
76
+ lower = True ,
77
+ ok = ok_chars ,
78
+ space_replacement = "-" ,
79
+ )
80
+ # Remove first character wile it's an invalid character for the beginning of the slug.
81
+ slugified = slugified .lstrip (ok_chars )
82
+ return slugified
83
+
84
+
85
+ def _normalize (value ):
86
+ """
87
+ Normalize some invalid characters (/, %, !, ?) to become a dash (``-``).
53
88
54
- class VersionSlugField ( models . CharField ) :
89
+ .. note: :
55
90
91
+ We replace these characters to a dash to keep compatibility with the
92
+ old behavior and also because it makes this more readable.
93
+
94
+ For example, ``release/1.0`` will become ``release-1.0``.
56
95
"""
57
- Inspired by ``django_extensions.db.fields.AutoSlugField``.
96
+ return re .sub ("[/%!?]" , "-" , value )
97
+
58
98
59
- Uses ``unicode-slugify`` to generate the slug.
99
+ def _uniquifying_suffix ( iteration ):
60
100
"""
101
+ Create a unique suffix.
61
102
62
- ok_chars = "-._" # dash, dot, underscore
63
- test_pattern = re .compile ("^{pattern}$" .format (pattern = VERSION_SLUG_REGEX ))
64
- fallback_slug = "unknown"
65
-
66
- def __init__ (self , * args , ** kwargs ):
67
- kwargs .setdefault ("db_index" , True )
68
-
69
- populate_from = kwargs .pop ("populate_from" , None )
70
- if populate_from is None :
71
- raise ValueError ("missing 'populate_from' argument" )
72
-
73
- self ._populate_from = populate_from
74
- super ().__init__ (* args , ** kwargs )
75
-
76
- def get_queryset (self , model_cls , slug_field ):
77
- for field , model in get_fields_with_model (model_cls ):
78
- if model and field == slug_field :
79
- return model ._default_manager .all ()
80
- return model_cls ._default_manager .all ()
81
-
82
- def _normalize (self , content ):
83
- """
84
- Normalize some invalid characters (/, %, !, ?) to become a dash (``-``).
85
-
86
- .. note::
87
-
88
- We replace these characters to a dash to keep compatibility with the
89
- old behavior and also because it makes this more readable.
90
-
91
- For example, ``release/1.0`` will become ``release-1.0``.
92
- """
93
- return re .sub ("[/%!?]" , "-" , content )
94
-
95
- def slugify (self , content ):
96
- """
97
- Make ``content`` a valid slug.
98
-
99
- It uses ``unicode-slugify`` behind the scenes which works properly with
100
- Unicode characters.
101
- """
102
- if not content :
103
- return ""
104
-
105
- normalized = self ._normalize (content )
106
- slugified = unicode_slugify (
107
- normalized ,
108
- only_ascii = True ,
109
- spaces = False ,
110
- lower = True ,
111
- ok = self .ok_chars ,
112
- space_replacement = "-" ,
113
- )
114
-
115
- # Remove first character wile it's an invalid character for the
116
- # beginning of the slug
117
- slugified = slugified .lstrip (self .ok_chars )
118
-
119
- if not slugified :
120
- return self .fallback_slug
121
- return slugified
122
-
123
- def uniquifying_suffix (self , iteration ):
124
- """
125
- Create a unique suffix.
126
-
127
- This creates a suffix based on the number given as ``iteration``. It
128
- will return a value encoded as lowercase ascii letter. So we have an
129
- alphabet of 26 letters. The returned suffix will be for example ``_yh``
130
- where ``yh`` is the encoding of ``iteration``. The length of it will be
131
- ``math.log(iteration, 26)``.
132
-
133
- Examples::
134
-
135
- uniquifying_suffix(0) == '_a'
136
- uniquifying_suffix(25) == '_z'
137
- uniquifying_suffix(26) == '_ba'
138
- uniquifying_suffix(52) == '_ca'
139
- """
140
- alphabet = string .ascii_lowercase
141
- length = len (alphabet )
142
- if iteration == 0 :
143
- power = 0
144
- else :
145
- power = int (math .log (iteration , length ))
146
- current = iteration
147
- suffix = ""
148
- for exp in reversed (list (range (0 , power + 1 ))):
149
- digit = int (truediv (current , length ** exp ))
150
- suffix += alphabet [digit ]
151
- current = current % length ** exp
152
- return "_{suffix}" .format (suffix = suffix )
153
-
154
- def create_slug (self , model_instance ):
155
- """Generate a unique slug for a model instance."""
156
-
157
- # get fields to populate from and slug field to set
158
- slug_field = model_instance ._meta .get_field (self .attname )
159
-
160
- slug = self .slugify (getattr (model_instance , self ._populate_from ))
161
- count = 0
162
-
163
- # strip slug depending on max_length attribute of the slug field
164
- # and clean-up
165
- slug_len = slug_field .max_length
166
- if slug_len :
167
- slug = slug [:slug_len ]
168
- original_slug = slug
169
-
170
- # exclude the current model instance from the queryset used in finding
171
- # the next valid slug
172
- queryset = self .get_queryset (model_instance .__class__ , slug_field )
173
- if model_instance .pk :
174
- queryset = queryset .exclude (pk = model_instance .pk )
175
-
176
- # form a kwarg dict used to implement any unique_together constraints
177
- kwargs = {}
178
- for params in model_instance ._meta .unique_together :
179
- if self .attname in params :
180
- for param in params :
181
- kwargs [param ] = getattr (model_instance , param , None )
182
- kwargs [self .attname ] = slug
183
-
184
- # increases the number while searching for the next valid slug
185
- # depending on the given slug, clean-up
186
- while not slug or queryset .filter (** kwargs ).exists ():
187
- slug = original_slug
188
- end = self .uniquifying_suffix (count )
189
- end_len = len (end )
190
- if slug_len and len (slug ) + end_len > slug_len :
191
- slug = slug [: slug_len - end_len ]
192
- slug = slug + end
193
- kwargs [self .attname ] = slug
194
- count += 1
195
-
196
- is_slug_valid = self .test_pattern .match (slug )
197
- if not is_slug_valid :
198
- # pylint: disable=broad-exception-raised
199
- raise Exception ("Invalid generated slug: {slug}" .format (slug = slug ))
200
- return slug
201
-
202
- def pre_save (self , model_instance , add ):
203
- value = getattr (model_instance , self .attname )
204
- # We only create a new slug if none was set yet.
205
- if not value and add :
206
- value = force_str (self .create_slug (model_instance ))
207
- setattr (model_instance , self .attname , value )
208
- return value
209
-
210
- def deconstruct (self ):
211
- name , path , args , kwargs = super ().deconstruct ()
212
- kwargs ["populate_from" ] = self ._populate_from
213
- return name , path , args , kwargs
103
+ This creates a suffix based on the number given as ``iteration``. It
104
+ will return a value encoded as lowercase ascii letter. So we have an
105
+ alphabet of 26 letters. The returned suffix will be for example ``yh``
106
+ where ``yh`` is the encoding of ``iteration``. The length of it will be
107
+ ``math.log(iteration, 26)``.
108
+
109
+ Examples::
110
+
111
+ uniquifying_suffix(0) == 'a'
112
+ uniquifying_suffix(25) == 'z'
113
+ uniquifying_suffix(26) == 'ba'
114
+ uniquifying_suffix(52) == 'ca'
115
+ """
116
+ alphabet = string .ascii_lowercase
117
+ length = len (alphabet )
118
+ if iteration == 0 :
119
+ power = 0
120
+ else :
121
+ power = int (math .log (iteration , length ))
122
+ current = iteration
123
+ suffix = ""
124
+ for exp in reversed (list (range (0 , power + 1 ))):
125
+ digit = int (truediv (current , length ** exp ))
126
+ suffix += alphabet [digit ]
127
+ current = current % length ** exp
128
+ return suffix
0 commit comments