diff --git a/config/settings/base.py b/config/settings/base.py index 160b1818..32460e36 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -529,8 +529,6 @@ def set_logging(level=None): SODAR_API_DEFAULT_HOST = env.url( 'SODAR_API_DEFAULT_HOST', 'http://0.0.0.0:8000' ) -# Default page size for paginated REST API list views -SODAR_API_PAGE_SIZE = env.int('SODAR_API_PAGE_SIZE', 100) # Projectroles app settings diff --git a/docs/source/app_projectroles_settings.rst b/docs/source/app_projectroles_settings.rst index 03e50342..27c42d1e 100644 --- a/docs/source/app_projectroles_settings.rst +++ b/docs/source/app_projectroles_settings.rst @@ -346,6 +346,15 @@ environment settings. Example: SODAR_API_DEFAULT_HOST = env.url('SODAR_API_DEFAULT_HOST', 'http://0.0.0.0:8000') +For enabling page size customization for pagination, it's recommended to set +``REST_FRAMEWORK['PAGE_SIZE']`` using an environment variable as follows: + +.. code-block:: python + + REST_FRAMEWORK = { + 'PAGE_SIZE': env.int('SODAR_API_PAGE_SIZE', 100), + } + LDAP/AD Configuration (Optional) ================================ diff --git a/example_project_app/plugins.py b/example_project_app/plugins.py index 6e9c90a5..d6b7a4c4 100644 --- a/example_project_app/plugins.py +++ b/example_project_app/plugins.py @@ -272,7 +272,7 @@ class ProjectAppPlugin(ProjectModifyPluginMixin, ProjectAppPluginPoint): 'options': get_example_setting_options, 'description': 'Example callable project user setting with options', }, - 'project_category_bool_setting': { + 'category_bool_setting': { 'scope': SODAR_CONSTANTS['APP_SETTING_SCOPE_PROJECT'], 'type': 'BOOLEAN', 'label': 'Category boolean setting', diff --git a/projectroles/static/projectroles/css/projectroles.css b/projectroles/static/projectroles/css/projectroles.css index 7d50d013..1ab500e4 100644 --- a/projectroles/static/projectroles/css/projectroles.css +++ b/projectroles/static/projectroles/css/projectroles.css @@ -397,7 +397,6 @@ dl dd { color: #ffffff !important; } - /* HACK for Bootstrap v4 button group alignment with dropdown enabled */ .btn { height: 2.35em; diff --git a/projectroles/tests/test_app_settings.py b/projectroles/tests/test_app_settings.py index 61ff9d03..0b2b55cd 100644 --- a/projectroles/tests/test_app_settings.py +++ b/projectroles/tests/test_app_settings.py @@ -448,11 +448,11 @@ def test_set_multi_project_user(self): def test_set_invalid_project_types(self): """Test set() with invalid project types scope""" - # Should fail because project_category_bool_setting has CATEGORY scope + # Should fail because category_bool_setting has CATEGORY scope with self.assertRaises(ValueError): app_settings.set( plugin_name=EXAMPLE_APP_NAME, - setting_name='project_category_bool_setting', + setting_name='category_bool_setting', project=self.project, value=True, ) @@ -732,7 +732,7 @@ def test_get_definitions_project(self): 'description': 'Example callable project setting with options', 'user_modifiable': True, }, - 'project_category_bool_setting': { + 'category_bool_setting': { 'scope': SODAR_CONSTANTS['APP_SETTING_SCOPE_PROJECT'], 'type': 'BOOLEAN', 'label': 'Category boolean setting', @@ -880,7 +880,8 @@ def test_get_definitions_project_user(self): 'type': 'STRING', 'default': get_example_setting_default, 'options': get_example_setting_options, - 'description': 'Example callable project user setting with options', + 'description': 'Example callable project user setting with ' + 'options', }, } defs = app_settings.get_definitions( diff --git a/projectroles/tests/test_models.py b/projectroles/tests/test_models.py index 49d54fb3..c82caf0c 100644 --- a/projectroles/tests/test_models.py +++ b/projectroles/tests/test_models.py @@ -68,7 +68,7 @@ def make_project( archive=False, sodar_uuid=None, ): - """Make and save a Project""" + """Create a Project object""" values = { 'title': title, 'type': type, @@ -115,7 +115,7 @@ class RoleAssignmentMixin: @classmethod def make_assignment(cls, project, user, role): - """Make and save a RoleAssignment""" + """Create a RoleAssignment object""" values = {'project': project, 'user': user, 'role': role} result = RoleAssignment(**values) result.save() @@ -136,7 +136,7 @@ def make_invite( date_expire=None, secret=None, ): - """Make and save a ProjectInvite""" + """Create a ProjectInvite object""" values = { 'email': email, 'project': project, @@ -177,7 +177,7 @@ def make_setting( user=None, sodar_uuid=None, ): - """Make and save a AppSetting""" + """Create an AppSetting object""" values = { 'app_plugin': ( None @@ -214,7 +214,7 @@ def make_site( secret=build_secret(), sodar_uuid=None, ): - """Make and save a RemoteSite""" + """Create a RemoteSite object""" values = { 'name': name, 'url': url, @@ -237,7 +237,7 @@ class RemoteProjectMixin: def make_remote_project( cls, project_uuid, site, level, date_access=None, project=None ): - """Make and save a RemoteProject""" + """Create a RemoteProject object""" if isinstance(project_uuid, str): project_uuid = uuid.UUID(project_uuid) values = { @@ -295,6 +295,7 @@ def make_sodar_user( sodar_uuid=None, password='password', ): + """Create a SODARUser object""" user = self.make_user(username, password) user.name = name user.first_name = first_name @@ -323,7 +324,7 @@ def make_email(self, user, email, verified=True, secret=None): class TestProject(ProjectMixin, RoleMixin, RoleAssignmentMixin, TestCase): - """Tests for model.Project""" + """Tests for Project""" def setUp(self): # Set up category and project @@ -436,41 +437,41 @@ def test_get_absolute_url(self): self.assertEqual(self.project.get_absolute_url(), expected_url) def test_get_children_category(self): - """Test children getting function for top category""" + """Test get_children() with top category""" children = self.category.get_children() self.assertEqual(children[0], self.project) def test_get_children_project(self): - """Test children getting function for sub project""" + """Test get_children() with sub project""" children = self.project.get_children() self.assertEqual(children.count(), 0) def test_get_depth_category(self): - """Test project depth getting function for top category""" + """Test get_depth() with top category""" self.assertEqual(self.category.get_depth(), 0) def test_get_depth_project(self): - """Test children getting function for sub project""" + """Test get_depth() with sub project""" self.assertEqual(self.project.get_depth(), 1) def test_get_parents_category(self): - """Test get parents function for top category""" + """Test get_parents() with top category""" self.assertEqual(self.category.get_parents(), []) def test_get_parents_project(self): - """Test get parents function for sub project""" + """Test get_parents() with sub project""" self.assertEqual(list(self.project.get_parents()), [self.category]) def test_is_remote(self): - """Test Project.is_remote() without remote projects""" + """Test is_remote() without remote projects""" self.assertEqual(self.project.is_remote(), False) def test_is_revoked(self): - """Test Project.is_revoked() without remote projects""" + """Test is_revoked() without remote projects""" self.assertEqual(self.project.is_revoked(), False) def test_set_public(self): - """Test Project.set_public()""" + """Test set_public()""" self.assertFalse(self.project.public_guest_access) self.project.set_public() # Default = True self.assertTrue(self.project.public_guest_access) @@ -482,7 +483,7 @@ def test_set_public(self): self.category.set_public(True) def test_set_archive(self): - """Test Project.set_archive()""" + """Test set_archive()""" self.assertFalse(self.project.archive) self.project.set_archive() # Default = True self.assertTrue(self.project.archive) @@ -492,13 +493,13 @@ def test_set_archive(self): self.assertTrue(self.project.archive) def test_set_archive_category(self): - """Test Project.set_archive() for a category (should fail)""" + """Test set_archive() with category (should fail)""" self.assertFalse(self.category.archive) with self.assertRaises(ValidationError): self.category.set_archive() def test_get_log_title(self): - """Test Project.get_log_title()""" + """Test get_log_title()""" expected = '"{}" ({})'.format( self.project.title, self.project.sodar_uuid ) @@ -516,7 +517,7 @@ def test_get_role(self): self.assertEqual(self.project.get_role(self.user_bob), project_as) def test_get_role_inherit_only(self): - """Test get_role() with only an inherited role""" + """Test get_role() with only inherited role""" self.assertEqual( self.project.get_role(self.user_alice), self.owner_as_cat ) @@ -998,8 +999,8 @@ def setUp(self): description='YYY', ) - def test_find_all(self): - """Test find() with any project type""" + def test_find(self): + """Test find()""" result = Project.objects.find(['test'], project_type=None) self.assertEqual(len(result), 2) result = Project.objects.find(['ThisFails'], project_type=None) @@ -1055,7 +1056,7 @@ def test_find_multi_fields(self): self.assertEqual(len(result), 2) -class TestProjectSetting( +class TestProjectAppSetting( ProjectMixin, RoleAssignmentMixin, AppSettingMixin, TestCase ): """Tests for AppSetting with PROJECT scope""" @@ -1193,7 +1194,7 @@ def test_get_value_json(self): self.assertEqual(val, {'Testing': 'good'}) -class TestUserSetting( +class TestUserAppSetting( ProjectMixin, RoleAssignmentMixin, AppSettingMixin, TestCase ): """Tests for AppSetting with USER scope""" @@ -1379,7 +1380,7 @@ def test__repr__(self): self.assertEqual(repr(self.site), expected) def test_validate_mode(self): - """Test _validate_mode() with an invalid mode (should fail)""" + """Test _validate_mode() with invalid mode (should fail)""" with self.assertRaises(ValidationError): self.make_site( name='New site', @@ -1538,16 +1539,14 @@ def test_is_revoked_target(self): @override_settings(PROJECTROLES_SITE_MODE=SITE_MODE_TARGET) @override_settings(PROJECTROLES_DELEGATE_LIMIT=1) def test_validate_remote_delegates(self): - """Test delegate validation: can add for remote project with limit""" + """Test remot project delegate validation""" self.site.mode = SITE_MODE_SOURCE self.site.save() self.make_assignment(self.project, self.user_bob, self.role_delegate) - try: - self.make_assignment( - self.project, self.user_alice, self.role_delegate - ) - except ValidationError as e: - self.fail(e) + remote_as = self.make_assignment( + self.project, self.user_alice, self.role_delegate + ) + self.assertIsNotNone(remote_as) def test_get_project(self): """Test get_project() with project and project_uuid""" @@ -1577,7 +1576,7 @@ def setUp(self): self.user = self.make_user() def test__str__(self): - """Test __str__()""" + """Test SODARUser __str__()""" self.assertEqual( self.user.__str__(), 'testuser' ) # This is the default username for self.make_user() diff --git a/projectroles/tests/test_ui.py b/projectroles/tests/test_ui.py index f9015c35..ad45ae11 100644 --- a/projectroles/tests/test_ui.py +++ b/projectroles/tests/test_ui.py @@ -76,7 +76,7 @@ PROJECT_SELECT_CSS = 'div[id="div_id_type"] div select[id="id_type"]' PROJECT_SETTING_ID = 'div_id_settings.example_project_app.project_int_setting' CATEGORY_SETTING_ID = ( - 'div_id_settings.example_project_app.project_category_bool_setting' + 'div_id_settings.example_project_app.category_bool_setting' ) PUBLIC_ACCESS_ID = 'id_public_guest_access' REMOTE_SITE_UUID = uuid.uuid4() diff --git a/projectroles/tests/test_views.py b/projectroles/tests/test_views.py index 739547b0..2be1d451 100644 --- a/projectroles/tests/test_views.py +++ b/projectroles/tests/test_views.py @@ -765,7 +765,7 @@ def test_post_top_level_category(self): settings = AppSetting.objects.filter(project=category) self.assertEqual(settings.count(), 1) setting = settings.first() - self.assertEqual(setting.name, 'project_category_bool_setting') + self.assertEqual(setting.name, 'category_bool_setting') # Assert owner role assignment owner_as = RoleAssignment.objects.get( @@ -1492,7 +1492,7 @@ def test_post_category(self): settings = AppSetting.objects.filter(project=self.category) self.assertEqual(settings.count(), 1) setting = settings.first() - self.assertEqual(setting.name, 'project_category_bool_setting') + self.assertEqual(setting.name, 'category_bool_setting') # Assert redirect with self.login(self.user): self.assertRedirects( diff --git a/projectroles/tests/test_views_api.py b/projectroles/tests/test_views_api.py index 5f54aef5..2452a1ed 100644 --- a/projectroles/tests/test_views_api.py +++ b/projectroles/tests/test_views_api.py @@ -2985,7 +2985,7 @@ def test_post_invalid_scope(self): def test_post_invalid_project_type(self): """Test POST with unaccepted project type (should fail)""" - setting_name = 'project_category_bool_setting' + setting_name = 'category_bool_setting' post_data = { 'plugin_name': EX_APP_NAME, 'setting_name': setting_name, diff --git a/projectroles/views.py b/projectroles/views.py index e05d5276..8253b9cb 100644 --- a/projectroles/views.py +++ b/projectroles/views.py @@ -3101,32 +3101,20 @@ def get(self, request, *args, **kwargs): class RemoteSiteModifyMixin(ModelFormMixin): - def form_valid(self, form): - timeline = get_backend_api('timeline_backend') - if self.object: - form_action = 'updated' - elif settings.PROJECTROLES_SITE_MODE == 'TARGET': - form_action = 'set' - else: - form_action = 'created' - self.object = form.save() - # Create timeline event - if timeline: - self.create_timeline_event( - self.object, self.request.user, form_action, timeline=timeline - ) - messages.success( - self.request, - '{} site "{}" {}.'.format( - self.object.mode.capitalize(), self.object.name, form_action - ), - ) - return redirect(reverse('projectroles:remote_sites')) + """Helpers for remote site modification""" - def create_timeline_event( - self, remote_site, user, form_action, timeline=None - ): - """Create timeline event for remote site creation/update""" + def _create_timeline_event(self, remote_site, user, form_action): + """ + Create timeline event for remote site creation/update. + + :param remote_site: RemoteSite object + :param user: SODARUser object + :param form_action: String + :param form_action: + """ + timeline = get_backend_api('timeline_backend') + if not timeline: + return status = form_action if form_action == 'set' else form_action[0:-1] if remote_site.mode == SITE_MODE_SOURCE: event_name = 'source_site_{}'.format(status) @@ -3159,6 +3147,25 @@ def create_timeline_event( obj=remote_site, label='remote_site', name=remote_site.name ) + def form_valid(self, form): + """Override form_valid() to save timeline event and handle UI""" + if self.object: + form_action = 'updated' + elif settings.PROJECTROLES_SITE_MODE == 'TARGET': + form_action = 'set' + else: + form_action = 'created' + self.object = form.save() + # Create timeline event + self._create_timeline_event(self.object, self.request.user, form_action) + messages.success( + self.request, + '{} site "{}" {}.'.format( + self.object.mode.capitalize(), self.object.name, form_action + ), + ) + return redirect(reverse('projectroles:remote_sites')) + class RemoteSiteCreateView( LoginRequiredMixin, diff --git a/projectroles/views_api.py b/projectroles/views_api.py index df4dc63c..075c79c1 100644 --- a/projectroles/views_api.py +++ b/projectroles/views_api.py @@ -265,7 +265,7 @@ class SODARAPIBaseProjectMixin(ProjectAccessMixin, SODARAPIBaseMixin): ``PROJECT_TYPE_PROJECT``, as defined in SODAR constants, by setting the ``project_type`` attribute in the view. - NOTE: The SODARAPIBaseMixin inheritance will be removed in + NOTE: The SODARAPIBaseMixin inheritance will be removed in v1.1 (see #1401). """ permission_classes = [SODARAPIProjectPermission]