or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

dynamic-states.mdexceptions.mdfield-types.mdindex.mdmodel-mixins.mdsignals.mdtransitions.mdvisualization.md

model-mixins.mddocs/

0

# Model Mixins and Advanced Features

1

2

Model mixins for enhanced FSM functionality including refresh_from_db support for protected fields and optimistic locking protection against concurrent transitions.

3

4

## Capabilities

5

6

### FSMModelMixin

7

8

Mixin that allows refresh_from_db for models with fsm protected fields. This mixin ensures that protected FSM fields are properly handled during database refresh operations.

9

10

```python { .api }

11

class FSMModelMixin(object):

12

def refresh_from_db(self, *args, **kwargs):

13

"""

14

Refresh model instance from database while respecting protected FSM fields.

15

16

Protected FSM fields are excluded from refresh to prevent corruption

17

of the finite state machine's integrity.

18

"""

19

20

def _get_protected_fsm_fields(self):

21

"""

22

Get set of protected FSM field attribute names.

23

24

Returns:

25

set: Attribute names of protected FSM fields

26

"""

27

```

28

29

Usage example:

30

31

```python

32

from django.db import models

33

from django_fsm import FSMField, FSMModelMixin, transition

34

35

class Order(FSMModelMixin, models.Model):

36

state = FSMField(default='pending', protected=True)

37

amount = models.DecimalField(max_digits=10, decimal_places=2)

38

39

@transition(field=state, source='pending', target='confirmed')

40

def confirm(self):

41

pass

42

43

# Protected fields are automatically excluded during refresh

44

order = Order.objects.get(pk=1)

45

order.refresh_from_db() # state field is preserved if protected

46

```

47

48

### ConcurrentTransitionMixin

49

50

Protects models from undesirable effects caused by concurrently executed transitions using optimistic locking. This mixin prevents race conditions where multiple processes try to modify the same object's state simultaneously.

51

52

```python { .api }

53

class ConcurrentTransitionMixin(object):

54

def __init__(self, *args, **kwargs):

55

"""Initialize concurrent transition protection."""

56

57

def save(self, *args, **kwargs):

58

"""

59

Save model with concurrent transition protection.

60

61

Raises:

62

ConcurrentTransition: If state has changed since object was fetched

63

"""

64

65

def refresh_from_db(self, *args, **kwargs):

66

"""Refresh from database and update internal state tracking."""

67

68

@property

69

def state_fields(self):

70

"""

71

Get all FSM fields in the model.

72

73

Returns:

74

filter: FSM field instances

75

"""

76

77

def _update_initial_state(self):

78

"""Update internal tracking of initial state values."""

79

80

def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):

81

"""

82

Internal method that performs the actual database update with state validation.

83

84

This method is called by Django's save mechanism and adds concurrent

85

transition protection by filtering the update query on the original

86

state values.

87

88

Parameters:

89

- base_qs: Base queryset for the update

90

- using: Database alias to use

91

- pk_val: Primary key value

92

- values: Values to update

93

- update_fields: Fields to update

94

- forced_update: Whether update is forced

95

96

Returns:

97

bool: True if update was successful

98

99

Raises:

100

ConcurrentTransition: If state has changed since object was fetched

101

"""

102

```

103

104

Usage example:

105

106

```python

107

from django.db import transaction

108

from django_fsm import FSMField, ConcurrentTransitionMixin, transition, ConcurrentTransition

109

110

class BankAccount(ConcurrentTransitionMixin, models.Model):

111

state = FSMField(default='active')

112

balance = models.DecimalField(max_digits=10, decimal_places=2)

113

114

@transition(field=state, source='active', target='frozen')

115

def freeze_account(self):

116

pass

117

118

# Safe concurrent usage pattern

119

def transfer_money(account_id, amount):

120

try:

121

with transaction.atomic():

122

account = BankAccount.objects.get(pk=account_id)

123

if account.balance >= amount:

124

account.balance -= amount

125

account.save() # Will raise ConcurrentTransition if state changed

126

except ConcurrentTransition:

127

# Handle concurrent modification

128

raise ValueError("Account was modified by another process")

129

```

130

131

## Advanced Integration Patterns

132

133

### Combining Both Mixins

134

135

You can use both mixins together for maximum protection:

136

137

```python

138

class SafeDocument(FSMModelMixin, ConcurrentTransitionMixin, models.Model):

139

state = FSMField(default='draft', protected=True)

140

workflow_state = FSMField(default='new')

141

content = models.TextField()

142

143

@transition(field=state, source='draft', target='published')

144

def publish(self):

145

pass

146

147

@transition(field=workflow_state, source='new', target='processing')

148

def start_workflow(self):

149

pass

150

```

