Dealing with concurrent users¶
Why worry about concurrent users?¶
If you’re not careful, one user can easily overwrite the data persisted by another user working on the same page at the same time.
- Consider this sequence of events:
user A opens page X
user B opens page X
user A changes input on a form on page X, and clicks on a
ButtonInput
that submits itthe database is changed as a result of user A’s changes in such a way that page X would not render the same anymore
user B still has the old page X open, and now makes similar changes on that page and clicks on a
ButtonInput
that submits the info
Without intervention in the above scenario user B’s changes might overwrite those of user A or the application could break - depending on how the code was written.
Reahl prevents changes to outdated forms¶
When a user clicks on a ButtonInput
, Reahl checks whether the Input
values have
been changed since it was rendered for this user. If a change is detected, an error
is shown (with an option to refresh the Form
).
Customising the up-to-date-check¶
You don’t have do any coding to enable this check. You may want to customise what should be included in this up-to-date check, and what not.
You should also ensure (as with all Form
s) that your form displays any errors
that may occur as is done with add_alert_for_domain_exception()
in:
def __init__(self, view):
super().__init__(view, 'simple_form')
self.use_layout(FormLayout())
if self.exception:
self.layout.add_alert_for_domain_exception(self.exception)
domain_object = self.get_or_create_domain_object()
link = self.add_child(A(view, Url('/'), description='Open another tab...'))
link.set_attribute('target', '_blank')
self.add_child(P(view, text='...and increment the value there. Come back here and submit the value. A Concurrency error will be shown'))
#Your own widget that tracks changes
self.add_child(MyConcurrencyWidget(view, domain_object))
self.layout.add_input(TextInput(self, domain_object.fields.some_field_value))
self.define_event_handler(domain_object.events.submit)
self.add_child(Button(self, domain_object.events.submit))
self.define_event_handler(domain_object.events.increment)
self.add_child(Button(self, domain_object.events.increment))
When a Form
is submitted, the new user input is not immediately applied. First, the values of
all of its PrimitiveInput
s are computed as they would render at this point and compared with
what they were when the Form
was rendered initially.
If any differences are detected here it means that someone must have changed relevant data in
the database since the Form
was rendered.
To prevent a specific PrimitiveInput
from participating in this check, pass ignore_concurrent_change=True
when constructing the PrimitiveInput
.
If you have a Widget
that is not a PrimitiveInput
, but that represents some data in the database
which you want to partipate in this check, override get_concurrency_hash_strings()
on your Widget
:
class MyConcurrencyWidget(Widget):
def __init__(self, view, domain_object):
super().__init__(view)
self.domain_object = domain_object
self.add_child(P(view, text='Counter: %s' % domain_object.counter))
def get_concurrency_hash_strings(self):
yield '%s' % self.domain_object.counter
get_concurrency_hash_strings()
yields one or more strings that represent the database state that
should influence the up-to-date check.