diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad7063e8..d34b11d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,9 +7,9 @@ jobs: strategy: matrix: python-version: - - 3.7 - - 3.8 - - 3.9 + - '3.8' + - '3.9' + - '3.10' services: postgres: image: postgres:9.6 @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@v2 - name: Install project Python dependencies run: | - pip install wheel==0.36.2 + pip install wheel==0.37.1 pip install -r requirements/local.txt pip install -r requirements/test.txt - name: Download icons diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b19dd933..e185d07a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,7 +23,7 @@ before_script: - python3 -m venv ./env - source ./env/bin/activate # - sh ./utility/install_python_dependencies.sh - - pip3 install wheel==0.36.2 + - pip3 install wheel==0.37.1 - pip3 install -r ./requirements/local.txt - pip3 install -r ./requirements/test.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8ac13718..ff024e4f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,62 @@ Changelog for the **SODAR Core** Django app package. Loosely follows the Note that the issue IDs here refer to ones in the private CUBI GitLab. +v0.10.8 (2022-02-02) +==================== + +Added +----- + +- **Projectroles** + - Disabling ``ManagementCommandLogger`` with ``LOGGING_DISABLE_CMD_OUTPUT`` (#894) +- **Siteinfo** + - Missing site settings in ``CORE_SETTINGS`` (#877) +- **Timeline** + - ``get_plugin_lookup()`` and ``get_app_icon_html()`` template tags (#888) + - Template tag tests (#891) + +Changed +------- + +- **General** + - Upgrade minimum Python version to v3.8, add v3.10 support (#885) + - Upgrade minimum Django version to v3.2.12 (#879, #902) + - Upgrade Python dependencies (#884, #893, #901) + - Upgrade to Chromedriver v97 (#905) +- **Projectroles** + - Display admin icon in user dropdown (#886) + - Refactor UI tests (#882) +- **Timeline** + - Improve event list layout responsivity (#887) + - Replace event list app column with app icon (#888) + - Set default kwarg values for model test helpers (#890) + - Move ``get_request()`` to ``TimelineAPIMixin`` + - Display recent events regardless of status in details card (#899) + - Optimize ``get_details_events()`` (#899) + +Fixed +----- + +- **Projectroles** + - Parent owner set as owner in project create form for non-owner category members (#878) + - Project header icon tooltip alignment (#895) + - Redundant public access icon display for categories (#896) + - Icon size syntax (#875) + - Content of ``sodar-code-input`` partially hidden in Chrome (#904) +- **Siteinfo** + - Layout responsivity issues with long labels (#883) +- **Timeline** + - Redundant app plugin queries in event list (#889, #900) + +Removed +------- + +- **Projectroles** + - ``_add_remote_association()`` helper from UI tests (#882) +- **Timeline** + - Unused ``get_app_url()`` template tag (#888) + + v0.10.7 (2021-12-14) ==================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 719f0171..71b40f64 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -112,10 +112,10 @@ Before you submit a pull request, check that it meets these guidelines: 4. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in ``CHANGELOG.rst``. -5. The pull request should work for Python version 3.7, 3.8 and 3.9. Check +5. The pull request should work for Python version 3.8, 3.9 and 3.10. Check https://github.com/bihealth/sodar-core/actions and make sure that the tests pass for supported Python versions. - From v1.0 onwards SODAR Core no longer supports Python 3.6. + From v0.10.8 onwards SODAR Core no longer supports Python 3.7. Deploying ========= diff --git a/README.rst b/README.rst index 1b204fdf..64f132e6 100644 --- a/README.rst +++ b/README.rst @@ -113,7 +113,7 @@ from PyPI as follows. .. code-block:: console - pip install django-sodar-core==0.10.7 + pip install django-sodar-core==0.10.8 For installing a development version you can point your dependency to a specific commit ID in GitHub. Note that these versions may not be stable. diff --git a/adminalerts/tests/test_ui.py b/adminalerts/tests/test_ui.py index 8f7df94c..fd9eb92c 100644 --- a/adminalerts/tests/test_ui.py +++ b/adminalerts/tests/test_ui.py @@ -4,11 +4,12 @@ from django.utils import timezone from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.common.by import By # Projectroles dependency from projectroles.tests.test_ui import TestUIBase -from .test_models import AdminAlertMixin +from adminalerts.tests.test_models import AdminAlertMixin class TestAlertUIBase(AdminAlertMixin, TestUIBase): @@ -38,7 +39,6 @@ def test_message(self): """Test visibility of alert message in home view""" expected = [(self.superuser, 1), (self.regular_user, 1)] url = reverse('home') - self.assert_element_count( expected, url, 'sodar-alert-site-app', 'class' ) @@ -50,7 +50,6 @@ def test_message_inactive(self): expected = [(self.superuser, 0), (self.regular_user, 0)] url = reverse('home') - self.assert_element_count( expected, url, 'sodar-alert-site-app', 'class' ) @@ -62,7 +61,6 @@ def test_message_expired(self): expected = [(self.superuser, 0), (self.regular_user, 0)] url = reverse('home') - self.assert_element_count( expected, url, 'sodar-alert-site-app', 'class' ) @@ -70,9 +68,8 @@ def test_message_expired(self): def test_message_login(self): """Test visibility of alert in login view with auth requirement""" self.selenium.get(self.build_selenium_url(reverse('login'))) - with self.assertRaises(NoSuchElementException): - self.selenium.find_element_by_class_name('sodar-alert-site-app') + self.selenium.find_element(By.CLASS_NAME, 'sodar-alert-site-app') def test_message_login_no_auth(self): """Test visibility of alert in login view without auth requirement""" @@ -81,7 +78,7 @@ def test_message_login_no_auth(self): self.selenium.get(self.build_selenium_url(reverse('login'))) self.assertIsNotNone( - self.selenium.find_element_by_class_name('sodar-alert-site-app') + self.selenium.find_element(By.CLASS_NAME, 'sodar-alert-site-app') ) @@ -92,12 +89,10 @@ def test_list_items(self): """Test existence of items in list""" expected = [(self.superuser, 1)] url = reverse('adminalerts:list') - self.assert_element_count(expected, url, 'sodar-aa-alert-item', 'id') def test_list_buttons(self): """Test existence of buttons in list""" expected = [(self.superuser, 1)] url = reverse('adminalerts:list') - self.assert_element_count(expected, url, 'sodar-aa-alert-buttons', 'id') diff --git a/appalerts/tests/test_ui.py b/appalerts/tests/test_ui.py index 8c2c6559..5f1fad7e 100644 --- a/appalerts/tests/test_ui.py +++ b/appalerts/tests/test_ui.py @@ -63,15 +63,15 @@ def test_alert_dismiss(self): url = reverse('appalerts:list') self.login_and_redirect(self.regular_user, url) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-count').text, '2' + self.selenium.find_element(By.ID, 'sodar-app-alert-count').text, '2' ) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-legend').text, + self.selenium.find_element(By.ID, 'sodar-app-alert-legend').text, 'alerts', ) - button = self.selenium.find_elements_by_class_name( - 'sodar-app-alert-btn-dismiss-single' + button = self.selenium.find_elements( + By.CLASS_NAME, 'sodar-app-alert-btn-dismiss-single' )[0] button.click() WebDriverWait(self.selenium, self.wait_time).until( @@ -80,16 +80,16 @@ def test_alert_dismiss(self): ) ) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-count').text, '1' + self.selenium.find_element(By.ID, 'sodar-app-alert-count').text, '1' ) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-legend').text, + self.selenium.find_element(By.ID, 'sodar-app-alert-legend').text, 'alert', ) self.assertEqual(AppAlert.objects.filter(active=True).count(), 1) self.assertFalse( - self.selenium.find_element_by_id( - 'sodar-app-alert-empty' + self.selenium.find_element( + By.ID, 'sodar-app-alert-empty' ).is_displayed() ) @@ -100,15 +100,15 @@ def test_alert_dismiss_all(self): url = reverse('appalerts:list') self.login_and_redirect(self.regular_user, url) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-count').text, '2' + self.selenium.find_element(By.ID, 'sodar-app-alert-count').text, '2' ) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-legend').text, + self.selenium.find_element(By.ID, 'sodar-app-alert-legend').text, 'alerts', ) - self.selenium.find_element_by_id( - 'sodar-app-alert-btn-dismiss-all' + self.selenium.find_element( + By.ID, 'sodar-app-alert-btn-dismiss-all' ).click() WebDriverWait(self.selenium, self.wait_time).until( ec.invisibility_of_element_located( @@ -116,10 +116,10 @@ def test_alert_dismiss_all(self): ) ) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-count').text, '' + self.selenium.find_element(By.ID, 'sodar-app-alert-count').text, '' ) self.assertEqual( - self.selenium.find_element_by_id('sodar-app-alert-legend').text, + self.selenium.find_element(By.ID, 'sodar-app-alert-legend').text, '', ) self.assertEqual(AppAlert.objects.filter(active=True).count(), 0) @@ -127,8 +127,8 @@ def test_alert_dismiss_all(self): ec.visibility_of_element_located((By.ID, 'sodar-app-alert-empty')) ) self.assertTrue( - self.selenium.find_element_by_id( - 'sodar-app-alert-empty' + self.selenium.find_element( + By.ID, 'sodar-app-alert-empty' ).is_displayed() ) @@ -140,12 +140,12 @@ def test_alert_reload(self): url = reverse('appalerts:list') self.login_and_redirect(self.regular_user, url) self.assertTrue( - self.selenium.find_element_by_id( - 'sodar-app-alert-empty' + self.selenium.find_element( + By.ID, 'sodar-app-alert-empty' ).is_displayed() ) with self.assertRaises(NoSuchElementException): - self.selenium.find_element_by_id('sodar-app-alert-reload') + self.selenium.find_element(By.ID, 'sodar-app-alert-reload') self._make_app_alert(user=self.regular_user, url=reverse('home')) @@ -153,8 +153,8 @@ def test_alert_reload(self): ec.visibility_of_element_located((By.ID, 'sodar-app-alert-reload')) ) self.assertTrue( - self.selenium.find_element_by_id( - 'sodar-app-alert-reload' + self.selenium.find_element( + By.ID, 'sodar-app-alert-reload' ).is_displayed() ) @@ -166,12 +166,12 @@ def test_render(self): """Test existence of alert badge for user with alerts""" url = reverse('home') self.login_and_redirect(self.regular_user, url) - alert_badge = self.selenium.find_element_by_id('sodar-app-alert-badge') + alert_badge = self.selenium.find_element(By.ID, 'sodar-app-alert-badge') self.assertIsNotNone(alert_badge) self.assertTrue(alert_badge.is_displayed()) - alert_count = self.selenium.find_element_by_id('sodar-app-alert-count') - alert_legend = self.selenium.find_element_by_id( - 'sodar-app-alert-legend' + alert_count = self.selenium.find_element(By.ID, 'sodar-app-alert-count') + alert_legend = self.selenium.find_element( + By.ID, 'sodar-app-alert-legend' ) self.assertEqual(alert_count.text, '2') self.assertEqual(alert_legend.text, 'alerts') @@ -180,7 +180,7 @@ def test_render_no_alerts(self): """Test existence of alert badge for user without alerts""" url = reverse('home') self.login_and_redirect(self.no_alert_user, url) - alert_badge = self.selenium.find_element_by_id('sodar-app-alert-badge') + alert_badge = self.selenium.find_element(By.ID, 'sodar-app-alert-badge') self.assertIsNotNone(alert_badge) self.assertFalse(alert_badge.is_displayed()) @@ -188,9 +188,9 @@ def test_render_add(self): """Test adding an alert for user with alerts""" url = reverse('home') self.login_and_redirect(self.regular_user, url) - alert_count = self.selenium.find_element_by_id('sodar-app-alert-count') - alert_legend = self.selenium.find_element_by_id( - 'sodar-app-alert-legend' + alert_count = self.selenium.find_element(By.ID, 'sodar-app-alert-count') + alert_legend = self.selenium.find_element( + By.ID, 'sodar-app-alert-legend' ) self.assertEqual(alert_count.text, '2') self.assertEqual(alert_legend.text, 'alerts') @@ -208,9 +208,9 @@ def test_render_delete(self): """Test deleting an alert from user with alerts""" url = reverse('home') self.login_and_redirect(self.regular_user, url) - alert_count = self.selenium.find_element_by_id('sodar-app-alert-count') - alert_legend = self.selenium.find_element_by_id( - 'sodar-app-alert-legend' + alert_count = self.selenium.find_element(By.ID, 'sodar-app-alert-count') + alert_legend = self.selenium.find_element( + By.ID, 'sodar-app-alert-legend' ) self.assertEqual(alert_count.text, '2') self.assertEqual(alert_legend.text, 'alerts') @@ -228,7 +228,7 @@ def test_render_delete_all(self): """Test deleting all alerts from user with alerts""" url = reverse('home') self.login_and_redirect(self.regular_user, url) - alert_badge = self.selenium.find_element_by_id('sodar-app-alert-badge') + alert_badge = self.selenium.find_element(By.ID, 'sodar-app-alert-badge') self.assertTrue(alert_badge.is_displayed()) self.alert.delete() @@ -242,7 +242,7 @@ def test_render_add_no_alerts(self): """Test adding an alert for user without prior alerts""" url = reverse('home') self.login_and_redirect(self.no_alert_user, url) - alert_badge = self.selenium.find_element_by_id('sodar-app-alert-badge') + alert_badge = self.selenium.find_element(By.ID, 'sodar-app-alert-badge') self.assertFalse(alert_badge.is_displayed()) self._make_app_alert(user=self.no_alert_user, url=reverse('home')) @@ -251,9 +251,9 @@ def test_render_add_no_alerts(self): ) self.assertTrue(alert_badge.is_displayed()) - alert_count = self.selenium.find_element_by_id('sodar-app-alert-count') - alert_legend = self.selenium.find_element_by_id( - 'sodar-app-alert-legend' + alert_count = self.selenium.find_element(By.ID, 'sodar-app-alert-count') + alert_legend = self.selenium.find_element( + By.ID, 'sodar-app-alert-legend' ) self.assertEqual(alert_count.text, '1') self.assertEqual(alert_legend.text, 'alert') @@ -265,8 +265,8 @@ def test_alert_dismiss_all(self): url = reverse('home') self.login_and_redirect(self.regular_user, url) - self.selenium.find_element_by_id( - 'sodar-app-alert-badge-btn-dismiss' + self.selenium.find_element( + By.ID, 'sodar-app-alert-badge-btn-dismiss' ).click() WebDriverWait(self.selenium, self.wait_time).until( ec.invisibility_of_element_located( diff --git a/codemeta.json b/codemeta.json index 8b791e38..362e074e 100644 --- a/codemeta.json +++ b/codemeta.json @@ -40,12 +40,12 @@ ], "identifier": "https://doi.org/10.5281/zenodo.4269346", "codeRepository": "https://github.com/bihealth/sodar_core", - "datePublished": "2021-12-14", - "dateModified": "2021-12-14", + "datePublished": "2022-02-02", + "dateModified": "2022-02-02", "dateCreated": "2019-06-26", "description": "SODAR Core: A Django-based framework for scientific data management and analysis web apps", "keywords": "Python, Django, scientific data managmenent, software library", "license": "MIT", "title": "SODAR Core", - "version": "v0.10.7" + "version": "v0.10.8" } diff --git a/config/settings/test.py b/config/settings/test.py index 68780f17..4b668b9c 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -64,6 +64,7 @@ LOGGING_LEVEL = env.str('LOGGING_LEVEL', 'CRITICAL') LOGGING = set_logging(LOGGING_LEVEL) +LOGGING_DISABLE_CMD_OUTPUT = True # Local App Settings diff --git a/docs/source/_static/app_timeline/sodar_timeline.png b/docs/source/_static/app_timeline/sodar_timeline.png index 9ed5be97..de212143 100644 Binary files a/docs/source/_static/app_timeline/sodar_timeline.png and b/docs/source/_static/app_timeline/sodar_timeline.png differ diff --git a/docs/source/app_projectroles_integration.rst b/docs/source/app_projectroles_integration.rst index 50d123fe..bb36bbe6 100644 --- a/docs/source/app_projectroles_integration.rst +++ b/docs/source/app_projectroles_integration.rst @@ -86,7 +86,7 @@ desired release tag or commit ID. .. code-block:: console -e git+https://github.com/mikkonie/django-plugins.git@42e86e7904e5c09f1da32173862b26843eda5dd8#egg=django-plugins - django-sodar-core==0.10.7 + django-sodar-core==0.10.8 Install the requirements for development: diff --git a/docs/source/app_projectroles_settings.rst b/docs/source/app_projectroles_settings.rst index f3fa3c18..ced592ac 100644 --- a/docs/source/app_projectroles_settings.rst +++ b/docs/source/app_projectroles_settings.rst @@ -645,3 +645,7 @@ production, ``ERROR`` debug level is recommended. The example site and SODAR Django Site template provide the ``LOGGING_APPS`` and ``LOGGING_FILE_PATH`` helpers for easily adding SODAR Core apps to logging and providing a system path for optional log file writing. + +If you are using ``ManagementCommandLogger`` for logging your management command +output, you can disable redundant console input in e.g. your test configuration +by setting ``LOGGING_DISABLE_CMD_OUTPUT`` to ``True``. diff --git a/docs/source/conf.py b/docs/source/conf.py index bb9880ed..5d7238ba 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,7 +29,7 @@ # The short X.Y version version = '0.10' # The full version, including alpha/beta/rc tags -release = '0.10.7' +release = '0.10.8' # -- General configuration --------------------------------------------------- diff --git a/docs/source/dev_general.rst b/docs/source/dev_general.rst index 9f73b2e6..2d8b19b8 100644 --- a/docs/source/dev_general.rst +++ b/docs/source/dev_general.rst @@ -63,7 +63,6 @@ used. An example can be seen below: {% include 'projectroles/_pagination.html' with pg_small=True %} - Management Command Logger ------------------------- @@ -87,6 +86,12 @@ to access the actual Python logger being used, you can access it via the example site and the SODAR Django Site template, including the use of a ``LOGGING_LEVEL`` Django settings variable. +.. hint:: + + To disable redundant console output from commands using this logger in e.g. + your site's test configuration, you can set the + ``LOGGING_DISABLE_CMD_OUTPUT`` Django setting to ``True``. + Using Icons =========== diff --git a/docs/source/dev_sodar_core.rst b/docs/source/dev_sodar_core.rst index a6e7936d..77fcc251 100644 --- a/docs/source/dev_sodar_core.rst +++ b/docs/source/dev_sodar_core.rst @@ -43,7 +43,7 @@ system of choice. System Installation ------------------- -First you need to install OS dependencies, PostgreSQL >=9.6 and Python >=3.7. +First you need to install OS dependencies, PostgreSQL >=9.6 and Python >=3.8. .. code-block:: console diff --git a/docs/source/for_the_impatient.rst b/docs/source/for_the_impatient.rst index c1910b6b..c9f93a28 100644 --- a/docs/source/for_the_impatient.rst +++ b/docs/source/for_the_impatient.rst @@ -54,7 +54,7 @@ PostgreSQL administrative user. Development Essentials - We assume that you have ``git``, Python 3.7 or above, and other essential + We assume that you have ``git``, Python 3.8 or above, and other essential tools installed. If you are using a mainstream Unix-like distribution (Mac qualifies) then you should be good to go. @@ -109,7 +109,7 @@ We simply use ``pip`` for this: # you must have your miniconda3 install sourced, skip if done already $ source ~/miniconda3/bin/activate - $ pip install django-sodar-core==0.10.7 + $ pip install django-sodar-core==0.10.8 Download Example Site diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 8756c601..e329c74f 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -17,7 +17,7 @@ the package is under active development and breaking changes are expected. .. code-block:: console - pip install django-sodar-core==0.10.7 + pip install django-sodar-core==0.10.8 Please note that the django-sodar-core package only installs :term:`Django apps`, which you need to include in a @@ -58,7 +58,7 @@ your Django site are listed below. For a complete requirement list, see the - Ubuntu (20.04 Focal recommended and supported) / CentOS 7 - System library requirements (see the ``utility`` directory and/or your own Django project) -- Python >=3.7 (**NOTE:** Python 3.6 no longer supported in SODAR Core v0.10+) +- Python >=3.8 (**NOTE:** Python 3.7 no longer supported in SODAR Core v0.10.8+) - Django 3.2 - PostgreSQL >=9.6 and psycopg2-binary - Bootstrap 4.x diff --git a/docs/source/major_changes.rst b/docs/source/major_changes.rst index 723f2186..bae40caf 100644 --- a/docs/source/major_changes.rst +++ b/docs/source/major_changes.rst @@ -10,6 +10,49 @@ older SODAR Core version. For a complete list of changes in current and previous releases, see the :ref:`full changelog`. +v0.10.8 (2022-02-02) +******************** + +Release Highlights +================== + +- Drop Python 3.7 support, add Python 3.10 support +- Display missing site settings in siteinfo app +- Fix project creation owner assignment for non-owner category members +- Improve layout in siteinfo and timeline apps +- Upgrade third party Python package dependencies +- Optimize queries in timeline app + +Breaking Changes +================ + +System Prerequisites +-------------------- + +SODAR Core no longer supports Python 3.7. Python 3.8 is currently both the +minimum and default version to run SODAR Core and its dependencies. + +Third party Python package dependencies have been upgraded. See the +``requirements`` directory for up-to-date package versions and upgrade your +project accordingly. + +Deprecated Selenium Methods +--------------------------- + +The minimum Selenium version has been upgraded to v4.0.x. Some test methods have +been deprecated in this version and will be removed in a future releases. UI +test helpers from this version onwards will use the non-deprecated versions. You +should the dependency in your projects, run tests, check the output and update +any deprecated method calls if used. + +Timeline App API Updated +------------------------ + +If you are using ``TimelineAPI.get_event_description()`` in your own apps, +please note that the method signature has changed. This may affect the use of +positional arguments. + + v0.10.7 (2021-12-14) ******************** diff --git a/projectroles/__init__.py b/projectroles/__init__.py index 472d70d4..2943e678 100644 --- a/projectroles/__init__.py +++ b/projectroles/__init__.py @@ -10,3 +10,7 @@ default_app_config = ( 'projectroles.apps.ProjectrolesConfig' # pylint: disable=invalid-name ) + +from . import _version # noqa + +__version__ = _version.get_versions()['version'] diff --git a/projectroles/_version.py b/projectroles/_version.py index 6b08142d..cb34852e 100644 --- a/projectroles/_version.py +++ b/projectroles/_version.py @@ -5,7 +5,7 @@ # that just contains the computed version number. # This file is released into the public domain. Generated by -# versioneer-0.19 (https://github.com/python-versioneer/python-versioneer) +# versioneer-0.21 (https://github.com/python-versioneer/python-versioneer) """Git implementation of _version.py.""" @@ -14,6 +14,7 @@ import re import subprocess import sys +from typing import Callable, Dict def get_keywords(): @@ -51,8 +52,8 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} def register_vcs_handler(vcs, method): # decorator @@ -73,20 +74,20 @@ def run_command( ): """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen( - [c] + args, + process = subprocess.Popen( + [command] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), ) break - except EnvironmentError: + except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue @@ -98,13 +99,13 @@ def run_command( if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip().decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): @@ -116,7 +117,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return { @@ -126,9 +127,8 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): "error": None, "date": None, } - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print( @@ -147,22 +147,21 @@ def git_get_keywords(versionfile_abs): # _version.py. keywords = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @@ -170,8 +169,8 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: # Use only the last line. Previous lines may contain GPG signature @@ -190,11 +189,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -203,7 +202,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -212,6 +211,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix) :] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %s" % r) return { @@ -234,7 +238,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -242,12 +246,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): version string, meaning we're inside a checked out source tree. """ GITS = ["git"] + TAG_PREFIX_REGEX = "*" if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] + TAG_PREFIX_REGEX = r"\*" - out, rc = run_command( - GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True - ) + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -255,7 +259,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command( + describe_out, rc = runner( GITS, [ "describe", @@ -264,7 +268,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): "--always", "--long", "--match", - "%s*" % tag_prefix, + "%s%s" % (tag_prefix, TAG_PREFIX_REGEX), ], cwd=root, ) @@ -272,7 +276,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -282,6 +286,40 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner( + GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root + ) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -298,7 +336,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ( "unable to parse git-describe output: '%s'" % describe_out ) @@ -326,13 +364,11 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command( - GITS, ["rev-list", "HEAD", "--count"], cwd=root - ) + count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ 0 ].strip() # Use only the last line. Previous lines may contain GPG signature @@ -374,16 +410,66 @@ def render_pep440(pieces): return rendered +def render_pep440_branch(pieces): + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). + + Exceptions: + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver): + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + def render_pep440_pre(pieces): - """TAG[.post0.devDISTANCE] -- No -dirty. + """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post0.devDISTANCE """ if pieces["closest-tag"]: - rendered = pieces["closest-tag"] if pieces["distance"]: - rendered += ".post0.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % ( + post_version + 1, + pieces["distance"], + ) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 rendered = "0.post0.dev%d" % pieces["distance"] @@ -417,6 +503,35 @@ def render_pep440_post(pieces): return rendered +def render_pep440_post_branch(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . @@ -495,10 +610,14 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -539,7 +658,7 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return { diff --git a/projectroles/forms.py b/projectroles/forms.py index c70eb0d1..3196ebf1 100644 --- a/projectroles/forms.py +++ b/projectroles/forms.py @@ -524,19 +524,19 @@ def __init__(self, project=None, current_user=None, *args, **kwargs): # Hide parent selection self.fields['parent'].widget = forms.HiddenInput() + # Set owner + if self.current_user.is_superuser and parent_project: + self.initial['owner'] = parent_project.get_owner().user + else: + self.initial['owner'] = self.current_user + # Hide owner select widget for regular users + if not self.current_user.is_superuser: + self.fields['owner'].widget = forms.HiddenInput() + # Creating a subproject if parent_project: # Parent must be current parent self.initial['parent'] = parent_project.sodar_uuid - # Set owner of parent category as initial owner - self.initial['owner'] = parent_project.get_owner().user - - # If current user is not parent owner, disable owner select - if ( - not self.current_user.is_superuser - and self.current_user != parent_project.get_owner().user - ): - self.fields['owner'].widget = forms.HiddenInput() # Creating a top level project else: @@ -549,8 +549,6 @@ def __init__(self, project=None, current_user=None, *args, **kwargs): self.fields['type'].widget = forms.HiddenInput() # Set up parent field self.initial['parent'] = None - # Set current user as initial owner - self.initial['owner'] = self.current_user if self.instance.is_remote(): self.fields['title'].widget = forms.HiddenInput() diff --git a/projectroles/management/logging.py b/projectroles/management/logging.py index db70c40a..691e8c41 100644 --- a/projectroles/management/logging.py +++ b/projectroles/management/logging.py @@ -15,12 +15,16 @@ class ManagementCommandLogger: logger = None #: Site logging level - log_level = getattr(settings, 'LOGGING_LEVEL', 'INFO') - site_level = getattr(logging, log_level, 'INFO') + site_level = getattr( + logging, getattr(settings, 'LOGGING_LEVEL', 'INFO'), 'INFO' + ) #: Whether console logging is enabled console_log = CONSOLE_HANDLER in settings.LOGGING.get('handlers', {}).keys() + #: Disable console output if True + disable_output = getattr(settings, 'LOGGING_DISABLE_CMD_OUTPUT', False) + def __init__(self, name): self.logger = logging.getLogger(name) @@ -35,6 +39,8 @@ def log(self, message, level=logging.INFO): if isinstance(level, str): level = getattr(logging, level) self.logger.log(level, message) + if self.disable_output: + return printed = False if level >= self.site_level: printed = self.console_log diff --git a/projectroles/static/projectroles/css/projectroles.css b/projectroles/static/projectroles/css/projectroles.css index 28fcc44f..3aa9ed3a 100644 --- a/projectroles/static/projectroles/css/projectroles.css +++ b/projectroles/static/projectroles/css/projectroles.css @@ -663,6 +663,10 @@ div#sodar-modal div.modal-dialog{ color: #dfdfdf; } +span.sodar-pr-header-icon { + display: inline-block; +} + a#sodar-pr-link-project-star { outline: 0; border: none; @@ -696,6 +700,7 @@ pre#sodar-email-body { .sodar-code-input { font-family: monospace; font-size: 85%; + line-height: 36px; /* Fix for Chrome rendering issue (#904) */ } .spin { diff --git a/projectroles/templates/projectroles/_project_header.html b/projectroles/templates/projectroles/_project_header.html index 6328087d..1c99cf70 100644 --- a/projectroles/templates/projectroles/_project_header.html +++ b/projectroles/templates/projectroles/_project_header.html @@ -24,9 +24,9 @@

