diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63b0e49b..58dd08e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,6 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.9" - cache: pip - cache-dependency-path: | - requirements/base.txt - requirements/dev.txt - name: Run pre-commit uses: pre-commit/action@v3.0.0 @@ -41,7 +37,7 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - max-parallel: 5 + max-parallel: 10 matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] @@ -59,7 +55,15 @@ jobs: python -m pip install --upgrade tox tox-gh-actions - name: Test with tox - run: tox + # Sometimes tox fails to build the package correctly and intermittently throws an OSError or BadZipFile error. + # Upon retry, it seems to work. + uses: nick-fields/retry@v2 + id: retry-sqlite + with: + timeout_minutes: 10 + max_attempts: 3 + retry_on: error + command: tox env: DBBACKEND: sqlite3 DBNAME: ":memory:" @@ -75,10 +79,10 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - max-parallel: 5 + max-parallel: 10 matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] - mariadb-version: ["10.3", "10.5"] + mariadb-version: ["10.4"] services: database: @@ -103,19 +107,16 @@ jobs: python -m pip install --upgrade pip python -m pip install --upgrade tox tox-gh-actions - - name: Test with tox - 10.3 - if: ${{ matrix.mariadb-version == '10.3' }} - run: tox --skip-env ".+?-django42-mysql" - env: - DBBACKEND: mysql - DBNAME: test - DBUSER: root - DBPASSWORD: rootpw - DBHOST: 127.0.0.1 - - - name: Test with tox - not 10.3 - if: ${{ matrix.mariadb-version != '10.3' }} - run: tox + - name: Test with tox + # Sometimes tox fails to build the package correctly and intermittently throws an OSError or BadZipFile error. + # Upon retry, it seems to work. + uses: nick-fields/retry@v2 + id: retry-mysql + with: + timeout_minutes: 10 + max_attempts: 3 + retry_on: error + command: tox env: DBBACKEND: mysql DBNAME: test @@ -143,7 +144,6 @@ jobs: - run: python -m pip install --upgrade coverage[toml] django==3.2.16 django-coverage-plugin - - name: Download coverage data. uses: actions/download-artifact@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6a6798..9b03d490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change log +## Devel + +* Add filtering in list views. + ## 0.18 (2023-10-03) * Include a workspace_data_object context variable for the `WorkspaceDetail` and `WorkspaceUpdate` views. diff --git a/anvil_consortium_manager/__init__.py b/anvil_consortium_manager/__init__.py index a506fc88..c59266c2 100644 --- a/anvil_consortium_manager/__init__.py +++ b/anvil_consortium_manager/__init__.py @@ -1 +1 @@ -__version__ = "0.18" +__version__ = "0.19dev1" diff --git a/anvil_consortium_manager/adapters/account.py b/anvil_consortium_manager/adapters/account.py index 3d35f9db..097d090d 100644 --- a/anvil_consortium_manager/adapters/account.py +++ b/anvil_consortium_manager/adapters/account.py @@ -5,6 +5,9 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_string +from django_filters import FilterSet + +from .. import models class BaseAccountAdapter(ABC): @@ -15,6 +18,11 @@ def list_table_class(self): """Table class to use in a list of Accounts.""" ... + @abstractproperty + def list_filterset_class(self): + """FilterSet subclass to use for Account filtering in the AccountList view.""" + ... + def get_list_table_class(self): """Return the table class to use for the AccountList view.""" if not self.list_table_class: @@ -23,6 +31,23 @@ def get_list_table_class(self): ) return self.list_table_class + def get_list_filterset_class(self): + """Return the FilterSet subclass to use for Account filtering in the AccountList view.""" + if not self.list_filterset_class: + raise ImproperlyConfigured( + "Set `list_filterset_class` in `{}`.".format(type(self)) + ) + if not issubclass(self.list_filterset_class, FilterSet): + raise ImproperlyConfigured( + "list_filterset_class must be a subclass of FilterSet." + ) + # Make sure it has the correct model set. + if self.list_filterset_class.Meta.model != models.Account: + raise ImproperlyConfigured( + "list_filterset_class Meta model field must be anvil_consortium_manager.models.Account." + ) + return self.list_filterset_class + def get_autocomplete_queryset(self, queryset, q): """Filter the Account `queryset` using the query `q` for use in the autocomplete.""" queryset = queryset.filter(email__icontains=q) diff --git a/anvil_consortium_manager/adapters/default.py b/anvil_consortium_manager/adapters/default.py index 10cce327..fbd985f3 100644 --- a/anvil_consortium_manager/adapters/default.py +++ b/anvil_consortium_manager/adapters/default.py @@ -1,6 +1,6 @@ """Default adapters for the app.""" -from .. import forms, models, tables +from .. import filters, forms, models, tables from .account import BaseAccountAdapter from .workspace import BaseWorkspaceAdapter @@ -9,6 +9,7 @@ class DefaultAccountAdapter(BaseAccountAdapter): """Default account adapter for use with the app.""" list_table_class = tables.AccountTable + list_filterset_class = filters.AccountListFilter class DefaultWorkspaceAdapter(BaseWorkspaceAdapter): diff --git a/anvil_consortium_manager/filters.py b/anvil_consortium_manager/filters.py new file mode 100644 index 00000000..97f97f34 --- /dev/null +++ b/anvil_consortium_manager/filters.py @@ -0,0 +1,31 @@ +from django_filters import FilterSet + +from . import forms, models + + +class AccountListFilter(FilterSet): + class Meta: + model = models.Account + fields = {"email": ["icontains"]} + form = forms.FilterForm + + +class BillingProjectListFilter(FilterSet): + class Meta: + model = models.BillingProject + fields = {"name": ["icontains"]} + form = forms.FilterForm + + +class ManagedGroupListFilter(FilterSet): + class Meta: + model = models.ManagedGroup + fields = {"name": ["icontains"]} + form = forms.FilterForm + + +class WorkspaceListFilter(FilterSet): + class Meta: + model = models.Workspace + fields = {"name": ["icontains"]} + form = forms.FilterForm diff --git a/anvil_consortium_manager/forms.py b/anvil_consortium_manager/forms.py index f7ea1742..14828136 100644 --- a/anvil_consortium_manager/forms.py +++ b/anvil_consortium_manager/forms.py @@ -1,5 +1,8 @@ """Forms classes for the anvil_consortium_manager app.""" +from crispy_bootstrap5.bootstrap5 import FloatingField +from crispy_forms import layout +from crispy_forms.helper import FormHelper from dal import autocomplete from django import VERSION as DJANGO_VERSION from django import forms @@ -22,6 +25,40 @@ class Media: } +class FilterForm(forms.Form): + """Custom form to pass to Filters defined in filters.py. + + This form displays the fields with floating fields in a single row. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.form_class = "form-floating" + self.helper.form_method = "get" + # Wrap all inputs in a FloatingField and Div with the correct class. + self.helper.all().wrap(FloatingField) + self.helper.all().wrap(layout.Div, css_class="col") + # Save the original layout so we can insert it into the form as desired. + tmp = self.helper.layout.copy() + # Modify the layout to wrap everything in a row div. + # This is necessary because wrap_together does not include the Submit field, but we want it wrapped as well. + self.helper.layout = layout.Layout( + layout.Div( + *tmp, + # Add a submit button with col-auto. This makes auto-sizes the column to just fit the submit button. + layout.Div( + # mb-3 to match what is done in FloatingField - this centers the button vertically. + layout.Submit( + "submit", "Filter", css_class="btn btn-secondary mb-3" + ), + css_class="col-auto", + ), + css_class="row align-items-center" + ), + ) + + class BillingProjectImportForm(forms.ModelForm): """Form to import a BillingProject from AnVIL""" diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/account_list.html b/anvil_consortium_manager/templates/anvil_consortium_manager/account_list.html index b1e353d4..4e3b2b17 100644 --- a/anvil_consortium_manager/templates/anvil_consortium_manager/account_list.html +++ b/anvil_consortium_manager/templates/anvil_consortium_manager/account_list.html @@ -2,6 +2,7 @@ {% load static %} {% load render_table from django_tables2 %} +{% load crispy_forms_tags %} {% block title %}Accounts{% endblock %} @@ -13,6 +14,10 @@