Skip to content

Commit

Permalink
add cleanappsettings check mode (#1520)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Jan 2, 2025
1 parent 4ff1d3c commit eab7b92
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 98 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Unreleased
Added
-----

- **Projectroles**
- Check mode in ``cleanappsettings`` command (#1520)
- **Timeline**
- ``get_event_name()`` template tag (#1524)

Expand Down
6 changes: 6 additions & 0 deletions docs/source/dev_resource.rst
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ the Django database:
$ ./manage.py cleanappsettings
.. hint::

If you want to ensure desired effects for cleanup when developing, run the
command with the ``-c`` or ``--check`` argument. This will log changes to be
made without actually altering the database.


Project Modifying API
=====================
Expand Down
1 change: 1 addition & 0 deletions docs/source/major_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ v1.0.4 (WIP)
Release Highlights
==================

- Add check mode to cleanappsettings management command
- Update timeline event displaying in UI
- Enable timeline search for display formatting of event name

Expand Down
53 changes: 36 additions & 17 deletions projectroles/management/commands/cleanappsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@


# Local constants
START_MSG = 'Checking database for undefined app settings..'
START_MSG = 'Querying database for undefined app settings..'
CHECK_MODE_MSG = 'Check mode enabled, database will not be altered'
END_MSG = 'OK'
DEFINITION_NOT_FOUND_MSG = 'definition not found'
DEF_NOT_FOUND_MSG = 'definition not found'
ALLOWED_TYPES_MSG = 'does not match allowed types'
CHECK_PREFIX_MSG = 'Found "{}" in project "{}": '
DELETE_PREFIX_MSG = 'Deleting "{}" from project "{}": '
DELETE_PROJECT_TYPE_MSG = 'project type "{}" {}: {}'
DELETE_SCOPE_MSG = 'user {} has no role in project'
Expand All @@ -39,15 +41,33 @@ def get_setting_str(db_setting):


class Command(BaseCommand):
help = 'Cleans up undefined app settings from the database.'
help = (
'Cleans up undefined or otherwise orphaned app settings from the '
'database.'
)

def add_arguments(self, parser):
pass
parser.add_argument(
'-c',
'--check',
dest='check',
required=False,
default=False,
action='store_true',
help='Log settings to be cleaned up without altering the database',
)

def handle(self, *args, **options):
check = options.get('check', False)
if check:
prefix_msg = CHECK_PREFIX_MSG
logger.info(CHECK_MODE_MSG)
else:
prefix_msg = DELETE_PREFIX_MSG
logger.info(START_MSG)
db_settings = AppSetting.objects.filter(user=None)
for s in db_settings:
# Undefined settings
def_kwargs = {'name': s.name}
if s.app_plugin:
def_kwargs['plugin'] = s.app_plugin.get_plugin()
Expand All @@ -57,38 +77,37 @@ def handle(self, *args, **options):
definition = app_settings.get_definition(**def_kwargs)
except ValueError:
logger.info(
DELETE_PREFIX_MSG.format(
get_setting_str(s), s.project.title
)
+ DEFINITION_NOT_FOUND_MSG
prefix_msg.format(get_setting_str(s), s.project.title)
+ DEF_NOT_FOUND_MSG
)
s.delete()
if not check:
s.delete()
continue
# Invalid scope
if s.project and s.project.type not in definition.get(
'project_types', ['PROJECT']
):
logger.info(
DELETE_PREFIX_MSG.format(
get_setting_str(s), s.project.title
)
prefix_msg.format(get_setting_str(s), s.project.title)
+ DELETE_PROJECT_TYPE_MSG.format(
s.project.type,
ALLOWED_TYPES_MSG,
definition.get('project_types', ['PROJECT']),
)
)
s.delete()
if not check:
s.delete()

db_settings = AppSetting.objects.filter(
project__isnull=False, user__isnull=False
)
for s in db_settings:
# No user role for PROJECT_USER scope setting
if not s.project.get_role(s.user):
logger.info(
DELETE_PREFIX_MSG.format(
get_setting_str(s), s.project.title
)
prefix_msg.format(get_setting_str(s), s.project.title)
+ DELETE_SCOPE_MSG.format(s.user.username)
)
s.delete()
if not check:
s.delete()
logger.info(END_MSG)
186 changes: 105 additions & 81 deletions projectroles/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from projectroles.management.commands.cleanappsettings import (
START_MSG,
END_MSG,
DEFINITION_NOT_FOUND_MSG,
DEF_NOT_FOUND_MSG,
ALLOWED_TYPES_MSG,
DELETE_PREFIX_MSG,
DELETE_PROJECT_TYPE_MSG,
Expand Down Expand Up @@ -702,7 +702,7 @@ def setUp(self):
)
self.plugin = Plugin.objects.get(name='example_project_app')

# Init test setting
# Init test settings
self.setting_str_values = {
'plugin_name': EXAMPLE_APP_NAME,
'project': self.project,
Expand Down Expand Up @@ -768,22 +768,20 @@ def setUp(self):
value_json=s['value'] if s['setting_type'] == 'JSON' else {},
project=s['project'],
)
self.logger_name = 'projectroles.management.commands.cleanappsettings'

def test_command_undefined(self):
"""Test cleanappsettings with undefined setting"""
undef_setting = AppSetting(
app_plugin=self.plugin,
project=self.project,
self.make_setting(
plugin_name=self.plugin.name,
name='ghost',
type='BOOLEAN',
setting_type='BOOLEAN',
value=True,
project=self.project,
)
undef_setting.save()
self.assertEqual(AppSetting.objects.count(), 6)

with self.assertLogs(
'projectroles.management.commands.cleanappsettings', level='INFO'
) as cm:
with self.assertLogs(self.logger_name, 'INFO') as cm:
call_command('cleanappsettings')
self.assertEqual(
cm.output,
Expand All @@ -795,101 +793,127 @@ def test_command_undefined(self):
'settings.example_project_app.ghost',
self.project.title,
)
+ DEFINITION_NOT_FOUND_MSG
+ DEF_NOT_FOUND_MSG
),
CLEAN_LOG_PREFIX + END_MSG,
],
)
self.assertEqual(AppSetting.objects.count(), 5)
self.assertQuerysetEqual(
AppSetting.objects.filter(name='ghost'),
[],
)
self.assertIsNone(AppSetting.objects.filter(name='ghost').first())

def test_command_project_types(self):
"""Test cleanappsettings with invalid project types"""
cat_setting = AppSetting(
app_plugin=self.plugin,
project=self.category,
def test_command_invalid_project_type(self):
"""Test cleanappsettings with invalid project type"""
self.make_setting(
plugin_name=self.plugin.name,
name='project_user_bool_setting',
type='BOOLEAN',
setting_type='BOOLEAN',
value=True,
project=self.category,
)
cat_setting.save()
self.assertEqual(AppSetting.objects.count(), 6)

with self.assertLogs(
'projectroles.management.commands.cleanappsettings', level='INFO'
) as cm:
with self.assertLogs(self.logger_name, 'INFO') as cm:
call_command('cleanappsettings')
self.assertEqual(len(cm.output), 3)
self.assertEqual(
cm.output,
[
CLEAN_LOG_PREFIX + START_MSG,
(
CLEAN_LOG_PREFIX
+ DELETE_PREFIX_MSG.format(
'settings.example_project_app.'
'project_user_bool_setting',
self.category.title,
)
+ DELETE_PROJECT_TYPE_MSG.format(
self.category.type,
ALLOWED_TYPES_MSG,
'[\'PROJECT\']',
)
),
CLEAN_LOG_PREFIX + END_MSG,
],
)
self.assertEqual(AppSetting.objects.count(), 5)
self.assertQuerysetEqual(
AppSetting.objects.filter(name='project_user_bool_setting'),
[],
cm.output[1],
(
CLEAN_LOG_PREFIX
+ DELETE_PREFIX_MSG.format(
'settings.example_project_app.'
'project_user_bool_setting',
self.category.title,
)
+ DELETE_PROJECT_TYPE_MSG.format(
self.category.type, ALLOWED_TYPES_MSG, '[\'PROJECT\']'
)
),
)
self.assertEqual(AppSetting.objects.count(), 5)
self.assertIsNone(
AppSetting.objects.filter(name='project_user_bool_setting').first()
)

def test_command_project_user_scope(self):
"""Test cleanappsettings with PROJECT_USER scope"""
def test_command_project_user_scope_no_role(self):
"""Test cleanappsettings with PROJECT_USER scope and no role"""
user_new = self.make_user('user_new')
user_new.save()
pu_setting = AppSetting(
app_plugin=self.plugin,
project=self.project,
user=user_new,
self.make_setting(
plugin_name=self.plugin.name,
name='project_user_bool_setting',
type='BOOLEAN',
setting_type='BOOLEAN',
value=True,
project=self.project,
user=user_new,
)
pu_setting.save()
self.assertEqual(AppSetting.objects.count(), 6)

with self.assertLogs(
'projectroles.management.commands.cleanappsettings', level='INFO'
) as cm:
with self.assertLogs(self.logger_name, 'INFO') as cm:
call_command('cleanappsettings')
self.assertEqual(len(cm.output), 3)
self.assertEqual(
cm.output,
[
CLEAN_LOG_PREFIX + START_MSG,
(
CLEAN_LOG_PREFIX
+ DELETE_PREFIX_MSG.format(
'settings.example_project_app.'
'project_user_bool_setting',
self.project.title,
)
+ DELETE_SCOPE_MSG.format(
user_new.username,
)
),
CLEAN_LOG_PREFIX + END_MSG,
],
)
self.assertEqual(AppSetting.objects.count(), 5)
self.assertQuerysetEqual(
AppSetting.objects.filter(name='project_user_bool_setting'),
[],
cm.output[1],
(
CLEAN_LOG_PREFIX
+ DELETE_PREFIX_MSG.format(
'settings.example_project_app.'
'project_user_bool_setting',
self.project.title,
)
+ DELETE_SCOPE_MSG.format(user_new.username)
),
)
self.assertEqual(AppSetting.objects.count(), 5)
self.assertIsNone(
AppSetting.objects.filter(name='project_user_bool_setting').first()
)

def test_command_project_user_scope_role(self):
"""Test cleanappsettings with PROJECT_USER scope and existing role"""
user_new = self.make_user('user_new')
self.make_assignment(self.project, user_new, self.role_contributor)
self.make_setting(
plugin_name=self.plugin.name,
name='project_user_bool_setting',
setting_type='BOOLEAN',
value=True,
project=self.project,
user=user_new,
)
self.assertEqual(AppSetting.objects.count(), 6)
call_command('cleanappsettings')
self.assertEqual(AppSetting.objects.count(), 6)

def test_command_check(self):
"""Test cleanappsettings with check mode enabled"""
user_new = self.make_user('user_new')
self.make_setting(
plugin_name=self.plugin.name,
name='ghost',
setting_type='BOOLEAN',
value=True,
project=self.project,
)
self.make_setting(
plugin_name=self.plugin.name,
name='project_user_bool_setting',
setting_type='BOOLEAN',
value=True,
project=self.category,
)
self.make_setting(
plugin_name=self.plugin.name,
name='project_user_bool_setting',
setting_type='BOOLEAN',
value=True,
project=self.project,
user=user_new,
)
self.assertEqual(AppSetting.objects.count(), 8)

with self.assertLogs(self.logger_name, 'INFO') as cm:
call_command('cleanappsettings', check=True)
self.assertEqual(len(cm.output), 6)
self.assertEqual(AppSetting.objects.count(), 8)


class TestSyncGroups(TestCase):
Expand Down

0 comments on commit eab7b92

Please sign in to comment.