{{ project.title }}

title="{% if project_starred %}Unstar{% else %}Star{% endif %}" data-toggle="tooltip" data-placement="top"> {% if project_starred %} - + {% else %} - + {% endif %} {% endif %} @@ -54,7 +54,8 @@

{{ project.title }}

{# Remote status #} {% if project.is_remote and request.user.is_superuser %} - {{ project.title }} {% endif %} {# Public access status #} - {% if project.public_guest_access %} - diff --git a/projectroles/templates/projectroles/_project_list_item.html b/projectroles/templates/projectroles/_project_list_item.html index ab30d103..39c83c7b 100644 --- a/projectroles/templates/projectroles/_project_list_item.html +++ b/projectroles/templates/projectroles/_project_list_item.html @@ -33,7 +33,7 @@ {% if request.user.is_superuser and remote_icon %} {{ remote_icon | safe }} {% endif %} - {% if p.public_guest_access %} + {% if p.type == 'PROJECT' and p.public_guest_access %} diff --git a/projectroles/templates/projectroles/_site_titlebar.html b/projectroles/templates/projectroles/_site_titlebar.html index ef57c713..e265cc42 100644 --- a/projectroles/templates/projectroles/_site_titlebar.html +++ b/projectroles/templates/projectroles/_site_titlebar.html @@ -73,7 +73,7 @@ - +