From a133988f364b9a66393c7ebf580a7301ae4a0033 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 21 Sep 2026 12:02:15 +0800 Subject: [PATCH 1/3] fix(spp_registry): bound registration_date and the birthdate onchange by the user's today The registration_date default used fields.Date.today() and _check_registration_date compared against date.today(), so a registrar east of UTC creating a registrant born earlier that local day was refused: the defaulted registration date was the server's yesterday, and entering the correct date was refused as future. _birthdate_onchange used the same server date and reset the valid birthdate in the form. All three now use fields.Date.context_today, the rule #397 gave _check_birthdate_not_future. Fixes #520 --- spp_registry/__manifest__.py | 2 +- spp_registry/models/individual.py | 6 +- spp_registry/models/registrant.py | 9 ++- spp_registry/readme/HISTORY.md | 4 ++ spp_registry/tests/test_constraints.py | 91 +++++++++++++++++++++++++- 5 files changed, 106 insertions(+), 6 deletions(-) diff --git a/spp_registry/__manifest__.py b/spp_registry/__manifest__.py index 2ed500e32..c0b3345c0 100644 --- a/spp_registry/__manifest__.py +++ b/spp_registry/__manifest__.py @@ -3,7 +3,7 @@ { "name": "OpenSPP Registry", "category": "OpenSPP/Core", - "version": "19.0.2.2.4", + "version": "19.0.2.2.6", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_registry/models/individual.py b/spp_registry/models/individual.py index 0bce94b42..3a2a8ab2f 100644 --- a/spp_registry/models/individual.py +++ b/spp_registry/models/individual.py @@ -92,9 +92,13 @@ def _birthdate_onchange(self): This function is used to validate and reset birthdate in case the birthdate date is being set greater than the date today. Resets to previous birthdate value if available, otherwise None. + + "Today" is the user's (``context_today``), the same rule as + ``_check_birthdate_not_future``, so the form never resets a date + the constraint accepts or keeps one it will refuse on save. """ for rec in self: - if rec.birthdate and rec.birthdate > fields.Date.today(): + if rec.birthdate and rec.birthdate > fields.Date.context_today(rec): # Restore previous birthdate or set to None if new record rec.birthdate = rec._origin.birthdate if rec._origin.id else None return { diff --git a/spp_registry/models/registrant.py b/spp_registry/models/registrant.py index a2cb246bc..1ee402dae 100644 --- a/spp_registry/models/registrant.py +++ b/spp_registry/models/registrant.py @@ -1,6 +1,5 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. import logging -from datetime import date from odoo import _, api, fields, models from odoo.exceptions import AccessError, ValidationError @@ -44,7 +43,7 @@ class SPPRegistrant(models.Model): phone_number_ids = fields.One2many("spp.phone.number", "partner_id", "Phone Numbers") company_id = fields.Many2one("res.company", required=True, default=lambda self: self.env.company) - registration_date = fields.Date(default=lambda self: fields.Date.today(), index=True) + registration_date = fields.Date(default=lambda self: fields.Date.context_today(self), index=True) tags_ids = fields.Many2many( "spp.vocabulary.code", relation="res_partner_registrant_tag_rel", @@ -126,9 +125,13 @@ def _onchange_negative_restrict(self): @api.constrains("registration_date") def _check_registration_date(self): + # Bounded by the user's today (context_today), not the server's UTC + # date: a registrar east of UTC is on tomorrow's date for part of + # each day and must not be refused a registration made that morning. + # Same rule as _check_birthdate_not_future and the field's default. for record in self: if record.registration_date: - if record.registration_date > date.today(): + if record.registration_date > fields.Date.context_today(record): error_message = "Registration date must be less than the current date." raise ValidationError(error_message) elif "birthdate" in record and record.birthdate and record.registration_date < record.birthdate: diff --git a/spp_registry/readme/HISTORY.md b/spp_registry/readme/HISTORY.md index d2f80ecda..8dd8e2624 100644 --- a/spp_registry/readme/HISTORY.md +++ b/spp_registry/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.2.6 + +- fix(registry): compare the registration date and the form's birthdate check against the user's today, not the server's UTC date, finishing what 19.0.2.2.4 started for `_check_birthdate_not_future`. The `registration_date` default was `fields.Date.today()` and `_check_registration_date` bounded it by `date.today()`, so a registrar east of UTC creating a registrant born earlier that local day was refused twice over: the defaulted registration date was the server's yesterday ("must be later than the birth date"), and entering the correct date was refused as future. `_birthdate_onchange` used the same server date and silently reset that valid birthdate in the form, while west of UTC it kept the user's tomorrow only for the constraint to refuse it on save. All three now use `fields.Date.context_today` (#520) + ### 19.0.2.2.4 - fix(registry): refuse a date of birth in the future on every write path. `_birthdate_onchange` only runs in the form UI, so ORM `create`/`write`, CSV/Excel import and API writes (XML-RPC, API v2, DCI) all persisted a future `birthdate` — which the non-stored `age` compute then rendered as a negative number in views, exports and API reads. A stored-field constraint now enforces it server-side, comparing against the user's own today so a registrar east of UTC is not refused a birth recorded earlier that local day, and naming the record and the offending value so a bad row in a bulk import can be found. The onchange is kept as the friendlier silent-reset UX in the form (#362) diff --git a/spp_registry/tests/test_constraints.py b/spp_registry/tests/test_constraints.py index 4d8809dc2..e25436658 100644 --- a/spp_registry/tests/test_constraints.py +++ b/spp_registry/tests/test_constraints.py @@ -10,9 +10,14 @@ lowercase-normalisation behaviour of ``create`` / ``write``. - res.partner (individual) ``_check_birthdate_not_future`` — a future date of birth is refused on ``create``, ``write`` and ``load``. +- registration-date default, ``_check_registration_date`` and + ``_birthdate_onchange`` all use the user's today, not the server's + (#520), exercised east and west of UTC on a frozen clock. """ -from datetime import date, timedelta +from datetime import date, datetime, time, timedelta + +from freezegun import freeze_time from odoo import fields from odoo.exceptions import ValidationError @@ -273,3 +278,87 @@ def test_approximate_future_birthdate_rejected(self): """An approximate DOB (birthdate_not_exact) still can't be future.""" with self.assertRaisesRegex(ValidationError, "Date of birth cannot be in the future"): self.individual_a.write({"birthdate": self.future, "birthdate_not_exact": True}) + + +@tagged("post_install", "-at_install") +class TestUserTodayAcrossTimezones(RegistryCommon): + """The registration-date default, ``_check_registration_date`` and + ``_birthdate_onchange`` compare against the user's today + (``fields.Date.context_today``), not the server's UTC date — the + follow-up to ``_check_birthdate_not_future`` (#397). + + The clock is frozen so these pass at any wall-clock hour: at 23:00 UTC a + registrar in Asia/Manila (UTC+8) is already on the next calendar day; at + 05:00 UTC one in America/Los_Angeles (UTC-7/-8) is still on the previous + one. The frozen instant is derived from the real date, so nothing ages. + """ + + EAST_TZ = "Asia/Manila" + WEST_TZ = "America/Los_Angeles" + + def _frozen_utc(self, hour): + return freeze_time(datetime.combine(date.today(), time(hour, 0))) + + def _new_registrant_vals(self, name, birthdate): + return {"name": name, "is_registrant": True, "is_group": False, "birthdate": birthdate} + + def test_east_of_utc_default_registration_date_is_users_today(self): + """A registrant born on the user's today is accepted with the defaulted + registration date, which is the user's today as well (it used to be the + server's date — the user's yesterday — and failed "later than birth").""" + with self._frozen_utc(23): + Partner = self.Partner.with_context(tz=self.EAST_TZ) + user_today = fields.Date.context_today(Partner) + self.assertEqual(user_today, date.today() + timedelta(days=1), "precondition: user is a day ahead") + + newborn = Partner.create(self._new_registrant_vals("Newborn East", user_today)) + + self.assertEqual(newborn.registration_date, user_today) + + def test_east_of_utc_registration_on_users_today_accepted(self): + """registration_date == the user's today is the boundary that must pass.""" + with self._frozen_utc(23): + individual = self.individual_a.with_context(tz=self.EAST_TZ) + user_today = fields.Date.context_today(individual) + + individual.write({"registration_date": user_today}) + + self.assertEqual(individual.registration_date, user_today) + + def test_registration_after_users_today_still_rejected(self): + """The upper bound still holds under a timezone context.""" + with self._frozen_utc(23): + individual = self.individual_a.with_context(tz=self.EAST_TZ) + future = fields.Date.context_today(individual) + timedelta(days=1) + with self.assertRaises(ValidationError): + individual.write({"registration_date": future}) + + def test_east_of_utc_birthdate_onchange_keeps_users_today(self): + """The form must not reset a birthdate the user has already reached.""" + with self._frozen_utc(23): + Partner = self.Partner.with_context(tz=self.EAST_TZ) + user_today = fields.Date.context_today(Partner) + form_record = Partner.new(self._new_registrant_vals("Newborn East", user_today)) + + result = form_record._birthdate_onchange() + + self.assertIsNone(result, "no warning for a date the user has reached") + self.assertEqual(form_record.birthdate, user_today) + + def test_west_of_utc_birthdate_onchange_resets_users_tomorrow(self): + """West of UTC the server's today is the user's tomorrow: the form must + reset it, as ``_check_birthdate_not_future`` would refuse it on save.""" + with self._frozen_utc(5): + Partner = self.Partner.with_context(tz=self.WEST_TZ) + server_today = date.today() + self.assertEqual( + fields.Date.context_today(Partner), + server_today - timedelta(days=1), + "precondition: user is a day behind", + ) + form_record = Partner.new(self._new_registrant_vals("Newborn West", server_today)) + + result = form_record._birthdate_onchange() + + self.assertIn("warning", result or {}, "the user's tomorrow must be refused in the form") + self.assertFalse(form_record.birthdate) From 92d258e97a817deb866c72ac103d4da6bad0dec3 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 21 Sep 2026 12:18:54 +0800 Subject: [PATCH 2/3] test(spp_registry): harden the user's-today tests after review - TestRegistrationDateConstraint anchors on context_today like the code under test (mirrors #397's fix to the birthdate tests) - every timezone test asserts the frozen clock and tz context are in effect, so none can pass by collapsing onto the server date - cover the west-of-UTC direction, where the bound moved a day earlier (server's today refused, default follows the user) - cover the onchange's restore-previous branch on a saved record - constraint rationale moved into a docstring; HISTORY describes the read-only form field accurately and adds a multi-timezone note --- spp_registry/models/registrant.py | 12 ++-- spp_registry/readme/HISTORY.md | 3 +- spp_registry/tests/test_constraints.py | 90 +++++++++++++++++++++----- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/spp_registry/models/registrant.py b/spp_registry/models/registrant.py index 1ee402dae..014c2e047 100644 --- a/spp_registry/models/registrant.py +++ b/spp_registry/models/registrant.py @@ -125,10 +125,14 @@ def _onchange_negative_restrict(self): @api.constrains("registration_date") def _check_registration_date(self): - # Bounded by the user's today (context_today), not the server's UTC - # date: a registrar east of UTC is on tomorrow's date for part of - # each day and must not be refused a registration made that morning. - # Same rule as _check_birthdate_not_future and the field's default. + """Registration date is bounded by the user's today and the birthdate. + + The upper bound is the *user's* today (``fields.Date.context_today``), + not the server's UTC date: a registrar east of UTC is on tomorrow's + date for part of each day and must not be refused a registration + made that morning. Same rule as ``_check_birthdate_not_future`` and + the field's default. + """ for record in self: if record.registration_date: if record.registration_date > fields.Date.context_today(record): diff --git a/spp_registry/readme/HISTORY.md b/spp_registry/readme/HISTORY.md index 8dd8e2624..afa064a66 100644 --- a/spp_registry/readme/HISTORY.md +++ b/spp_registry/readme/HISTORY.md @@ -1,6 +1,7 @@ ### 19.0.2.2.6 -- fix(registry): compare the registration date and the form's birthdate check against the user's today, not the server's UTC date, finishing what 19.0.2.2.4 started for `_check_birthdate_not_future`. The `registration_date` default was `fields.Date.today()` and `_check_registration_date` bounded it by `date.today()`, so a registrar east of UTC creating a registrant born earlier that local day was refused twice over: the defaulted registration date was the server's yesterday ("must be later than the birth date"), and entering the correct date was refused as future. `_birthdate_onchange` used the same server date and silently reset that valid birthdate in the form, while west of UTC it kept the user's tomorrow only for the constraint to refuse it on save. All three now use `fields.Date.context_today` (#520) +- fix(registry): compare the registration date and the form's birthdate check against the user's today, not the server's UTC date, finishing what 19.0.2.2.4 started for `_check_birthdate_not_future`. The `registration_date` default was `fields.Date.today()` and `_check_registration_date` bounded it by `date.today()`, so a registrar east of UTC creating a registrant born earlier that local day was refused twice over: the defaulted registration date was the server's yesterday ("must be later than the birth date") with no way to correct it in the form, where the field is read-only once defaulted, and setting the correct date through the API or an import was refused as future. `_birthdate_onchange` used the same server date and silently reset that valid birthdate in the form, while west of UTC it kept the user's tomorrow only for the constraint to refuse it on save. All three now use `fields.Date.context_today` (#520) +- behaviour note for teams spread across timezones: "today" is now each user's own, so a registration date stored by a user east of UTC can read as tomorrow to a colleague further west until their local midnight, and an API or import write of that value by the second user is refused as future until then. The form is unaffected because `registration_date` is read-only there once set. ### 19.0.2.2.4 diff --git a/spp_registry/tests/test_constraints.py b/spp_registry/tests/test_constraints.py index e25436658..d207c1b86 100644 --- a/spp_registry/tests/test_constraints.py +++ b/spp_registry/tests/test_constraints.py @@ -117,18 +117,27 @@ def test_no_head_code_in_vocabulary_short_circuits(self): @tagged("post_install", "-at_install") class TestRegistrationDateConstraint(RegistryCommon): - """spp_registry/models/registrant.py::_check_registration_date""" + """spp_registry/models/registrant.py::_check_registration_date + + Dates are based on the user's today (``context_today``) to match the + constraint; anchoring on the server date would make these depend on the + test user's timezone. + """ + + def setUp(self): + super().setUp() + self.today = fields.Date.context_today(self.individual_a) + self.future = self.today + timedelta(days=1) def test_future_registration_date_rejected(self): """Registration date in the future raises ValidationError.""" - future = date.today() + timedelta(days=1) - with self.assertRaises(ValidationError): - self.individual_a.write({"registration_date": future}) + with self.assertRaisesRegex(ValidationError, "Registration date must be less than the current date"): + self.individual_a.write({"registration_date": self.future}) def test_today_is_allowed(self): """Registration date == today is the boundary that must pass.""" - self.individual_a.write({"registration_date": date.today()}) - self.assertEqual(self.individual_a.registration_date, date.today()) + self.individual_a.write({"registration_date": self.today}) + self.assertEqual(self.individual_a.registration_date, self.today) def test_registration_before_birthdate_rejected(self): """Registration date < birthdate raises ValidationError. @@ -296,9 +305,24 @@ class TestUserTodayAcrossTimezones(RegistryCommon): EAST_TZ = "Asia/Manila" WEST_TZ = "America/Los_Angeles" + FUTURE_MESSAGE = "Registration date must be less than the current date" + def _frozen_utc(self, hour): return freeze_time(datetime.combine(date.today(), time(hour, 0))) + def _user_today(self, record, shift): + """The user's today, asserting the frozen clock and tz context really + shift it by ``shift`` days from the server's; without this every test + would still pass if freezegun or the tz context silently stopped + applying, with ``user_today`` collapsing onto the server date.""" + user_today = fields.Date.context_today(record) + self.assertEqual( + user_today, + date.today() + timedelta(days=shift), + "precondition: frozen clock / tz context not in effect", + ) + return user_today + def _new_registrant_vals(self, name, birthdate): return {"name": name, "is_registrant": True, "is_group": False, "birthdate": birthdate} @@ -308,8 +332,7 @@ def test_east_of_utc_default_registration_date_is_users_today(self): server's date — the user's yesterday — and failed "later than birth").""" with self._frozen_utc(23): Partner = self.Partner.with_context(tz=self.EAST_TZ) - user_today = fields.Date.context_today(Partner) - self.assertEqual(user_today, date.today() + timedelta(days=1), "precondition: user is a day ahead") + user_today = self._user_today(Partner, +1) newborn = Partner.create(self._new_registrant_vals("Newborn East", user_today)) @@ -319,7 +342,7 @@ def test_east_of_utc_registration_on_users_today_accepted(self): """registration_date == the user's today is the boundary that must pass.""" with self._frozen_utc(23): individual = self.individual_a.with_context(tz=self.EAST_TZ) - user_today = fields.Date.context_today(individual) + user_today = self._user_today(individual, +1) individual.write({"registration_date": user_today}) @@ -329,15 +352,37 @@ def test_registration_after_users_today_still_rejected(self): """The upper bound still holds under a timezone context.""" with self._frozen_utc(23): individual = self.individual_a.with_context(tz=self.EAST_TZ) - future = fields.Date.context_today(individual) + timedelta(days=1) - with self.assertRaises(ValidationError): + future = self._user_today(individual, +1) + timedelta(days=1) + with self.assertRaisesRegex(ValidationError, self.FUTURE_MESSAGE): individual.write({"registration_date": future}) + def test_west_of_utc_default_registration_date_is_users_today(self): + """West of UTC the default follows the user too: the stored date is the + user's today, a day behind the server's, and coexists with a past + birthdate without tripping the registration-before-birth branch.""" + with self._frozen_utc(5): + Partner = self.Partner.with_context(tz=self.WEST_TZ) + user_today = self._user_today(Partner, -1) + + registrant = Partner.create(self._new_registrant_vals("Registrant West", date(1990, 1, 1))) + + self.assertEqual(registrant.registration_date, user_today) + + def test_west_of_utc_registration_on_servers_today_rejected(self): + """The bound moved a day earlier for users west of UTC, by design: the + server's today is their tomorrow and is refused as future. This is the + one direction in which a value accepted before is refused now.""" + with self._frozen_utc(5): + individual = self.individual_a.with_context(tz=self.WEST_TZ) + self._user_today(individual, -1) + with self.assertRaisesRegex(ValidationError, self.FUTURE_MESSAGE): + individual.write({"registration_date": date.today()}) + def test_east_of_utc_birthdate_onchange_keeps_users_today(self): """The form must not reset a birthdate the user has already reached.""" with self._frozen_utc(23): Partner = self.Partner.with_context(tz=self.EAST_TZ) - user_today = fields.Date.context_today(Partner) + user_today = self._user_today(Partner, +1) form_record = Partner.new(self._new_registrant_vals("Newborn East", user_today)) result = form_record._birthdate_onchange() @@ -351,14 +396,25 @@ def test_west_of_utc_birthdate_onchange_resets_users_tomorrow(self): with self._frozen_utc(5): Partner = self.Partner.with_context(tz=self.WEST_TZ) server_today = date.today() - self.assertEqual( - fields.Date.context_today(Partner), - server_today - timedelta(days=1), - "precondition: user is a day behind", - ) + self._user_today(Partner, -1) form_record = Partner.new(self._new_registrant_vals("Newborn West", server_today)) result = form_record._birthdate_onchange() self.assertIn("warning", result or {}, "the user's tomorrow must be refused in the form") self.assertFalse(form_record.birthdate) + + def test_birthdate_onchange_restores_saved_value_on_existing_record(self): + """Editing a saved registrant to the user's tomorrow restores the saved + birthdate rather than clearing it (the ``_origin`` branch).""" + saved_birthdate = date(1990, 1, 1) + self.individual_a.write({"birthdate": saved_birthdate}) + with self._frozen_utc(5): + Partner = self.Partner.with_context(tz=self.WEST_TZ) + self._user_today(Partner, -1) + form_record = Partner.new({"birthdate": date.today()}, origin=self.individual_a) + + result = form_record._birthdate_onchange() + + self.assertIn("warning", result or {}) + self.assertEqual(form_record.birthdate, saved_birthdate) From 98b4af587b86cafe827a95730617e3116a8cec7a Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 21 Sep 2026 12:24:55 +0800 Subject: [PATCH 3/3] docs(spp_registry): regenerate README from fragments (CI output) --- spp_registry/README.rst | 24 +++++++++++++ spp_registry/static/description/index.html | 39 ++++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/spp_registry/README.rst b/spp_registry/README.rst index 4eb0e993a..691b39135 100644 --- a/spp_registry/README.rst +++ b/spp_registry/README.rst @@ -139,6 +139,30 @@ Dependencies Changelog ========= +19.0.2.2.6 +~~~~~~~~~~ + +- fix(registry): compare the registration date and the form's birthdate + check against the user's today, not the server's UTC date, finishing + what 19.0.2.2.4 started for ``_check_birthdate_not_future``. The + ``registration_date`` default was ``fields.Date.today()`` and + ``_check_registration_date`` bounded it by ``date.today()``, so a + registrar east of UTC creating a registrant born earlier that local + day was refused twice over: the defaulted registration date was the + server's yesterday ("must be later than the birth date") with no way + to correct it in the form, where the field is read-only once + defaulted, and setting the correct date through the API or an import + was refused as future. ``_birthdate_onchange`` used the same server + date and silently reset that valid birthdate in the form, while west + of UTC it kept the user's tomorrow only for the constraint to refuse + it on save. All three now use ``fields.Date.context_today`` (#520) +- behaviour note for teams spread across timezones: "today" is now each + user's own, so a registration date stored by a user east of UTC can + read as tomorrow to a colleague further west until their local + midnight, and an API or import write of that value by the second user + is refused as future until then. The form is unaffected because + ``registration_date`` is read-only there once set. + 19.0.2.2.4 ~~~~~~~~~~ diff --git a/spp_registry/static/description/index.html b/spp_registry/static/description/index.html index 48cfcf7c2..3dde87e87 100644 --- a/spp_registry/static/description/index.html +++ b/spp_registry/static/description/index.html @@ -518,6 +518,31 @@

