API

Ordered model

class alliance_platform.ordered_model.models.OrderedModel(*args, **kwargs)[source]

OrderedModel will maintain the ordering field for you using database triggers.

Warning

As this uses database triggers only postgres is supported

Warning

Don’t add a unique constraint to the order_field_name (or unique_together when using order_with_respect_to) - OrderedModel will guarantee uniqueness for you. This is a technical limitation due to how the triggers are implemented.

Guarantees that all records will have a unique value for order_field_name with respect to order_with_respect_to (if specified). Whenever the order_field_name changes on a record all records will be updated as necessary to guarantee their order_field_name is 2 away from each other. For example if you have 4 records then the ordering values will be [2, 4, 6, 8].

This structure allows easy re-ordering by assigning an odd number to order_field_name. eg. To move the first item between the second last and last it would be assigned to 7.

If a new record is created without an explicit ordering field value then it will be given the last current ordering value + 2 (ie. added to the end).

For example Phase is sorted on the sort_key column with respect to board.

class Phase(OrderedModel):
    board = models.ForeignKey(Board, on_delete=models.CASCADE, related_name="phases")
    title = models.CharField(max_length=255)
    sort_key = models.PositiveIntegerField(blank=True)

    order_field_name = "sort_key"
    order_with_respect_to = ("board", )

To move a record to a new position with strict checks use move_between(). This will validate that the relative order of the items moving between have not changed and will raise an error if they have. If strict checking isn’t desirable then you can call move_before(), move_after(), move_start() or move_end().

When saving an OrderedModel the order_field_name is excluded by default. This is to avoid overwriting a new ordering that may have changed after the record was loaded. To explicitly save the field pass it to update_fields on save.

record.sort_key = move_after_record.sort_key + 1
record.save(update_fields=['sort_key'])

Warning

The value or order_field_name on a record may be inconsistent with the database value after a save due to updates caused by database triggers. If you need to read the value after an update you should call refresh_from_db or re-fetch the record.

Bulk Updates

In some cases you may wish to defer saving individual changes to ordering and instead save the entire state in one go. You can use bulk_update() and the trigger will happen once. Make sure to specify update_fields.

Shop.objects.bulk_update(shops, [Shop.order_field_name])

If you specify batch_size then things won’t work properly as the trigger would run after each batch. To avoid problems with this or with saving multiple individual items you can use defer_triggers(). This will disable the normal triggers until the end of the context block and instead update the order_field_name manually once at the end.

NOTE: When using order_with_respect_to this will update all rows in the table even if they don’t require it. If using notify_on_reorder it will fire even if nothing has changed.

with Shop.defer_triggers():
    Shop.objects.bulk_update(
        shops_to_update, [Shop.order_field_name], batch_size=batch_size
    )
classmethod defer_triggers(notification_op='UPDATE')[source]

A context manager that disables the ordering maintenance triggers and manually updates the ordering once at the end

This is useful when you need to do multiple database writes the change the order_field_name value and having the triggers run each time would cause issues.

Usage:

with Shop.defer_triggers():
    Shop.objects.bulk_update(
        shops_to_update, [Shop.order_field_name], batch_size=10
    )

Warning

When using order_with_respect_to all rows in the table will be updated even if they don’t require it. If using notify_on_reorder it will fire even if nothing has changed and it will be fired for each unique combination of order_with_respect_to.

Parameters:

notification_op – as it’s not possible to automatically determine what operation actually happened within the context block any notifications will have operation set to UPDATE. If this is not correct for a particular use case pass notification_op="INSERT" or notification_op="DELETE" instead. Only relevant when notify_on_reorder is set.

move_after(after)[source]

Move this item after the specified record or primary key

Parameters:

after (Model | str | int) – Either the record or primary key of record to move this item after

move_before(before)[source]

Move this item before the specified record or primary key

Parameters:

before (Model | str | int) – Either the record or primary key of record to move this item before

move_between(before, after)[source]

Insert this card between before and after_pk.

Raises an error if before and after are no longer adjacent (eg. something else moved them) or the ordering of the current item has changed since it was loaded.

Parameters:
  • before (Model | str | int)

  • after (Model | str | int)

move_end()[source]

Move this item to the first position

move_start()[source]

Move this item to the first position

notify_on_reorder: str | None = None[source]

When specified pg_notify will be called with notify_on_reorder as the channel name after a reorder occurs.

Notifications will occur per update. If order_with_respect_to is set it will happen once per unique grouping (eg. if a bulk_update occurs across different groupings there will be mulitiple notifications).

Notifications won’t occur if an update happens that doesn’t change any order_field_name value.

Notification payload is a JSON sting that looks like:

{'operation': 'UPDATE',
 'order_with_respect_to': {'plaza': 116},
 'schema': 'public',
 'table': 'test_common_lib_shop',
 'timestamp': '2021-06-30 00:00:53.928499+00'}
order_field_name: str = 'sort_key'[source]

The name of the field to sort by. Your model must provide the field definition.

order_with_respect_to: str | tuple[str, ...] | None = None[source]

If ordering is in respect to other field(s) specify them here

save(force_insert=False, force_update=False, using=None, update_fields=None)[source]

Saves record but excludes order_field_name by default

class alliance_platform.ordered_model.models.UnexpectedOrderError[source]

Thrown when calling move_between and items are not in the expected order