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

transitions.mddocs/

0

# Transition Management

1

2

Decorator for marking methods as state transitions with comprehensive configuration options and utility functions for checking transition availability and permissions.

3

4

## Capabilities

5

6

### Transition Decorator

7

8

The core decorator that marks model methods as state transitions, enabling declarative finite state machine behavior.

9

10

```python { .api }

11

def transition(field,

12

source="*",

13

target=None,

14

on_error=None,

15

conditions=[],

16

permission=None,

17

custom={}):

18

"""

19

Method decorator to mark allowed state transitions.

20

21

Parameters:

22

- field: FSM field instance or field name string

23

- source: Source state(s) - string, list, tuple, set, "*" (any), or "+" (any except target)

24

- target: Target state, State instance (GET_STATE, RETURN_VALUE), or None for validation-only

25

- on_error: State to transition to if method raises exception

26

- conditions: List of functions that must return True for transition to proceed

27

- permission: Permission string or callable for authorization checking

28

- custom: Dictionary of custom metadata for the transition

29

30

Returns:

31

Decorated method that triggers state transitions when called

32

"""

33

```

34

35

### Basic Transition Usage

36

37

Simple state transitions with single source and target states:

38

39

```python

40

from django.db import models

41

from django_fsm import FSMField, transition

42

43

class Order(models.Model):

44

state = FSMField(default='pending')

45

46

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

47

def confirm(self):

48

# Business logic here

49

self.confirmed_at = timezone.now()

50

51

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

52

def ship(self):

53

# Generate tracking number, notify customer, etc.

54

pass

55

```

56

57

### Multiple Source States

58

59

Transitions can be triggered from multiple source states:

60

61

```python

62

class BlogPost(models.Model):

63

state = FSMField(default='draft')

64

65

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

66

def publish(self):

67

self.published_at = timezone.now()

68

69

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

70

def archive(self):

71

pass

72

```

73

74

### Wildcard Source States

75

76

Use special source values for flexible transitions:

77

78

```python

79

class Document(models.Model):

80

state = FSMField(default='new')

81

82

# Can be called from any state

83

@transition(field=state, source='*', target='error')

84

def mark_error(self):

85

pass

86

87

# Can be called from any state except 'deleted'

88

@transition(field=state, source='+', target='deleted')

89

def delete(self):

90

pass

91

```

92

93

### Conditional Transitions

94

95

Add conditions that must be met before transition can proceed:

96

97

```python

98

def can_publish(instance):

99

return instance.content and instance.title

100

101

def has_approval(instance):

102

return instance.approved_by is not None

103

104

class Article(models.Model):

105

state = FSMField(default='draft')

106

content = models.TextField()

107

title = models.CharField(max_length=200)

108

approved_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)

109

110

@transition(

111

field=state,

112

source='draft',

113

target='published',

114

conditions=[can_publish, has_approval]

115

)

116

def publish(self):

117

self.published_at = timezone.now()

118

```

119

120

### Permission-Based Transitions

121

122

Control access to transitions using Django's permission system:

123

124

```python

125

class Document(models.Model):

126

state = FSMField(default='draft')

127

128

# String permission

129

@transition(

130

field=state,

131

source='review',

132

target='approved',

133

permission='myapp.can_approve_document'

134

)

135

def approve(self):

136

pass

137

138

# Callable permission

139

def can_reject(instance, user):

140

return user.is_staff or user == instance.author

141

142

@transition(

143

field=state,

144

source='review',

145

target='rejected',

146

permission=can_reject

147

)

148

def reject(self):

149

pass

150

```

151

152

### Error Handling in Transitions

153

154

Specify fallback states when transitions raise exceptions:

155

156

```python

157

class PaymentOrder(models.Model):

158

state = FSMField(default='pending')

159

160

@transition(

161

field=state,

162

source='pending',

163

target='paid',

164

on_error='failed'

165

)

166

def process_payment(self):

167

# If this raises an exception, state becomes 'failed'

168

result = payment_gateway.charge(self.amount)

169

if not result.success:

170

raise PaymentException("Payment failed")

171

```