151

152

### Custom Concurrent Protection

153

154

Customize which fields are used for concurrent protection:

155

156

```python

157

class CustomProtectedModel(ConcurrentTransitionMixin, models.Model):

158

primary_state = FSMField(default='new')

159

secondary_state = FSMField(default='inactive')

160

version = models.PositiveIntegerField(default=1)

161

162

def save(self, *args, **kwargs):

163

# Custom logic before concurrent protection

164

if self.primary_state == 'published':

165

self.version += 1

166

167

# Call parent save with concurrent protection

168

super().save(*args, **kwargs)

169

```

170

171

### Handling Concurrent Exceptions

172

173

Proper exception handling for concurrent modifications:

174

175

```python

176

from django_fsm import ConcurrentTransition

177

import time

178

import random

179

180

def safe_state_change(model_instance, transition_method, max_retries=3):

181

"""

182

Safely execute state transition with retry logic for concurrent conflicts.

183

"""

184

for attempt in range(max_retries):

185

try:

186

with transaction.atomic():

187

# Refresh to get latest state

188

model_instance.refresh_from_db()

189

190

# Execute transition

191

transition_method()

192

model_instance.save()

193

return True

194

195

except ConcurrentTransition:

196

if attempt == max_retries - 1:

197

raise

198

199

# Wait before retry with exponential backoff

200

time.sleep(0.1 * (2 ** attempt) + random.uniform(0, 0.1))

201

202

return False

203

204

# Usage

205

try:

206

safe_state_change(order, order.confirm)

207

except ConcurrentTransition:

208

# Final failure after retries

209

logger.error(f"Failed to confirm order {order.id} after retries")

210

```

211

212

### Integration with Django Signals

213

214

Combining mixins with Django FSM signals for comprehensive state management:

215

216

```python

217

from django_fsm.signals import post_transition

218

from django.dispatch import receiver

219

220

class AuditedOrder(FSMModelMixin, ConcurrentTransitionMixin, models.Model):

221

state = FSMField(default='pending', protected=True)

222

audit_trail = models.JSONField(default=list)

223

224

@transition(field=state, source='pending', target='confirmed')

225

def confirm(self):

226

pass

227

228

@receiver(post_transition, sender=AuditedOrder)

229

def log_state_change(sender, instance, name, source, target, **kwargs):

230

"""Log all state changes for audit purposes."""

231

instance.audit_trail.append({

232

'timestamp': timezone.now().isoformat(),

233

'transition': name,

234

'from_state': source,

235

'to_state': target,

236

'user_id': getattr(kwargs.get('user'), 'id', None)

237

})

238

239

# Save without triggering state machine (direct field update)

240

sender.objects.filter(pk=instance.pk).update(

241

audit_trail=instance.audit_trail

242

)

243

```

244

245

## Performance Considerations

246

247

### Optimizing Protected Field Queries

248

249

When using FSMModelMixin with many protected fields:

250

251

```python

252

class OptimizedModel(FSMModelMixin, models.Model):

253

state = FSMField(default='new', protected=True)

254

workflow = FSMField(default='pending', protected=True)

255

256

# Use select_related/prefetch_related for efficient queries

257

@classmethod

258

def get_with_relations(cls, pk):

259

return cls.objects.select_related('related_field').get(pk=pk)

260

```

261

262

### Batch Operations with Concurrent Protection

263

264

Handle bulk operations safely:

265

266

```python

267

def bulk_state_update(queryset, transition_method_name):

268

"""

269

Safely update multiple objects with concurrent protection.

270

"""

271

updated_count = 0

272

273

for obj in queryset:

274

try:

275

with transaction.atomic():

276

obj.refresh_from_db()

277

method = getattr(obj, transition_method_name)

278

method()

279

obj.save()

280

updated_count += 1

281

except ConcurrentTransition:

282

# Log failure but continue with other objects

283

logger.warning(f"Concurrent update conflict for {obj.pk}")

284

285

return updated_count

286

```

287

288

### Database Constraints and State Consistency

289

290

Ensure database-level consistency:

291

292

```python

293

class ConstrainedOrder(ConcurrentTransitionMixin, models.Model):

294

state = FSMField(default='pending')

295

payment_confirmed = models.BooleanField(default=False)

296

297

class Meta:

298

constraints = [

299

models.CheckConstraint(

300

check=~(models.Q(state='shipped') & models.Q(payment_confirmed=False)),

301

name='shipped_orders_must_be_paid'

302

)

303

]

304

305

@transition(field=state, source='confirmed', target='shipped')

306

def ship(self):

307

if not self.payment_confirmed:

308

raise ValueError("Cannot ship unpaid order")

309

```