-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmodels.py
255 lines (207 loc) · 8.1 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import logging
import uuid
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from bx_django_utils.models.timetracking import TimetrackingBaseModel
logger = logging.getLogger(__name__)
class BaseApproveModel(TimetrackingBaseModel):
"""
Base model class for approve models *and* this relation models.
"""
id = models.UUIDField(
# The internal primary key
primary_key=True,
default=uuid.uuid4,
editable=False,
verbose_name=_('BaseApproveModel.pk.verbose_name'),
help_text=_('BaseApproveModel.pk.help_text')
)
is_draft = models.BooleanField(
# Marks this entry as "draft"
# Only drafts can be "approve"
default=True,
editable=False,
db_index=True,
verbose_name=_('BaseApproveModel.is_draft.verbose_name'),
help_text=_('BaseApproveModel.is_draft.help_text')
)
approved = models.OneToOneField(
# Relation to the "approved" instance
# Set by self.approve() to the draft instance
'self',
on_delete=models.SET_NULL,
related_name='draft',
null=True,
editable=False,
verbose_name=_('BaseApproveModel.approved.verbose_name'),
help_text=_('BaseApproveModel.approved.help_text')
)
blocked = models.BooleanField(
default=False,
verbose_name=_('BaseApproveModel.blocked.verbose_name'),
help_text=_('BaseApproveModel.blocked.help_text')
)
# Fields are required for *approve* instances (should be used in forms):
REQUIRED_FIELDS_PUBLIC = ()
def is_approved(self):
if not self.is_draft:
# We are the "approved" instance
return True
return self.approved_id is not None
is_approved.short_description = _('Approved')
is_approved.boolean = True
def get_missing_field_info(self, ignore_fields=None, extra_fields=None):
needed_fields = set(self.REQUIRED_FIELDS_PUBLIC)
if extra_fields:
needed_fields |= set(extra_fields)
if ignore_fields:
needed_fields -= set(ignore_fields)
missing = []
for field_name in sorted(needed_fields):
field = self._meta.get_field(field_name)
if field.remote_field:
attname = field_name
verbose_name = field.name
else:
attname = field.get_attname()
verbose_name = field.verbose_name
value = getattr(self, attname)
if field.remote_field and not isinstance(field.remote_field, models.ManyToOneRel):
# Exists a many2many
value = value.exists()
if not value:
missing.append(
(attname, verbose_name, value)
)
return missing
def full_clean(self, **kwargs):
super().full_clean(**kwargs)
if not self.is_draft:
missing = self.get_missing_field_info()
errors = {}
for attname, _verbose_name, _value in missing:
errors[attname] = ValidationError(
_('This field cannot be blank.'),
code='blank'
)
if errors:
raise ValidationError(errors)
def _copy_normal_fields(self, approve_instance):
"""
Just assign all normal field values from draft to approve version.
"""
raise NotImplementedError
def _copy_relations(self, approve_instance):
"""
Assign relations from draft to approve version.
"""
raise NotImplementedError
def approve(self, **values):
"""
1. validate that draft in complete filled
2. Create/update a approve instance with all data from the draft
"""
logger.debug('Approve %s', self)
if not self.is_draft:
raise ValidationError('Only drafts can be approve!')
if self.blocked:
raise ValidationError('Blocked entries can not approved!')
# Check validation before creating the approve version
self.full_clean()
with transaction.atomic():
if self.approved is None:
approve_instance = self.__class__(
is_draft=False,
)
else:
approve_instance = self.approved
assert approve_instance.is_draft is False
for key, value in values.items():
setattr(approve_instance, key, value)
# "normal" field values can be assigned before a new approve instance
# has been saved and has a primary key
self._copy_normal_fields(approve_instance)
# Transfer date times:
approve_instance.create_dt = self.create_dt
approve_instance.update_dt = self.update_dt
# Save the instance to set a primary key
# PK is needed to assign relations
approve_instance.save(
# Maybe relations are needed -> skip validation
# validation will be done later, see below
call_clean=False,
update_dt=False # Keep date time from draft
)
# Assign all relation data
self._copy_relations(approve_instance)
# If fields are missing or validation failed
# -> raise and error and abort the transaction
approve_instance.full_clean()
if self.approved_id is not approve_instance.pk:
# Save approve instance to draft version
self.approved_id = approve_instance.pk
self.save(
update_fields=('approved_id',),
update_dt=False # Keep date time from draft
)
logger.info('%s was approved to %s', self, approve_instance)
return approve_instance
def save(self, call_clean=True, **kwargs):
if call_clean:
# The approve version should be validated
self.full_clean()
return super().save(**kwargs)
def __repr__(self):
if self.is_draft:
info = 'draft'
else:
info = 'approved'
return f'<{self.__class__.__name__} pk:{self.pk} ({info})>'
def __str__(self):
return self.__repr__()
class Meta:
default_permissions = ('add', 'change', 'delete', 'view', 'approve') # Add 'approve'
abstract = True
class BaseApproveWorkflowModel(BaseApproveModel):
"""
Base model for approve workflow models.
Don't used this for approve relation models!
"""
ready_to_approve = models.BooleanField(
# Marks when it's ready to approve
default=False,
null=False,
blank=True,
verbose_name=_('BaseApproveWorkflowModel.ready_to_approve.verbose_name'),
help_text=_('BaseApproveWorkflowModel.ready_to_approve.help_text')
)
def approve(self, **values):
if not self.is_draft:
raise ValidationError('Only drafts can be approve!')
if not self.ready_to_approve:
logger.warning('Not ready to approve: pk:%r model:%s', self.pk, type(self).__name__)
with transaction.atomic():
# Remove the "ready" flag for next edit:
self.ready_to_approve = False
self.save(
update_fields=('ready_to_approve',),
update_dt=False # Keep date time from draft
)
approve_instance = super().approve(**values)
# self._copy_normal_fields() should not copy the "ready" flag:
assert not approve_instance.ready_to_approve
return approve_instance
def __repr__(self):
if self.is_draft:
if self.ready_to_approve:
info = 'draft, ready to approve'
else:
info = 'draft, not ready'
else:
info = 'approved'
return f'<{self.__class__.__name__} pk:{self.pk} ({info})>'
def __str__(self):
return self.__repr__()
class Meta(BaseApproveModel.Meta):
abstract = True