diff --git a/.dockerignore b/.dockerignore index f86479bc7e23f02f6a2fbc981fa2d3ed3b2599aa..0e6eb27466cb61ff5c9c5e396a137fa40bcc1b75 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,10 @@ env* +smash/*.txt +smash/*.sqlite3 **/*.pyc .vscode/* Dockerfile docker-compose.yml **/__pycache__/* smash/__pycache__/* +*.pem diff --git a/.gitignore b/.gitignore index c6232b596b3b16d1a71ce5b18d9955ead3f5b132..54229cf8866b6933833500c015ed4342c33d1ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ rpm #OS generated folders **/.DS_Store + +*.pem \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b5b1c58467f3f6778429f98d013fba236f4952d..1020eb338b7b49147818f5682726f24cb26df7df 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,10 @@ variables: POSTGRES_DB: smash POSTGRES_USER: runner POSTGRES_PASSWORD: password + MARIADB_USER: runner + MARIADB_PASSWORD: password + MARIADB_ROOT_PASSWORD: password + MARIADB_DATABASE: dbtest gemnasium-python-dependency_scanning: before_script: @@ -15,7 +19,7 @@ gemnasium-python-dependency_scanning: .test_template: &test_definition stage: test before_script: - - apt-get update && apt-get install -y --allow-unauthenticated libsasl2-dev libssl-dev locales locales-all libsasl2-dev libldap2-dev libssl-dev + - apt-get update && apt-get install -y --allow-unauthenticated libsasl2-dev libssl-dev locales locales-all libsasl2-dev libldap2-dev libssl-dev default-libmysqlclient-dev - python -V - pip install --upgrade pip --default-timeout=180 -i https://repomanager.lcsb.uni.lu/repository/pypi-proxy/simple/ --extra-index-url https://pypi.python.org/simple/ @@ -24,7 +28,7 @@ gemnasium-python-dependency_scanning: - pip install -r requirements-dev.txt --default-timeout=180 -i https://repomanager.lcsb.uni.lu/repository/pypi-proxy/simple/ --extra-index-url https://pypi.python.org/simple/ -test_migrations_created: +test_migrations_created_postgres: <<: *test_definition services: - postgres:latest @@ -34,6 +38,16 @@ test_migrations_created: - python manage.py makemigrations --check --dry-run - test 1 = $(python manage.py makemigrations --check --dry-run | grep 'No changes detected' |wc -l) +test_migrations_created_mariadb: + <<: *test_definition + services: + - mariadb:10.6-ubi + script: + - cp "local_settings_ci_mariadb.py" "smash/smash/local_settings.py" + - cd smash + - python manage.py makemigrations --check --dry-run + - test 1 = $(python manage.py makemigrations --check --dry-run | grep 'No changes detected' |wc -l) + test_postgres: <<: *test_definition services: @@ -44,6 +58,18 @@ test_postgres: - coverage run --source web manage.py test -v3 - coverage report -m --omit="*/test*,*/migrations*,*debug_utils*" +test_mariadb: + <<: *test_definition + services: + - mariadb:10.6-ubi + script: + - cp "local_settings_ci_mariadb.py" "smash/smash/local_settings.py" + - cd smash + - sleep 10 + - coverage run --source web manage.py test --noinput -v3 + - coverage report -m --omit="*/test*,*/migrations*,*debug_utils*" + + test_sqlite: <<: *test_definition script: diff --git a/.pylintrc b/.pylintrc index 003bd7d1290970436db631d79a7c47f30565b601..1072a8ecf34605bcdef4b76121744d762fcac490 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,6 +7,10 @@ disable= C0114, # missing-module-docstring C0115, # missing-class-docstring C0116, # missing-function-docstring + R0917, # too-many-positional-arguments + R1731, # consider-using-max-builtin + R1730, # consider-using-min-builtin, + E1101, # no-member # TO BE CHECKED W0143, # comparison-with-callable diff --git a/CHANGELOG b/CHANGELOG index ef620269143aa9bc6ce13c102b6c6e671cdc0cb0..21cb55fdc0dbbe162ea3934b333c328f55d6f72c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ +smasch (1.4.5-1) stable; urgency=medium + * added support for mariadb + * allow overriding the redcap test instance in settings. + + -- Carlos Vega <carlos.vega@lih.lu> Wed, 31 Jul 2024 10:11:00 +0200 + smasch (1.4.4-1) stable; urgency=high * fix bug in api/availabilities endpoint + -- Carlos Vega <carlos.vega@lih.lu> Thu, 13 Jun 2024 9:56:00 +0200 + smasch (1.4.3-1) stable; urgency=medium * make export faster * make API endpoint /api/subjects/GENERIC faster diff --git a/Dockerfile b/Dockerfile index d6e9c0a795c0b5da71e17ba7cad33e6a6546e1aa..95606d491429f50b7b26c01d9229aa1cbda5071d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN node --version \ FROM python:3.11-bookworm RUN apt-get update \ - && apt-get install -y --allow-unauthenticated libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev locales locales-all + && apt-get install -y --allow-unauthenticated libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev locales locales-all default-libmysqlclient-dev RUN mkdir /code ADD . /code/ diff --git a/local_settings_ci.py b/local_settings_ci.py index 54c32696d747b51f9a9e353644f67ecdc131fd2b..7d323b3a14620baa358b103602a34bf579cd1dcd 100644 --- a/local_settings_ci.py +++ b/local_settings_ci.py @@ -1,5 +1,5 @@ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'Paste long random string here' # Insert long random string +SECRET_KEY = "Paste long random string here" # Insert long random string # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -10,28 +10,31 @@ SERVE_STATIC = True # https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'smash', # Insert your database's name - 'USER': 'runner', # Insert your database's user - 'PASSWORD': 'password', # Insert your user's password - 'HOST': 'postgres', - 'PORT': '', - 'TEST': { - 'NAME': 'dbtest', + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "smash", # Insert your database's name + "USER": "runner", # Insert your database's user + "PASSWORD": "password", # Insert your user's password + "HOST": "postgres", + "PORT": "", + "TEST": { + "NAME": "dbtest", }, # '' === default one # Empty string is OK - # If to use sqlite # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } -STATIC_ROOT = '/tmp/static' # Warning! `/tmp` directory can be flushed in any moment; use a persistent one; e.g. ~/tmp/static -MEDIA_ROOT = '/tmp/media' # Warning! `/tmp` directory can be flushed in any moment; use a persistent one, e.g. ~/tmp/media -UPLOAD_ROOT = '~/tmp/upload' -ETL_ROOT = '/tmp/etl' +STATIC_ROOT = ( + "/tmp/static" # Warning! `/tmp` directory can be flushed in any moment; use a persistent one; e.g. ~/tmp/static +) +MEDIA_ROOT = ( + "/tmp/media" # Warning! `/tmp` directory can be flushed in any moment; use a persistent one, e.g. ~/tmp/media +) +UPLOAD_ROOT = "/tmp/upload" +ETL_ROOT = "/tmp/etl" -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" ALLOWED_HOSTS = ["127.0.0.1", "localhost"] diff --git a/local_settings_ci_mariadb.py b/local_settings_ci_mariadb.py new file mode 100644 index 0000000000000000000000000000000000000000..1c4ecc5e8df6a7860dc125c5245050e8fa3b3c47 --- /dev/null +++ b/local_settings_ci_mariadb.py @@ -0,0 +1,35 @@ +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "Paste long random string here" # Insert long random string + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": "smash", + "USER": "runner", + "PASSWORD": "password", + "HOST": "mariadb", + "PORT": "", + "TEST": { + "NAME": "dbtest", + }, + } +} + +STATIC_ROOT = ( + "/tmp/static" # Warning! `/tmp` directory can be flushed in any moment; use a persistent one; e.g. ~/tmp/static +) +MEDIA_ROOT = ( + "/tmp/media" # Warning! `/tmp` directory can be flushed in any moment; use a persistent one, e.g. ~/tmp/media +) +UPLOAD_ROOT = "/tmp/upload" +ETL_ROOT = "/tmp/etl" + +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" + +ALLOWED_HOSTS = ["127.0.0.1", "localhost"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 82d46b071491f2f523491e3a2ab196fcec63262c..a577b36cdca50a526db9e8700a582a1a5710b24b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,9 @@ -coverage==7.3.2 +coverage==7.6.1 django-debug-toolbar==4.2.0 fakeldap==0.6.6 Faker==19.13.0 mock==5.1.0 -mockito==1.4.0 parameterized==0.9.0 pycodestyle==2.11.1 -pylint==3.0.2 +pylint==3.3.1 pylint_django==2.5.5 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f651e25d83f1b34700cea736b071787eca38dd05..1ab039f81732573c411d21377e23a997a4f31025 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,68 +1,35 @@ pycurl==7.45.2 -asn1crypto==1.5.1 Babel==2.13.1 -backports.functools-lru-cache==1.6.6 -certifi==2023.7.22 -cffi==1.16.0 -chardet==5.2.0 -coverage==7.3.2 -cryptography==41.0.5 -cycler==0.12.1 +certifi==2024.8.30 Django==3.2.22 -django_auth_ldap==4.6.0 -django-cleanup==8.0.0 -django-common-helpers==0.9.2 +django_auth_ldap==4.8.0 +django-cron==0.6.0 +django-cleanup==9.0.0 django-cron==0.6.0 django-excel==0.0.10 -django-formtools==2.4.1 django-npm==1.0.0 -django-otp==1.2.4 +django-otp==1.5.4 django-phonenumber-field==6.4.0 django-stronghold==0.4.0 -django-two-factor-auth==1.15.5 -enum34==1.1.10 -funcsigs==1.0.2 -gunicorn==21.2.0 -idna==3.4 -ipaddress==1.0.23 -kiwisolver==1.4.5 -lml==0.1.0 +django-two-factor-auth==1.16.0 +gunicorn==23.0.0 luhn==0.2.0 -lxml==4.9.3 -matplotlib==3.8.1 -mockito==1.4.0 +matplotlib==3.9.2 +mockito==1.5.1 +mysqlclient==2.2.4 nexmo==2.5.2 -numpy==1.26.1 -pandas==2.1.2 -packaging==23.2 +numpy==1.26.4 +pandas==2.2.3 django-datatables-view==1.20.0 -phonenumberslite==8.13.24 -Pillow==10.1.0 +phonenumberslite==8.13.46 psycopg2==2.9.9 -pycparser==2.21 -pyexcel==0.7.0 -pyexcel-io==0.6.6 -pyexcel-webio==0.1.4 -pyexcel-xls==0.7.0 -PyJWT==2.8.0 -pyparsing==3.1.1 python-dateutil==2.8.2 -python-docx==1.0.1 -pytz==2023.3.post1 -py==1.10.0 +python-docx==1.1.2 +pytz==2024.2 pytest==7.4.3 -qrcode==7.4.2 -requests==2.31.0 +pyexcel-xls==0.7.0 six==1.16.0 -sqlparse==0.4.4 -subprocess32==3.5.4 -text-unidecode==1.3 -texttable==1.7.0 timeout-decorator==0.5.0 -urllib3==2.0.7 -whitenoise==6.6.0 -xlrd==2.0.1 -wheel==0.41.3 -xlwt==1.3.0 +whitenoise==6.7.0 parameterized==0.9.0 setuptools==68.2.2 diff --git a/smash/smash/settings.py b/smash/smash/settings.py index 53366991ed9bfea8a41a5ea7d788f979333c82d7..12e6fc6b411a20ef64351e023c6f51102f91ec4b 100644 --- a/smash/smash/settings.py +++ b/smash/smash/settings.py @@ -11,18 +11,29 @@ https://docs.djangoproject.com/en/1.10/ref/settings/ """ import functools +import logging import os import ldap from django_auth_ldap.config import LDAPSearch from django.contrib.staticfiles import storage +logger = logging.getLogger(__name__) + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ +# To run the unit tests we need a redcap instance. +# UL +REDCAP_TEST_API_TOKEN = os.environ.get('REDCAP_TEST_API_TOKEN', None) +REDCAP_TEST_URL = os.environ.get('REDCAP_TEST_URL', None) + +if REDCAP_TEST_API_TOKEN is None or REDCAP_TEST_URL is None: + logging.warning('Environment variables REDCAP_TEST_API_TOKEN or REDCAP_TEST_URL does not exist. Unit tests for redcap will fail without them having proper values.') + COPYRIGHT_NOTE = "2024 Bioinformatics Core, Luxembourg Centre for Systems Biomedicine" DEBUG = True diff --git a/smash/web/__init__.py b/smash/web/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a5faf6259d3efda9ac2dc277bdc29872cee556a5 100644 --- a/smash/web/__init__.py +++ b/smash/web/__init__.py @@ -0,0 +1,16 @@ +from functools import wraps + + +def disable_for_loaddata(signal_handler): + """ + Decorator that turns off signal handlers when loading fixture data. + https://docs.djangoproject.com/en/3.2/ref/django-admin/#what-s-a-fixture + """ + + @wraps(signal_handler) + def wrapper(*args, **kwargs): + if "raw" in kwargs and kwargs["raw"]: + return + signal_handler(*args, **kwargs) + + return wrapper diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py index d919b7bd26de4c9e5f7a867ef11d14c63dd5f188..3f9cee1e19426bb73238ea253ac4b8cbf9cdfd95 100644 --- a/smash/web/api_views/serialization_utils.py +++ b/smash/web/api_views/serialization_utils.py @@ -21,7 +21,7 @@ def bool_to_yes_no_null(val: bool): def str_to_yes_no(val: str): - if val.lower() == 'true': + if val.lower() == "true": return "YES" else: return "NO" @@ -30,7 +30,7 @@ def str_to_yes_no(val: str): def str_to_yes_no_null(val: str): if val is None: return None - if val.lower() == 'true': + if val.lower() == "true": return "YES" else: return "NO" @@ -52,7 +52,7 @@ def location_to_str(location): def serialize_date(date): if date is not None: - result = date.strftime('%Y-%m-%d') + result = date.strftime("%Y-%m-%d") else: result = "" return result @@ -60,15 +60,23 @@ def serialize_date(date): def serialize_datetime(date): if date is not None: - result = date.strftime('%Y-%m-%d %H:%M') + result = date.strftime("%Y-%m-%d %H:%M") else: result = "" return result -def add_column(result: List, name: str, field_name: str, column_list: object, param_filter: Optional[str], - columns_used_in_study: Optional[object] = None, visible_param: Optional[bool] = None, - sortable: Optional[bool] = True, add_param: Optional[bool] = True): +def add_column( + result: List, + name: str, + field_name: str, + column_list: object, + param_filter: Optional[str], + columns_used_in_study: Optional[object] = None, + visible_param: Optional[bool] = None, + sortable: Optional[bool] = True, + add_param: Optional[bool] = True, +): add = add_param if columns_used_in_study: add = getattr(columns_used_in_study, field_name) @@ -79,21 +87,30 @@ def add_column(result: List, name: str, field_name: str, column_list: object, pa visible = True else: visible = getattr(column_list, field_name) - result.append({ - "type": field_name, - "name": name, - "filter": param_filter, - "visible": visible, - "sortable": sortable - }) + result.append( + { + "type": field_name, + "name": name, + "filter": param_filter, + "visible": visible, + "sortable": sortable, + } + ) def get_filters_for_data_table_request(request_data): filters = [] column_id = 0 - while request_data.get("columns[" + str(column_id) + "][search][value]", "unknown") != "unknown": - val = request_data.get("columns[" + str(column_id) + "][search][value]", "unknown") + while ( + request_data.get("columns[" + str(column_id) + "][search][value]", "unknown") + != "unknown" + ): + val = request_data.get( + "columns[" + str(column_id) + "][search][value]", "unknown" + ) if val != "": - filters.append([request_data.get("columns[" + str(column_id) + "][data]"), val]) + filters.append( + [request_data.get("columns[" + str(column_id) + "][data]"), val] + ) column_id += 1 return filters diff --git a/smash/web/auth.py b/smash/web/auth.py index 8fc88205e85212f77d25d1b3cbda6118f59ce6a3..bf8c54958f8c7becc4f2347c411ce1a4140a4b4d 100644 --- a/smash/web/auth.py +++ b/smash/web/auth.py @@ -8,9 +8,8 @@ logger = logging.getLogger(__name__) def do_login(request): - user_login = request.POST.get('username', 'none') - user = authenticate(username=user_login, - password=request.POST.get('password', 'none')) + user_login = request.POST.get("username", "none") + user = authenticate(username=user_login, password=request.POST.get("password", "none")) if user is not None: login(request, user) return True, "ok" @@ -31,15 +30,15 @@ def do_logout(request): def user_logged_in_callback(sender, request, user, **kwargs): # pylint: disable=unused-argument # to cover more complex cases: # http://stackoverflow.com/questions/4581789/how-do-i-get-user-ip-address-in-django - ip = request.META.get('REMOTE_ADDR') - logger.info('login user: %s via ip: %s', user, ip) + ip = request.META.get("REMOTE_ADDR") + logger.info("login user: %s via ip: %s", user, ip) @receiver(user_logged_out) def user_logged_out_callback(sender, request, user, **kwargs): # pylint: disable=unused-argument - ip = request.META.get('REMOTE_ADDR') + ip = request.META.get("REMOTE_ADDR") - logger.info('logout user: %s via ip: %s', user, ip) + logger.info("logout user: %s via ip: %s", user, ip) @receiver(user_login_failed) diff --git a/smash/web/importer/etl_common.py b/smash/web/importer/etl_common.py index dc3ba581ad46ba3633222abcee36373ddcaeab3d..e1d0191489635190a56d8bdaad92b7569965520f 100644 --- a/smash/web/importer/etl_common.py +++ b/smash/web/importer/etl_common.py @@ -44,45 +44,65 @@ class EtlCommon: return new_value return new_value - def create_provenance_and_change_data(self, object_to_change: models.Model, field_name: str, new_value: object, - object_type: Type[models.Model]) -> Optional[Provenance]: + def create_provenance_and_change_data( + self, + object_to_change: models.Model, + field_name: str, + new_value: object, + object_type: Type[models.Model], + ) -> Optional[Provenance]: old_value = getattr(object_to_change, field_name) if old_value != new_value: setattr(object_to_change, field_name, new_value) - return self.create_provenance(field_name, new_value, object_to_change, object_type, old_value) + return self.create_provenance( + field_name, new_value, object_to_change, object_type, old_value + ) return None - def create_provenance(self, field_name: str, new_value: object, object_to_change: models.Model, - object_type: Type[models.Model], old_value: object) -> Provenance: + def create_provenance( + self, + field_name: str, + new_value: object, + object_to_change: models.Model, + object_type: Type[models.Model], + old_value: object, + ) -> Provenance: description = f'{field_name} changed from "{old_value}" to "{new_value}"' - p = Provenance(modified_table=object_type._meta.db_table, - modified_table_id=object_to_change.id, - modification_author=self.etl_data.import_worker, - previous_value=old_value, - new_value=new_value, - modification_description=description, - modified_field=field_name, - ) + p = Provenance( + modified_table=object_type._meta.db_table, + modified_table_id=object_to_change.id, + modification_author=self.etl_data.import_worker, + previous_value=old_value, + new_value=new_value, + modification_description=description, + modified_field=field_name, + ) p.save() return p - def create_provenance_for_new_object(self, object_type: Type[models.Model], new_object: models.Model) -> list: + def create_provenance_for_new_object( + self, object_type: Type[models.Model], new_object: models.Model + ) -> list: result = [] for field in object_type._meta.get_fields(): - if field.get_internal_type() == "CharField" or \ - field.get_internal_type() == "DateField" or \ - field.get_internal_type() == "IntegerField" or \ - field.get_internal_type() == "DateTimeField" or \ - field.get_internal_type() == "BooleanField": + if ( + field.get_internal_type() == "CharField" + or field.get_internal_type() == "DateField" + or field.get_internal_type() == "IntegerField" + or field.get_internal_type() == "DateTimeField" + or field.get_internal_type() == "BooleanField" + ): new_value = getattr(new_object, field.name) if new_value is not None and new_value != "": - p = self.create_provenance(field.name, new_value, new_object, object_type, '') + p = self.create_provenance( + field.name, new_value, new_object, object_type, "" + ) result.append(p) return result @staticmethod def remove_bom(line) -> str: if isinstance(line, str): - return line[3:] if line.encode('utf8').startswith(codecs.BOM_UTF8) else line + return line[3:] if line.encode("utf8").startswith(codecs.BOM_UTF8) else line else: return line[3:] if line.startswith(codecs.BOM_UTF8) else line diff --git a/smash/web/management/commands/holidays.py b/smash/web/management/commands/holidays.py index 8804121a0d32d5c66889642e7459a5ad31983a72..c8b1878292a34ab7719fcdd89f85ee9c4eca3f74 100644 --- a/smash/web/management/commands/holidays.py +++ b/smash/web/management/commands/holidays.py @@ -14,27 +14,47 @@ def get_ascension_day(easter_sunday): class Command(BaseCommand): - help = 'import holidays for the specified years' + help = "import holidays for the specified years" def add_arguments(self, parser): - parser.add_argument('year', nargs='+', type=int) + parser.add_argument("year", nargs="+", type=int) def handle(self, *args, **options): for location in Location.objects.exclude(name="Flying Team").all(): - for year in options['year']: - self.stdout.write(f"importing holidays for year {year} and location {location}") + for year in options["year"]: + self.stdout.write( + f"importing holidays for year {year} and location {location}" + ) # new years day self.create_holiday(year, 1, 1, "New Years Day", location) # easter monday easter_sunday = get_easter_sunday_date(year) easter_monday = get_easter_monday(easter_sunday) - self.create_holiday(year, easter_monday.month, easter_monday.day, "Easter Monday", location) + self.create_holiday( + year, + easter_monday.month, + easter_monday.day, + "Easter Monday", + location, + ) # ascension day ascension_day = get_ascension_day(easter_sunday) - self.create_holiday(year, ascension_day.month, ascension_day.day, "Ascension Day", location) + self.create_holiday( + year, + ascension_day.month, + ascension_day.day, + "Ascension Day", + location, + ) # pentecost monday pentecost_day = get_pentecost_day(easter_sunday) - self.create_holiday(year, pentecost_day.month, pentecost_day.day, "Pentecost Monday", location) + self.create_holiday( + year, + pentecost_day.month, + pentecost_day.day, + "Pentecost Monday", + location, + ) # labour day self.create_holiday(year, 5, 1, "Labour Day", location) # national day @@ -49,22 +69,33 @@ class Command(BaseCommand): def create_holiday(self, year, month, day, comment, location): # check if already exists: - count = Appointment.objects.filter(datetime_when__year=year, datetime_when__month=month, datetime_when__day=day, - location=location, comment=comment).count() + count = Appointment.objects.filter( + datetime_when__year=year, + datetime_when__month=month, + datetime_when__day=day, + location=location, + comment=comment, + ).count() if count != 0: self.stdout.write( - f'an holiday with the same description already exists for the same day: {comment}') + f"an holiday with the same description already exists for the same day: {comment}" + ) return holiday = Appointment() - holiday.datetime_when = datetime.datetime(year=year, month=month, day=day, hour=9) + holiday.datetime_when = datetime.datetime( + year=year, month=month, day=day, hour=9 + ) holiday.location = location holiday.length = 60 * 8 holiday.comment = comment holiday.visit_id = None holiday.save() - appointment_type_other, _ = AppointmentType.objects.get_or_create(code='OTHER', - defaults={'default_duration': 60}) - link = AppointmentTypeLink(appointment=holiday, appointment_type=appointment_type_other) + appointment_type_other, _ = AppointmentType.objects.get_or_create( + code="OTHER", defaults={"default_duration": 60} + ) + link = AppointmentTypeLink( + appointment=holiday, appointment_type=appointment_type_other + ) link.save() diff --git a/smash/web/migrations/0001_initial.py b/smash/web/migrations/0001_initial.py index 545ed4a1da48b3249d8e711983d73b08b80c46ab..688877651ca5d2fe8a1190cd40f39c0522e83418 100644 --- a/smash/web/migrations/0001_initial.py +++ b/smash/web/migrations/0001_initial.py @@ -29,7 +29,7 @@ class Migration(migrations.Migration): fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("code", models.CharField(max_length=20, verbose_name="Appointment code")), - ("description", models.CharField(max_length=2000, verbose_name="Appointment description")), + ("description", models.TextField(max_length=2000, verbose_name="Appointment description")), ("default_duration", models.IntegerField(verbose_name="Default duration (in minutes)")), ("rest_time", models.IntegerField(verbose_name="Suggested rest time")), ], @@ -125,7 +125,7 @@ class Migration(migrations.Migration): ), ), ("main_pseudonym", models.CharField(max_length=45, verbose_name="Pseudonym")), - ("comments", models.CharField(max_length=2000, verbose_name="Comments")), + ("comments", models.TextField(max_length=2000, verbose_name="Comments")), ("date_added", models.DateField(auto_now=True, verbose_name="Added on")), ("referral", models.CharField(max_length=128, null=True, verbose_name="Referred by")), ("diagnosis", models.CharField(max_length=128, null=True, verbose_name="Diagnosis")), diff --git a/smash/web/migrations/0001_version-1-0-0.py b/smash/web/migrations/0001_version-1-0-0.py index 251793ee9a7b8c4a8748cdb02693b5a8e922e99a..beb180f00eeb90635d1df801f6c162712f737e76 100644 --- a/smash/web/migrations/0001_version-1-0-0.py +++ b/smash/web/migrations/0001_version-1-0-0.py @@ -6,6 +6,7 @@ import django.core.validators import django.db.models.deletion from django.conf import settings from django.db import migrations, models +from django.db import connection from web.models.appointment_list import APPOINTMENT_LIST_UNFINISHED from web.models.constants import ( @@ -1596,7 +1597,7 @@ class Migration(migrations.Migration): fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("code", models.CharField(max_length=20, verbose_name="Code")), - ("description", models.CharField(blank=True, max_length=1024, verbose_name="Description")), + ("description", models.TextField(blank=True, max_length=1024, verbose_name="Description")), ("study", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="web.study")), ], ), @@ -1673,7 +1674,7 @@ class Migration(migrations.Migration): fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("code", models.CharField(max_length=20, verbose_name="Appointment code")), - ("description", models.CharField(max_length=2000, verbose_name="Appointment description")), + ("description", models.TextField(max_length=2000, verbose_name="Appointment description")), ("default_duration", models.IntegerField(verbose_name="Default duration (in minutes)")), ("rest_time", models.IntegerField(default=0, verbose_name="Suggested rest time")), ( @@ -1694,11 +1695,11 @@ class Migration(migrations.Migration): verbose_name="Type of worker required for appointment", ), ), - ("calendar_color", models.CharField(default="#cfc600", max_length=2000, verbose_name="Calendar color")), + ("calendar_color", models.TextField(default="#cfc600", max_length=2000, verbose_name="Calendar color")), ("calendar_color_priority", models.IntegerField(default=1, verbose_name="Calendar color priority")), ( "calendar_font_color", - models.CharField(default="#00000", max_length=2000, verbose_name="Calendar font color"), + models.TextField(default="#00000", max_length=2000, verbose_name="Calendar font color"), ), ("can_be_parallelized", models.BooleanField(default=False, verbose_name="Can be parallelized")), ], @@ -2157,7 +2158,7 @@ class Migration(migrations.Migration): max_length=20, ), ), - ("possible_values", models.CharField(blank=True, default="", max_length=1024, null=True)), + ("possible_values", models.TextField(blank=True, default="", max_length=1024, null=True)), ("default_value", models.CharField(blank=True, max_length=256, null=True)), ("readonly", models.BooleanField(default=False)), ("required", models.BooleanField(default=False)), @@ -2177,7 +2178,7 @@ class Migration(migrations.Migration): name="CustomStudySubjectValue", fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("value", models.CharField(blank=True, max_length=2048, null=True)), + ("value", models.TextField(blank=True, max_length=2048, null=True)), ( "study_subject", models.ForeignKey( @@ -2371,15 +2372,15 @@ class Migration(migrations.Migration): name="Provenance", fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("modified_table", models.CharField(max_length=1024, null=True, verbose_name="Modified table")), + ("modified_table", models.CharField(max_length=128, null=True, verbose_name="Modified table")), ("modified_table_id", models.IntegerField(default=0, null=True, verbose_name="Modified table row")), ("modification_date", models.DateTimeField(auto_now_add=True, verbose_name="Modified on")), ( "previous_value", - models.CharField(blank=True, max_length=2048, null=True, verbose_name="Previous Value"), + models.TextField(blank=True, max_length=2048, null=True, verbose_name="Previous Value"), ), - ("new_value", models.CharField(blank=True, max_length=2048, null=True, verbose_name="New Value")), - ("modification_description", models.CharField(max_length=20480, verbose_name="Description")), + ("new_value", models.TextField(blank=True, max_length=2048, null=True, verbose_name="New Value")), + ("modification_description", models.TextField(max_length=20480, verbose_name="Description")), ( "modification_author", models.ForeignKey( @@ -2389,10 +2390,10 @@ class Migration(migrations.Migration): verbose_name="Worker who modified the row", ), ), - ("modified_field", models.CharField(blank="", max_length=1024, verbose_name="Modified field")), + ("modified_field", models.CharField(blank="", max_length=128, verbose_name="Modified field")), ( "request_path", - models.CharField(blank=True, max_length=20480, null=True, verbose_name="Request Path"), + models.TextField(blank=True, max_length=20480, null=True, verbose_name="Request Path"), ), ("request_ip_addr", models.GenericIPAddressField(null=True, verbose_name="Request IP Address")), ], @@ -2566,7 +2567,7 @@ class Migration(migrations.Migration): ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("type", models.CharField(editable=False, max_length=50, verbose_name="Type")), ("name", models.CharField(editable=False, max_length=255, verbose_name="Name")), - ("value", models.CharField(max_length=1024, verbose_name="Value")), + ("value", models.TextField(max_length=1024, verbose_name="Value")), ( "value_type", models.CharField( @@ -2703,7 +2704,7 @@ class Migration(migrations.Migration): ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("table_name", models.CharField(blank=True, default="", max_length=128)), ("column_name", models.CharField(blank=True, default="", max_length=128)), - ("csv_column_name", models.CharField(blank=True, default="", max_length=1024)), + ("csv_column_name", models.TextField(blank=True, default="", max_length=1024)), ("enabled", models.BooleanField(default=True)), ( "etl_data", @@ -2992,3 +2993,6 @@ class Migration(migrations.Migration): migrations.RunPython(configuration_items__0171), migrations.RunPython(configuration_items__0176), ] + + if connection.vendor == "mysql": + operations = [migrations.RunSQL("SET SESSION sql_mode='ANSI_QUOTES';")] + operations diff --git a/smash/web/migrations/0014_auto_20170220_0812.py b/smash/web/migrations/0014_auto_20170220_0812.py index 190888eddad882ffef72c0ac8f43c4b86d55610e..8073a8f299e9f0d9aca48dd34baa41e2a7110868 100644 --- a/smash/web/migrations/0014_auto_20170220_0812.py +++ b/smash/web/migrations/0014_auto_20170220_0812.py @@ -36,7 +36,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="subject", name="comments", - field=models.CharField(blank=True, max_length=2000, verbose_name="Comments"), + field=models.TextField(blank=True, max_length=2000, verbose_name="Comments"), ), migrations.AlterField( model_name="subject", diff --git a/smash/web/migrations/0017_auto_20170301_1600.py b/smash/web/migrations/0017_auto_20170301_1600.py index 77814d62961c638605d9d8ed25e06f524e696a1d..b9018c9abb351be55fb6667fbd024a9d3debb481 100644 --- a/smash/web/migrations/0017_auto_20170301_1600.py +++ b/smash/web/migrations/0017_auto_20170301_1600.py @@ -8,43 +8,43 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('web', '0016_auto_20170228_1652'), + ("web", "0016_auto_20170228_1652"), ] operations = [ migrations.AddField( - model_name='appointment', - name='comment', - field=models.CharField(blank=True, max_length=1024, null=True, verbose_name='Comment'), + model_name="appointment", + name="comment", + field=models.TextField(blank=True, max_length=1024, null=True, verbose_name="Comment"), ), migrations.AddField( - model_name='appointmenttype', - name='calendar_color', - field=models.CharField(default='#cfc600', max_length=2000, verbose_name='Calendar color'), + model_name="appointmenttype", + name="calendar_color", + field=models.TextField(default="#cfc600", max_length=2000, verbose_name="Calendar color"), ), migrations.AddField( - model_name='appointmenttype', - name='calendar_color_priority', - field=models.IntegerField(default=1, verbose_name='Calendar color priority'), + model_name="appointmenttype", + name="calendar_color_priority", + field=models.IntegerField(default=1, verbose_name="Calendar color priority"), ), migrations.AddField( - model_name='appointmenttype', - name='calendar_font_color', - field=models.CharField(default='#00000', max_length=2000, verbose_name='Calendar color'), + model_name="appointmenttype", + name="calendar_font_color", + field=models.TextField(default="#00000", max_length=2000, verbose_name="Calendar color"), ), migrations.AlterField( - model_name='appointment', - name='is_finished', - field=models.BooleanField(default=False, editable=False, verbose_name='Has the appointment ended?'), + model_name="appointment", + name="is_finished", + field=models.BooleanField(default=False, editable=False, verbose_name="Has the appointment ended?"), ), migrations.AlterField( - model_name='subject', - name='dead', - field=models.BooleanField(default=False, editable=False, verbose_name='Dead'), + model_name="subject", + name="dead", + field=models.BooleanField(default=False, editable=False, verbose_name="Dead"), ), migrations.AlterField( - model_name='subject', - name='resigned', - field=models.BooleanField(default=False, editable=False, verbose_name='Resigned'), + model_name="subject", + name="resigned", + field=models.BooleanField(default=False, editable=False, verbose_name="Resigned"), ), ] diff --git a/smash/web/migrations/0090_vouchertype_vouchertypeprice.py b/smash/web/migrations/0090_vouchertype_vouchertypeprice.py index 57c52c5a706f8b5ac583e4367c799b8ed24b629b..76e6d8bdc810049cb6480af11453158492a10ab2 100644 --- a/smash/web/migrations/0090_vouchertype_vouchertypeprice.py +++ b/smash/web/migrations/0090_vouchertype_vouchertypeprice.py @@ -9,27 +9,27 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('web', '0089_unfinshed_appointment_list'), + ("web", "0089_unfinshed_appointment_list"), ] operations = [ migrations.CreateModel( - name='VoucherType', + name="VoucherType", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, verbose_name='Code')), - ('description', models.CharField(blank=True, max_length=1024, verbose_name='Description')), - ('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Study')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("code", models.CharField(max_length=20, verbose_name="Code")), + ("description", models.TextField(blank=True, max_length=1024, verbose_name="Description")), + ("study", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="web.Study")), ], ), migrations.CreateModel( - name='VoucherTypePrice', + name="VoucherTypePrice", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('price', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Price')), - ('start_date', models.DateField(verbose_name='Start date')), - ('end_date', models.DateField(verbose_name='End date')), - ('voucher_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.VoucherType')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("price", models.DecimalField(decimal_places=2, max_digits=6, verbose_name="Price")), + ("start_date", models.DateField(verbose_name="Start date")), + ("end_date", models.DateField(verbose_name="End date")), + ("voucher_type", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="web.VoucherType")), ], ), ] diff --git a/smash/web/migrations/0097_auto_20171211_1616.py b/smash/web/migrations/0097_auto_20171211_1616.py index 9ae00ede5a5b0508d61b648430ef65cf7478827c..715aee1fd5cb4369ab85da3b221819fc139da265 100644 --- a/smash/web/migrations/0097_auto_20171211_1616.py +++ b/smash/web/migrations/0097_auto_20171211_1616.py @@ -95,7 +95,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="studysubject", name="screening", - field=models.CharField(blank=True, max_length=1024, null=True, verbose_name="Screening"), + field=models.TextField(blank=True, max_length=1024, null=True, verbose_name="Screening"), ), migrations.AddField( model_name="studysubject", @@ -112,7 +112,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="studysubject", name="diagnosis", - field=models.CharField(blank=True, max_length=1024, null=True, verbose_name="Diagnosis"), + field=models.TextField(blank=True, max_length=1024, null=True, verbose_name="Diagnosis"), ), migrations.AlterField( model_name="voucher", diff --git a/smash/web/migrations/0141_auto_20200319_1040.py b/smash/web/migrations/0141_auto_20200319_1040.py index 1ec81c4fa0c40d903297532068676e1b987c3ec2..12f552a6f96c5ca64ed17f2bad92bf74071a6387 100644 --- a/smash/web/migrations/0141_auto_20200319_1040.py +++ b/smash/web/migrations/0141_auto_20200319_1040.py @@ -8,13 +8,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('web', '0140_auto_20190528_0953'), + ("web", "0140_auto_20190528_0953"), ] operations = [ migrations.AlterField( - model_name='appointmenttype', - name='calendar_font_color', - field=models.CharField(default='#00000', max_length=2000, verbose_name='Calendar font color'), + model_name="appointmenttype", + name="calendar_font_color", + field=models.TextField(default="#00000", max_length=2000, verbose_name="Calendar font color"), ), ] diff --git a/smash/web/migrations/0142_provenance.py b/smash/web/migrations/0142_provenance.py index eb818bfcbd81506d18322e88cf0e7c93c95b9b51..1bbd6a42e320b06401abd8227e13ac820e8f5754 100644 --- a/smash/web/migrations/0142_provenance.py +++ b/smash/web/migrations/0142_provenance.py @@ -16,15 +16,15 @@ class Migration(migrations.Migration): name="Provenance", fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("modified_table", models.CharField(max_length=1024, verbose_name="Modified table")), - ("modified_table_id", models.CharField(max_length=1024, verbose_name="Modified table row")), + ("modified_table", models.CharField(max_length=128, verbose_name="Modified table")), + ("modified_table_id", models.IntegerField(default=0, null=True, verbose_name="Modified table row")), ("modification_date", models.DateTimeField(verbose_name="Modified on")), ( "previous_value", - models.CharField(blank=True, max_length=2048, null=True, verbose_name="Previous Value"), + models.TextField(blank=True, max_length=2048, null=True, verbose_name="Previous Value"), ), - ("new_value", models.CharField(blank=True, max_length=2048, null=True, verbose_name="New Value")), - ("modification_description", models.CharField(max_length=2048, verbose_name="Description")), + ("new_value", models.TextField(blank=True, max_length=2048, null=True, verbose_name="New Value")), + ("modification_description", models.TextField(max_length=2048, verbose_name="Description")), ( "modification_author", models.ForeignKey( diff --git a/smash/web/migrations/0144_auto_20200319_1221.py b/smash/web/migrations/0144_auto_20200319_1221.py index e8b8c4bfdeb810ed9d57b6d3172d1098ab9cbc78..d7c3e07ce9e67ba8992d09dc6a2768887f9eada2 100644 --- a/smash/web/migrations/0144_auto_20200319_1221.py +++ b/smash/web/migrations/0144_auto_20200319_1221.py @@ -8,19 +8,19 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('web', '0143_auto_20200319_1121'), + ("web", "0143_auto_20200319_1121"), ] operations = [ migrations.AddField( - model_name='provenance', - name='modified_field', - field=models.CharField(default='', max_length=1024, verbose_name='Modified field'), + model_name="provenance", + name="modified_field", + field=models.TextField(default="", max_length=1024, verbose_name="Modified field"), preserve_default=False, ), migrations.AlterField( - model_name='provenance', - name='modification_description', - field=models.CharField(max_length=20480, verbose_name='Description'), + model_name="provenance", + name="modification_description", + field=models.TextField(max_length=20480, verbose_name="Description"), ), ] diff --git a/smash/web/migrations/0145_auto_20200319_1404.py b/smash/web/migrations/0145_auto_20200319_1404.py index 21dfc1004facf6f15a6d8fdd4454cf275112825d..8353bfa7c3be7ad72a67e1278642df39848cdb7f 100644 --- a/smash/web/migrations/0145_auto_20200319_1404.py +++ b/smash/web/migrations/0145_auto_20200319_1404.py @@ -8,13 +8,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('web', '0144_auto_20200319_1221'), + ("web", "0144_auto_20200319_1221"), ] operations = [ migrations.AlterField( - model_name='provenance', - name='modified_field', - field=models.CharField(blank='', max_length=1024, verbose_name='Modified field'), + model_name="provenance", + name="modified_field", + field=models.TextField(blank="", max_length=1024, verbose_name="Modified field"), ), ] diff --git a/smash/web/migrations/0148_auto_20200319_1301.py b/smash/web/migrations/0148_auto_20200319_1301.py index 0d9ecd0e3b6720ee41082ba333d972cfbf0e312d..3c4b96684ddf0f4e1e994a000be2541cfa22180b 100644 --- a/smash/web/migrations/0148_auto_20200319_1301.py +++ b/smash/web/migrations/0148_auto_20200319_1301.py @@ -7,17 +7,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('web', '0147_auto_20200320_0931'), + ("web", "0147_auto_20200320_0931"), ] operations = [ migrations.AlterModelOptions( - name='appointmenttypelink', - options={'permissions': [('view_daily_planning', 'Can see daily planning')]}, + name="appointmenttypelink", + options={"permissions": [("view_daily_planning", "Can see daily planning")]}, ), migrations.AlterField( - model_name='appointmenttype', - name='calendar_font_color', - field=models.CharField(default='#00000', max_length=2000, verbose_name='Calendar font color'), + model_name="appointmenttype", + name="calendar_font_color", + field=models.TextField(default="#00000", max_length=2000, verbose_name="Calendar font color"), ), ] diff --git a/smash/web/migrations/0162_auto_20200416_1212.py b/smash/web/migrations/0162_auto_20200416_1212.py index 231b4fa2e2530b832ac05af08ccb2e875e9e86dd..4114d664b5bb98eb85d84ff322d45702f949a2ba 100644 --- a/smash/web/migrations/0162_auto_20200416_1212.py +++ b/smash/web/migrations/0162_auto_20200416_1212.py @@ -8,18 +8,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('web', '0161_auto_20200416_0736'), + ("web", "0161_auto_20200416_0736"), ] operations = [ migrations.AlterField( - model_name='configurationitem', - name='value', - field=models.CharField(max_length=1024, verbose_name='Value'), + model_name="configurationitem", + name="value", + field=models.TextField(max_length=1024, verbose_name="Value"), ), migrations.AlterField( - model_name='subject', - name='next_of_keen_name', - field=models.CharField(blank=True, max_length=255, verbose_name='Next of keen'), + model_name="subject", + name="next_of_keen_name", + field=models.CharField(blank=True, max_length=255, verbose_name="Next of keen"), ), ] diff --git a/smash/web/migrations/0168_rename_radcap_field.py b/smash/web/migrations/0168_rename_radcap_field.py index 37be8307a63d50b0248938df0d12d3df8d6aa438..bbec2ef46c3427638dab73a497332cb4b840ce4d 100644 --- a/smash/web/migrations/0168_rename_radcap_field.py +++ b/smash/web/migrations/0168_rename_radcap_field.py @@ -28,14 +28,14 @@ class Migration(migrations.Migration): operations = [ migrations.RunSQL( - "update web_configurationitem set type = '" + "update web_configurationitem, set type = '" + RED_CAP_KIT_ID_FIELD_TYPE + "' where type = '" + RED_CAP_SAMPLE_DATE_FIELD_TYPE + "';" ), migrations.RunSQL( - "update web_configurationitem set name = 'Redcap field for sample kit id in the visit' where type = '" + "update web_configurationitem, set name = 'Redcap field for sample kit id in the visit' where type = '" + RED_CAP_KIT_ID_FIELD_TYPE + "';" ), diff --git a/smash/web/migrations/0173_auto_20201105_1142.py b/smash/web/migrations/0173_auto_20201105_1142.py index 74e66773d22e0c801421ca5c425c7708fd6d2aa0..b20401e89bdb9439bd4ff16d8653917469ba23fd 100644 --- a/smash/web/migrations/0173_auto_20201105_1142.py +++ b/smash/web/migrations/0173_auto_20201105_1142.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="provenance", name="modified_table", - field=models.CharField(max_length=1024, null=True, verbose_name="Modified table"), + field=models.TextField(max_length=1024, null=True, verbose_name="Modified table"), ), migrations.AlterField( model_name="provenance", diff --git a/smash/web/migrations/0174_auto_20201105_1157.py b/smash/web/migrations/0174_auto_20201105_1157.py index fdb995fec343f8d80b5af2fd26c832a6488e208e..a14890dc4b1f8fb6af8cca3b5ec6673acc6a69f1 100644 --- a/smash/web/migrations/0174_auto_20201105_1157.py +++ b/smash/web/migrations/0174_auto_20201105_1157.py @@ -12,6 +12,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name="provenance", name="request_path", - field=models.CharField(blank=True, max_length=20480, null=True, verbose_name="Request Path"), + field=models.TextField(blank=True, max_length=20480, null=True, verbose_name="Request Path"), ) ] diff --git a/smash/web/migrations/0177_customstudysubjectfield_customstudysubjectvalue.py b/smash/web/migrations/0177_customstudysubjectfield_customstudysubjectvalue.py index 053afbc6db84ede5d570f4094df5338999a79ce8..9284b02930abc3dca02e6836fde5cd931a32b65a 100644 --- a/smash/web/migrations/0177_customstudysubjectfield_customstudysubjectvalue.py +++ b/smash/web/migrations/0177_customstudysubjectfield_customstudysubjectvalue.py @@ -29,7 +29,7 @@ class Migration(migrations.Migration): max_length=20, ), ), - ("possible_values", models.CharField(blank=True, default="", max_length=1024, null=True)), + ("possible_values", models.TextField(blank=True, default="", max_length=1024, null=True)), ("default_value", models.CharField(blank=True, max_length=20, null=True)), ("readonly", models.BooleanField(default=False)), ("required", models.BooleanField(default=False)), @@ -49,7 +49,7 @@ class Migration(migrations.Migration): name="CustomStudySubjectValue", fields=[ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("value", models.CharField(blank=True, max_length=2048, null=True)), + ("value", models.TextField(blank=True, max_length=2048, null=True)), ( "study_subject", models.ForeignKey( diff --git a/smash/web/migrations/0178_auto_20201116_1250.py b/smash/web/migrations/0178_auto_20201116_1250.py index 91febe10fabdb42e82726f4c02d8ab0dad3e08af..da37d7a941837d1d2bc8bd6d479f328bf1a1425c 100644 --- a/smash/web/migrations/0178_auto_20201116_1250.py +++ b/smash/web/migrations/0178_auto_20201116_1250.py @@ -118,7 +118,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="appointmenttype", name="calendar_font_color", - field=models.CharField(default="#00000", max_length=2000, verbose_name="Calendar font color"), + field=models.TextField(default="#00000", max_length=2000, verbose_name="Calendar font color"), ), migrations.AlterField( model_name="availability", @@ -166,7 +166,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="configurationitem", name="value", - field=models.CharField(max_length=1024, verbose_name="Value"), + field=models.TextField(max_length=1024, verbose_name="Value"), ), migrations.AlterField( model_name="contactattempt", @@ -774,22 +774,22 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="provenance", name="modification_description", - field=models.CharField(max_length=20480, verbose_name="Description"), + field=models.TextField(max_length=20480, verbose_name="Description"), ), migrations.AlterField( model_name="provenance", name="modified_field", - field=models.CharField(blank="", max_length=1024, verbose_name="Modified field"), + field=models.TextField(blank="", max_length=1024, verbose_name="Modified field"), ), migrations.AlterField( model_name="provenance", name="new_value", - field=models.CharField(blank=True, max_length=2048, null=True, verbose_name="New Value"), + field=models.TextField(blank=True, max_length=2048, null=True, verbose_name="New Value"), ), migrations.AlterField( model_name="provenance", name="previous_value", - field=models.CharField(blank=True, max_length=2048, null=True, verbose_name="Previous Value"), + field=models.TextField(blank=True, max_length=2048, null=True, verbose_name="Previous Value"), ), migrations.AlterField( model_name="study", @@ -1248,7 +1248,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="studysubject", name="diagnosis", - field=models.CharField(blank=True, max_length=1024, null=True, verbose_name="Diagnosis"), + field=models.TextField(blank=True, max_length=1024, null=True, verbose_name="Diagnosis"), ), migrations.AlterField( model_name="studysubject", @@ -1341,7 +1341,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="studysubject", name="screening", - field=models.CharField(blank=True, max_length=1024, null=True, verbose_name="Screening"), + field=models.TextField(blank=True, max_length=1024, null=True, verbose_name="Screening"), ), migrations.AlterField( model_name="studysubject", @@ -1889,7 +1889,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="vouchertype", name="description", - field=models.CharField(blank=True, max_length=1024, verbose_name="Description"), + field=models.TextField(blank=True, max_length=1024, verbose_name="Description"), ), migrations.AlterField( model_name="vouchertypeprice", diff --git a/smash/web/migrations/0184_visitimportdata.py b/smash/web/migrations/0184_visitimportdata.py index 366d3b1b119aca08bc3aa38559257340bdd4aa3f..1b90921513dba56a63999bae0a261675c9855863 100644 --- a/smash/web/migrations/0184_visitimportdata.py +++ b/smash/web/migrations/0184_visitimportdata.py @@ -140,7 +140,7 @@ class Migration(migrations.Migration): ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("table_name", models.CharField(blank=True, default="", max_length=128)), ("column_name", models.CharField(blank=True, default="", max_length=128)), - ("csv_column_name", models.CharField(blank=True, default="", max_length=1024)), + ("csv_column_name", models.TextField(blank=True, default="", max_length=1024)), ("enabled", models.BooleanField(default=True)), ( "etl_data", diff --git a/smash/web/migrations/0215_alter_studysubject_referral_letter.py b/smash/web/migrations/0215_alter_studysubject_referral_letter.py new file mode 100644 index 0000000000000000000000000000000000000000..532b2c10e05e5e432e11df7c2d4e5479eb2598f7 --- /dev/null +++ b/smash/web/migrations/0215_alter_studysubject_referral_letter.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.22 on 2024-07-30 06:09 + +import django.core.files.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("web", "0214_merge_20240404_0917"), + ] + + operations = [ + migrations.AlterField( + model_name="studysubject", + name="referral_letter", + field=models.FileField( + blank=True, + null=True, + storage=django.core.files.storage.FileSystemStorage(location="/tmp/upload"), + upload_to="referral_letters", + verbose_name="Referral letter", + ), + ), + ] diff --git a/smash/web/migrations/0216_auto_20240730_0621.py b/smash/web/migrations/0216_auto_20240730_0621.py new file mode 100644 index 0000000000000000000000000000000000000000..cc86d63259d3cb4a8752f9b6055e33a3abfe7d9b --- /dev/null +++ b/smash/web/migrations/0216_auto_20240730_0621.py @@ -0,0 +1,85 @@ +# Generated by Django 3.2.22 on 2024-07-30 06:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("web", "0215_alter_studysubject_referral_letter"), + ] + + operations = [ + migrations.AlterField( + model_name="appointmenttype", + name="calendar_color", + field=models.TextField(default="#cfc600", max_length=2000, verbose_name="Calendar color"), + ), + migrations.AlterField( + model_name="appointmenttype", + name="calendar_font_color", + field=models.TextField(default="#00000", max_length=2000, verbose_name="Calendar font color"), + ), + migrations.AlterField( + model_name="appointmenttype", + name="description", + field=models.TextField(max_length=2000, verbose_name="Appointment description"), + ), + migrations.AlterField( + model_name="configurationitem", + name="value", + field=models.TextField(max_length=1024, verbose_name="Value"), + ), + migrations.AlterField( + model_name="customstudysubjectfield", + name="possible_values", + field=models.TextField(blank=True, default="", max_length=1024, null=True), + ), + migrations.AlterField( + model_name="customstudysubjectvalue", + name="value", + field=models.TextField(blank=True, max_length=2048, null=True), + ), + migrations.AlterField( + model_name="etlcolumnmapping", + name="csv_column_name", + field=models.TextField(blank=True, default="", max_length=1024), + ), + migrations.AlterField( + model_name="etldata", + name="run_at_times", + field=models.TextField( + blank=True, default="", max_length=1024, verbose_name="At what time automatic import should run" + ), + ), + migrations.AlterField( + model_name="provenance", + name="modification_description", + field=models.TextField(max_length=20480, verbose_name="Description"), + ), + migrations.AlterField( + model_name="provenance", + name="modified_field", + field=models.CharField(blank="", max_length=128, verbose_name="Modified field"), + ), + migrations.AlterField( + model_name="provenance", + name="modified_table", + field=models.CharField(max_length=128, null=True, verbose_name="Modified table"), + ), + migrations.AlterField( + model_name="provenance", + name="new_value", + field=models.TextField(blank=True, max_length=2048, null=True, verbose_name="New Value"), + ), + migrations.AlterField( + model_name="provenance", + name="previous_value", + field=models.TextField(blank=True, max_length=2048, null=True, verbose_name="Previous Value"), + ), + migrations.AlterField( + model_name="provenance", + name="request_path", + field=models.TextField(blank=True, max_length=20480, null=True, verbose_name="Request Path"), + ), + ] diff --git a/smash/web/migrations/0217_alter_vouchertype_description.py b/smash/web/migrations/0217_alter_vouchertype_description.py new file mode 100644 index 0000000000000000000000000000000000000000..eadca30d52af9a7fda1cd48986639fd5e6103e73 --- /dev/null +++ b/smash/web/migrations/0217_alter_vouchertype_description.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.22 on 2024-07-30 07:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0216_auto_20240730_0621'), + ] + + operations = [ + migrations.AlterField( + model_name='vouchertype', + name='description', + field=models.CharField(blank=True, max_length=1024, verbose_name='Description'), + ), + ] diff --git a/smash/web/models/appointment_type.py b/smash/web/models/appointment_type.py index 3514afa477bd767d807fe109b4ff921dc4b33ce9..6fa48352951a59ee10a88fc18317a823cbf88e27 100644 --- a/smash/web/models/appointment_type.py +++ b/smash/web/models/appointment_type.py @@ -6,52 +6,26 @@ from .constants import APPOINTMENT_TYPE_DEFAULT_COLOR, APPOINTMENT_TYPE_DEFAULT_ class AppointmentType(models.Model): class Meta: - app_label = 'web' - ordering = ['description'] + app_label = "web" + ordering = ["description"] - required_equipment = models.ManyToManyField("web.Item", - verbose_name='Required equipment', - blank=True - ) - code = models.CharField(max_length=20, - verbose_name='Appointment code' - ) - description = models.CharField(max_length=2000, - verbose_name='Appointment description' - ) - default_duration = models.IntegerField( - verbose_name='Default duration (in minutes)' + required_equipment = models.ManyToManyField("web.Item", verbose_name="Required equipment", blank=True) + code = models.CharField(max_length=20, verbose_name="Appointment code") + description = models.TextField(max_length=2000, verbose_name="Appointment description") + default_duration = models.IntegerField(verbose_name="Default duration (in minutes)") + calendar_color_priority = models.IntegerField(verbose_name="Calendar color priority", default=1) + calendar_color = models.TextField( + max_length=2000, verbose_name="Calendar color", default=APPOINTMENT_TYPE_DEFAULT_COLOR ) - calendar_color_priority = models.IntegerField( - verbose_name='Calendar color priority', - default=1 + calendar_font_color = models.TextField( + max_length=2000, verbose_name="Calendar font color", default=APPOINTMENT_TYPE_DEFAULT_FONT_COLOR ) - calendar_color = models.CharField(max_length=2000, - verbose_name='Calendar color', - default=APPOINTMENT_TYPE_DEFAULT_COLOR - ) - calendar_font_color = models.CharField(max_length=2000, - verbose_name='Calendar font color', - default=APPOINTMENT_TYPE_DEFAULT_FONT_COLOR - ) - rest_time = models.IntegerField( - verbose_name='Suggested rest time', - default=0 + rest_time = models.IntegerField(verbose_name="Suggested rest time", default=0) + can_be_parallelized = models.BooleanField(verbose_name="Can be parallelized", default=False) + REQ_ROLE_CHOICES = (("DOCTOR", "Doctor"), ("NURSE", "Nurse"), ("PSYCHOLOGIST", "Psychologist"), ("ANY", "Any")) + required_worker = models.CharField( + max_length=20, choices=REQ_ROLE_CHOICES, verbose_name="Type of worker required for appointment", default="ANY" ) - can_be_parallelized = models.BooleanField( - verbose_name='Can be parallelized', - default=False - ) - REQ_ROLE_CHOICES = ( - ('DOCTOR', 'Doctor'), - ('NURSE', 'Nurse'), - ('PSYCHOLOGIST', 'Psychologist'), - ('ANY', 'Any') - ) - required_worker = models.CharField(max_length=20, choices=REQ_ROLE_CHOICES, - verbose_name='Type of worker required for appointment', - default='ANY' - ) def __str__(self): return self.description diff --git a/smash/web/models/configuration_item.py b/smash/web/models/configuration_item.py index a38cba7ce76980de662e831c9f0e54a522c95a41..f3fded7fb661fd251df2c79011246867f5e2381f 100644 --- a/smash/web/models/configuration_item.py +++ b/smash/web/models/configuration_item.py @@ -6,11 +6,20 @@ from django.core.validators import validate_email from django.db import models from web.utils import strtobool -from web.models.constants import CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE, \ - NO_SHOW_APPOINTMENT_COLOR_CONFIGURATION_TYPE, KIT_EMAIL_HOUR_CONFIGURATION_TYPE, \ - KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE, KIT_DAILY_EMAIL_TIME_FORMAT_TYPE, KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE, \ - VALUE_TYPE_CHOICES, VALUE_TYPE_TEXT, DEFAULT_FROM_EMAIL, VIRUS_EMAIL_HOUR_CONFIGURATION_TYPE, \ - KIT_RECIPIENT_EMAIL_CONFIGURATION_TYPE, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO +from web.models.constants import ( + CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE, + NO_SHOW_APPOINTMENT_COLOR_CONFIGURATION_TYPE, + KIT_EMAIL_HOUR_CONFIGURATION_TYPE, + KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE, + KIT_DAILY_EMAIL_TIME_FORMAT_TYPE, + KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE, + VALUE_TYPE_CHOICES, + VALUE_TYPE_TEXT, + DEFAULT_FROM_EMAIL, + VIRUS_EMAIL_HOUR_CONFIGURATION_TYPE, + KIT_RECIPIENT_EMAIL_CONFIGURATION_TYPE, + VISIT_SHOW_VISIT_NUMBER_FROM_ZERO, +) def is_valid_email(value: str) -> bool: @@ -23,36 +32,29 @@ def is_valid_email(value: str) -> bool: class ConfigurationItem(models.Model): class Meta: - app_label = 'web' + app_label = "web" - type = models.CharField(max_length=50, - verbose_name='Type', - editable=False - ) - name = models.CharField(max_length=255, - verbose_name='Name', - editable=False - ) + type = models.CharField(max_length=50, verbose_name="Type", editable=False) + name = models.CharField(max_length=255, verbose_name="Name", editable=False) - value = models.CharField(max_length=1024, - verbose_name='Value', - ) - value_type = models.CharField(max_length=32, - choices=VALUE_TYPE_CHOICES, - verbose_name='Value type', - default=VALUE_TYPE_TEXT - ) + value = models.TextField( + max_length=1024, + verbose_name="Value", + ) + value_type = models.CharField( + max_length=32, choices=VALUE_TYPE_CHOICES, verbose_name="Value type", default=VALUE_TYPE_TEXT + ) def __str__(self): return f"{self.name} {self.value}" @staticmethod - def is_valid(item: 'ConfigurationItem') -> bool: + def is_valid(item: "ConfigurationItem") -> bool: message = ConfigurationItem.validation_error(item) return message == "" @staticmethod - def validation_error(item: 'ConfigurationItem') -> str: + def validation_error(item: "ConfigurationItem") -> str: pattern = None if item.type in (CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE, NO_SHOW_APPOINTMENT_COLOR_CONFIGURATION_TYPE): pattern = "^#[0-9a-fA-F]+$" @@ -66,7 +68,7 @@ class ConfigurationItem(models.Model): pattern = "^(%Y-%m-%d|%Y-%m-%d %H:%M)$" if item.type == KIT_RECIPIENT_EMAIL_CONFIGURATION_TYPE: for email in item.value.split(";"): - if email != '' and not is_valid_email(email): + if email != "" and not is_valid_email(email): return f"Email {email} address is invalid" if item.type == DEFAULT_FROM_EMAIL: diff --git a/smash/web/models/custom_data/custom_study_subject_field.py b/smash/web/models/custom_data/custom_study_subject_field.py index 9d754d61041e4f09987faeb3d627390516414d90..5664f821636eba80232e71c240ffafa9c91a02ac 100644 --- a/smash/web/models/custom_data/custom_study_subject_field.py +++ b/smash/web/models/custom_data/custom_study_subject_field.py @@ -9,7 +9,7 @@ class CustomStudySubjectField(models.Model): name = models.CharField(max_length=64, null=False, blank=False) type = models.CharField(max_length=20, choices=CUSTOM_FIELD_TYPE, null=False, blank=False) - possible_values = models.CharField(max_length=1024, null=True, blank=True, default='') + possible_values = models.TextField(max_length=1024, null=True, blank=True, default="") default_value = models.CharField(max_length=256, null=True, blank=True) readonly = models.BooleanField(default=False) @@ -20,12 +20,7 @@ class CustomStudySubjectField(models.Model): tracked = models.BooleanField(default=False) - study = models.ForeignKey("web.Study", - verbose_name='Study', - editable=False, - null=False, - on_delete=models.CASCADE - ) + study = models.ForeignKey("web.Study", verbose_name="Study", editable=False, null=False, on_delete=models.CASCADE) def get_study_subject_field_id(study_subject_field: CustomStudySubjectField) -> str: diff --git a/smash/web/models/custom_data/custom_study_subject_value.py b/smash/web/models/custom_data/custom_study_subject_value.py index 6398dfe615cdbcb1e5d07ae20a179f5348e46830..0967f117bab3c46a0f46660f2227250cb580c3fb 100644 --- a/smash/web/models/custom_data/custom_study_subject_value.py +++ b/smash/web/models/custom_data/custom_study_subject_value.py @@ -4,17 +4,11 @@ from django.db import models class CustomStudySubjectValue(models.Model): - value = models.CharField(max_length=2048, null=True, blank=True) - - study_subject_field = models.ForeignKey("web.CustomStudySubjectField", - verbose_name='Custom Field', - editable=False, - null=False, - on_delete=models.CASCADE - ) - study_subject = models.ForeignKey("web.StudySubject", - verbose_name='Study', - editable=False, - null=False, - on_delete=models.CASCADE - ) + value = models.TextField(max_length=2048, null=True, blank=True) + + study_subject_field = models.ForeignKey( + "web.CustomStudySubjectField", verbose_name="Custom Field", editable=False, null=False, on_delete=models.CASCADE + ) + study_subject = models.ForeignKey( + "web.StudySubject", verbose_name="Study", editable=False, null=False, on_delete=models.CASCADE + ) diff --git a/smash/web/models/etl/etl_column_mapping.py b/smash/web/models/etl/etl_column_mapping.py index 9bc36b991eede5b48ffef5e0bbadeb246cf49a96..a08674ac2dc6dce724a338a7646eaad603b3c3a6 100644 --- a/smash/web/models/etl/etl_column_mapping.py +++ b/smash/web/models/etl/etl_column_mapping.py @@ -7,33 +7,19 @@ logger = logging.getLogger(__name__) class EtlColumnMapping(models.Model): - etl_data = models.ForeignKey("web.EtlData", - verbose_name='Importer', - editable=False, - null=False, - on_delete=models.CASCADE, - related_name="column_mappings" - ) - - table_name = models.CharField(max_length=128, - default='', - null=False, - blank=True - ) - - column_name = models.CharField(max_length=128, - default='', - null=False, - blank=True - ) - - csv_column_name = models.CharField(max_length=1024, - default='', - null=False, - blank=True - ) - - enabled = models.BooleanField(default=True, - null=False, - blank=False - ) + etl_data = models.ForeignKey( + "web.EtlData", + verbose_name="Importer", + editable=False, + null=False, + on_delete=models.CASCADE, + related_name="column_mappings", + ) + + table_name = models.CharField(max_length=128, default="", null=False, blank=True) + + column_name = models.CharField(max_length=128, default="", null=False, blank=True) + + csv_column_name = models.TextField(max_length=1024, default="", null=False, blank=True) + + enabled = models.BooleanField(default=True, null=False, blank=False) diff --git a/smash/web/models/etl/etl_data.py b/smash/web/models/etl/etl_data.py index 9316adb93a6b3337f74496ef00c265fbd9b5c4c2..f3cbd22ad12c509babf5c13cc8171e42362e951a 100644 --- a/smash/web/models/etl/etl_data.py +++ b/smash/web/models/etl/etl_data.py @@ -13,48 +13,32 @@ logger = logging.getLogger(__name__) class EtlData(models.Model): - study = models.ForeignKey("web.Study", - verbose_name='Study', - editable=False, - null=False, - on_delete=models.CASCADE - ) - - filename = models.CharField(max_length=128, - verbose_name='File used for automatic import', - default='', - null=False, - blank=True - ) - run_at_times = models.CharField(max_length=1024, - verbose_name='At what time automatic import should run', - default='', - null=False, - blank=True - ) - - csv_delimiter = models.CharField(max_length=1, - verbose_name='CSV delimiter', - default=',', - null=False, - blank=False - ) - date_format = models.CharField(max_length=20, - verbose_name='Date format', - default='%Y-%m-%d', - null=False, - blank=False - ) + study = models.ForeignKey("web.Study", verbose_name="Study", editable=False, null=False, on_delete=models.CASCADE) + + filename = models.CharField( + max_length=128, verbose_name="File used for automatic import", default="", null=False, blank=True + ) + run_at_times = models.TextField( + max_length=1024, verbose_name="At what time automatic import should run", default="", null=False, blank=True + ) + + csv_delimiter = models.CharField(max_length=1, verbose_name="CSV delimiter", default=",", null=False, blank=False) + date_format = models.CharField( + max_length=20, verbose_name="Date format", default="%Y-%m-%d", null=False, blank=False + ) def set_column_mapping(self, object_type: Type[models.Model], column_name: str, csv_column_name: str): for entry in self.column_mappings.all(): - if entry.table_name == object_type._meta.db_table and \ - entry.column_name == column_name: + if entry.table_name == object_type._meta.db_table and entry.column_name == column_name: entry.csv_column_name = csv_column_name entry.save() return - EtlColumnMapping.objects.create(etl_data=self, table_name=object_type._meta.db_table, column_name=column_name, - csv_column_name=csv_column_name) + EtlColumnMapping.objects.create( + etl_data=self, + table_name=object_type._meta.db_table, + column_name=column_name, + csv_column_name=csv_column_name, + ) def get_absolute_file_path(self) -> str: return os.path.join(settings.ETL_ROOT, self.filename) diff --git a/smash/web/models/privacy_notice.py b/smash/web/models/privacy_notice.py index 62e7fc177dfa2d87c05feedbf21f3cfcf66a78b4..03a68bbc4c3efc0df028d72b600ad2ff83d090f6 100644 --- a/smash/web/models/privacy_notice.py +++ b/smash/web/models/privacy_notice.py @@ -5,19 +5,20 @@ from django.db import models from django.dispatch import receiver from web.templatetags.filters import basename +from web import disable_for_loaddata class PrivacyNotice(models.Model): - name = models.CharField(max_length=255, verbose_name='Name') - created_at = models.DateTimeField(auto_now_add=True, verbose_name='Created at') - updated_at = models.DateTimeField(auto_now=True, verbose_name='Updated at') - summary = models.CharField(max_length=255, verbose_name='Summary', blank=False, null=False) - document = models.FileField(upload_to='privacy_notices/', - verbose_name='Study Privacy Notice file', - null=False, editable=True) + name = models.CharField(max_length=255, verbose_name="Name") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created at") + updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated at") + summary = models.CharField(max_length=255, verbose_name="Summary", blank=False, null=False) + document = models.FileField( + upload_to="privacy_notices/", verbose_name="Study Privacy Notice file", null=False, editable=True + ) def __str__(self): - return f'{self.name} ({basename(self.document.url)})' + return f"{self.name} ({basename(self.document.url)})" @property def all_studies(self): @@ -26,7 +27,9 @@ class PrivacyNotice(models.Model): # These two auto-delete files from filesystem when they are unneeded: + @receiver(models.signals.post_delete, sender=PrivacyNotice) +@disable_for_loaddata def auto_delete_file_on_delete(sender, instance: PrivacyNotice, **kwargs): # pylint: disable=unused-argument """ Deletes file from filesystem @@ -38,6 +41,7 @@ def auto_delete_file_on_delete(sender, instance: PrivacyNotice, **kwargs): # py @receiver(models.signals.pre_save, sender=PrivacyNotice) +@disable_for_loaddata def auto_delete_file_on_change(sender, instance: PrivacyNotice, **kwargs): # pylint: disable=unused-argument """ Deletes old file from filesystem diff --git a/smash/web/models/provenance.py b/smash/web/models/provenance.py index 8739b6a3abee7287f888dd56a9379d5e25fff592..ae91de57b9ef398479bc3c0b60dbb8178961a65a 100644 --- a/smash/web/models/provenance.py +++ b/smash/web/models/provenance.py @@ -4,48 +4,31 @@ from django.db import models class Provenance(models.Model): class Meta: - app_label = 'web' - index_together = ['modified_table', 'modified_table_id', 'modification_date'] + app_label = "web" + index_together = ["modified_table", "modified_table_id", "modification_date"] - modified_table = models.CharField(max_length=1024, - verbose_name='Modified table', - blank=False, null=True - ) + modified_table = models.CharField(max_length=128, verbose_name="Modified table", blank=False, null=True) - modified_table_id = models.IntegerField(default=0, verbose_name='Modified table row', blank=False, null=True) + modified_table_id = models.IntegerField(default=0, verbose_name="Modified table row", blank=False, null=True) - modification_date = models.DateTimeField( - verbose_name='Modified on', - null=False, blank=False, - auto_now_add=True - ) + modification_date = models.DateTimeField(verbose_name="Modified on", null=False, blank=False, auto_now_add=True) - modification_author = models.ForeignKey("web.Worker", - verbose_name='Worker who modified the row', - null=True, blank=False, on_delete=models.deletion.CASCADE - ) + modification_author = models.ForeignKey( + "web.Worker", + verbose_name="Worker who modified the row", + null=True, + blank=False, + on_delete=models.deletion.CASCADE, + ) - modified_field = models.CharField(max_length=1024, - verbose_name='Modified field', - blank='', null=False - ) + modified_field = models.CharField(max_length=128, verbose_name="Modified field", blank="", null=False) - previous_value = models.CharField(max_length=2048, - verbose_name='Previous Value', - blank=True, null=True) + previous_value = models.TextField(max_length=2048, verbose_name="Previous Value", blank=True, null=True) - new_value = models.CharField(max_length=2048, - verbose_name='New Value', - blank=True, null=True) + new_value = models.TextField(max_length=2048, verbose_name="New Value", blank=True, null=True) - modification_description = models.CharField(max_length=20480, - verbose_name='Description', - blank=False, null=False - ) + modification_description = models.TextField(max_length=20480, verbose_name="Description", blank=False, null=False) - request_path = models.CharField(max_length=20480, - verbose_name='Request Path', - blank=True, null=True - ) + request_path = models.TextField(max_length=20480, verbose_name="Request Path", blank=True, null=True) - request_ip_addr = models.GenericIPAddressField(verbose_name='Request IP Address', null=True) + request_ip_addr = models.GenericIPAddressField(verbose_name="Request IP Address", null=True) diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 49d87c7509d0059335cfc3f190f1cadfea008ee9..f7ecc3be65a470b18d6b56bcf8b2dab1e0dbb835 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -10,6 +10,7 @@ from django.dispatch import receiver from web.models import Appointment, Location, Provenance, Visit, VoucherType from web.models.constants import BOOL_CHOICES, FILE_STORAGE from web.models.custom_data import CustomStudySubjectField, CustomStudySubjectValue +from web import disable_for_loaddata logger = logging.getLogger(__name__) @@ -214,16 +215,12 @@ class StudySubject(models.Model): def custom_data_values(self): # find the custom fields that have not yet been populated into the study subject # https://docs.djangoproject.com/en/3.2/ref/models/querysets/#filteredrelation-objects - fields = ( - CustomStudySubjectField.objects - .annotate( - t=FilteredRelation( - "customstudysubjectvalue", - condition=Q(customstudysubjectvalue__study_subject=self), - ) + fields = CustomStudySubjectField.objects.annotate( + t=FilteredRelation( + "customstudysubjectvalue", + condition=Q(customstudysubjectvalue__study_subject=self), ) - .filter(t__study_subject_field__isnull=True, study=self.study) - ) + ).filter(t__study_subject_field__isnull=True, study=self.study) for field in fields: CustomStudySubjectValue.objects.create( @@ -273,6 +270,7 @@ class StudySubject(models.Model): # SIGNALS @receiver(post_save, sender=StudySubject) +@disable_for_loaddata def set_as_resigned_or_excluded_or_endpoint_reached(sender, instance, **kwargs): # pylint: disable=unused-argument if instance.excluded: instance.mark_as_excluded() diff --git a/smash/web/models/subject.py b/smash/web/models/subject.py index c704304db4cc058faa5fcc33e9063c01e4968ebd..3838ab81664eb61048912a7b1b7c13d29c998ce3 100644 --- a/smash/web/models/subject.py +++ b/smash/web/models/subject.py @@ -7,6 +7,7 @@ from django.dispatch import receiver from .constants import SEX_CHOICES, COUNTRY_OTHER_ID from web.models import Country, Visit, Appointment, Provenance +from web import disable_for_loaddata from . import Language logger = logging.getLogger(__name__) @@ -14,121 +15,69 @@ logger = logging.getLogger(__name__) class Subject(models.Model): class Meta: - app_label = 'web' + app_label = "web" permissions = [ ("export_subjects", "Can export subject data to excel/csv"), ] @property def provenances(self): - return Provenance.objects.filter(modified_table=Subject._meta.db_table, modified_table_id=self.id)\ - .order_by('-modification_date') - - sex = models.CharField(max_length=1, - choices=SEX_CHOICES, - verbose_name='Sex' - ) - - first_name = models.CharField(max_length=50, - verbose_name='First name' - ) - - social_security_number = models.CharField(max_length=50, - verbose_name='Social security number', - blank=True, - ) - - last_name = models.CharField(max_length=50, - verbose_name='Last name' - ) - - languages = models.ManyToManyField(Language, - blank=True, - verbose_name='Known languages' - ) - - default_written_communication_language = models.ForeignKey(Language, - null=True, - blank=True, - related_name="subjects_written_communication", - verbose_name='Default language for document generation', - on_delete=models.SET_NULL - ) - phone_number = models.CharField(max_length=64, - null=True, - blank=True, - verbose_name='Phone number' - ) - - phone_number_2 = models.CharField(max_length=64, - null=True, - blank=True, - verbose_name='Phone number 2' - ) - - phone_number_3 = models.CharField(max_length=64, - null=True, - blank=True, - verbose_name='Phone number 3' - ) - - email = models.EmailField( - null=True, + return Provenance.objects.filter(modified_table=Subject._meta.db_table, modified_table_id=self.id).order_by( + "-modification_date" + ) + + sex = models.CharField(max_length=1, choices=SEX_CHOICES, verbose_name="Sex") + + first_name = models.CharField(max_length=50, verbose_name="First name") + + social_security_number = models.CharField( + max_length=50, + verbose_name="Social security number", blank=True, - verbose_name='E-mail' ) - date_born = models.DateField( + last_name = models.CharField(max_length=50, verbose_name="Last name") + + languages = models.ManyToManyField(Language, blank=True, verbose_name="Known languages") + + default_written_communication_language = models.ForeignKey( + Language, null=True, blank=True, - verbose_name='Date of birth (YYYY-MM-DD)' + related_name="subjects_written_communication", + verbose_name="Default language for document generation", + on_delete=models.SET_NULL, ) + phone_number = models.CharField(max_length=64, null=True, blank=True, verbose_name="Phone number") + + phone_number_2 = models.CharField(max_length=64, null=True, blank=True, verbose_name="Phone number 2") + + phone_number_3 = models.CharField(max_length=64, null=True, blank=True, verbose_name="Phone number 3") + + email = models.EmailField(null=True, blank=True, verbose_name="E-mail") - address = models.CharField(max_length=255, - blank=True, - verbose_name='Address' - ) - - postal_code = models.CharField(max_length=7, - blank=True, - verbose_name='Postal code' - ) - - city = models.CharField(max_length=50, - blank=True, - verbose_name='City' - ) - - country = models.ForeignKey(Country, - null=False, - blank=False, - default=COUNTRY_OTHER_ID, - verbose_name='Country', on_delete=models.CASCADE - ) - - next_of_kin_name = models.CharField(max_length=255, - blank=True, - verbose_name='Next of kin' - ) - - next_of_kin_phone = models.CharField(max_length=50, - blank=True, - verbose_name='Next of kin phone' - ) - - next_of_kin_address = models.TextField(max_length=2000, - blank=True, - verbose_name='Next of kin address' - ) - - dead = models.BooleanField( - verbose_name='Deceased', - default=False, - editable=True + date_born = models.DateField(null=True, blank=True, verbose_name="Date of birth (YYYY-MM-DD)") + + address = models.CharField(max_length=255, blank=True, verbose_name="Address") + + postal_code = models.CharField(max_length=7, blank=True, verbose_name="Postal code") + + city = models.CharField(max_length=50, blank=True, verbose_name="City") + + country = models.ForeignKey( + Country, null=False, blank=False, default=COUNTRY_OTHER_ID, verbose_name="Country", on_delete=models.CASCADE ) + next_of_kin_name = models.CharField(max_length=255, blank=True, verbose_name="Next of kin") + + next_of_kin_phone = models.CharField(max_length=50, blank=True, verbose_name="Next of kin phone") + + next_of_kin_address = models.TextField(max_length=2000, blank=True, verbose_name="Next of kin address") + + dead = models.BooleanField(verbose_name="Deceased", default=False, editable=True) + def pretty_address(self): - return f'{self.address} ({self.postal_code}), {self.city}. {self.country}' + return f"{self.address} ({self.postal_code}), {self.city}. {self.country}" def mark_as_dead(self): self.dead = True @@ -142,8 +91,9 @@ class Subject(models.Model): visit.save() def finish_all_appointments(self): - appointments = Appointment.objects.filter(visit__subject__subject=self, - status=Appointment.APPOINTMENT_STATUS_SCHEDULED) + appointments = Appointment.objects.filter( + visit__subject__subject=self, status=Appointment.APPOINTMENT_STATUS_SCHEDULED + ) for appointment in appointments: appointment.status = Appointment.APPOINTMENT_STATUS_CANCELLED appointment.save() @@ -154,13 +104,17 @@ class Subject(models.Model): # SIGNALS @receiver(post_save, sender=Subject) +@disable_for_loaddata def set_as_deceased(sender, instance, **kwargs): # pylint: disable=unused-argument if instance.dead: - p = Provenance(modified_table=Subject._meta.db_table, - modified_table_id=instance.id, modification_author=None, - previous_value=instance.dead, new_value=True, - modification_description=f'Subject "{instance}" marked as dead', - modified_field='dead', - ) + p = Provenance( + modified_table=Subject._meta.db_table, + modified_table_id=instance.id, + modification_author=None, + previous_value=instance.dead, + new_value=True, + modification_description=f'Subject "{instance}" marked as dead', + modified_field="dead", + ) instance.mark_as_dead() p.save() diff --git a/smash/web/models/visit.py b/smash/web/models/visit.py index 47fe940691c7b2b845bb508b3466238a6e1f8a2b..c45661e194e6700dce0f7a704990ec5172ef32de 100644 --- a/smash/web/models/visit.py +++ b/smash/web/models/visit.py @@ -8,61 +8,65 @@ from django.db.models.signals import post_save from django.dispatch import receiver from web.models.constants import BOOL_CHOICES +from web import disable_for_loaddata logger = logging.getLogger(__name__) class Visit(models.Model): class Meta: - app_label = 'web' + app_label = "web" - subject = models.ForeignKey("web.StudySubject", on_delete=models.CASCADE, verbose_name='Subject' - ) - datetime_begin = models.DateTimeField( - verbose_name='Visit starts at' + subject = models.ForeignKey( + "web.StudySubject", on_delete=models.CASCADE, verbose_name="Subject" ) + datetime_begin = models.DateTimeField(verbose_name="Visit starts at") datetime_end = models.DateTimeField( - verbose_name='Visit ends at' + verbose_name="Visit ends at" ) # Deadline before which all appointments need to be scheduled - is_finished = models.BooleanField( - verbose_name='Has ended', - default=False + is_finished = models.BooleanField(verbose_name="Has ended", default=False) + post_mail_sent = models.BooleanField( + choices=BOOL_CHOICES, verbose_name="Post mail sent", default=False + ) + appointment_types = models.ManyToManyField( + "web.AppointmentType", + verbose_name="Requested appointments", + blank=True, ) - post_mail_sent = models.BooleanField(choices=BOOL_CHOICES, - verbose_name='Post mail sent', - default=False - ) - appointment_types = models.ManyToManyField("web.AppointmentType", - verbose_name='Requested appointments', - blank=True, - ) # this value is automatically computed by signal handled by # update_visit_number method - visit_number = models.IntegerField( - verbose_name='Visit number', - default=1 - ) + visit_number = models.IntegerField(verbose_name="Visit number", default=1) @property def next_visit(self): - return Visit.objects.filter(subject=self.subject, visit_number=self.visit_number + 1) \ - .order_by('datetime_begin', 'datetime_end').first() + return ( + Visit.objects.filter( + subject=self.subject, visit_number=self.visit_number + 1 + ) + .order_by("datetime_begin", "datetime_end") + .first() + ) @property def future_visits(self): - return Visit.objects.filter(subject=self.subject).filter(visit_number__gt=self.visit_number) \ - .order_by('datetime_begin', 'datetime_end') + return ( + Visit.objects.filter(subject=self.subject) + .filter(visit_number__gt=self.visit_number) + .order_by("datetime_begin", "datetime_end") + ) def __str__(self): - start = self.datetime_begin.strftime('%Y-%m-%d') - end = self.datetime_end.strftime('%Y-%m-%d') - finished = '✓' if self.is_finished else '' - return f'#{self.visit_number:02} ' \ - f'| {start} / {end} ' \ - f'| {self.subject.subject.first_name} {self.subject.subject.last_name} ' \ - f'| {finished}' + start = self.datetime_begin.strftime("%Y-%m-%d") + end = self.datetime_end.strftime("%Y-%m-%d") + finished = "✓" if self.is_finished else "" + return ( + f"#{self.visit_number:02} " + f"| {start} / {end} " + f"| {self.subject.subject.first_name} {self.subject.subject.last_name} " + f"| {finished}" + ) def mark_as_finished(self): self.is_finished = True @@ -82,46 +86,68 @@ class Visit(models.Model): if create_follow_up: if self.subject.visit_used_to_compute_followup_date is not None: - visit_started = self.subject.visit_used_to_compute_followup_date.datetime_begin - start_number = self.subject.visit_used_to_compute_followup_date.visit_number + visit_started = ( + self.subject.visit_used_to_compute_followup_date.datetime_begin + ) + start_number = ( + self.subject.visit_used_to_compute_followup_date.visit_number + ) else: - visit_started = Visit.objects.filter(subject=self.subject, visit_number=1).first().datetime_begin + visit_started = ( + Visit.objects.filter(subject=self.subject, visit_number=1) + .first() + .datetime_begin + ) start_number = 1 follow_up_number = Visit.objects.filter(subject=self.subject).count() + 1 study = self.subject.study - args = {self.subject.type.follow_up_delta_units: self.subject.type.follow_up_delta_time} + args = { + self.subject.type.follow_up_delta_units: self.subject.type.follow_up_delta_time + } - time_to_next_visit = relativedelta(**args) * (follow_up_number - start_number) + time_to_next_visit = relativedelta(**args) * ( + follow_up_number - start_number + ) - logger.warning('new visit: %s %s %s', args, relativedelta(**args), time_to_next_visit) + logger.warning( + "new visit: %s %s %s", args, relativedelta(**args), time_to_next_visit + ) Visit.objects.create( subject=self.subject, datetime_begin=visit_started + time_to_next_visit, - datetime_end=visit_started + time_to_next_visit + relativedelta( - months=study.default_visit_duration_in_months) + datetime_end=visit_started + + time_to_next_visit + + relativedelta(months=study.default_visit_duration_in_months), ) def unfinish(self): # if ValueError messages are changed, change test/view/test_visit.py # check visit is indeed finished if not self.is_finished: - raise ValueError('The visit is not finished.') + raise ValueError("The visit is not finished.") # check if there are some unfinished visits before this visit - unfinished_visits = Visit.objects.filter(subject=self.subject, - is_finished=False, datetime_begin__lt=self.datetime_begin).count() + unfinished_visits = Visit.objects.filter( + subject=self.subject, + is_finished=False, + datetime_begin__lt=self.datetime_begin, + ).count() if unfinished_visits > 0: - raise ValueError("Visit can't be unfinished. There is at least one unfinished visit.") + raise ValueError( + "Visit can't be unfinished. There is at least one unfinished visit." + ) # check that there is only one future visit future_visits = self.future_visits if len(future_visits) > 1: - raise ValueError("Visit can't be unfinished. " - "Only visits with one immediate future visit (without appointments) can be unfinished.") + raise ValueError( + "Visit can't be unfinished. " + "Only visits with one immediate future visit (without appointments) can be unfinished." + ) elif len(future_visits) == 1: # check that the future visit has no appointments # remove visit if it has no appointments @@ -132,7 +158,9 @@ class Visit(models.Model): self.subject.save() next_visit.delete() else: - raise ValueError("Visit can't be unfinished. The next visit has appointments.") + raise ValueError( + "Visit can't be unfinished. The next visit has appointments." + ) else: # this can happen when there is no auto follow up visit @@ -143,33 +171,58 @@ class Visit(models.Model): @receiver(post_save, sender=Visit) -def check_visit_number(sender, instance, created, **kwargs): # pylint: disable=unused-argument +@disable_for_loaddata +def check_visit_number( + sender, instance, created, **kwargs +): # pylint: disable=unused-argument # no other solution to ensure the visit_number is in chronological order than to sort the whole list if there are # future visits visit = instance - if visit.subject is not None: # not sure if select_for_update has an effect, the tests work as well without it + if ( + visit.subject is not None + ): # not sure if select_for_update has an effect, the tests work as well without it # new visit, sort only future visit respect to the new one if created: - visits_before = Visit.objects.select_for_update().filter(subject=visit.subject) \ - .filter(datetime_begin__lt=visit.datetime_begin).count() + visits_before = ( + Visit.objects.select_for_update() + .filter(subject=visit.subject) + .filter(datetime_begin__lt=visit.datetime_begin) + .count() + ) # we need to sort the future visits respect to the new one, if any - visits = Visit.objects.select_for_update().filter(subject=visit.subject) \ - .filter(datetime_begin__gte=visit.datetime_begin).order_by('datetime_begin', 'id') + visits = ( + Visit.objects.select_for_update() + .filter(subject=visit.subject) + .filter(datetime_begin__gte=visit.datetime_begin) + .order_by("datetime_begin", "id") + ) with transaction.atomic(): # not sure if it has an effect, the tests work as well without it for i, v in enumerate(visits): - expected_visit_number = (visits_before + i + 1) + expected_visit_number = visits_before + i + 1 if v.visit_number != expected_visit_number: # does not rise post_save, we avoid recursion - Visit.objects.filter(id=v.id).update(visit_number=expected_visit_number) - if v.id == visit.id: # if the iteration visit is the same that the instance that produced the + Visit.objects.filter(id=v.id).update( + visit_number=expected_visit_number + ) + if ( + v.id == visit.id + ): # if the iteration visit is the same that the instance that produced the # signal call this ensures that the upper saved object is also updated, otherwise, # refresh_from_db should be called visit.visit_number = v.visit_number else: # if visits are modified, then, check everything - visits = Visit.objects.select_for_update().filter(subject=visit.subject).order_by('datetime_begin', 'id') + visits = ( + Visit.objects.select_for_update() + .filter(subject=visit.subject) + .order_by("datetime_begin", "id") + ) with transaction.atomic(): for i, v in enumerate(visits): - expected_visit_number = (i + 1) - if v.visit_number != expected_visit_number: # update only those with wrong numbers - Visit.objects.filter(id=v.id).update(visit_number=expected_visit_number) + expected_visit_number = i + 1 + if ( + v.visit_number != expected_visit_number + ): # update only those with wrong numbers + Visit.objects.filter(id=v.id).update( + visit_number=expected_visit_number + ) diff --git a/smash/web/officeAvailability.py b/smash/web/officeAvailability.py index f7a154194f7e27517f79e674776c8470f4540344..21590071cd576aec1879b0b28b3637191f46fb81 100644 --- a/smash/web/officeAvailability.py +++ b/smash/web/officeAvailability.py @@ -29,7 +29,15 @@ class OfficeAvailability: Docs: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.date_range.html """ - def __init__(self, name, start=None, end=None, office_start="8:00", office_end="18:00", minimum_slot="1T"): + def __init__( + self, + name, + start=None, + end=None, + office_start="8:00", + office_end="18:00", + minimum_slot="1T", + ): self.business_hours = None today_midnight = get_today_midnight_date() @@ -49,8 +57,12 @@ class OfficeAvailability: self.office_start = office_start self.office_end = office_end self.minimum_slot = minimum_slot - self.range = pd.date_range(start=self.start, end=self.end, freq=self.minimum_slot) - logger.debug("Name: %s. Min index: %s. Max index: %s", self.name, self.start, self.end) + self.range = pd.date_range( + start=self.start, end=self.end, freq=self.minimum_slot + ) + logger.debug( + "Name: %s. Min index: %s. Max index: %s", self.name, self.start, self.end + ) self.availability = pd.Series(index=self.range, data=0) # initialize range at 0 def _get_duration(self): @@ -66,7 +78,11 @@ class OfficeAvailability: """ availability_range = availability_range.round(self.minimum_slot) if only_working_hours: - availability_range = availability_range.to_series().between_time(self.office_start, self.office_end).index + availability_range = ( + availability_range.to_series() + .between_time(self.office_start, self.office_end) + .index + ) self.availability[availability_range] = 1 def remove_availability(self, availability_range, only_working_hours=False): @@ -76,7 +92,11 @@ class OfficeAvailability: """ availability_range = availability_range.round(self.minimum_slot) if only_working_hours: - availability_range = availability_range.to_series().between_time(self.office_start, self.office_end).index + availability_range = ( + availability_range.to_series() + .between_time(self.office_start, self.office_end) + .index + ) self.availability[availability_range] = 0 def _ensure_dates_are_in_bounds(self, given_start, given_end): @@ -101,7 +121,9 @@ class OfficeAvailability: return start, end - def consider_this(self, appointment_availability_or_holiday, only_working_hours=False): + def consider_this( + self, appointment_availability_or_holiday, only_working_hours=False + ): """ :appointment_availability_or_holiday can be an object from the following classes: Availability, Holiday, Appointment, AppointmentTypeLink. @@ -132,9 +154,16 @@ class OfficeAvailability: start = appointment_availability_or_holiday.available_from end = appointment_availability_or_holiday.available_till weekday = appointment_availability_or_holiday.day_number - logger.debug("Considering Availability from %s to %s for weekday %d", start, end, weekday) + logger.debug( + "Considering Availability from %s to %s for weekday %d", + start, + end, + weekday, + ) # selects the weekdays and then the specific hours - portion = self.availability[self.availability.index.weekday == (weekday - 1)].between_time(start, end) + portion = self.availability[ + self.availability.index.weekday == (weekday - 1) + ].between_time(start, end) set_to = 1 elif isinstance(appointment_availability_or_holiday, Holiday): start = appointment_availability_or_holiday.datetime_start @@ -143,27 +172,41 @@ class OfficeAvailability: # from 1960 to 2120 creating a huge pd.Range) logger.debug( "Considering %s from %s to %s", - "Extra Availability" if appointment_availability_or_holiday.kind == AVAILABILITY_EXTRA else "Holiday", + ( + "Extra Availability" + if appointment_availability_or_holiday.kind == AVAILABILITY_EXTRA + else "Holiday" + ), start, end, ) try: start, end = self._ensure_dates_are_in_bounds(start, end) except ValueError: - logger.debug("Holiday range does not overlap the availability range. Ignoring Holiday.") + logger.debug( + "Holiday range does not overlap the availability range. Ignoring Holiday." + ) return portion = self.availability[ pd.date_range(start=start, end=end, freq=self.minimum_slot) ] # select the specific range - set_to = 1 if appointment_availability_or_holiday.kind == AVAILABILITY_EXTRA else 0 + set_to = ( + 1 + if appointment_availability_or_holiday.kind == AVAILABILITY_EXTRA + else 0 + ) elif isinstance(appointment_availability_or_holiday, Appointment): start = appointment_availability_or_holiday.datetime_when - end = start + datetime.timedelta(minutes=appointment_availability_or_holiday.length) + end = start + datetime.timedelta( + minutes=appointment_availability_or_holiday.length + ) logger.debug("Considering General Appointment from %s to %s", start, end) try: start, end = self._ensure_dates_are_in_bounds(start, end) except ValueError: - logger.debug("Appointment range does not overlap the availability range. Ignoring Appointment.") + logger.debug( + "Appointment range does not overlap the availability range. Ignoring Appointment." + ) return portion = self.availability[ pd.date_range(start=start, end=end, freq=self.minimum_slot) @@ -187,7 +230,9 @@ class OfficeAvailability: ] # select the specific range set_to = 0 else: - logger.error("Expected Availability, Holiday, Appointment or AppointmentTypeLink objects.") + logger.error( + "Expected Availability, Holiday, Appointment or AppointmentTypeLink objects." + ) raise TypeError if only_working_hours: @@ -195,7 +240,8 @@ class OfficeAvailability: # limit portion to be changed to the bounds of the object time space (solution 1 of the aforementioned problem) portion = portion[ - (self.availability.index.min() <= portion.index) & (portion.index <= self.availability.index.max()) + (self.availability.index.min() <= portion.index) + & (portion.index <= self.availability.index.max()) ] self.availability[portion.index] = set_to @@ -219,18 +265,25 @@ class OfficeAvailability: 10000 loops each) """ if only_working_hours: - availability = self.availability.between_time(self.office_start, self.office_end) + availability = self.availability.between_time( + self.office_start, self.office_end + ) else: availability = self.availability - return availability.mean() * 100 # better to isolate the operation in case we change it later + return ( + availability.mean() * 100 + ) # better to isolate the operation in case we change it later def is_available(self, only_working_hours=False): """ Returns True if on the selected period is available at least 50% of the time Otherwise returns False """ - return self.get_availability_percentage(only_working_hours=only_working_hours) > 50.0 + return ( + self.get_availability_percentage(only_working_hours=only_working_hours) + > 50.0 + ) def plot_availability(self): """ @@ -244,7 +297,12 @@ class OfficeAvailability: mask = business_hours.between_time(self.office_start, self.office_end).index business_hours[mask] = 1 axes = business_hours.plot( - kind="area", alpha=0.33, color="#1190D8", label="Business Hours", legend=True, ax=axes + kind="area", + alpha=0.33, + color="#1190D8", + label="Business Hours", + legend=True, + ax=axes, ) # calculate good xticks @@ -293,4 +351,6 @@ class OfficeAvailability: axes.set_xlabel("Date & Time") fig.tight_layout() - fig.savefig(f"{self.name}_{self.start.strftime('%Y%m%d%H%M')}_{self.end.strftime('%Y%m%d%H%M')}.pdf") + fig.savefig( + f"{self.name}_{self.start.strftime('%Y%m%d%H%M')}_{self.end.strftime('%Y%m%d%H%M')}.pdf" + ) diff --git a/smash/web/statistics.py b/smash/web/statistics.py index 75991b35fca4b40715fd8fd4e8f3119bc63151af..bd3eb875fab49207d0cb1f07dd1f86836ff0e0a9 100644 --- a/smash/web/statistics.py +++ b/smash/web/statistics.py @@ -10,7 +10,7 @@ from django.db.models import Q, Count from web.migration_functions import is_sqlite_db from .models import AppointmentType, Appointment, Visit, SubjectType -__author__ = 'Valentin Grouès' +__author__ = "Valentin Grouès" def extract_month_sql(field_name): @@ -31,59 +31,94 @@ QUERY_VISITS_RANKS = """ SELECT DISTINCT(visit_number) AS rank FROM web_visit """ -QUERY_APPOINTMENTS_COUNT = """ +QUERY_APPOINTMENTS_COUNT = ( + """ SELECT count(*) FROM web_appointment LEFT JOIN (SELECT id, subject_id as web_visit_subject_id, visit_number AS rnk FROM web_visit) a ON a.id = web_appointment.visit_id LEFT JOIN web_studysubject ON web_studysubject.id = web_visit_subject_id WHERE a.rnk = %s -AND """ + extract_month_sql("web_appointment.datetime_when") + """ = %s -AND """ + extract_year_sql("web_appointment.datetime_when") + """ = %s +AND """ + + extract_month_sql("web_appointment.datetime_when") + + """ = %s +AND """ + + extract_year_sql("web_appointment.datetime_when") + + """ = %s """ +) -QUERY_VISITS_ENDED_COUNT = """ +QUERY_VISITS_ENDED_COUNT = ( + """ SELECT count(*) FROM (SELECT id, subject_id as web_visit_subject_id, datetime_begin, datetime_end, visit_number AS rnk FROM web_visit) a LEFT JOIN web_studysubject ON web_studysubject.id = web_visit_subject_id WHERE a.rnk = %s -AND """ + extract_month_sql("a.datetime_end") + """ = %s -AND """ + extract_year_sql("a.datetime_end") + """ = %s +AND """ + + extract_month_sql("a.datetime_end") + + """ = %s +AND """ + + extract_year_sql("a.datetime_end") + + """ = %s """ +) -QUERY_VISITS_STARTED_COUNT = """ +QUERY_VISITS_STARTED_COUNT = ( + """ SELECT count(*) FROM (SELECT id, subject_id as web_visit_subject_id, datetime_begin, datetime_end, visit_number AS rnk FROM web_visit) a LEFT JOIN web_studysubject ON web_studysubject.id = web_visit_subject_id WHERE a.rnk = %s -AND """ + extract_month_sql("a.datetime_begin") + """ = %s -AND """ + extract_year_sql("a.datetime_begin") + """ = %s +AND """ + + extract_month_sql("a.datetime_begin") + + """ = %s +AND """ + + extract_year_sql("a.datetime_begin") + + """ = %s """ +) -QUERY_APPOINTMENTS = """ +QUERY_APPOINTMENTS = ( + """ SELECT types.appointment_type_id, web_appointment.status, count(*) FROM web_appointment LEFT JOIN (SELECT id, subject_id as web_visit_subject_id, visit_number AS rnk FROM web_visit) a ON a.id = web_appointment.visit_id LEFT JOIN web_appointmenttypelink types ON types.appointment_id = web_appointment.id LEFT JOIN web_studysubject ON web_studysubject.id = web_visit_subject_id WHERE a.rnk = %s -AND """ + extract_month_sql("web_appointment.datetime_when") + """ = %s -AND """ + extract_year_sql("web_appointment.datetime_when") + """ = %s +AND """ + + extract_month_sql("web_appointment.datetime_when") + + """ = %s +AND """ + + extract_year_sql("web_appointment.datetime_when") + + """ = %s {} -GROUP BY TYPES.appointment_type_id, web_appointment.status +GROUP BY types.appointment_type_id, web_appointment.status """ +) class StatisticsManager: def __init__(self): - self.appointment_types = {appointment_type.id: appointment_type for appointment_type in - AppointmentType.objects.all()} - self.statuses_list = Appointment.objects.filter().values_list('status', flat=True).distinct().order_by( - 'status').all() - self.statuses_labels = [Appointment.APPOINTMENT_STATUS_CHOICES.get(status, status.title()) for status in - self.statuses_list] + self.appointment_types = { + appointment_type.id: appointment_type + for appointment_type in AppointmentType.objects.all() + } + self.statuses_list = ( + Appointment.objects.filter() + .values_list("status", flat=True) + .distinct() + .order_by("status") + .all() + ) + self.statuses_labels = [ + Appointment.APPOINTMENT_STATUS_CHOICES.get(status, status.title()) + for status in self.statuses_list + ] self.visits_ranks = self._get_visits_ranks() - def get_statistics_for_month(self, month, year, subject_type: SubjectType = None, visit=None): + def get_statistics_for_month( + self, month, year, subject_type: SubjectType = None, visit=None + ): """ Build dict with statistics for a given month of a given year. Statistics include: @@ -108,15 +143,21 @@ class StatisticsManager: if visit is not None: visit = int(visit) - filters_month_year_appointments, filters_month_year_visits_ended, filters_month_year_visits_started = \ - self._build_filters(month, subject_type, year) - - number_of_appointments = self._get_number_of_appointments(filters_month_year_appointments, visit, month, year, - subject_type) - number_of_visits_started = self._get_number_visits_started(filters_month_year_visits_started, visit, month, - year, subject_type) - number_of_visits_ended = self._get_number_visits_ended(filters_month_year_visits_ended, visit, month, year, - subject_type) + ( + filters_month_year_appointments, + filters_month_year_visits_ended, + filters_month_year_visits_started, + ) = self._build_filters(month, subject_type, year) + + number_of_appointments = self._get_number_of_appointments( + filters_month_year_appointments, visit, month, year, subject_type + ) + number_of_visits_started = self._get_number_visits_started( + filters_month_year_visits_started, visit, month, year, subject_type + ) + number_of_visits_ended = self._get_number_visits_ended( + filters_month_year_visits_ended, visit, month, year, subject_type + ) general_results["appointments"] = number_of_appointments general_results["visits_started"] = number_of_visits_started @@ -124,38 +165,70 @@ class StatisticsManager: results["general"] = general_results - results_appointments = self.get_appointments_per_type_and_status(filters_month_year_appointments, month, - self.statuses_list, visit, year, subject_type) + results_appointments = self.get_appointments_per_type_and_status( + filters_month_year_appointments, + month, + self.statuses_list, + visit, + year, + subject_type, + ) results["appointments"] = results_appointments results["statuses_list"] = self.statuses_labels - appointment_types_values = list(map(attrgetter('code'), list(self.appointment_types.values()))) + appointment_types_values = list( + map(attrgetter("code"), list(self.appointment_types.values())) + ) sorted_appointment_types_values = sorted(appointment_types_values) results["appointments_types_list"] = sorted_appointment_types_values return results - def get_appointments_per_type_and_status(self, filters_month_year_appointments, month, statuses_list, visit, year, - subject_type=None): + def get_appointments_per_type_and_status( + self, + filters_month_year_appointments, + month, + statuses_list, + visit, + year, + subject_type=None, + ): if not visit: results_appointments = {} for appointment_type in list(self.appointment_types.values()): - appointment_type_filters = copy.deepcopy(filters_month_year_appointments) - appointment_type_filters.add(Q(appointment_types=appointment_type), Q.AND) - results_appointment_set = Appointment.objects.filter(appointment_type_filters).values( - 'status').order_by( - 'status').annotate( - Count('status')) - results_appointment = [Appointment.objects.filter(appointment_type_filters).count()] - results_appointment_per_status = {result['status']: result['status__count'] for result in - results_appointment_set} - - results_appointment.extend([results_appointment_per_status.get(status, 0) for status in statuses_list]) + appointment_type_filters = copy.deepcopy( + filters_month_year_appointments + ) + appointment_type_filters.add( + Q(appointment_types=appointment_type), Q.AND + ) + results_appointment_set = ( + Appointment.objects.filter(appointment_type_filters) + .values("status") + .order_by("status") + .annotate(Count("status")) + ) + results_appointment = [ + Appointment.objects.filter(appointment_type_filters).count() + ] + results_appointment_per_status = { + result["status"]: result["status__count"] + for result in results_appointment_set + } + + results_appointment.extend( + [ + results_appointment_per_status.get(status, 0) + for status in statuses_list + ] + ) results_appointments[appointment_type.code] = results_appointment else: results_appointment_set = defaultdict(dict) query = QUERY_APPOINTMENTS subject_type_clause = "" if subject_type is not None: - subject_type_clause = f" AND web_studysubject.type_id = '{subject_type.id}'" + subject_type_clause = ( + f" AND web_studysubject.type_id = '{subject_type.id}'" + ) query = query.format(subject_type_clause) with connection.cursor() as cursor: cursor.execute(query, [visit, month, year]) @@ -166,42 +239,86 @@ class StatisticsManager: results_appointments = {} for appointment_type in list(self.appointment_types.values()): if appointment_type.id not in results_appointment_set: - results_appointments[appointment_type.code] = [0 * i for i in range(0, len(statuses_list) + 1)] + results_appointments[appointment_type.code] = [ + 0 * i for i in range(0, len(statuses_list) + 1) + ] continue - results_appointment_set_for_type = results_appointment_set[appointment_type.id] + results_appointment_set_for_type = results_appointment_set[ + appointment_type.id + ] total = [sum(results_appointment_set_for_type.values())] - total.extend([results_appointment_set_for_type.get(status, 0) for status in statuses_list]) + total.extend( + [ + results_appointment_set_for_type.get(status, 0) + for status in statuses_list + ] + ) results_appointments[appointment_type.code] = total return results_appointments @staticmethod - def _get_count_from_filters_or_sql(model, filters, query, visit, month, year, subject_type: SubjectType): + def _get_count_from_filters_or_sql( + model, filters, query, visit, month, year, subject_type: SubjectType + ): if visit: if subject_type is not None: query += f" AND web_studysubject.type_id = '{subject_type.id}'" with connection.cursor() as cursor: - cursor.execute( - query, - [visit, month, year]) + cursor.execute(query, [visit, month, year]) row = cursor.fetchone() count = int(row[0]) else: count = model.objects.filter(filters).count() return count - def _get_number_visits_started(self, filters_month_year_visits_started, visit, month, year, - subject_type: SubjectType = None): - return self._get_count_from_filters_or_sql(Visit, filters_month_year_visits_started, QUERY_VISITS_STARTED_COUNT, - visit, month, year, subject_type) - - def _get_number_visits_ended(self, filters_month_year_visits_ended, visit, month, year, - subject_type: SubjectType = None): - return self._get_count_from_filters_or_sql(Visit, filters_month_year_visits_ended, QUERY_VISITS_ENDED_COUNT, - visit, month, year, subject_type) - - def _get_number_of_appointments(self, filters, visit, month, year, subject_type: SubjectType = None): - return self._get_count_from_filters_or_sql(Appointment, filters, QUERY_APPOINTMENTS_COUNT, visit, month, year, - subject_type) + def _get_number_visits_started( + self, + filters_month_year_visits_started, + visit, + month, + year, + subject_type: SubjectType = None, + ): + return self._get_count_from_filters_or_sql( + Visit, + filters_month_year_visits_started, + QUERY_VISITS_STARTED_COUNT, + visit, + month, + year, + subject_type, + ) + + def _get_number_visits_ended( + self, + filters_month_year_visits_ended, + visit, + month, + year, + subject_type: SubjectType = None, + ): + return self._get_count_from_filters_or_sql( + Visit, + filters_month_year_visits_ended, + QUERY_VISITS_ENDED_COUNT, + visit, + month, + year, + subject_type, + ) + + def _get_number_of_appointments( + self, filters, visit, month, year, subject_type: SubjectType = None + ): + return self._get_count_from_filters_or_sql( + Appointment, + filters, + QUERY_APPOINTMENTS_COUNT, + visit, + month, + year, + subject_type, + ) @staticmethod def _build_filters(month, subject_type, year): @@ -223,7 +340,11 @@ class StatisticsManager: filters_month_year_visits_ended.add(subject_type_filter, Q.AND) subject_type_filter_appointments = Q(visit__subject__type=subject_type) filters_month_year_appointments.add(subject_type_filter_appointments, Q.AND) - return filters_month_year_appointments, filters_month_year_visits_ended, filters_month_year_visits_started + return ( + filters_month_year_appointments, + filters_month_year_visits_ended, + filters_month_year_visits_started, + ) @staticmethod def _get_visits_ranks(): diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html index c4a0863a1c611aa62ec9f83b7cf23201951a226b..3c75a7588ad9327438d41f38965c353f427c64a2 100644 --- a/smash/web/templates/_base.html +++ b/smash/web/templates/_base.html @@ -258,7 +258,7 @@ desired effect {% block footer %} <!-- To the right --> <div class="pull-right hidden-xs"> - Version: <strong>1.4.4</strong> + Version: <strong>1.4.5</strong> </div> <!-- Default to the left --> diff --git a/smash/web/tests/api_views/test_flying_team.py b/smash/web/tests/api_views/test_flying_team.py index f735413e8806a27c492bf051b47230c412282afa..e07eb6ab19c0fca591684b59f0ba337d4242f377 100644 --- a/smash/web/tests/api_views/test_flying_team.py +++ b/smash/web/tests/api_views/test_flying_team.py @@ -12,17 +12,17 @@ class TestFlyingTeamApi(LoggedInTestCase): def test_flying_teams(self): flying_team_name = "some flying_team" - response = self.client.get(reverse('web.api.flying_teams')) + response = self.client.get(reverse("web.api.flying_teams")) self.assertEqual(response.status_code, 200) create_flying_team(flying_team_name) - response = self.client.get(reverse('web.api.flying_teams')) - flying_teams = json.loads(response.content)['flying_teams'] + response = self.client.get(reverse("web.api.flying_teams")) + flying_teams = json.loads(response.content)["flying_teams"] found = False for flying_team in flying_teams: - if flying_team['name'] == flying_team_name: + if flying_team["name"] == flying_team_name: found = True self.assertTrue(found) diff --git a/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py b/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py index a96a7aff4f62f6d1c046be6f825416d44df43907..e1aaa3eb45c5d1e5431d222f2c60da69bde09dbe 100644 --- a/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py +++ b/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py @@ -2,37 +2,60 @@ from django.test import TestCase from parameterized import parameterized from web.forms.custom_study_subject_field_forms import CustomStudySubjectFieldAddForm -from web.models.constants import CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, \ - CUSTOM_FIELD_TYPE_DOUBLE, CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE +from web.models.constants import ( + CUSTOM_FIELD_TYPE_TEXT, + CUSTOM_FIELD_TYPE_BOOLEAN, + CUSTOM_FIELD_TYPE_INTEGER, + CUSTOM_FIELD_TYPE_DOUBLE, + CUSTOM_FIELD_TYPE_DATE, + CUSTOM_FIELD_TYPE_SELECT_LIST, + CUSTOM_FIELD_TYPE_FILE, +) from web.models.custom_data import CustomStudySubjectField from web.tests.functions import get_test_study class CustomStudySubjectFieldAddFormTest(TestCase): - @parameterized.expand([ - ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True), - ('bool valid', CUSTOM_FIELD_TYPE_BOOLEAN, 'True', True), - ('bool invalid', CUSTOM_FIELD_TYPE_BOOLEAN, 'bla', False), - ('int valid', CUSTOM_FIELD_TYPE_INTEGER, '102', True), - ('int invalid', CUSTOM_FIELD_TYPE_INTEGER, 'bla', False), - ('double valid', CUSTOM_FIELD_TYPE_DOUBLE, '202.25', True), - ('double invalid', CUSTOM_FIELD_TYPE_DOUBLE, 'bla', False), - ('date valid', CUSTOM_FIELD_TYPE_DATE, '2021-01-20', True), - ('date invalid', CUSTOM_FIELD_TYPE_DATE, 'bla', False), - ('select list valid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'abc', True, 'abc;def;xyz'), - ('select list invalid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'bla', False, 'abc;def'), - ('file', CUSTOM_FIELD_TYPE_FILE, None, True), - ('file invalid', CUSTOM_FIELD_TYPE_FILE, 'tmp', False), - ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True, True), - ]) - def test_add_field(self, _, field_type, default_value, valid, possible_values='', tracked=False): + @parameterized.expand( + [ + ("text", CUSTOM_FIELD_TYPE_TEXT, "bla", True), + ("bool valid", CUSTOM_FIELD_TYPE_BOOLEAN, "True", True), + ("bool invalid", CUSTOM_FIELD_TYPE_BOOLEAN, "bla", False), + ("int valid", CUSTOM_FIELD_TYPE_INTEGER, "102", True), + ("int invalid", CUSTOM_FIELD_TYPE_INTEGER, "bla", False), + ("double valid", CUSTOM_FIELD_TYPE_DOUBLE, "202.25", True), + ("double invalid", CUSTOM_FIELD_TYPE_DOUBLE, "bla", False), + ("date valid", CUSTOM_FIELD_TYPE_DATE, "2021-01-20", True), + ("date invalid", CUSTOM_FIELD_TYPE_DATE, "bla", False), + ( + "select list valid", + CUSTOM_FIELD_TYPE_SELECT_LIST, + "abc", + True, + "abc;def;xyz", + ), + ( + "select list invalid", + CUSTOM_FIELD_TYPE_SELECT_LIST, + "bla", + False, + "abc;def", + ), + ("file", CUSTOM_FIELD_TYPE_FILE, None, True), + ("file invalid", CUSTOM_FIELD_TYPE_FILE, "tmp", False), + ("text", CUSTOM_FIELD_TYPE_TEXT, "bla", True, True), + ] + ) + def test_add_field( + self, _, field_type, default_value, valid, possible_values="", tracked=False + ): sample_data = { - 'default_value': default_value, - 'name': '1. name', - 'type': field_type, - 'possible_values': possible_values, - 'tracked': tracked, + "default_value": default_value, + "name": "1. name", + "type": field_type, + "possible_values": possible_values, + "tracked": tracked, } form = CustomStudySubjectFieldAddForm(sample_data, study=get_test_study()) @@ -40,7 +63,9 @@ class CustomStudySubjectFieldAddFormTest(TestCase): self.assertTrue(form.is_valid()) field = form.save() - self.assertEqual(1, CustomStudySubjectField.objects.filter(id=field.id).count()) + self.assertEqual( + 1, CustomStudySubjectField.objects.filter(id=field.id).count() + ) self.assertEqual(default_value, field.default_value) self.assertEqual(tracked, field.tracked) else: diff --git a/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py b/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py index cdbd8aa67710d2f5f70f9f32299e07eeacec1792..a9dd10e22530fb376b708ac881e9224241154282 100644 --- a/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py +++ b/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py @@ -2,39 +2,64 @@ from django.test import TestCase from parameterized import parameterized from web.forms.custom_study_subject_field_forms import CustomStudySubjectFieldEditForm -from web.models.constants import CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, \ - CUSTOM_FIELD_TYPE_DOUBLE, CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE +from web.models.constants import ( + CUSTOM_FIELD_TYPE_TEXT, + CUSTOM_FIELD_TYPE_BOOLEAN, + CUSTOM_FIELD_TYPE_INTEGER, + CUSTOM_FIELD_TYPE_DOUBLE, + CUSTOM_FIELD_TYPE_DATE, + CUSTOM_FIELD_TYPE_SELECT_LIST, + CUSTOM_FIELD_TYPE_FILE, +) from web.models.custom_data import CustomStudySubjectField from web.tests.functions import get_test_study class CustomStudySubjectFieldEditFormTest(TestCase): - @parameterized.expand([ - ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True), - ('bool valid', CUSTOM_FIELD_TYPE_BOOLEAN, 'True', True), - ('bool invalid', CUSTOM_FIELD_TYPE_BOOLEAN, 'bla', False), - ('int valid', CUSTOM_FIELD_TYPE_INTEGER, '911', True), - ('int invalid', CUSTOM_FIELD_TYPE_INTEGER, 'bla', False), - ('double valid', CUSTOM_FIELD_TYPE_DOUBLE, '821.45', True), - ('double invalid', CUSTOM_FIELD_TYPE_DOUBLE, 'bla', False), - ('date valid', CUSTOM_FIELD_TYPE_DATE, '2020-10-04', True), - ('date invalid', CUSTOM_FIELD_TYPE_DATE, 'bla', False), - ('select list valid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'abc', True, 'abc;def;xyz'), - ('select list invalid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'bla', False, 'abc;def'), - ('file', CUSTOM_FIELD_TYPE_FILE, None, True), - ('file invalid', CUSTOM_FIELD_TYPE_FILE, 'tmp', False), - ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True, True), - ]) - def test_edit_field(self, _, field_type, default_value, valid, possible_values='', tracked=False): - field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="", type=field_type) + @parameterized.expand( + [ + ("text", CUSTOM_FIELD_TYPE_TEXT, "bla", True), + ("bool valid", CUSTOM_FIELD_TYPE_BOOLEAN, "True", True), + ("bool invalid", CUSTOM_FIELD_TYPE_BOOLEAN, "bla", False), + ("int valid", CUSTOM_FIELD_TYPE_INTEGER, "911", True), + ("int invalid", CUSTOM_FIELD_TYPE_INTEGER, "bla", False), + ("double valid", CUSTOM_FIELD_TYPE_DOUBLE, "821.45", True), + ("double invalid", CUSTOM_FIELD_TYPE_DOUBLE, "bla", False), + ("date valid", CUSTOM_FIELD_TYPE_DATE, "2020-10-04", True), + ("date invalid", CUSTOM_FIELD_TYPE_DATE, "bla", False), + ( + "select list valid", + CUSTOM_FIELD_TYPE_SELECT_LIST, + "abc", + True, + "abc;def;xyz", + ), + ( + "select list invalid", + CUSTOM_FIELD_TYPE_SELECT_LIST, + "bla", + False, + "abc;def", + ), + ("file", CUSTOM_FIELD_TYPE_FILE, None, True), + ("file invalid", CUSTOM_FIELD_TYPE_FILE, "tmp", False), + ("text", CUSTOM_FIELD_TYPE_TEXT, "bla", True, True), + ] + ) + def test_edit_field( + self, _, field_type, default_value, valid, possible_values="", tracked=False + ): + field = CustomStudySubjectField.objects.create( + study=get_test_study(), default_value="", type=field_type + ) sample_data = { - 'default_value': default_value, - 'name': '1. name', - 'type': field_type, - 'possible_values': possible_values, - 'tracked': tracked, + "default_value": default_value, + "name": "1. name", + "type": field_type, + "possible_values": possible_values, + "tracked": tracked, } form = CustomStudySubjectFieldEditForm(sample_data, instance=field) diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index 51d91ea3e089bcc4d87e9c376fc83cef3ab18d39..759e14f05782511adcd1be3799190419bc3a7b5a 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -6,14 +6,46 @@ from typing import Union from django.contrib.auth.models import Permission from django.contrib.auth import get_user_model from django.utils.timezone import make_aware, is_aware - -from web.models import Location, AppointmentType, StudySubject, Worker, Visit, Appointment, ConfigurationItem, \ - Language, ContactAttempt, FlyingTeam, Availability, Subject, Study, StudyColumns, StudyNotificationParameters, \ - VoucherType, VoucherTypePrice, Voucher, Room, Item, WorkerStudyRole, StudyRedCapColumns, EtlColumnMapping, \ - SubjectImportData, SubjectType -from web.models.constants import REDCAP_TOKEN_CONFIGURATION_TYPE, REDCAP_BASE_URL_CONFIGURATION_TYPE, \ - SEX_CHOICES_MALE, CONTACT_TYPES_PHONE, \ - MONDAY_AS_DAY_OF_WEEK, COUNTRY_AFGHANISTAN_ID, VOUCHER_STATUS_NEW, GLOBAL_STUDY_ID, DEFAULT_LOCALE_NAME +from django.conf import settings + +from web.models import ( + Location, + AppointmentType, + StudySubject, + Worker, + Visit, + Appointment, + ConfigurationItem, + Language, + ContactAttempt, + FlyingTeam, + Availability, + Subject, + Study, + StudyColumns, + StudyNotificationParameters, + VoucherType, + VoucherTypePrice, + Voucher, + Room, + Item, + WorkerStudyRole, + StudyRedCapColumns, + EtlColumnMapping, + SubjectImportData, + SubjectType, +) +from web.models.constants import ( + REDCAP_TOKEN_CONFIGURATION_TYPE, + REDCAP_BASE_URL_CONFIGURATION_TYPE, + SEX_CHOICES_MALE, + CONTACT_TYPES_PHONE, + MONDAY_AS_DAY_OF_WEEK, + COUNTRY_AFGHANISTAN_ID, + VOUCHER_STATUS_NEW, + GLOBAL_STUDY_ID, + DEFAULT_LOCALE_NAME, +) from web.models.worker_study_role import ROLE_CHOICES_DOCTOR, WORKER_VOUCHER_PARTNER from web.redcap_connector import RedcapSubject from web.views.notifications import get_today_midnight_date @@ -37,10 +69,12 @@ def create_voucher_type(): def create_voucher_type_price(): - return VoucherTypePrice.objects.create(voucher_type=create_voucher_type(), - price=12.34, - start_date=get_today_midnight_date(), - end_date=get_today_midnight_date()) + return VoucherTypePrice.objects.create( + voucher_type=create_voucher_type(), + price=12.34, + start_date=get_today_midnight_date(), + end_date=get_today_midnight_date(), + ) def create_empty_study_columns(): @@ -68,8 +102,12 @@ def create_study(name="test"): study_columns = StudyColumns.objects.create() notification_parameters = StudyNotificationParameters.objects.create() redcap_columns = StudyRedCapColumns.objects.create() - return Study.objects.create(name=name, columns=study_columns, notification_parameters=notification_parameters, - redcap_columns=redcap_columns) + return Study.objects.create( + name=name, + columns=study_columns, + notification_parameters=notification_parameters, + redcap_columns=redcap_columns, + ) TEST_ID_COUNTER = 0 @@ -90,14 +128,16 @@ def create_voucher(study_subject=None, partner=None, worker=None): if worker is None: worker = create_worker() number = str(get_test_id()) - return Voucher.objects.create(number=number, - study_subject=study_subject, - issue_date=get_today_midnight_date(), - expiry_date=get_today_midnight_date(), - voucher_type=create_voucher_type(), - usage_partner=partner, - issue_worker=worker, - status=VOUCHER_STATUS_NEW) + return Voucher.objects.create( + number=number, + study_subject=study_subject, + issue_date=get_today_midnight_date(), + expiry_date=get_today_midnight_date(), + voucher_type=create_voucher_type(), + usage_partner=partner, + issue_worker=worker, + status=VOUCHER_STATUS_NEW, + ) def create_empty_notification_parameters(): @@ -158,7 +198,7 @@ def get_test_study() -> Study: return create_study("test-study") -def create_appointment_type(code='C', default_duration=10, description='test'): +def create_appointment_type(code="C", default_duration=10, description="test"): return AppointmentType.objects.create( code=code, default_duration=default_duration, @@ -172,13 +212,14 @@ def create_contact_attempt(subject=None, worker=None): if worker is None: worker = create_worker() - return ContactAttempt.objects.create(subject=subject, - worker=worker, - type=CONTACT_TYPES_PHONE, - datetime_when=get_today_midnight_date(), - success=True, - comment="Successful contact attempt", - ) + return ContactAttempt.objects.create( + subject=subject, + worker=worker, + type=CONTACT_TYPES_PHONE, + datetime_when=get_today_midnight_date(), + success=True, + comment="Successful contact attempt", + ) def create_subject(): @@ -186,12 +227,16 @@ def create_subject(): first_name="Piotr", last_name="Gawron", sex=SEX_CHOICES_MALE, - country_id=COUNTRY_AFGHANISTAN_ID + country_id=COUNTRY_AFGHANISTAN_ID, ) -def create_study_subject(subject_id: int = 1, subject: Subject = None, nd_number: str = 'ND0001', - study: Study = None) -> StudySubject: +def create_study_subject( + subject_id: int = 1, + subject: Subject = None, + nd_number: str = "ND0001", + study: Study = None, +) -> StudySubject: if study is None: study = get_test_study() if subject is None: @@ -201,9 +246,11 @@ def create_study_subject(subject_id: int = 1, subject: Subject = None, nd_number type=get_control_subject_type(), screening_number="piotr's number" + str(subject_id), study=study, - subject=subject + subject=subject, ) - if nd_number is not None: # null value in column "nd_number" violates not-null constraint + if ( + nd_number is not None + ): # null value in column "nd_number" violates not-null constraint study_subject.nd_number = nd_number study_subject.save() @@ -211,25 +258,27 @@ def create_study_subject(subject_id: int = 1, subject: Subject = None, nd_number def get_control_subject_type() -> SubjectType: - return SubjectType.objects.filter(name='CONTROL').first() + return SubjectType.objects.filter(name="CONTROL").first() def get_patient_subject_type() -> SubjectType: - return SubjectType.objects.filter(name='PATIENT').first() + return SubjectType.objects.filter(name="PATIENT").first() -def create_study_subject_with_multiple_screening_numbers(subject_id=1, subject=None, screening_number=None): +def create_study_subject_with_multiple_screening_numbers( + subject_id=1, subject=None, screening_number=None +): if subject is None: subject = create_subject() if screening_number is None: - screening_number = f'E-00{subject_id}; L-00{subject_id}' + screening_number = f"E-00{subject_id}; L-00{subject_id}" return StudySubject.objects.create( default_location=get_test_location(), type=get_control_subject_type(), screening_number=screening_number, study=get_test_study(), - subject=subject + subject=subject, ) @@ -239,15 +288,16 @@ def create_red_cap_subject(): return result -def create_user(username: str = None, password: str = None, email: str = 'jacob@bla') -> get_user_model(): +def create_user( + username: str = None, password: str = None, email: str = "jacob@bla" +) -> get_user_model(): if username is None: - username = 'piotr' + username = "piotr" if password is None: - password = 'top_secret' + password = "top_secret" user = get_user_model().objects.create_user( - username=username, - email=email, - password=password) + username=username, email=email, password=password + ) create_worker(user) return user @@ -262,63 +312,75 @@ def add_permissions_to_worker(worker, codenames): def create_worker(user=None, with_test_location=False): worker = Worker.objects.create( - first_name='piotr', + first_name="piotr", last_name="gawron", - email='jacob@bla.com', + email="jacob@bla.com", user=user, specialization="spec", unit="LCSB", - phone_number="0123456789" + phone_number="0123456789", ) if with_test_location: worker.locations.set([get_test_location()]) worker.save() WorkerStudyRole.objects.create( - worker=worker, study_id=GLOBAL_STUDY_ID, name=ROLE_CHOICES_DOCTOR) + worker=worker, study_id=GLOBAL_STUDY_ID, name=ROLE_CHOICES_DOCTOR + ) return worker def create_voucher_partner(): worker = Worker.objects.create( - first_name='piotr', + first_name="piotr", last_name="gawron", - email='jacob@bla.com', + email="jacob@bla.com", specialization="spec", unit="LCSB", - phone_number="0123456789" + phone_number="0123456789", ) WorkerStudyRole.objects.create( - worker=worker, study_id=GLOBAL_STUDY_ID, name=WORKER_VOUCHER_PARTNER) + worker=worker, study_id=GLOBAL_STUDY_ID, name=WORKER_VOUCHER_PARTNER + ) return worker -def create_availability(worker=None, available_from=None, available_till=None, day_number=MONDAY_AS_DAY_OF_WEEK): +def create_availability( + worker=None, + available_from=None, + available_till=None, + day_number=MONDAY_AS_DAY_OF_WEEK, +): if available_from is None: - available_from = '8:00' + available_from = "8:00" if available_till is None: - available_till = '18:00' + available_till = "18:00" if worker is None: worker = create_worker() - availability = Availability.objects.create(person=worker, - day_number=day_number, - available_from=available_from, - available_till=available_till, - ) + availability = Availability.objects.create( + person=worker, + day_number=day_number, + available_from=available_from, + available_till=available_till, + ) return availability -def create_visit(subject: StudySubject = None, datetime_begin=None, datetime_end=None) -> Visit: +def create_visit( + subject: StudySubject = None, datetime_begin=None, datetime_end=None +) -> Visit: if subject is None: subject = create_study_subject() if datetime_begin is None: datetime_begin = get_today_midnight_date() + datetime.timedelta(days=-31) if datetime_end is None: datetime_end = get_today_midnight_date() + datetime.timedelta(days=31) - return Visit.objects.create(datetime_begin=datetime_begin, - datetime_end=datetime_end, - subject=subject, - is_finished=False) + return Visit.objects.create( + datetime_begin=datetime_begin, + datetime_end=datetime_end, + subject=subject, + is_finished=False, + ) def create_appointment(visit=None, when=None, length=30) -> Appointment: @@ -339,7 +401,8 @@ def create_appointment(visit=None, when=None, length=30) -> Appointment: length=length, location=get_test_location(), status=Appointment.APPOINTMENT_STATUS_SCHEDULED, - datetime_when=when_datetime) + datetime_when=when_datetime, + ) def create_appointment_without_visit(when=None, length=30): @@ -347,7 +410,8 @@ def create_appointment_without_visit(when=None, length=30): length=length, location=get_test_location(), status=Appointment.APPOINTMENT_STATUS_SCHEDULED, - datetime_when=when) + datetime_when=when, + ) def create_configuration_item(): @@ -366,19 +430,31 @@ def create_flying_team(place=None): return result -def create_item(name='Test item', is_fixed=False, disposable=False): +def create_item(name="Test item", is_fixed=False, disposable=False): item = Item(name=name, is_fixed=is_fixed, disposable=disposable) item.save() return item -def create_room(owner='Test owner', city='Test city', - address='Test address', equipment=None, - floor=1, is_vehicle=False, room_number=1): +def create_room( + owner="Test owner", + city="Test city", + address="Test address", + equipment=None, + floor=1, + is_vehicle=False, + room_number=1, +): if equipment is None: equipment = [] - room = Room(owner=owner, city=city, address=address, - floor=floor, is_vehicle=is_vehicle, room_number=room_number) + room = Room( + owner=owner, + city=city, + address=address, + floor=floor, + is_vehicle=is_vehicle, + room_number=room_number, + ) room.save() room.equipment.set(equipment) # Cannot be made in constructor/with single save room.save() @@ -392,14 +468,14 @@ def create_language(name="French", locale=DEFAULT_LOCALE_NAME) -> Language: def get_resource_path(filename): - return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', filename) + return os.path.join(os.path.dirname(os.path.realpath(__file__)), "data", filename) def format_form_field(value): if isinstance(value, datetime.date): - return value.strftime('%Y-%m-%d') + return value.strftime("%Y-%m-%d") elif isinstance(value, datetime.datetime): - return value.strftime('%Y-%m-%d %H:%M') + return value.strftime("%Y-%m-%d %H:%M") elif value is None: return "" else: @@ -409,67 +485,92 @@ def format_form_field(value): def prepare_test_redcap_connection(): Language.objects.create(name="Finnish").save() Language.objects.create(name="Italian").save() - token_item = ConfigurationItem.objects.filter( - type=REDCAP_TOKEN_CONFIGURATION_TYPE)[0] + token_item = ConfigurationItem.objects.filter(type=REDCAP_TOKEN_CONFIGURATION_TYPE)[ + 0 + ] # noinspection SpellCheckingInspection - token_item.value = "5DC21D45E3A2E068659F11046EA88734" + token_item.value = settings.REDCAP_TEST_API_TOKEN token_item.save() url_item = ConfigurationItem.objects.filter( - type=REDCAP_BASE_URL_CONFIGURATION_TYPE)[0] - url_item.value = "https://luxparktest.lcsb.uni.lu/redcap/" + type=REDCAP_BASE_URL_CONFIGURATION_TYPE + )[0] + url_item.value = settings.REDCAP_TEST_URL url_item.save() def datetimeify_date(date: Union[datetime.date, str, bytes]) -> datetime.datetime: - if isinstance(date, datetime.date): # If it's date, then just make sure that timezone support is there + if isinstance( + date, datetime.date + ): # If it's date, then just make sure that timezone support is there if is_aware(date): return date else: return make_aware(date) if isinstance(date, bytes): # If it's bytes, then convert to string and carry on... - date = date.decode('utf8') - if isinstance(date, str): # If it's string, convert to datetime with timezone support - return make_aware(datetime.datetime.strptime(date, '%Y-%m-%d')) + date = date.decode("utf8") + if isinstance( + date, str + ): # If it's string, convert to datetime with timezone support + return make_aware(datetime.datetime.strptime(date, "%Y-%m-%d")) actual_type = str(type(date)) raise TypeError( - f"Date should be either a subclass of 'datetime.date', string or bytes! But is: {actual_type} instead") + f"Date should be either a subclass of 'datetime.date', string or bytes! But is: {actual_type} instead" + ) def create_tns_column_mapping(subject_import_data: SubjectImportData): - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="nd_number", - csv_column_name="donor_id", - table_name=StudySubject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="comments", - csv_column_name="treatingphysician", - table_name=StudySubject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="first_name", - csv_column_name="firstname", - table_name=Subject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="first_name", - csv_column_name="sig_firstname", - table_name=Subject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="last_name", - csv_column_name="lastname", - table_name=Subject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="last_name", - csv_column_name="sig_lastname", - table_name=Subject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="phone_number", - csv_column_name="phonenr", - table_name=Subject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="date_born", - csv_column_name="dateofbirth", - table_name=Subject._meta.db_table) - EtlColumnMapping.objects.create(etl_data=subject_import_data, - column_name="next_of_kin_name", - csv_column_name="representative", - table_name=Subject._meta.db_table) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="nd_number", + csv_column_name="donor_id", + table_name=StudySubject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="comments", + csv_column_name="treatingphysician", + table_name=StudySubject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="first_name", + csv_column_name="firstname", + table_name=Subject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="first_name", + csv_column_name="sig_firstname", + table_name=Subject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="last_name", + csv_column_name="lastname", + table_name=Subject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="last_name", + csv_column_name="sig_lastname", + table_name=Subject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="phone_number", + csv_column_name="phonenr", + table_name=Subject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="date_born", + csv_column_name="dateofbirth", + table_name=Subject._meta.db_table, + ) + EtlColumnMapping.objects.create( + etl_data=subject_import_data, + column_name="next_of_kin_name", + csv_column_name="representative", + table_name=Subject._meta.db_table, + ) diff --git a/smash/web/tests/test_statistics.py b/smash/web/tests/test_statistics.py index f48ead719afde2fe3eba98391c5e17c1101be3d6..869a3f6c8272d615a01d2b60216cc7ea900c5216 100644 --- a/smash/web/tests/test_statistics.py +++ b/smash/web/tests/test_statistics.py @@ -5,11 +5,15 @@ from django.test import TestCase from web.models import Visit, AppointmentTypeLink from web.statistics import get_previous_year_and_month_for_date, StatisticsManager -from web.tests.functions import create_appointment, create_appointment_type, get_control_subject_type, \ - get_patient_subject_type +from web.tests.functions import ( + create_appointment, + create_appointment_type, + get_control_subject_type, + get_patient_subject_type, +) from web.views.notifications import get_today_midnight_date -__author__ = 'Valentin Grouès' +__author__ = "Valentin Grouès" class TestStatistics(TestCase): @@ -17,7 +21,9 @@ class TestStatistics(TestCase): self.now = get_today_midnight_date() self.appointment_type = create_appointment_type() appointment = create_appointment(when=self.now) - AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=self.appointment_type) + AppointmentTypeLink.objects.create( + appointment=appointment, appointment_type=self.appointment_type + ) self.visit_start = appointment.visit.datetime_begin self.visit_end = appointment.visit.datetime_end appointment.save() @@ -35,67 +41,112 @@ class TestStatistics(TestCase): self.assertEqual(12, previous_month) def test_get_statistics_for_month_one_appointment(self): - statistics = self.statistics_manager.get_statistics_for_month(self.visit_start.month, self.visit_start.year) - self.check_statistics(statistics, 1, 0, 0, {"C": [0, 0]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.visit_start.month, self.visit_start.year + ) + self.check_statistics(statistics, 1, 0, 0, {"C": [0, 0]}, ["Scheduled"]) - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year) - self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year + ) + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ["Scheduled"]) - statistics = self.statistics_manager.get_statistics_for_month(self.visit_end.month, self.visit_end.year) - self.check_statistics(statistics, 0, 1, 0, {"C": [0, 0]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.visit_end.month, self.visit_end.year + ) + self.check_statistics(statistics, 0, 1, 0, {"C": [0, 0]}, ["Scheduled"]) def test_get_statistics_for_month_one_appointment_visit(self): - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="1") - self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year, visit="1" + ) + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ["Scheduled"]) - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="2") - self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year, visit="2" + ) + self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ["Scheduled"]) def test_get_statistics_for_month_one_appointment_subject_type(self): - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, - subject_type=get_control_subject_type()) - self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year, subject_type=get_control_subject_type() + ) + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ["Scheduled"]) - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, - subject_type=get_patient_subject_type()) - self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year, subject_type=get_patient_subject_type() + ) + self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ["Scheduled"]) def test_get_statistics_for_month_one_appointment_subject_type_and_visit(self): - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, - subject_type=get_control_subject_type(), - visit='1') - self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ['Scheduled']) - - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, - subject_type=get_patient_subject_type(), - visit='1') - self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ['Scheduled']) + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, + self.now.year, + subject_type=get_control_subject_type(), + visit="1", + ) + self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1]}, ["Scheduled"]) + + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, + self.now.year, + subject_type=get_patient_subject_type(), + visit="1", + ) + self.check_statistics(statistics, 0, 0, 0, {"C": [0, 0]}, ["Scheduled"]) def test_get_statistics_for_month_multiple_visits(self): - second_visit = Visit.objects.create(datetime_begin=self.now + datetime.timedelta(days=-32), - datetime_end=self.now + datetime.timedelta(days=31), - subject=self.subject, - is_finished=False) + second_visit = Visit.objects.create( + datetime_begin=self.now + datetime.timedelta(days=-32), + datetime_end=self.now + datetime.timedelta(days=31), + subject=self.subject, + is_finished=False, + ) second_appointment = create_appointment(second_visit, when=self.now) - AppointmentTypeLink.objects.create(appointment=second_appointment, appointment_type=self.appointment_type) + AppointmentTypeLink.objects.create( + appointment=second_appointment, appointment_type=self.appointment_type + ) second_appointment.status = "Cancelled" second_appointment.save() self.statistics_manager = StatisticsManager() - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year) - self.check_statistics(statistics, 0, 0, 2, {"C": [2, 1, 1]}, ['Cancelled', 'Scheduled']) - - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="1") - self.check_statistics(statistics, 0, 0, 1, {"C": [1, 1, 0]}, ['Cancelled', 'Scheduled']) - - statistics = self.statistics_manager.get_statistics_for_month(self.now.month, self.now.year, visit="2") - self.check_statistics(statistics, 0, 0, 1, {"C": [1, 0, 1]}, ['Cancelled', 'Scheduled']) - - def check_statistics(self, statistics, expected_visits_started, expected_visits_ended, expected_appointments_count, - expected_appointments_details, expected_statuses): - self.assertEqual(expected_visits_started, statistics['general']['visits_started']) - self.assertEqual(expected_visits_ended, statistics['general']['visits_ended']) - self.assertEqual(expected_statuses, statistics['statuses_list']) - self.assertEqual(expected_appointments_count, statistics['general']['appointments']) - self.assertEqual(expected_appointments_details, statistics['appointments']) + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year + ) + self.check_statistics( + statistics, 0, 0, 2, {"C": [2, 1, 1]}, ["Cancelled", "Scheduled"] + ) + + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year, visit="1" + ) + self.check_statistics( + statistics, 0, 0, 1, {"C": [1, 1, 0]}, ["Cancelled", "Scheduled"] + ) + + statistics = self.statistics_manager.get_statistics_for_month( + self.now.month, self.now.year, visit="2" + ) + self.check_statistics( + statistics, 0, 0, 1, {"C": [1, 0, 1]}, ["Cancelled", "Scheduled"] + ) + + def check_statistics( + self, + statistics, + expected_visits_started, + expected_visits_ended, + expected_appointments_count, + expected_appointments_details, + expected_statuses, + ): + self.assertEqual( + expected_visits_started, statistics["general"]["visits_started"] + ) + self.assertEqual(expected_visits_ended, statistics["general"]["visits_ended"]) + self.assertEqual(expected_statuses, statistics["statuses_list"]) + self.assertEqual( + expected_appointments_count, statistics["general"]["appointments"] + ) + self.assertEqual(expected_appointments_details, statistics["appointments"])