Skip to content

Commit

Permalink
Merge pull request #393 from UW-GAC/feature/add-search-functionality-…
Browse files Browse the repository at this point in the history
…for-models

Feature/add search functionality for models

Closes #294
  • Loading branch information
amstilp authored Oct 24, 2023
2 parents 54d0e57 + d29fdb4 commit 6ad526f
Show file tree
Hide file tree
Showing 34 changed files with 861 additions and 73 deletions.
44 changes: 22 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
Expand All @@ -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"]

Expand All @@ -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:"
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion anvil_consortium_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.18"
__version__ = "0.19dev1"
25 changes: 25 additions & 0 deletions anvil_consortium_manager/adapters/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion anvil_consortium_manager/adapters/default.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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):
Expand Down
31 changes: 31 additions & 0 deletions anvil_consortium_manager/filters.py
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions anvil_consortium_manager/forms.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% load static %}

{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}

{% block title %}Accounts{% endblock %}

Expand All @@ -13,6 +14,10 @@

<h2>Accounts</h2>

<div class="container pt-3">
{% crispy filter.form %}
</div>

{% render_table table %}

</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% load static %}

{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}

{% block title %}Billing Projects{% endblock %}

Expand All @@ -13,6 +14,10 @@

<h2>Billing Projects</h2>

<div class="container pt-3">
{% crispy filter.form %}
</div>

{% render_table table %}

</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
{% load static %}

{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}

{% block title %}Managed Groups{% endblock %}

{% block content %}
<div class="container">

<div class="row">
<div class="col-sm-12">

<h2>Managed Groups</h2>

<div class="container pt-3">
{% crispy filter.form %}
</div>

{% render_table table %}

</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
{% load static %}

{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}

{% block title %}{{workspace_type_display_name}}s{% endblock %}

{% block content %}
<div class="container">

<div class="row">
<div class="col-sm-12">

<h2>{{workspace_type_display_name}}s</h2>

<div class="container pt-3">
{% crispy filter.form %}
</div>

{% render_table table %}

</div>
Expand Down
43 changes: 42 additions & 1 deletion anvil_consortium_manager/tests/test_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
BaseWorkspaceAdapter,
WorkspaceAdapterRegistry,
)
from ..filters import AccountListFilter, BillingProjectListFilter
from ..forms import DefaultWorkspaceDataForm, WorkspaceForm
from ..models import Account, DefaultWorkspaceData
from ..tables import AccountTable, WorkspaceTable
from . import factories
from .test_app import forms, models, tables
from .test_app import filters, forms, models, tables
from .test_app.adapters import TestWorkspaceAdapter


Expand All @@ -26,6 +27,7 @@ def get_test_adapter(self):

class TestAdapter(BaseAccountAdapter):
list_table_class = tables.TestAccountTable
list_filterset_class = filters.TestAccountListFilter

return TestAdapter

Expand All @@ -46,6 +48,45 @@ def test_list_table_class_none(self):
with self.assertRaises(ImproperlyConfigured):
TestAdapter().get_list_table_class()

def test_list_filterset_class_default(self):
"""get_list_filterset_class returns the correct filter when using the default adapter."""
self.assertEqual(
DefaultAccountAdapter().get_list_filterset_class(), AccountListFilter
)

def test_list_filterset_class_custom(self):
"""get_list_filterset_class returns the correct filter when using a custom adapter."""
TestAdapter = self.get_test_adapter()
setattr(TestAdapter, "list_filterset_class", filters.TestAccountListFilter)
self.assertEqual(
TestAdapter().get_list_filterset_class(), filters.TestAccountListFilter
)

def test_list_filterset_class_none(self):
"""get_list_filterset_class raises ImproperlyConfigured when get_list_filterset_class is not set."""
TestAdapter = self.get_test_adapter()
setattr(TestAdapter, "list_filterset_class", None)
with self.assertRaises(ImproperlyConfigured):
TestAdapter().get_list_filterset_class()

def test_list_filterset_class_different_model(self):
"""get_list_filterset_class raises ImproperlyConfigured when incorrect model is used."""
TestAdapter = self.get_test_adapter()
setattr(TestAdapter, "list_filterset_class", BillingProjectListFilter)
with self.assertRaises(ImproperlyConfigured):
TestAdapter().get_list_filterset_class()

def test_list_filterset_class_not_filterset(self):
"""get_list_filterset_class raises ImproperlyConfigured when not a subclass of FilterSet."""

class Foo:
pass

TestAdapter = self.get_test_adapter()
setattr(TestAdapter, "list_filterset_class", Foo)
with self.assertRaises(ImproperlyConfigured):
TestAdapter().get_list_filterset_class()

def test_get_autocomplete_queryset_default(self):
"""get_autocomplete_queryset returns the correct queryset when using the default adapter."""
account_1 = factories.AccountFactory.create(email="[email protected]")
Expand Down
Loading

0 comments on commit 6ad526f

Please sign in to comment.