From 633be829d73505fb48e11e2d55ce9ad09e6aea27 Mon Sep 17 00:00:00 2001 From: Jamie Lentin Date: Tue, 23 Apr 2024 10:31:03 +0000 Subject: [PATCH] controllers/tour: Reset unspecified fields to DB default #776 If a value is unspecified, update_or_insert() will leave alone on update. We want missing values to go back to their default, as they would have done if they weren't specified in the first place. Add the database defaults at the start of merging the tourstop dict, so this happens. Add some missing defaults for non-null fields so we merge the right values. --- controllers/tour.py | 2 + models/db.py | 6 +-- tests/unit/test_controllers_tour.py | 65 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/controllers/tour.py b/controllers/tour.py index 9f92b786..810ab9a9 100644 --- a/controllers/tour.py +++ b/controllers/tour.py @@ -217,6 +217,8 @@ def data(): tss_targets = {} for i, ts in enumerate(request.vars['tourstops']): ts = { + # All fields not otherwise specfied return to their default + **{k:db.tourstop.get(k).default for k in db.tourstop.fields if k not in ['created', 'updated']}, **ts_shared, **ts, # Deep-clone template_data, should always exist diff --git a/models/db.py b/models/db.py index 57c69a51..4068c6e6 100755 --- a/models/db.py +++ b/models/db.py @@ -772,10 +772,10 @@ Field('identifier', type='string', notnull=True), # Unique identifier for tourstop, for establishing symlinks Field('ord', type='integer', notnull=True), # The position of this tourstop in the tour, starting with 1 Field('ott', type='integer', notnull=False), # The OTT this tourstop points at. NULL => return to start - Field('secondary_ott', type='integer', notnull=True), # A second OTT when targeting a common ancestor + Field('secondary_ott', type='integer', notnull=True, default=0), # A second OTT when targeting a common ancestor Field('qs_opts', type='string', notnull=False, default=''), # QS-style options to apply to modify tourstop, e.g. into_node=true&initmark=... Field('author', type='text', notnull=True, default=''), # Author of tourstop (doesn't necessarily match tour in case of remix) - Field('transition_in', type='string', notnull=True, requires=IS_IN_SET(('fly', 'leap', 'fly_straight'))), # Transition to use when flying to stop + Field('transition_in', type='string', notnull=True, requires=IS_IN_SET(('fly', 'leap', 'fly_straight')), default='fly'), # Transition to use when flying to stop Field('fly_in_speed', type='double', notnull=False, default=1), # Speed relative to global_anim_speed Field('transition_in_wait', type='integer', notnull=False), # How long to wait before entering into transition Field('stop_wait', type='integer', notnull=False), # How long to wait at this stop (null => wait until "next" is pressed) @@ -783,7 +783,7 @@ Field('template_data', type='text', notnull=True, filter_in=lambda obj: json.dumps(obj), filter_out=lambda txt: json.loads(txt)), # JSON template data for tourstop - Field('lang', type='string', notnull=True, length=3), #the 'primary' 2 or 3 letter 'lang' code for this name (e.g. 'en', 'cmn'). See http://www.w3.org/International/articles/language-tags/ + Field('lang', type='string', notnull=True, length=3, default='en'), #the 'primary' 2 or 3 letter 'lang' code for this name (e.g. 'en', 'cmn'). See http://www.w3.org/International/articles/language-tags/ Field('checked_by', type='string'), # Who has checked the validity of this data? Field('checked_updated', 'datetime', default=None), # When did they do it? Field('visible_in_search', 'boolean', notnull=True, default=True), # Available in tourstop-search for remixing a tour? diff --git a/tests/unit/test_controllers_tour.py b/tests/unit/test_controllers_tour.py index abec1811..7b66a4e7 100644 --- a/tests/unit/test_controllers_tour.py +++ b/tests/unit/test_controllers_tour.py @@ -361,6 +361,71 @@ def test_data_shareddata(self): ] ) + def test_data_resettodefault(self): + """Removing values will reset them to their default""" + otts = util.find_unsponsored_otts(10) + + # Can insert a tour with a common ancestor pinpoint + t = self.tour_put('UT::TOUR', dict( + title="A unit test tour", + description="It's a nice tour", + author="UT::Author", + tourstop_shared=dict( + stop_wait=1234, + ), + tourstops=[ + dict( + ott=otts[0], + identifier="ott0", + stop_wait=3, + author="Frank", + ), + dict( + ott=otts[1], + identifier="ott1", + author="Gelda", + ), + ], + )) + # One entry uses default + self.assertEqual( + [ts['stop_wait'] for ts in self.tour_get('UT::TOUR')['tourstops']], + [3, 1234], + ) + # Both have an Author + self.assertEqual( + [ts['author'] for ts in self.tour_get('UT::TOUR')['tourstops']], + ['Frank', 'Gelda'], + ) + + # Clear author, stop_wait. Get set back to their DB defaults + t = self.tour_put('UT::TOUR', dict( + title="A unit test tour", + description="It's a nice tour", + author="UT::Author", + tourstop_shared=dict( + ), + tourstops=[ + dict( + ott=otts[0], + identifier="ott0", + stop_wait=3, + ), + dict( + ott=otts[1], + identifier="ott1", + ), + ], + )) + self.assertEqual( + [ts['stop_wait'] for ts in self.tour_get('UT::TOUR')['tourstops']], + [3, None], + ) + self.assertEqual( + [ts['author'] for ts in self.tour_get('UT::TOUR')['tourstops']], + ['', ''], + ) + def test_list(self): def t_list(tours, include_rest=""): return util.call_controller(