Changelog

+

19.0.2.2.6

+
    +
  • fix(registry): compare the registration date and the form’s birthdate +check against the user’s today, not the server’s UTC date, finishing +what 19.0.2.2.4 started for _check_birthdate_not_future. The +registration_date default was fields.Date.today() and +_check_registration_date bounded it by date.today(), so a +registrar east of UTC creating a registrant born earlier that local +day was refused twice over: the defaulted registration date was the +server’s yesterday (“must be later than the birth date”) with no way +to correct it in the form, where the field is read-only once +defaulted, and setting the correct date through the API or an import +was refused as future. _birthdate_onchange used the same server +date and silently reset that valid birthdate in the form, while west +of UTC it kept the user’s tomorrow only for the constraint to refuse +it on save. All three now use fields.Date.context_today (#520)
  • +
  • behaviour note for teams spread across timezones: “today” is now each +user’s own, so a registration date stored by a user east of UTC can +read as tomorrow to a colleague further west until their local +midnight, and an API or import write of that value by the second user +is refused as future until then. The form is unaffected because +registration_date is read-only there once set.
  • +
+
+

19.0.2.2.4

  • fix(registry): refuse a date of birth in the future on every write @@ -538,7 +563,7 @@

    19.0.2.2.4

    SELECT id, display_name, birthdate FROM res_partner WHERE birthdate > CURRENT_DATE;
-
+

19.0.2.2.3

  • fix(registry): repair the stored status/is_ended computes on @@ -562,7 +587,7 @@

    19.0.2.2.3

    CREATE INDEX CONCURRENTLY IF NOT EXISTS spp_group_membership__ended_date_index ON spp_group_membership (ended_date) WHERE ended_date IS NOT NULL;
-
+

19.0.2.2.2

  • fix(registry): let an ID type be used again after its ID was removed. @@ -574,7 +599,7 @@

    19.0.2.2.2

    surfacing a database error (#1136)
-
+

19.0.2.2.1

  • feat(registry): registry configuration is consolidated into one @@ -585,7 +610,7 @@

    19.0.2.2.1

    framework refuses a settings save from anyone else (#1009)
-
+

19.0.2.1.4

  • fix(registry): remove the dead @api.constrains("age") @@ -597,7 +622,7 @@

    19.0.2.1.4

    dropped
-
+

19.0.2.1.3

  • fix(registry): show an ID Status column on the group form @@ -608,7 +633,7 @@

    19.0.2.1.3

    (#1110)
-
+

19.0.2.1.1

  • fix(views): add reusable x2many_no_padding JS widget that @@ -618,7 +643,7 @@

    19.0.2.1.1

    don’t bloat the layout (#943).
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2