172

173

### Validation-Only Transitions

174

175

Set target to None to validate state without changing it:

176

177

```python

178

class Contract(models.Model):

179

state = FSMField(default='draft')

180

181

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

182

def validate_draft(self):

183

# Perform validation logic

184

# State remains 'draft' if successful

185

if not self.is_valid():

186

raise ValidationError("Contract is invalid")

187

```

188

189

### Custom Transition Metadata

190

191

Store additional metadata with transitions:

192

193

```python

194

class Workflow(models.Model):

195

state = FSMField(default='new')

196

197

@transition(

198

field=state,

199

source='new',

200

target='processing',

201

custom={

202

'priority': 'high',

203

'requires_notification': True,

204

'estimated_time': 300

205

}

206

)

207

def start_processing(self):

208

pass

209

```

210

211

## Transition Objects

212

213

### Transition Class

214

215

The Transition class represents individual state transitions and provides metadata and permission checking capabilities.

216

217

```python { .api }

218

class Transition(object):

219

def __init__(self, method, source, target, on_error, conditions, permission, custom):

220

"""

221

Represents a single state machine transition.

222

223

Parameters:

224

- method: The transition method

225

- source: Source state for the transition

226

- target: Target state for the transition

227

- on_error: Error state (optional)

228

- conditions: List of condition functions

229

- permission: Permission string or callable

230

- custom: Custom metadata dictionary

231

232

Attributes:

233

- name: Name of the transition method

234

- source: Source state

235

- target: Target state

236

- on_error: Error fallback state

237

- conditions: Transition conditions

238

- permission: Permission configuration

239

- custom: Custom transition metadata

240

"""

241

242

@property

243

def name(self):

244

"""Return the name of the transition method."""

245

246

def has_perm(self, instance, user):

247

"""

248

Check if user has permission to execute this transition.

249

250

Parameters:

251

- instance: Model instance

252

- user: User to check permissions for

253

254

Returns:

255

bool: True if user has permission

256

"""

257

```

258

259

Transition objects are created automatically by the @transition decorator and can be accessed through various utility functions:

260

261

```python

262

from django_fsm import FSMField, transition

263

264

class Order(models.Model):

265

state = FSMField(default='pending')

266

267

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

268

def confirm(self):

269

pass

270

271

# Access transition objects

272

order = Order()

273

transitions = order.get_available_state_transitions()

274

for transition in transitions:

275

print(f"Transition: {transition.name}")

276

print(f"From {transition.source} to {transition.target}")

277

print(f"Custom metadata: {transition.custom}")

278

```

279

280

### FSMMeta Class

281

282

The FSMMeta class manages transition metadata for individual methods, storing all configured transitions and their conditions.

283

284

```python { .api }

285

class FSMMeta(object):

286

def __init__(self, field, method):

287

"""

288

Initialize FSM metadata for a transition method.

289

290

Parameters:

291

- field: FSM field instance

292

- method: Transition method

293

294

Attributes:

295

- field: Associated FSM field

296

- transitions: Dictionary mapping source states to Transition objects

297

"""

298

299

def add_transition(self, method, source, target, on_error=None, conditions=[], permission=None, custom={}):

300

"""

301

Add a transition configuration to this method.

302

303

Parameters:

304

- method: Transition method

305

- source: Source state

306

- target: Target state

307

- on_error: Error state (optional)

308

- conditions: List of condition functions

309

- permission: Permission string or callable

310

- custom: Custom metadata dictionary

311

"""

312

313

def has_transition(self, state):

314

"""

315

Check if any transition exists from the given state.

316

317

Parameters:

318

- state: Current state to check

319

320

Returns:

321

bool: True if transition is possible

322

"""

323

324

def conditions_met(self, instance, state):

325

"""

326

Check if all transition conditions are satisfied.

327

328

Parameters:

329

- instance: Model instance

330

- state: Current state

331

332

Returns:

333

bool: True if all conditions are met

334

"""

335

336

def has_transition_perm(self, instance, state, user):

337

"""

338

Check if user has permission for transition from given state.

339

340

Parameters:

341

- instance: Model instance

342

- state: Current state

343

- user: User to check permissions for

344

345

Returns:

346

bool: True if user has permission

347

"""

348

```

