Skip to content
This repository has been archived by the owner on Aug 15, 2024. It is now read-only.

Commit

Permalink
Work toward a major update to improve docs, add hooks, and improve va…
Browse files Browse the repository at this point in the history
…lidation
  • Loading branch information
jacklinke committed Jan 26, 2024
1 parent 8dd0846 commit 0c97968
Show file tree
Hide file tree
Showing 15 changed files with 651 additions and 578 deletions.
37 changes: 37 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: Bug report
about: Create a report to help us improve the package
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Browser (please complete the following information):**
- Type [e.g. chrome, safari, etc]
- Version [e.g. 22]

**Database (please complete the following information):**
- Device: [e.g. iPhone6]
- Database Version [e.g. 22]
- Running local, remote (e.g. DBaaS), or locally with provided docker-compose
- OS running on: [e.g. Ubuntu 20.04]

**Additional context**
Add any other context about the problem here.
20 changes: 20 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.
38 changes: 38 additions & 0 deletions .github/workflows/django.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Django CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v4
with:
python-version: 3.9
- uses: actions/checkout@v3
- run: python -m pip install black
- run: black -l 119 --check --diff .

isort:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v4
with:
python-version: 3.9
- uses: actions/checkout@v3
- run: python -m pip install isort
- run: isort --profile=black --line-length=119 .

flake8:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v4
with:
python-version: 3.9
- uses: actions/checkout@v3
- run: python -m pip install flake8
- run: flake8 --max-line-length=119
429 changes: 252 additions & 177 deletions README.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions flexible_list_of_values/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
class LOVValueType(models.TextChoices):
"""Allowable selections for List Of Values Value Types"""

MANDATORY = "dm", _("Default Mandatory") # Entity can see, but not change this value
OPTIONAL = "do", _("Default Optional") # Entity can see and select/unselect this value
CUSTOM = "cu", _("Custom") # Entity created and can select/unselect this value
MANDATORY = "dm", _("Default Mandatory") # Tenant can see, but not change this value
OPTIONAL = "do", _("Default Optional") # Tenant can see and select/unselect this value
CUSTOM = "cu", _("Custom") # Tenant created and can select/unselect this value
8 changes: 8 additions & 0 deletions flexible_list_of_values/app_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.conf import settings
from django.db.models.base import ModelBase


# There is likely no reason ever to change the model base, but it is provided as an setting
# here for completeness.
LOV_MODEL_BASE = getattr(settings, 'LOV_MODEL_BASE', ModelBase)

29 changes: 16 additions & 13 deletions flexible_list_of_values/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
class ModelClassParsingError(Exception):
"""
Used when a model class cannot be parsed from the value provided
"""
pass
"""Used when a model class cannot be parsed from the value provided."""

def __init__(self, message="Model class cannot be parsed from the value provided"):
self.message = message
super().__init__(self.message)


class IncorrectSubclassError(Exception):
"""
Used when the value of `lov_value_model` is not a subclass of the correct abstract model
"""
pass
"""Used when the value of `lov_value_model` is not a subclass of the correct abstract model."""

def __init__(self, message="Incorrect subclass usage for lov_value_model"):
self.message = message
super().__init__(self.message)


class NoTenantProvidedFromViewError(Exception):
"""Used when a view does not pass an lov_tenant instance from the view to a form."""

class NoEntityProvidedFromViewError(Exception):
"""
Used when a view does not pass an lov_entity instance from the view to a form
"""
pass
def __init__(self, message="The view did not pass an lov_tenant instance to the form"):
self.message = message
super().__init__(self.message)
77 changes: 42 additions & 35 deletions flexible_list_of_values/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.db import IntegrityError, transaction

from . import LOVValueType
from .exceptions import NoEntityProvidedFromViewError
from .exceptions import NoTenantProvidedFromViewError


logger = logging.getLogger("flexible_list_of_values")
Expand All @@ -14,68 +14,74 @@


class LOVFormBaseMixin:
"""
Checks that we have a valid lov_entity value passed in from the view
"""
"""Checks that we have a valid lov_tenant value passed in from the view."""

def __init__(self, *args, **kwargs):
self.lov_entity = kwargs.pop("lov_entity", None)
if not self.lov_entity:
raise NoEntityProvidedFromViewError("No lov_entity model class was provided to the form from the view")
self.lov_tenant = kwargs.pop("lov_tenant", None)
if not self.lov_tenant:
raise NoTenantProvidedFromViewError(
"No lov_tenant model class was provided to the form from the view. "
"Ensure that 'lov_tenant' is passed as an argument."
)
super().__init__(*args, **kwargs)


