Merge pull request #70 from Dresdn/issue-69-tooling

New tooling and test refactoring
This commit is contained in:
Mike 2021-07-19 14:30:25 +00:00 committed by GitHub
commit ac05b63690
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 2857 additions and 300 deletions

View file

@ -1,4 +0,0 @@
[run]
omit =
*/migrations/*
eav/__init__.py

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
# Check http://editorconfig.org for more information
# This is the main config file for this project:
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
indent_style = space
insert_final_newline = true
indent_size = 2
[*.py]
indent_size = 4
[*.pyi]
indent_size = 4

View file

@ -1,30 +0,0 @@
language: python
matrix:
include:
- python: 3.6
env: TOXENV=py36-django31
- python: 3.6
env: TOXENV=py36-django32
- python: 3.7
env: TOXENV=py37-django31
- python: 3.7
env: TOXENV=py37-django32
- python: 3.8
env: TOXENV=py38-django31
- python: 3.8
env: TOXENV=py38-django32
- python: 3.9
env: TOXENV=py39-django31
- python: 3.9
env: TOXENV=py39-django32
install:
- pip install Django>=3.1
- pip install coveralls==1.3.0
- pip install coverage==4.5.1
- pip install tox-travis==0.10
before_script:
- coverage erase
script:
- coverage run --source=eav runtests; tox
after_success:
- COVERALLS_REPO_TOKEN=71NkMDQFpFKB9QYXoK12LYuWUEmQ2wD6V coveralls

View file

@ -1,6 +1,6 @@
[![Build Status](https://travis-ci.org/lvm/django-eav2.svg?branch=master)](https://travis-ci.org/lvm/django-eav2)
![Python Version](https://img.shields.io/badge/Python-3.6,%203.7,%203.8,%203.9-blue.svg)
![Django Version](https://img.shields.io/badge/Django-3.1,%203.2-green.svg)
![Django Version](https://img.shields.io/badge/Django-2.2,%20,3.1,%203.2-green.svg)
[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/)
## Django EAV 2 - Entity-Attribute-Value storage for Django
@ -14,16 +14,16 @@ You can find documentation <a href="https://django-eav2.rtfd.io">here</a>.
Data in EAV is stored as a 3-tuple (typically corresponding to three distinct tables):
* The entity: the item being described, e.g. `Person(name='Mike')`.
* The attribute: often a foreign key into a table of attributes, e.g. `Attribute(slug='height', datatype=FLOAT)`.
* The value of the attribute, with links both an attribute and an entity, e.g. `Value(value_float=15.5, person=mike, attr=height)`.
- The entity: the item being described, e.g. `Person(name='Mike')`.
- The attribute: often a foreign key into a table of attributes, e.g. `Attribute(slug='height', datatype=FLOAT)`.
- The value of the attribute, with links both an attribute and an entity, e.g. `Value(value_float=15.5, person=mike, attr=height)`.
Entities in **django-eav2** are your typical Django model instances. Attributes (name and type) are stored in their own table, which makes it easy to manipulate the list of available attributes in the system. Values are an intermediate table between attributes and entities, each instance holding a single value.
This implementation also makes it easy to edit attributes in Django Admin and form instances.
You will find detailed description of the EAV here:
* [Wikipedia - Entityattributevalue model](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model)
- [Wikipedia - Entityattributevalue model](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model)
## EAV - The Good, the Bad or the Ugly?
@ -35,24 +35,24 @@ Originally, EAV was introduced to workaround a problem which cannot be easily so
Typical application of the EAV model sets to solve the problem of sparse data with a large number of applicable attributes, but only a small fraction that applies to a given entity that may not be known beforehand. Consider the classic example:
> A problem that data modelers commonly encounter in the biomedical domain is organizing and storing highly diverse and heterogeneous data. For example, a single patient may have thousands of applicable descriptive parameters, all of which need to be easily accessible in an electronic patient record system. These requirements pose significant modeling and implementation challenges. [1]
> A problem that data modelers commonly encounter in the biomedical domain is organizing and storing highly diverse and heterogeneous data. For example, a single patient may have thousands of applicable descriptive parameters, all of which need to be easily accessible in an electronic patient record system. These requirements pose significant modeling and implementation challenges. [1]
And:
And:
> [...] what do you do when you have customers that demand real-time, on-demand addition of attributes that they want to store? In one of the systems I manage, our customers wanted to do exactly this. Since we run a SaaS (software as a service) application, we have many customers across several different industries, who in turn want to use our system to store different types of information about *their* customers. A salon chain might want to record facts such as 'hair color,' 'hair type,' and 'haircut frequency'; while an investment company might want to record facts such as 'portfolio name,' 'last portfolio adjustment date,' and 'current portfolio balance.' [2]
> [...] what do you do when you have customers that demand real-time, on-demand addition of attributes that they want to store? In one of the systems I manage, our customers wanted to do exactly this. Since we run a SaaS (software as a service) application, we have many customers across several different industries, who in turn want to use our system to store different types of information about _their_ customers. A salon chain might want to record facts such as 'hair color,' 'hair type,' and 'haircut frequency'; while an investment company might want to record facts such as 'portfolio name,' 'last portfolio adjustment date,' and 'current portfolio balance.' [2]
In both of these problems we have to deal with sparse and heterogeneous properties that apply only to potentially different subsets of particular entities. Applying EAV to a sub-schema of the database allows to model the desired behaviour. Traditional solution would involves wide tables with many columns storing NULL values for attributes that don't apply to an entity.
In both of these problems we have to deal with sparse and heterogeneous properties that apply only to potentially different subsets of particular entities. Applying EAV to a sub-schema of the database allows to model the desired behaviour. Traditional solution would involves wide tables with many columns storing NULL values for attributes that don't apply to an entity.
Very common use case for EAV are custom product attributes in E-commerce implementations, such as Magento. [3]
As a rule of thumb, EAV can be used when:
* Model attributes are to be added and removed by end users (or are unknowable in some different way). EAV supports these without ALTER TABLE statements and allows the attributes to be strongly typed and easily searchable.
* There will be many attributes and values are sparse, in contrast to having tables with mostly-null columns.
* The data is highly dynamic/volatile/vulnerable to change. This problem is present in the second example given above. Other example would be rapidly evolving system, such as a prototype with constantly changing requirements.
* We want to store meta-data or supporting information, e.g. to customize system's behavior.
* Numerous classes of data need to be represented, each class has a limited number of attributes, but the number of instances of each class is very small.
* We want to minimise programmer's input when changing the data model.
As a rule of thumb, EAV can be used when:
- Model attributes are to be added and removed by end users (or are unknowable in some different way). EAV supports these without ALTER TABLE statements and allows the attributes to be strongly typed and easily searchable.
- There will be many attributes and values are sparse, in contrast to having tables with mostly-null columns.
- The data is highly dynamic/volatile/vulnerable to change. This problem is present in the second example given above. Other example would be rapidly evolving system, such as a prototype with constantly changing requirements.
- We want to store meta-data or supporting information, e.g. to customize system's behavior.
- Numerous classes of data need to be represented, each class has a limited number of attributes, but the number of instances of each class is very small.
- We want to minimise programmer's input when changing the data model.
For more throughout discussion on the appriopriate use-cases see:
@ -85,6 +85,7 @@ In some use-cases, JSONB (binary JSON data) datatype (Postgres 9.4+ and analogou
## Installation
You can install **django-eav2** from three sources:
```bash
# From PyPI via pip
pip install django-eav2

View file

@ -1,5 +1,3 @@
__version__ = '0.14.1'
def register(model_cls, config_cls=None):
from .registry import Registry
Registry.register(model_cls, config_cls)

View file

@ -1,13 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.test_settings")
def main() -> None:
"""
Main function.
from django.core.management import execute_from_command_line
It does several things:
1. Sets default settings module, if it is not set
2. Warns if Django is not installed
3. Executes any given command
"""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings')
execute_from_command_line(sys.argv)
try:
from django.core import management # noqa: WPS433
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
+ 'available on your PYTHONPATH environment variable? Did you '
+ 'forget to activate a virtual environment?',
)
management.execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

2220
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

79
pyproject.toml Normal file
View file

@ -0,0 +1,79 @@
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.nitpick]
style = "https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/master/styles/nitpick-style-wemake.toml"
[tool.black]
target-version = ['py36', 'py37', 'py38', 'py39']
skip-string-normalization = true
include = '\.pyi?$'
[tool.poetry]
name = "eav"
description = "Entity-Attribute-Value storage for Django"
version = "0.14.1"
license = "GNU Lesser General Public License (LGPL), Version 3"
authors = [
"Mauro Lizaur <mauro@sdf.org>",
]
readme = "README.md"
repository = "https://github.com/jazzband/django-eav2"
keywords = [
"django",
"django-eav2",
"database",
"eav",
"sql",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Web Environment",
"Framework :: Django",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Programming Language :: Python",
"Topic :: Database",
"Topic :: Software Development :: Libraries :: Python Modules",
"Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.1",
"Framework :: Django :: 3.2",
]
[tool.poetry.dependencies]
python = "^3.6.2"
[tool.poetry.dev-dependencies]
django = "^3.2"
mypy = "^0.902"
wemake-python-styleguide = "^0.15"
flake8-pytest-style = "^1.4"
nitpick = "^0.26"
safety = "^1.10"
pytest = "^6.2"
pytest-cov = "^2.12"
pytest-randomly = "^3.0"
sphinx = "^4.0"
sphinx-autodoc-typehints = "^1.12"
doc8 = "^0.8"
m2r2 = "^0.2"
tomlkit = "^0.7"
pytest-pythonpath = "^0.7.3"
pytest-django = "^4.4.0"
tox-poetry-installer = "^0.8.1"
black = "^21.6b0"

View file

@ -1 +0,0 @@
Django>=3.1

129
setup.cfg Normal file
View file

@ -0,0 +1,129 @@
# All configuration for plugins and other utils is defined here.
# Read more about `setup.cfg`:
# https://docs.python.org/3/distutils/configfile.html
[flake8]
format = wemake
show-source = True
doctests = False
statistics = False
# darglint configuration:
# https://github.com/terrencepreilly/darglint
strictness = long
docstring-style = numpy
# Plugins:
max-complexity = 6
max-line-length = 80
exclude =
# Trash and cache:
.git
__pycache__
.venv
.eggs
*.egg
temp
ignore =
D100,
D104,
D401,
W504,
X100,
RST303,
RST304,
DAR103,
DAR203
per-file-ignores =
test_project/migrations/*.py: N806, WPS102, WPS114
test_project/settings.py: S105, WPS226, WPS407
tests/test_*.py: N806, S101, S404, S603, S607, WPS118, WPS226, WPS432, WPS442
[isort]
# isort configuration:
# https://github.com/timothycrosley/isort/wiki/isort-Settings
include_trailing_comma = true
use_parentheses = true
# See https://github.com/timothycrosley/isort#multi-line-output-modes
multi_line_output = 3
line_length = 80
# Useful for our test app:
known_first_party = test_project
[tool:pytest]
# Django options:
# https://pytest-django.readthedocs.io/en/latest/
DJANGO_SETTINGS_MODULE = test_project.settings
# PYTHONPATH configuration:
# https://github.com/bigsassy/pytest-pythonpath
python_paths = ./eav
# py.test options:
norecursedirs =
*.egg
.eggs
dist
build
docs
.tox
.git
__pycache__
# You will need to measure your tests speed with `-n auto` and without it,
# so you can see whether it gives you any performance gain, or just gives
# you an overhead. See `docs/template/development-process.rst`.
addopts =
-p no:randomly
--strict-markers
--strict-config
--doctest-modules
--cov=eav
--cov-report=term-missing:skip-covered
--cov-report=html
--cov-report=xml
--cov-branch
--cov-fail-under=10
[coverage:run]
# Exclude tox output from coverage calculation
omit = */.tox/*
[coverage:report]
skip_covered = True
show_missing = True
sort = Cover
exclude_lines =
pragma: no cover
# type hinting related code
if TYPE_CHECKING:
[mypy]
# mypy configurations: http://bit.ly/2zEl9WI
allow_redefinition = False
check_untyped_defs = True
disallow_any_explicit = True
disallow_any_generics = True
disallow_untyped_calls = True
ignore_errors = False
ignore_missing_imports = True
implicit_reexport = False
strict_optional = True
strict_equality = True
local_partial_types = True
no_implicit_optional = True
warn_no_return = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
warn_unreachable = True

View file

@ -1,25 +0,0 @@
from setuptools import setup, find_packages
setup(
name = 'django-eav2',
version = __import__('eav').__version__,
license = 'GNU Lesser General Public License (LGPL), Version 3',
requires = ['python (>= 3.6)', 'django (>= 3.1)'],
provides = ['eav'],
description = 'Entity-Attribute-Value storage for Django',
url = 'http://github.com/lvm/django-eav2',
packages = find_packages(),
maintainer = 'Mauro Lizaur',
maintainer_email = 'mauro@sdf.org',
classifiers = [
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Programming Language :: Python',
'Topic :: Database',
'Topic :: Software Development :: Libraries :: Python Modules',
],
)

0
test_project/__init__.py Normal file
View file

5
test_project/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class TestAppConfig(AppConfig):
name = 'test_project'

View file

@ -0,0 +1,120 @@
# Generated by Django 3.2.4 on 2021-06-17 22:20
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='ExampleMetaclassModel',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=12)),
],
),
migrations.CreateModel(
name='ExampleModel',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=12)),
],
),
migrations.CreateModel(
name='RegisterTestModel',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=12)),
],
),
migrations.CreateModel(
name='Patient',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=12)),
(
'example',
models.ForeignKey(
blank=True,
null=True,
on_delete=models.deletion.PROTECT,
to='examplemodel',
),
),
],
),
migrations.CreateModel(
name='M2MModel',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=12)),
('models', models.ManyToManyField(to='ExampleModel')),
],
),
migrations.CreateModel(
name='Encounter',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('num', models.PositiveSmallIntegerField()),
(
'patient',
models.ForeignKey(
on_delete=models.deletion.PROTECT,
to='patient',
),
),
],
),
]

View file

View file

@ -1,11 +1,24 @@
from django.db import models
from eav.decorators import register_eav
from eav.models import EAVModelMeta
class Patient(models.Model):
class TestBase(models.Model):
"""Base class for test models."""
class Meta(object):
"""Define common options."""
app_label = 'test_project'
abstract = True
class Patient(TestBase):
name = models.CharField(max_length=12)
example = models.ForeignKey(
'ExampleModel', null=True, blank=True, on_delete=models.PROTECT)
'ExampleModel', null=True, blank=True, on_delete=models.PROTECT
)
def __str__(self):
return self.name
@ -14,7 +27,7 @@ class Patient(models.Model):
return self.name
class Encounter(models.Model):
class Encounter(TestBase):
num = models.PositiveSmallIntegerField()
patient = models.ForeignKey(Patient, on_delete=models.PROTECT)
@ -26,7 +39,7 @@ class Encounter(models.Model):
@register_eav()
class ExampleModel(models.Model):
class ExampleModel(TestBase):
name = models.CharField(max_length=12)
def __unicode__(self):
@ -34,9 +47,23 @@ class ExampleModel(models.Model):
@register_eav()
class M2MModel(models.Model):
class M2MModel(TestBase):
name = models.CharField(max_length=12)
models = models.ManyToManyField(ExampleModel)
def __unicode__(self):
return self.name
class ExampleMetaclassModel(TestBase, metaclass=EAVModelMeta):
name = models.CharField(max_length=12)
def __str__(self):
return self.name
class RegisterTestModel(TestBase, metaclass=EAVModelMeta):
name = models.CharField(max_length=12)
def __str__(self):
return self.name

99
test_project/settings.py Normal file
View file

@ -0,0 +1,99 @@
from pathlib import Path
from typing import List
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'secret!' # noqa: S105
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS: List[str] = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.postgres',
# Test Project:
'test_project.apps.TestAppConfig',
# Our app:
'eav',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = []
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'

View file

@ -1,18 +0,0 @@
from django.db import models
from eav.models import EAVModelMeta
class ExampleMetaclassModel(models.Model):
__metaclass__ = EAVModelMeta
name = models.CharField(max_length=12)
def __unicode__(self):
return self.name
class RegisterTestModel(models.Model):
__metaclass__ = EAVModelMeta
name = models.CharField(max_length=12)
def __unicode__(self):
return self.name

View file

@ -1,16 +0,0 @@
from django.db import models
from eav.models import EAVModelMeta
class ExampleMetaclassModel(models.Model, metaclass=EAVModelMeta):
name = models.CharField(max_length=12)
def __str__(self):
return self.name
class RegisterTestModel(models.Model, metaclass=EAVModelMeta):
name = models.CharField(max_length=12)
def __str__(self):
return self.name

View file

@ -1,18 +1,11 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
import sys
import eav
from eav.exceptions import IllegalAssignmentException
from eav.models import Attribute, Value
from eav.registry import EavConfig
from .models import Encounter, Patient
if sys.version_info[0] > 2:
from .metaclass_models3 import RegisterTestModel
else:
from .metaclass_models2 import RegisterTestModel
from test_project.models import Encounter, Patient, RegisterTestModel
class Attributes(TestCase):

View file

@ -1,17 +1,14 @@
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
import eav
from eav.models import Attribute, Value, EnumValue, EnumGroup
from .models import Patient
from eav.models import Attribute, EnumGroup, EnumValue, Value
from test_project.models import Patient
class DataValidation(TestCase):
def setUp(self):
eav.register(Patient)
@ -32,7 +29,9 @@ class DataValidation(TestCase):
p.eav.age = 5
p.save()
Attribute.objects.create(name='Weight', datatype=Attribute.TYPE_INT, required=True)
Attribute.objects.create(
name='Weight', datatype=Attribute.TYPE_INT, required=True
)
p.eav.age = 6
self.assertRaises(ValidationError, p.save)
p = Patient.objects.get(name='Bob')
@ -43,10 +42,12 @@ class DataValidation(TestCase):
self.assertEqual(p.eav.weight, 23)
def test_create_required_field(self):
Attribute.objects.create(name='Weight', datatype=Attribute.TYPE_INT, required=True)
self.assertRaises(ValidationError,
Patient.objects.create,
name='Joe', eav__age=5)
Attribute.objects.create(
name='Weight', datatype=Attribute.TYPE_INT, required=True
)
self.assertRaises(
ValidationError, Patient.objects.create, name='Joe', eav__age=5
)
self.assertEqual(Patient.objects.count(), 0)
self.assertEqual(Value.objects.count(), 0)
@ -55,9 +56,9 @@ class DataValidation(TestCase):
self.assertEqual(Value.objects.count(), 2)
def test_validation_error_create(self):
self.assertRaises(ValidationError,
Patient.objects.create,
name='Joe', eav__age='df')
self.assertRaises(
ValidationError, Patient.objects.create, name='Joe', eav__age='df'
)
self.assertEqual(Patient.objects.count(), 0)
self.assertEqual(Value.objects.count(), 0)
@ -108,7 +109,7 @@ class DataValidation(TestCase):
p.eav.height = 15
p.save()
self.assertEqual(Patient.objects.get(pk=p.pk).eav.height, 15)
p.eav.height='2.3'
p.eav.height = '2.3'
p.save()
self.assertEqual(Patient.objects.get(pk=p.pk).eav.height, 2.3)
@ -150,7 +151,9 @@ class DataValidation(TestCase):
ynu.values.add(yes)
ynu.values.add(no)
ynu.values.add(unkown)
Attribute.objects.create(name='Fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
Attribute.objects.create(
name='Fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu
)
p = Patient.objects.create(name='Joe')
p.eav.fever = 5
@ -204,4 +207,6 @@ class DataValidation(TestCase):
self.assertRaises(ValidationError, p.save)
p.eav.multi = "one;two;three"
p.save()
self.assertEqual(Patient.objects.get(pk=p.pk).eav.multi, ["one","two","three"])
self.assertEqual(
Patient.objects.get(pk=p.pk).eav.multi, ["one", "two", "three"]
)

View file

@ -1,16 +1,16 @@
from django.test import TestCase
import sys
from django.contrib.admin.sites import AdminSite
from django.core.handlers.base import BaseHandler
from django.forms import ModelForm
from django.test import TestCase
from django.test.client import RequestFactory
import eav
import sys
from eav.admin import *
from .models import Patient, M2MModel, ExampleModel
from eav.models import Attribute
from eav.forms import BaseDynamicEntityForm
from django.contrib import admin
from django.core.handlers.base import BaseHandler
from django.test.client import RequestFactory
from django.forms import ModelForm
from eav.models import Attribute
from test_project.models import ExampleModel, M2MModel, Patient
class MockRequest(RequestFactory):
@ -24,8 +24,10 @@ class MockRequest(RequestFactory):
if sys.version_info[0] < 2:
for middleware_method in handler._request_middleware:
if middleware_method(request):
raise Exception("Couldn't create request mock object - "
"request middleware returned a response")
raise Exception(
"Couldn't create request mock object - "
"request middleware returned a response"
)
return request
@ -66,9 +68,7 @@ class Forms(TestCase):
gender_group.values.add(self.female, self.male)
Attribute.objects.create(
name='gender',
datatype=Attribute.TYPE_ENUM,
enum_group=gender_group
name='gender', datatype=Attribute.TYPE_ENUM, enum_group=gender_group
)
self.instance = Patient.objects.create(name='Jim Morrison')

View file

@ -1,9 +1,8 @@
from django.test import TestCase
from eav.models import EnumGroup, Attribute, Value, EnumValue
import eav
from .models import Patient
from eav.models import Attribute, EnumGroup, EnumValue, Value
from test_project.models import Patient
class MiscModels(TestCase):
@ -14,7 +13,9 @@ class MiscModels(TestCase):
def test_attribute_help_text(self):
desc = 'Patient Age'
a = Attribute.objects.create(name='age', description=desc, datatype=Attribute.TYPE_INT)
a = Attribute.objects.create(
name='age', description=desc, datatype=Attribute.TYPE_INT
)
self.assertEqual(a.help_text, desc)
def test_setting_to_none_deletes_value(self):
@ -32,7 +33,9 @@ class MiscModels(TestCase):
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
ynu.values.add(yes)
ynu.values.add(no)
Attribute.objects.create(name='is_patient', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
Attribute.objects.create(
name='is_patient', datatype=Attribute.TYPE_ENUM, enum_group=ynu
)
eav.register(Patient)
p = Patient.objects.create(name='Joe')
p.eav.is_patient = 'yes'

View file

@ -6,8 +6,7 @@ from django.test import TestCase
import eav
from eav.models import Attribute, EnumGroup, EnumValue, Value
from eav.registry import EavConfig
from .models import Encounter, Patient
from test_project.models import Encounter, Patient
class Queries(TestCase):
@ -32,7 +31,9 @@ class Queries(TestCase):
ynu.values.add(self.no)
ynu.values.add(self.unknown)
Attribute.objects.create(name='fever', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
Attribute.objects.create(
name='fever', datatype=Attribute.TYPE_ENUM, enum_group=ynu
)
def tearDown(self):
eav.unregister(Encounter)
@ -46,26 +47,27 @@ class Queries(TestCase):
# Name, age, fever,
# city, country, extras
# possible illness
['Anne', 3, no,
'New York', 'USA', {"chills": "yes"},
"cold"
],
['Bob', 15, no,
'Bamako', 'Mali', {},
""
],
['Cyrill', 15, yes,
'Kisumu', 'Kenya', {"chills": "yes", "headache": "no"},
"flu"
],
['Daniel', 3, no,
'Nice', 'France', {"headache": "yes"},
"cold"
],
['Eugene', 2, yes,
'France', 'Nice', {"chills": "no", "headache": "yes"},
"flu;cold"
]
['Anne', 3, no, 'New York', 'USA', {"chills": "yes"}, "cold"],
['Bob', 15, no, 'Bamako', 'Mali', {}, ""],
[
'Cyrill',
15,
yes,
'Kisumu',
'Kenya',
{"chills": "yes", "headache": "no"},
"flu",
],
['Daniel', 3, no, 'Nice', 'France', {"headache": "yes"}, "cold"],
[
'Eugene',
2,
yes,
'France',
'Nice',
{"chills": "no", "headache": "yes"},
"flu;cold",
],
]
for row in data:
@ -76,7 +78,7 @@ class Queries(TestCase):
eav__city=row[3],
eav__country=row[4],
eav__extras=row[5],
eav__illness=row[6]
eav__illness=row[6],
)
def test_get_or_create_with_eav(self):
@ -95,7 +97,9 @@ class Queries(TestCase):
self.assertEqual(Patient.objects.get(eav__age=6), p1)
Patient.objects.create(name='Fred', eav__age=6)
self.assertRaises(MultipleObjectsReturned, lambda: Patient.objects.get(eav__age=6))
self.assertRaises(
MultipleObjectsReturned, lambda: Patient.objects.get(eav__age=6)
)
def test_filtering_on_normal_and_eav_fields(self):
self.init_data()
@ -110,8 +114,8 @@ class Queries(TestCase):
self.assertEqual(p.count(), 0)
# Anne, Daniel
q1 = Q(eav__age__gte=3) # Everyone except Eugene
q2 = Q(eav__age__lt=15) # Anne, Daniel, Eugene
q1 = Q(eav__age__gte=3) # Everyone except Eugene
q2 = Q(eav__age__lt=15) # Anne, Daniel, Eugene
p = Patient.objects.filter(q2 & q1)
self.assertEqual(p.count(), 2)
@ -140,11 +144,11 @@ class Queries(TestCase):
self.assertEqual(p.count(), 5)
# Anne, Bob, Daniel
q1 = Q(eav__fever=self.no) # Anne, Bob, Daniel
q2 = Q(eav__fever=self.yes) # Cyrill, Eugene
q3 = Q(eav__country__contains='e') # Cyrill, Daniel, Eugene
q4 = q2 & q3 # Cyrill, Daniel, Eugene
q5 = (q1 | q4) & q1 # Anne, Bob, Daniel
q1 = Q(eav__fever=self.no) # Anne, Bob, Daniel
q2 = Q(eav__fever=self.yes) # Cyrill, Eugene
q3 = Q(eav__country__contains='e') # Cyrill, Daniel, Eugene
q4 = q2 & q3 # Cyrill, Daniel, Eugene
q5 = (q1 | q4) & q1 # Anne, Bob, Daniel
p = Patient.objects.filter(q5)
self.assertEqual(p.count(), 3)
@ -218,7 +222,6 @@ class Queries(TestCase):
p = Patient.objects.filter(~q1)
self.assertEqual(p.count(), 1)
def _order(self, ordering):
query = Patient.objects.all().order_by(*ordering)
return list(query.values_list('name', flat=True))
@ -226,32 +229,32 @@ class Queries(TestCase):
def assert_order_by_results(self, eav_attr='eav'):
self.assertEqual(
['Bob', 'Eugene', 'Cyrill', 'Anne', 'Daniel'],
self._order(['%s__city' % eav_attr])
self._order(['%s__city' % eav_attr]),
)
self.assertEqual(
['Eugene', 'Anne', 'Daniel', 'Bob', 'Cyrill'],
self._order(['%s__age' % eav_attr, '%s__city' % eav_attr])
self._order(['%s__age' % eav_attr, '%s__city' % eav_attr]),
)
self.assertEqual(
['Eugene', 'Cyrill', 'Anne', 'Daniel', 'Bob'],
self._order(['%s__fever' % eav_attr, '%s__age' % eav_attr])
self._order(['%s__fever' % eav_attr, '%s__age' % eav_attr]),
)
self.assertEqual(
['Eugene', 'Cyrill', 'Daniel', 'Bob', 'Anne'],
self._order(['%s__fever' % eav_attr, '-name'])
self._order(['%s__fever' % eav_attr, '-name']),
)
self.assertEqual(
['Eugene', 'Daniel', 'Cyrill', 'Bob', 'Anne'],
self._order(['-name', '%s__age' % eav_attr])
self._order(['-name', '%s__age' % eav_attr]),
)
self.assertEqual(
['Anne', 'Bob', 'Cyrill', 'Daniel', 'Eugene'],
self._order(['example__name'])
self._order(['example__name']),
)
with self.assertRaises(NotSupportedError):
@ -265,7 +268,6 @@ class Queries(TestCase):
self.assert_order_by_results()
def test_order_by_with_custom_config(self):
class CustomConfig(EavConfig):
eav_attr = "data"
generic_relation_attr = "data_values"

View file

@ -1,15 +1,13 @@
from django.test import TestCase
import sys
import eav
from eav.registry import EavConfig
from .models import Encounter, ExampleModel, Patient
if sys.version_info[0] > 2:
from .metaclass_models3 import ExampleMetaclassModel
else:
from .metaclass_models2 import ExampleMetaclassModel
from test_project.models import (
Encounter,
ExampleMetaclassModel,
ExampleModel,
Patient,
)
class RegistryTests(TestCase):
@ -81,9 +79,13 @@ class RegistryTests(TestCase):
self.assertFalse(ExampleModel.objects.__class__.__name__ == 'EntityManager')
def test_unregistering_via_metaclass(self):
self.assertTrue(ExampleMetaclassModel.objects.__class__.__name__ == 'EntityManager')
self.assertTrue(
ExampleMetaclassModel.objects.__class__.__name__ == 'EntityManager'
)
eav.unregister(ExampleMetaclassModel)
self.assertFalse(ExampleMetaclassModel.objects.__class__.__name__ == 'EntityManager')
self.assertFalse(
ExampleMetaclassModel.objects.__class__.__name__ == 'EntityManager'
)
def test_unregistering_unregistered_model_proceeds_silently(self):
eav.unregister(Patient)
@ -94,6 +96,7 @@ class RegistryTests(TestCase):
def test_doesnt_register_nonmodel(self):
with self.assertRaises(ValueError):
@eav.decorators.register_eav()
class Foo(object):
pass

View file

@ -2,8 +2,7 @@ from django.test import TestCase
import eav
from eav.registry import EavConfig
from .models import Patient, Encounter
from test_project.models import Encounter, Patient
class RegistryTests(TestCase):
@ -19,8 +18,8 @@ class RegistryTests(TestCase):
eav_attr = 'eav_field'
generic_relation_attr = 'encounter_eav_values'
generic_relation_related_name = 'encounters'
eav.register(Encounter, EncounterEav)
eav.register(Encounter, EncounterEav)
def test_registering_with_defaults(self):
eav.register(Patient)
@ -28,10 +27,8 @@ class RegistryTests(TestCase):
self.assertEqual(Patient._eav_config_cls.manager_attr, 'objects')
self.assertFalse(Patient._eav_config_cls.manager_only)
self.assertEqual(Patient._eav_config_cls.eav_attr, 'eav')
self.assertEqual(Patient._eav_config_cls.generic_relation_attr,
'eav_values')
self.assertEqual(Patient._eav_config_cls.generic_relation_related_name,
None)
self.assertEqual(Patient._eav_config_cls.generic_relation_attr, 'eav_values')
self.assertEqual(Patient._eav_config_cls.generic_relation_related_name, None)
eav.unregister(Patient)
def test_registering_overriding_defaults(self):

View file

@ -1,49 +0,0 @@
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'fake-key'
SITE_ID = 1
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.sites',
'django.contrib.admin',
'django.contrib.messages', # Required for admin app.
'django.contrib.contenttypes',
'tests',
'eav'
]
MIDDLEWARE = [
# Following 3 middleware required for admin app.
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware'
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'TEST_NAME': os.path.join(BASE_DIR, 'test_db.sqlite3'),
}
}

36
tox.ini
View file

@ -1,32 +1,16 @@
[tox]
isolated_build = true
envlist =
py36-django{31,32,tip},
py37-django{31,32,tip},
py38-django{31,32,tip}
py39-django{31,32,tip}
migrationscheck
py{36,37,38,39}-django22
py{36,37,38,39}-django{31,32}
py{38,39}-djangomain
[testenv]
pip_pre=True
allowlist_externals = pytest
deps =
django31: Django >= 3.1, <3.2
django32: Django >= 3.2
djangotip: https://github.com/django/django/archive/refs/heads/main.tar.gz
django22: django ~= 2.2.0
django31: django ~= 3.1.0
django32: django ~= 3.2.0
djangomain: https://github.com/django/django/archive/main.tar.gz
commands =
./runtests
[testenv:migrationscheck]
pip_pre=True
deps =
Django
setenv =
DJANGO_SETTINGS_MODULE=tests.test_settings
# make test fail if missing migrations
commands =
django-admin makemigrations --check --dry-run
pytest --cov {envsitepackagesdir}/eav