django-appointment 📦¶
v3.11.0 🆕
Release Notes for Version 3.11.0¶
Introduction 📜¶
Version 3.11.0 is a feature release on top of 3.10.1. It adds unavailabilities — a way for a staff member to block part of a single day without losing the whole day — brings Django 6.0 and 6.1 support, and finishes the localization work so dates, times and prices follow the visitor's locale everywhere instead of only in some places.
It also drops Python 3.8 and 3.9. See Breaking Changes before upgrading.
New Features ✨¶
Unavailabilities 🗓️¶
A staff member can now record an unavailability: a start time, an end time and a date, with an optional reason. A lunch break, a meeting, an errand — anything that makes them unavailable for part of a day. Slots overlapping one disappear from the booking page while the rest of that day stays bookable.
This is the short-range counterpart to a day off, which keeps removing whole days.
The feature ships as:
- a new
Unavailabilitymodel, registered in the Django admin; - add, update and delete pages under
app-admin/, reachable from the staff member's profile, with the same ownership rules as days off — staff members manage their own, superusers manage anyone's; - slot filtering, so an unavailability is honoured by the booking page, the reschedule page and the
next-available-date lookup alike — including the slots rendered with the booking page itself, not only those
fetched afterwards when the client picks a date. It is applied by the same pass that removes slots taken by
existing appointments, so the service's real duration and
slot_gap_timeare respected around it too.
See the model reference and the admin views.
Django 6.0 and 6.1 support 🐍¶
Django 6.0 and 6.1 are supported and covered by the compatibility matrix, which was reworked
at the same time. The declared range moves from Django>=4.2,<6.0 to Django>=4.2,<7.0, tested across Python 3.10
to 3.14.
CheckConstraint's check argument was renamed to condition in Django 5.1 and removed in Django 6.0, which no
single spelling can satisfy across the supported range. A small appointment/compat.py shim now picks the keyword
the running Django accepts, so the package's check constraints work from 4.2 to 6.1 without a version-specific
branch in every model.
Localization everywhere 🌍¶
Dates, times and prices now follow the active locale consistently, rather than falling back to a US format in the places the earlier work had not reached:
- the booking calendar starts the week on the locale's first day, via Django's
FIRST_DAY_OF_WEEK; - the chosen date and the offered time slots are rendered through Django's localization rather than a hardcoded
format, and the slots are sent to the browser as
[iso_datetime, localized_time]pairs so the page can display one and submit the other; - the down payment is localized like the price already was;
- the date and time pickers in the administration pages get their format from the locale. Two new helpers,
js_timepicker_display_format()andjs_datepicker_display_format(), translate Django's format characters into their Moment.js equivalents, since the widgets cannot read Django's. They reach the templates throughlocalized_formatsin the generic context. - the working hours form splits each field into a localized one the user sees and a hidden pre-formatted one it submits, so what is displayed and what is parsed can differ without ambiguity;
- the default email templates use the full weekday name instead of a locale-dependent abbreviation, and the admin appointment view shows a localized time rather than a whole datetime.
The French catalogue was refreshed to match. Spanish is still looking for a maintainer — see the internationalization guide.
Smoother booking for logged-in users¶
- A logged-in user booking an appointment no longer goes through email verification.
ClientDataFormpre-fills and disables the identity fields for a logged-in user, and the appointment is created fromrequest.userrather than from the submitted data.- The address field is no longer required.
- Django messages stay on screen 5 seconds longer.
Template overrides that survive a mistake¶
The custom template lookup added in 3.10 now accepts more than one name per template, and skips a candidate that
exists but fails to compile — an unclosed {% if %}, an unknown tag, a bad {% load %} — falling back to the
packaged default instead of taking the page down with it. Compile failures are logged with a warning naming the
template, so a silently ignored override is visible in the logs.
The two reschedule emails are the first to use the multiple-name lookup: reschedule.html and
reschedule_admin.html are accepted alongside their older internal names. See
Custom templates.
Context processors in verification emails¶
send_verification_email now accepts the request, and it is passed to every renderer, so a custom email template
can use your context processors.
Deprecations ⚠️¶
convert_12_hour_time_to_24_hour_time() and convert_24_hour_time_to_12_hour_time() are deprecated. Both now emit a
DeprecationWarning and will be removed in 4.0.0. Neither is used internally any more: times are formatted
through Django's localization framework. If you call them from your own code, move to
django.utils.formats.time_format() or the time template filter.
exclude_booked_slots() is deprecated in favour of exclude_unavailable_slots(), which replaced it. It still works
and still emits a DeprecationWarning, but it keeps the old argument order and hardcodes the newer parameters to
None, so it cannot exclude unavailabilities, account for a service longer than the slot step, or apply the rest
time between appointments. See db_helpers.py.
Nothing was removed in this release.
Bug Fixes 🐛¶
- Fixed a
500on the reschedule page when noConfigrow exists. Whether clients may change staff member on reschedule now falls back to the field's default instead of dereferencingNone. - The cached
Configis now dropped whenever it is saved or deleted. An edit made in the admin previously took up to an hour to reach the booking pages, for as long as the stale entry survived. - Django-Q is only considered available when it is both installed and listed in
INSTALLED_APPS. It was previously enough for the package to be importable, so a project that had it installed as an indirect dependency could have reminders scheduled against a cluster that was never going to run. - The appointment buffer time is applied as a rolling window — no appointment may start before now plus the buffer, whatever the day — rather than only on the current day.
- Slots no longer extend past closing time: the service duration is subtracted from the end of the working day before candidate slots are generated.
- Fixed several
gettextaliases shadowed bygettext_lazy, and a local variable named_shadowinggettext's_in the cleanup task and inviews_admin.py. Affected strings were always rendered in the source language. - Fixed the guard in
appointment.jsthat ran when no date had been selected. - Fixed date formatting arguments in the email templates.
- Fixed the name of the
areRequiredFieldsFilled()JavaScript function. - Fixed a CodeQL finding about clear-text logging of sensitive information.
- Creating a
StaffMembernow grants that user Django'sis_staffflag, whichever way the record was created — the Django admin, the staff settings form, a fixture. Previously only the "create new staff member" flow set it, so a staff member created any other way was locked out of the very pages they had been created for. Superusers are left untouched. Unavailability.clean()compared adateagainst adatetimeand raisedTypeErrorinstead of validating. Anything callingfull_clean()— the Django admin's add and change forms among them — hit the error.- Removed leftover debug
print()calls fromviews_admin.py. - The booking page rendered its first batch of slots without consulting unavailabilities, so a slot blocked by one was offered until the client picked a date and the ajax lookup replaced the list. Both paths now agree.
- The unavailability and working hours forms answer malformed input with a
400and theINVALID_DATAerror code instead of raising. Their submitted values are also converted todateandtimebefore reaching the model, rather than being handed wholedatetimeobjects. - Several helpers used a mutable list as a default argument (
unavailabilities=[],appointments=[]); they now default toNone. Calling them without those arguments is unchanged.
Improvements 📈¶
- The documentation site lives in this repository under
docs/and is built with MkDocs Material. The reference pages are now generated from, and checked against, the code they describe. - Slot availability tests no longer depend on the date they are run on, or on state cached by an earlier test.
- Dependency updates across the board: Django, Pillow, phonenumbers, django-phonenumber-field, icalendar, django-q2, python-dotenv, requests, setuptools and the CI actions.
Breaking Changes 🚨¶
- Python 3.8 and 3.9 are no longer supported.
python_requiresis now>=3.10. Both are past end of life; if you are still on either, stay on 3.10.1 until you can upgrade. - A migration is required. The new
Unavailabilitymodel has to be created in your database. No existing field changes meaning and no data is rewritten — see the migration guide. - Projects that create
StaffMemberrows directly and deliberately relied on those users not being Django staff should be aware of theis_staffchange described under Bug Fixes.
Getting Started 🚀¶
Installation 📥:¶
Database Migration 🔧:¶
Previous Version Highlights 🔙¶
Support & Feedback 📞¶
Feedback is welcome. For support, documentation, and further details, please refer to the documentation site or open an issue on GitHub.