class LOVValueCreateFormMixin(LOVFormBaseMixin):
"""
Used in forms that allow an entity to create a new custom value.
"""Used in forms that allow an tenant to create a new custom value.
It requires an `lov_entity` argument to be passed from the view. This should be an instance of the model
class provided in the lov_entity_model parameter of the concrete LOVValueModel.
It requires an `lov_tenant` argument to be passed from the view. This should be an instance of the model
class provided in the lov_tenant_model parameter of the concrete LOVValueModel.
Usage:
class MyValueCreateForm(EntityValueCreateFormMixin, forms.ModelForm):
class MyValueCreateForm(TenantValueCreateFormMixin, forms.ModelForm):
class Meta:
model = MyConcreteLOVValueModel
def my_values_view(request):
form = MyValueCreateForm(request.POST, lov_entity=request.user.tenant)
form = MyValueCreateForm(request.POST, lov_tenant=request.user.tenant)
"""

def get_value_type_widget(self):
"""Returns the widget to use for the value_type field."""
return HiddenInput()

def get_lov_tenant_widget(self):
"""Returns the widget to use for the lov_tenant field."""
return HiddenInput()

def __init__(self, *args, **kwargs):
"""Sets the value_type and lov_tenant fields to the correct values."""
super().__init__(*args, **kwargs)

# Set the value_type field value to LOVValueType.CUSTOM and use a HiddenInput widget
self.fields["value_type"].widget = HiddenInput()
self.fields["value_type"].widget = self.get_value_type_widget()
self.fields["value_type"].initial = LOVValueType.CUSTOM

# Set the lov_entity field value to the entity instance provided from the view
# Set the lov_tenant field value to the tenant instance provided from the view
# and use a HiddenInput widget
self.fields["lov_entity"].widget = HiddenInput()
self.fields["lov_entity"].initial = self.lov_entity
self.fields["lov_tenant"].widget = self.get_lov_tenant_widget()
self.fields["lov_tenant"].initial = self.lov_tenant

def clean(self):
"""Ensures that the value_type and lov_tenant fields are correct even if the HiddenInput widget was manipulated."""
cleaned_data = super().clean()

# # if this Item already exists
# if self.instance:
# # ensure value_type and lov_entity is correct even if HiddenField was manipulated
# cleaned_data["value_type"] = LOVValueType.CUSTOM
# cleaned_data["lov_entity"] = self.lov_entity

# ensure value_type and lov_entity is correct even if HiddenField was manipulated
# ensure value_type and lov_tenant is correct even if HiddenField was manipulated
cleaned_data["value_type"] = LOVValueType.CUSTOM
cleaned_data["lov_entity"] = self.lov_entity
cleaned_data["lov_tenant"] = self.lov_tenant
return cleaned_data


class LOVModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
Displays objects and shows which are mandatory
"""
"""Displays objects and shows which are mandatory."""

def label_from_instance(self, obj):
"""Displays objects and shows which are mandatory.
Override this method to change the display of the objects in the field.
"""
if obj.value_type == LOVValueType.MANDATORY:
return f"{obj.name} (mandatory)"
return obj.name
Expand All @@ -100,19 +106,20 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Get all allowed values for this tenant
self.fields["lov_selections"].queryset = self.lov_selection_model.objects.values_for_entity(self.lov_entity)
self.fields["lov_selections"].queryset = self.lov_selection_model.objects.values_for_tenant(self.lov_tenant)

# Make sure the current selections (and mandatory values) are selected in the form
self.fields["lov_selections"].initial = self.lov_selection_model.objects.selected_values_for_entity(
self.lov_entity
self.fields["lov_selections"].initial = self.lov_selection_model.objects.selected_values_for_tenant(
self.lov_tenant
)

self.fields["lov_selections"].widget.attrs["size"] = "10"

def clean(self):
"""Ensures that the lov_selections field is correct even if the HiddenInput widget was manipulated."""
cleaned_data = super().clean()

# ensure value_type and lov_selections contain mandatory items even if HiddenField was manipulated
# ensure lov_selections contain mandatory items even if HiddenField was manipulated
cleaned_lov_selections = (
cleaned_data["lov_selections"] | self.lov_value_model.objects.filter(value_type=LOVValueType.MANDATORY)
).distinct()
Expand All @@ -127,8 +134,8 @@ def save(self, *args, **kwargs):
try:
with transaction.atomic():
for selection in self.cleaned_data["lov_selections"]:
self.lov_selection_model.objects.update_or_create(lov_entity=self.lov_entity, lov_value=selection)
self.lov_selection_model.objects.update_or_create(lov_tenant=self.lov_tenant, lov_value=selection)
self.lov_selection_model.objects.filter(lov_value__in=self.removed_selections).delete()
except IntegrityError as e:
logger.warning(f"Problem creating or deleting selections for {self.lov_entity}: {e}")
super().__init__(*args, **kwargs, lov_entity=self.lov_entity)
logger.warning(f"Problem creating or deleting selections for {self.lov_tenant}: {e}")
super().save(*args, **kwargs)
Loading

0 comments on commit 0c97968

Please sign in to comment.