diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d51cb9688..853f9baf1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [RDMO 2.1.2](https://github.com/rdmorganiser/rdmo/compare/2.1.1...2.1.2) (Jan 15, 2024) + +* Fix a bug with webpack font paths +* Fix a bug with option set provider plugins +* Fix a bug with the autocomplete widget +* Add invite.email to send_invite_email context + ## [RDMO 2.1.1](https://github.com/rdmorganiser/rdmo/compare/2.1.0...2.1.1) (Dec 21, 2023) * Fix translations diff --git a/rdmo/__init__.py b/rdmo/__init__.py index 58039f5051..e1a0667327 100644 --- a/rdmo/__init__.py +++ b/rdmo/__init__.py @@ -1 +1 @@ -__version__ = "2.1.1" +__version__ = "2.1.2.dev1" diff --git a/rdmo/options/providers.py b/rdmo/options/providers.py index f0b014b818..9a36d2af48 100644 --- a/rdmo/options/providers.py +++ b/rdmo/options/providers.py @@ -19,14 +19,17 @@ def get_options(self, project, search=None): return [ { 'id': 'simple_1', - 'text': 'Simple answer 1' + 'text': 'Simple answer 1', + 'help': 'One' }, { 'id': 'simple_2', - 'text': 'Simple answer 2' + 'text': 'Simple answer 2', + 'help': 'Two' }, { 'id': 'simple_3', - 'text': 'Simple answer 3' + 'text': 'Simple answer 3', + 'help': 'Three' } ] diff --git a/rdmo/projects/static/projects/js/project_questions/services.js b/rdmo/projects/static/projects/js/project_questions/services.js index 7cad4350e8..1d5fbc84d5 100644 --- a/rdmo/projects/static/projects/js/project_questions/services.js +++ b/rdmo/projects/static/projects/js/project_questions/services.js @@ -656,10 +656,26 @@ angular.module('project_questions') angular.forEach(question.options, function(option) { if (value.autocomplete_locked === false && option.id === value.option) { value.autocomplete_locked = true; - value.autocomplete_input = option.text_and_help; - value.autocomplete_text = option.text_and_help; + value.autocomplete_input = option.text; + value.autocomplete_text = option.text; } }); + } else if (value.external_id) { + value.autocomplete_locked = false; + angular.forEach(question.options, function(option) { + if (value.autocomplete_locked === false && option.id === value.external_id) { + value.autocomplete_locked = true; + value.autocomplete_input = option.text; + value.autocomplete_text = option.text; + } + }) + + // if no option was found (for autocomplete search fields), use the text + if (value.text) { + value.autocomplete_locked = true; + value.autocomplete_input = value.text; + value.autocomplete_text = value.text; + } } else if (value.text) { value.autocomplete_locked = true; value.autocomplete_input = value.text; @@ -776,7 +792,7 @@ angular.module('project_questions') // loop over options angular.forEach(question.options, function(option) { if (option.has_provider && value.selected === option.id) { - value.text = option.text_and_help; + value.text = option.text; // has to be value.text, since the help is not supposed to be stored value.external_id = option.id; } else if (value.selected === option.id.toString()) { // get text from additional_input for the selected option @@ -1451,7 +1467,7 @@ angular.module('project_questions') } if (angular.isDefined(next)) { next.active = true; - value.autocomplete_input = next.text_and_help; + value.autocomplete_input = next.text; } } else if ($event.code == 'Enter' || $event.code == 'NumpadEnter') { if (value.autocomplete_input == '') { diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py index adf83dc608..8e9d8b6371 100644 --- a/rdmo/projects/tests/test_viewset_project.py +++ b/rdmo/projects/tests/test_viewset_project.py @@ -43,6 +43,7 @@ 'detail': 'v1-projects:project-detail', 'overview': 'v1-projects:project-overview', 'navigation': 'v1-projects:project-navigation', + 'options': 'v1-projects:project-options', 'resolve': 'v1-projects:project-resolve', } @@ -54,6 +55,10 @@ section_id = 1 +optionset_id = 4 + +project_id = 1 + @pytest.mark.parametrize('username,password', users) def test_list(db, client, username, password): client.login(username=username, password=password) @@ -311,3 +316,46 @@ def test_resolve(db, client, username, password, project_id, condition_id): assert response.status_code == 404 else: assert response.status_code == 401 + + +@pytest.mark.parametrize('username,password', users) +def test_options(db, client, username, password): + client.login(username=username, password=password) + + url = reverse(urlnames['options'], args=[project_id]) + f'?optionset={optionset_id}' + response = client.get(url) + + if project_id in view_project_permission_map.get(username, []): + assert response.status_code == 200 + assert isinstance(response.json(), list) + + for item in response.json(): + assert item['text_and_help'] == '{text} [{help}]'.format(**item) + else: + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 + + +def test_options_text_and_help(db, client, mocker): + mocker.patch('rdmo.options.providers.SimpleProvider.get_options', return_value=[ + { + 'id': 'simple_1', + 'text': 'Simple answer 1' + } + ]) + + client.login(username='author', password='author') + + url = reverse(urlnames['options'], args=[project_id]) + f'?optionset={optionset_id}' + response = client.get(url) + + assert response.status_code == 200 + assert response.json() == [ + { + 'id': 'simple_1', + 'text': 'Simple answer 1', + 'text_and_help': 'Simple answer 1' + } + ] diff --git a/rdmo/projects/utils.py b/rdmo/projects/utils.py index 5e5c374d43..c23b0137fa 100644 --- a/rdmo/projects/utils.py +++ b/rdmo/projects/utils.py @@ -151,6 +151,7 @@ def send_invite_email(request, invite): context = { 'invite_url': request.build_absolute_uri(project_invite_path), 'invite_user': invite.user, + 'invite_email': invite.email, 'project': invite.project, 'user': request.user, 'site': Site.objects.get_current() diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py index 235573deb9..11b9c296c6 100644 --- a/rdmo/projects/viewsets.py +++ b/rdmo/projects/viewsets.py @@ -165,10 +165,19 @@ def options(self, request, pk=None): project.catalog.prefetch_elements() if Question.objects.filter_by_catalog(project.catalog).filter(optionsets=optionset) and \ optionset.provider is not None: - options = [ - dict(**option, text_and_help=option.get('text_and_help', 'text')) - for option in optionset.provider.get_options(project, search=request.GET.get('search')) - ] + options = [] + for option in optionset.provider.get_options(project, search=request.GET.get('search')): + if 'id' not in option: + raise RuntimeError(f"'id' is missing in options of '{optionset.provider.class_name}'") + elif 'text' not in option: + raise RuntimeError(f"'text' is missing in options of '{optionset.provider.class_name}'") + if 'text_and_help' not in option: + if 'help' in option: + option['text_and_help'] = '{text} [{help}]'.format(**option) + else: + option['text_and_help'] = '{text}'.format(**option) + options.append(option) + return Response(options) except OptionSet.DoesNotExist: diff --git a/webpack/common.config.js b/webpack/common.config.js index 15ca8de8a7..57f6f89b9b 100644 --- a/webpack/common.config.js +++ b/webpack/common.config.js @@ -40,7 +40,9 @@ const base = { test: /(fonts|files)\/.*\.(svg|woff2?|ttf|eot|otf)(\?.*)?$/, loader: 'file-loader', options: { - name: 'fonts/[name].[ext]' + name: '[name].[ext]', + outputPath: 'fonts', + postTransformPublicPath: (p) => `'../' + ${p}` } } ]