349

350

Access FSMMeta through transition methods:

351

352

```python

353

# Access FSM metadata

354

order = Order()

355

meta = order.confirm._django_fsm # FSMMeta instance

356

357

# Check transition availability

358

if meta.has_transition(order.state):

359

print("Confirm transition is available")

360

361

# Check conditions

362

if meta.conditions_met(order, order.state):

363

print("All conditions are satisfied")

364

```

365

366

## Transition Utility Functions

367

368

### can_proceed

369

370

Check if a transition method can be called from the current state:

371

372

```python { .api }

373

def can_proceed(bound_method, check_conditions=True):

374

"""

375

Returns True if model state allows calling the bound transition method.

376

377

Parameters:

378

- bound_method: Bound method with @transition decorator

379

- check_conditions: Whether to check transition conditions (default: True)

380

381

Returns:

382

bool: True if transition is allowed

383

384

Raises:

385

TypeError: If method is not a transition

386

"""

387

```

388

389

Usage example:

390

391

```python

392

from django_fsm import can_proceed

393

394

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

395

396

# Check without conditions

397

if can_proceed(order.ship, check_conditions=False):

398

print("Ship transition is possible from current state")

399

400

# Check with conditions

401

if can_proceed(order.ship):

402

order.ship()

403

order.save()

404

else:

405

print("Cannot ship order yet")

406

```

407

408

### has_transition_perm

409

410

Check if a user has permission to execute a transition:

411

412

```python { .api }

413

def has_transition_perm(bound_method, user):

414

"""

415

Returns True if model state allows calling method and user has permission.

416

417

Parameters:

418

- bound_method: Bound method with @transition decorator

419

- user: User instance to check permissions for

420

421

Returns:

422

bool: True if user can execute transition

423

424

Raises:

425

TypeError: If method is not a transition

426

"""

427

```

428

429

Usage example:

430

431

```python

432

from django_fsm import has_transition_perm

433

434

def process_approval(request, document_id):

435

document = Document.objects.get(pk=document_id)

436

437

if has_transition_perm(document.approve, request.user):

438

document.approve()

439

document.save()

440

return redirect('success')

441

else:

442

return HttpResponseForbidden("You don't have permission to approve")

443

```

444

445

## Advanced Transition Patterns

446

447

### Multi-Field State Machines

448

449

Different fields can have their own independent state machines:

450

451

```python

452

class Order(models.Model):

453

payment_state = FSMField(default='unpaid')

454

fulfillment_state = FSMField(default='pending')

455

456

@transition(field=payment_state, source='unpaid', target='paid')

457

def process_payment(self):

458

pass

459

460

@transition(field=fulfillment_state, source='pending', target='shipped')

461

def ship_order(self):

462

pass

463

```

464

465

### Cascading Transitions

466

467

Transitions can trigger other transitions:

468

469

```python

470

class Order(models.Model):

471

state = FSMField(default='new')

472

473

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

474

def start_processing(self):

475

pass

476

477

@transition(field=state, source='processing', target='completed')

478

def complete(self):

479

# Automatically trigger another action

480

self.notify_customer()

481

482

def notify_customer(self):

483

# Send notification logic

484

pass

485

```

486

487

### State-Dependent Method Behavior

488

489

Use current state to determine method behavior:

490

491

```python

492

class Document(models.Model):

493

state = FSMField(default='draft')

494

495

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

496

if self.state == 'published':

497

# Extra validation for published documents

498

self.full_clean()

499

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

500

501

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

502

def publish(self):

503

self.published_at = timezone.now()

504

```