From 8446525f86e57cd1aa1d0768df05ba8b042d0ba2 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Fri, 8 May 2020 15:43:05 -0400 Subject: [PATCH 001/109] Update ISSUE_TEMPLATE.md Add complexity estimate --- .github/ISSUE_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index f328367f..449bb81d 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,6 +3,9 @@ Describe a feature request proposal explaining the benefits of having such featu ## Proposal +## Estimate +(Complexity ranked 1 to 10) + # Bug report Describe the current behavior and what is the expected behavior. Include logs and screenshots if possible. From 8b1bb386c82ab6ced73264223d8bf400f749ccf7 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sat, 9 May 2020 15:04:11 -0400 Subject: [PATCH 002/109] update travis configs --- .travis.yml | 6 +++--- scripts/deploy-aws.sh | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 scripts/deploy-aws.sh diff --git a/.travis.yml b/.travis.yml index 6fb73da4..22edb73a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ install: script: - flake8 - bandit -r . -ll - - pytest --cache-clear + - pytest --cov-config=.coveragerc --cache-clear - docker build -t $DOCKER_REPO . env: global: @@ -51,6 +51,6 @@ env: LDAP_BASE_DN: "dc=example,dc=org" deploy: provider: script - script: bash scripts/deploy.sh + script: bash scripts/deploy-aws.sh on: - branch: master \ No newline at end of file + branch: master diff --git a/scripts/deploy-aws.sh b/scripts/deploy-aws.sh new file mode 100644 index 00000000..0cc17bf5 --- /dev/null +++ b/scripts/deploy-aws.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +docker --version # document the version travis is using +pip install --user awscli # install aws cli w/o sudo +export PATH=$PATH:$HOME/.local/bin # put aws in the path +eval $(aws ecr get-login --region us-west-2) #needs AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY envvars +docker build -t buildly-core . +docker tag buildly-core:latest 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest +docker push 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest From 6fddbe6e6c161cc3793ad675c8ba5a9e87a58354 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sat, 9 May 2020 15:23:21 -0400 Subject: [PATCH 003/109] update travis configs --- scripts/deploy-aws.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/deploy-aws.sh b/scripts/deploy-aws.sh index 0cc17bf5..fd1551cb 100644 --- a/scripts/deploy-aws.sh +++ b/scripts/deploy-aws.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash -docker --version # document the version travis is using pip install --user awscli # install aws cli w/o sudo export PATH=$PATH:$HOME/.local/bin # put aws in the path eval $(aws ecr get-login --region us-west-2) #needs AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY envvars -docker build -t buildly-core . -docker tag buildly-core:latest 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest +docker tag buildly:latest 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest docker push 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest From 109c64677bd45de0268ed35a8a7a510442391b4d Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 11:23:07 -0400 Subject: [PATCH 004/109] move build to travis script --- .travis.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 22edb73a..09c7b71c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,6 +51,20 @@ env: LDAP_BASE_DN: "dc=example,dc=org" deploy: provider: script - script: bash scripts/deploy-aws.sh + script: pip install --user awscli | export PATH=$PATH:$HOME/.local/bin | eval $(aws ecr get-login --region us-west-2) on: branch: master + tags: true + + + build-docker-image-tag: + image: plugins/docker + insecure: true + registry: 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path + repo: 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core + file: Dockerfile + auto_tag: true + secrets: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY] + when: + event: [tag] + status: [success] From 4a75e0d81449c7ec301c4e24e86aa39cdb6dfe35 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 11:56:51 -0400 Subject: [PATCH 005/109] update travis for aws --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 09c7b71c..421fc84f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_script: install: - cat requirements/base.txt | grep "^Django==\|^psycopg2" | xargs pip install - pip install -r requirements/ci.txt + - pip install awscli script: - flake8 - bandit -r . -ll From c4ea278d1899c292973c8d126f9c9de488d84272 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 20:07:51 -0400 Subject: [PATCH 006/109] update travis for aws --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 421fc84f..86233d95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ env: LDAP_BASE_DN: "dc=example,dc=org" deploy: provider: script - script: pip install --user awscli | export PATH=$PATH:$HOME/.local/bin | eval $(aws ecr get-login --region us-west-2) + script: pip install awscli | export PATH=$PATH:$HOME/.local/bin | eval $(aws ecr get-login --region us-west-2) on: branch: master tags: true From 5f824397af2a6f6a93819ecdbb4b40e63364ee0d Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 20:22:11 -0400 Subject: [PATCH 007/109] update travis for aws --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 86233d95..83fe5215 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ env: LDAP_BASE_DN: "dc=example,dc=org" deploy: provider: script - script: pip install awscli | export PATH=$PATH:$HOME/.local/bin | eval $(aws ecr get-login --region us-west-2) + script: pip install awscli on: branch: master tags: true From 6bb21303216a43b7680c57b185b054c63902b40e Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 20:47:26 -0400 Subject: [PATCH 008/109] update travis for aws --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83fe5215..ceefcd18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,7 @@ env: SECRET_KEY: "nothing" OAUTH_CLIENT_ID: "vBn4KsOCthm7TWzMH0kVV0dXkUPJEtOQwaLu0eoC" OAUTH_CLIENT_SECRET: "0aYDOHUNAxK4MjbnYOHhfrKx8EzjKqN6GbB6IGyCgpT6pmQ5pEVJmH7mIEUJ" - DOCKER_REPO: "buildly/buildly" + DOCKER_REPO: "transparent-path/buildly-core" LDAP_ENABLE: "True" LDAP_HOST: "ldap://localhost:389" LDAP_USERNAME: "cn=admin,dc=example,dc=org" @@ -52,7 +52,8 @@ env: LDAP_BASE_DN: "dc=example,dc=org" deploy: provider: script - script: pip install awscli + script: pip install awscli | aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 684870619712.dkr.ecr.us-west-2.amazonaws.com + on: branch: master tags: true From 813e3b889b3baaa69a416363ee8195f0004be1fd Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 21:13:46 -0400 Subject: [PATCH 009/109] update travis for aws --- .travis.yml | 2 -- scripts/deploy-aws.sh | 11 ++++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index ceefcd18..f57c7c73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,12 +53,10 @@ env: deploy: provider: script script: pip install awscli | aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 684870619712.dkr.ecr.us-west-2.amazonaws.com - on: branch: master tags: true - build-docker-image-tag: image: plugins/docker insecure: true diff --git a/scripts/deploy-aws.sh b/scripts/deploy-aws.sh index fd1551cb..2aa60659 100644 --- a/scripts/deploy-aws.sh +++ b/scripts/deploy-aws.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash -pip install --user awscli # install aws cli w/o sudo -export PATH=$PATH:$HOME/.local/bin # put aws in the path -eval $(aws ecr get-login --region us-west-2) #needs AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY envvars + +# Push image to ECR +################### +pip install awscli +aws ecr get-login-password --region us-west-2 +docker login --username AWS --password-stdin 684870619712.dkr.ecr.us-west-2.amazonaws.com + +# update latest version docker tag buildly:latest 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest docker push 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest From 5c4f1969a5b1a8093b1c06496ac87d25c4d95628 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 21:22:16 -0400 Subject: [PATCH 010/109] update travis for aws --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f57c7c73..bbcffa8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ env: LDAP_BASE_DN: "dc=example,dc=org" deploy: provider: script - script: pip install awscli | aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 684870619712.dkr.ecr.us-west-2.amazonaws.com + script: scripts/deploy-aws.sh on: branch: master tags: true From 284c8230b9b0f0033b71db535f6d40ea005337a6 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 10 May 2020 21:33:38 -0400 Subject: [PATCH 011/109] update travis for aws --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bbcffa8e..75dcca10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ env: LDAP_BASE_DN: "dc=example,dc=org" deploy: provider: script - script: scripts/deploy-aws.sh + script: bash scripts/deploy-aws.sh on: branch: master tags: true From 565b5807c43fbbc9d6dad5070827986c73148d2f Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Mon, 11 May 2020 08:56:27 -0400 Subject: [PATCH 012/109] update travis for aws --- .travis.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 75dcca10..8185474c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,15 +56,3 @@ deploy: on: branch: master tags: true - - build-docker-image-tag: - image: plugins/docker - insecure: true - registry: 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path - repo: 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core - file: Dockerfile - auto_tag: true - secrets: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY] - when: - event: [tag] - status: [success] From d5e780854049e8f89f34b293da8e5970af088b63 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Mon, 11 May 2020 09:46:22 -0400 Subject: [PATCH 013/109] update travis for aws --- scripts/deploy-aws.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy-aws.sh b/scripts/deploy-aws.sh index 2aa60659..c510a3dc 100644 --- a/scripts/deploy-aws.sh +++ b/scripts/deploy-aws.sh @@ -7,5 +7,5 @@ aws ecr get-login-password --region us-west-2 docker login --username AWS --password-stdin 684870619712.dkr.ecr.us-west-2.amazonaws.com # update latest version -docker tag buildly:latest 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest +docker tag transparent-path/buildly-core:latest 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest docker push 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest From 3e719c8092f2ab32f09375a739b9bc09a2a0dd2f Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Mon, 11 May 2020 12:19:10 -0400 Subject: [PATCH 014/109] update travis for aws --- scripts/deploy-aws.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/deploy-aws.sh b/scripts/deploy-aws.sh index c510a3dc..20ee6ff6 100644 --- a/scripts/deploy-aws.sh +++ b/scripts/deploy-aws.sh @@ -3,8 +3,7 @@ # Push image to ECR ################### pip install awscli -aws ecr get-login-password --region us-west-2 -docker login --username AWS --password-stdin 684870619712.dkr.ecr.us-west-2.amazonaws.com +aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 684870619712.dkr.ecr.us-west-2.amazonaws.com # update latest version docker tag transparent-path/buildly-core:latest 684870619712.dkr.ecr.us-west-2.amazonaws.com/transparent-path/buildly-core:latest From 731212daccd52f98aa0628612c7ecc65e962668a Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 14 Jun 2020 13:28:29 -0400 Subject: [PATCH 015/109] cleanup initital data --- buildly/management/commands/loadinitialdata.py | 13 ------------- docker-compose.yml | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index fda5f653..ee3f60e6 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -29,18 +29,6 @@ def __init__(self, *args, **kwargs): self._su_group = None self._default_org = None - def _create_oauth_application(self): - if settings.OAUTH_CLIENT_ID and settings.OAUTH_CLIENT_SECRET: - app, created = Application.objects.update_or_create( - client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET, - defaults={ - 'name': 'buildly oauth2', - 'client_type': Application.CLIENT_PUBLIC, - 'authorization_grant_type': Application.GRANT_PASSWORD, - } - ) - self._application = app def _create_default_organization(self): if settings.DEFAULT_ORG: @@ -89,5 +77,4 @@ def _create_user(self): def handle(self, *args, **options): self._create_groups() self._create_default_organization() - self._create_oauth_application() self._create_user() diff --git a/docker-compose.yml b/docker-compose.yml index 0a252987..6b0ab98d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "2.0" +version: "3.1" services: From 1ad19664eda399786139ea7d2cc51c43e4a16017 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 14 Jun 2020 13:35:04 -0400 Subject: [PATCH 016/109] cleanup initital data --- buildly/management/commands/loadinitialdata.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index ee3f60e6..f67bc043 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -5,8 +5,6 @@ from django.core.management.base import BaseCommand from django.db import transaction -from oauth2_provider.models import Application - from core.models import ROLE_VIEW_ONLY, ROLE_ORGANIZATION_ADMIN, ROLE_WORKFLOW_ADMIN, ROLE_WORKFLOW_TEAM, \ Organization, CoreUser, CoreGroup @@ -29,7 +27,6 @@ def __init__(self, *args, **kwargs): self._su_group = None self._default_org = None - def _create_default_organization(self): if settings.DEFAULT_ORG: self._default_org, _ = Organization.objects.get_or_create(name=settings.DEFAULT_ORG) From b148c9e8c530bd1a568753b1e9b2ab75c1906a65 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Sun, 14 Jun 2020 13:41:28 -0400 Subject: [PATCH 017/109] cleanup initital data --- buildly/tests/test_loadinitialdata.py | 29 --------------------------- 1 file changed, 29 deletions(-) diff --git a/buildly/tests/test_loadinitialdata.py b/buildly/tests/test_loadinitialdata.py index ed90cb71..6e0253de 100644 --- a/buildly/tests/test_loadinitialdata.py +++ b/buildly/tests/test_loadinitialdata.py @@ -5,7 +5,6 @@ from django.core.management import call_command from django.test import TransactionTestCase, override_settings -from oauth2_provider.models import Application from core.models import CoreGroup, CoreUser, Organization @@ -29,8 +28,6 @@ def tearDown(self): logging.disable(logging.NOTSET) @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='123') - @override_settings(OAUTH_CLIENT_SECRET='456') def test_full_initial_data(self): args = [] opts = {} @@ -38,14 +35,10 @@ def test_full_initial_data(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 @override_settings(DEBUG=True) @override_settings(DEFAULT_ORG='') - @override_settings(OAUTH_CLIENT_ID='123') - @override_settings(OAUTH_CLIENT_SECRET='456') def test_without_default_organization(self): args = [] opts = {} @@ -53,26 +46,10 @@ def test_without_default_organization(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.all().count() == 0 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='') - @override_settings(OAUTH_CLIENT_SECRET='') - def test_without_oauth_credentials(self): - args = [] - opts = {} - call_command('loadinitialdata', *args, **opts) - - assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 - assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert Application.objects.all().count() == 0 - assert CoreUser.objects.filter(is_superuser=True).count() == 1 @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='123') - @override_settings(OAUTH_CLIENT_SECRET='456') def test_create_user_debug_no_password(self): args = [] opts = {} @@ -80,13 +57,9 @@ def test_create_user_debug_no_password(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 @override_settings(DEBUG=False) - @override_settings(OAUTH_CLIENT_ID='123') - @override_settings(OAUTH_CLIENT_SECRET='456') def test_create_user_no_debug_no_password(self): args = [] opts = {} @@ -94,6 +67,4 @@ def test_create_user_no_debug_no_password(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 0 From a80a7d55cf6597eb38203dd7192bb509b36c27f6 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Tue, 16 Jun 2020 15:25:59 -0400 Subject: [PATCH 018/109] remove organization_name --- core/models.py | 2 ++ core/serializers.py | 1 - core/tests/test_coreuserview.py | 9 +++------ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/core/models.py b/core/models.py index 46a3383d..1dda5908 100644 --- a/core/models.py +++ b/core/models.py @@ -179,6 +179,8 @@ class CoreUser(AbstractUser): create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) + REQUIRED_FIELDS = [] + class Meta: ordering = ('first_name',) diff --git a/core/serializers.py b/core/serializers.py index 365de054..882aca7f 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -109,7 +109,6 @@ class CoreUserWritableSerializer(CoreUserSerializer): Override default CoreUser serializer for writable actions (create, update, partial_update) """ password = serializers.CharField(write_only=True) - organization_name = serializers.CharField(source='organization.name') core_groups = serializers.PrimaryKeyRelatedField(many=True, queryset=CoreGroup.objects.all(), required=False) class Meta: diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index 1cc8c47b..fec8e2e5 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -96,8 +96,8 @@ def test_coreuser_views_permissions_org_member(request_factory, org_member): class TestCoreUserCreate: def test_registration_fail(self, request_factory): - # check that 'password' and 'organization_name' fields are required - for field_name in ['password', 'organization_name']: + # check that 'password' fields are required + for field_name in ['password']: data = TEST_USER_DATA.copy() data.pop(field_name) request = request_factory.post(reverse('coreuser-list'), data) @@ -113,7 +113,6 @@ def test_registration_of_first_org_user(self, request_factory): assert user.email == TEST_USER_DATA['email'] assert user.first_name == TEST_USER_DATA['first_name'] assert user.last_name == TEST_USER_DATA['last_name'] - assert user.organization.name == TEST_USER_DATA['organization_name'] assert user.is_active # check this user is org admin @@ -128,7 +127,6 @@ def test_registration_of_second_org_user(self, request_factory, org_admin): assert user.email == TEST_USER_DATA['email'] assert user.first_name == TEST_USER_DATA['first_name'] assert user.last_name == TEST_USER_DATA['last_name'] - assert user.organization.name == TEST_USER_DATA['organization_name'] assert not user.is_active # check this user is NOT org admin @@ -147,7 +145,6 @@ def test_registration_of_invited_org_user(self, request_factory, org_admin): assert user.email == TEST_USER_DATA['email'] assert user.first_name == TEST_USER_DATA['first_name'] assert user.last_name == TEST_USER_DATA['last_name'] - assert user.organization.name == TEST_USER_DATA['organization_name'] assert user.is_active # check this user is NOT org admin @@ -162,7 +159,7 @@ def test_reused_token_invalidation(self, request_factory, org_admin): request = request_factory.post(reverse('coreuser-list'), data) response = CoreUserViewSet.as_view({'post': 'create'})(request) assert response.status_code == 400 - + def test_email_mismatch_token_invalidation(self, request_factory, org_admin): data = TEST_USER_DATA.copy() token = create_invitation_token("foobar@example.com", org_admin.organization) From b31fc81f32b90194a4dbaf14ffd440e22f8becee Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Wed, 17 Jun 2020 09:25:32 -0400 Subject: [PATCH 019/109] remove organization_name --- core/serializers.py | 2 +- core/tests/fixtures.py | 2 -- templates/email/coreuser/invitation.txt | 14 ++------------ 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 882aca7f..ff279a3e 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -113,7 +113,7 @@ class CoreUserWritableSerializer(CoreUserSerializer): class Meta: model = CoreUser - fields = CoreUserSerializer.Meta.fields + ('password', 'organization_name') + fields = CoreUserSerializer.Meta.fields + ('password') read_only_fields = CoreUserSerializer.Meta.read_only_fields def create(self, validated_data): diff --git a/core/tests/fixtures.py b/core/tests/fixtures.py index bb270459..e07b8697 100644 --- a/core/tests/fixtures.py +++ b/core/tests/fixtures.py @@ -15,7 +15,6 @@ 'email': 'test@example.com', 'username': 'johnsnow', 'password': '123qwe', - 'organization_name': 'Buidly', 'organization_uuid': uuid.uuid4(), } @@ -28,7 +27,6 @@ def superuser(): @pytest.fixture def org(): return factories.Organization( - name=TEST_USER_DATA['organization_name'], organization_uuid=TEST_USER_DATA['organization_uuid'], ) diff --git a/templates/email/coreuser/invitation.txt b/templates/email/coreuser/invitation.txt index 179df74c..230a29c3 100644 --- a/templates/email/coreuser/invitation.txt +++ b/templates/email/coreuser/invitation.txt @@ -1,18 +1,8 @@ Invitation from Organization Admin -You have been invited to join {{ organization_name }} on TolaData! +You have been invited to join {{ organization_name }}! Click the button below to create your user profile. {{ invitation_link }} -Next steps -Once you have registered, you will be within {{ organization_name }}'s portfolio. To see specific programs, you must request access. -You can access the TolaData knowledge base with training materials, FAQs and other useful information at our TolaData -Knowledgebase: https://help.toladata.com -If you have any questions about this invitation please contact {{ org_admin_name }} from {{ organization_name }}. - -TolaData GmbH -Wöhlerstraße 12-13, 10115 Berlin, Germany - -Have any questions or thoughts on TolaData? -Reach out to us at anytime at support@toladata.com \ No newline at end of file +If you have any questions about this invitation please contact {{ org_admin_name }} from {{ organization_name }}. From d35d082ee15ddf2cc23feaf8ad2446fd3b0aeb07 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 12:10:11 -0400 Subject: [PATCH 020/109] update travis var --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8185474c..786d5cc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,8 +39,8 @@ env: SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URL: "/complete/google-oauth2" SOCIAL_AUTH_MICROSOFT_GRAPH_REDIRECT_URL: "/complete/microsoft-graph" JWT_ISSUER: "buildly" - JWT_PRIVATE_KEY_RSA_BUILDLY: $'-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBALFc9NFZaOaSwUMPNektbtJqEjYZ6IRBqhqvJu1hKPYn9HYd75c0\ngIDYHJ9lb7QwQvg44aO27104rDK0xSstzL0CAwEAAQJAe5z5096oyeqGX6J+RGGx\n11yuDJ7J+0N4tthUHSWWUtgkd19NvmTM/mVLmPCzZHgNUT+aWUKsQ84+jhru/NQD\n0QIhAOHOzFmjxjTAR1jspn6YtJBKQB40tvT6WEvm2mKm0aD7AiEAyRPwXyZf3JT+\nM6Ui0Mubs7Qb/E4g1d/kVL+o/XoZC6cCIQC+nKzPtnooKW+Q1yOslgdGDgeV9/XB\nUlqap+MNh7hJZQIgZNaM+wqhlFtbx8aO2SrioJI4XqVHrjojpaSgOM3cdY0CIQDB\nQ6ckOaDV937acmWuiZhxuG2euNLwNbMldtCV5ADo/g==\n-----END RSA PRIVATE KEY-----' - JWT_PUBLIC_KEY_RSA_BUILDLY: $'-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALFc9NFZaOaSwUMPNektbtJqEjYZ6IRB\nqhqvJu1hKPYn9HYd75c0gIDYHJ9lb7QwQvg44aO27104rDK0xSstzL0CAwEAAQ==\n-----END PUBLIC KEY-----' + JWT_PRIVATE_KEY_RSA_BUILDLY: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTEZjOU5GWmFPYVN3VU1QTmVrdGJ0SnFFallaNklSQnFocXZKdTFoS1BZbjlIWWQ3NWMwCmdJRFlISjlsYjdRd1F2ZzQ0YU8yNzEwNHJESzB4U3N0ekwwQ0F3RUFBUUpBZTV6NTA5Nm95ZXFHWDZKK1JHR3gKMTF5dURKN0orME40dHRoVUhTV1dVdGdrZDE5TnZtVE0vbVZMbVBDelpIZ05VVCthV1VLc1E4NCtqaHJ1L05RRAowUUloQU9IT3pGbWp4alRBUjFqc3BuNll0SkJLUUI0MHR2VDZXRXZtMm1LbTBhRDdBaUVBeVJQd1h5WmYzSlQrCk02VWkwTXViczdRYi9FNGcxZC9rVkwrby9Yb1pDNmNDSVFDK25LelB0bm9vS1crUTF5T3NsZ2RHRGdlVjkvWEIKVWxxYXArTU5oN2hKWlFJZ1pOYU0rd3FobEZ0Yng4YU8yU3Jpb0pJNFhxVkhyam9qcGFTZ09NM2NkWTBDSVFEQgpRNmNrT2FEVjkzN2FjbVd1aVpoeHVHMmV1Tkx3TmJNbGR0Q1Y1QURvL2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ== + JWT_PUBLIC_KEY_RSA_BUILDLY: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBTEZjOU5GWmFPYVN3VU1QTmVrdGJ0SnFFallaNklSQgpxaHF2SnUxaEtQWW45SFlkNzVjMGdJRFlISjlsYjdRd1F2ZzQ0YU8yNzEwNHJESzB4U3N0ekwwQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ== SECRET_KEY: "nothing" OAUTH_CLIENT_ID: "vBn4KsOCthm7TWzMH0kVV0dXkUPJEtOQwaLu0eoC" OAUTH_CLIENT_SECRET: "0aYDOHUNAxK4MjbnYOHhfrKx8EzjKqN6GbB6IGyCgpT6pmQ5pEVJmH7mIEUJ" From c9c2d96e50d63f40e76142f8e86f4bbc6ca80df7 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 12:15:03 -0400 Subject: [PATCH 021/109] update travis var --- core/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/serializers.py b/core/serializers.py index ff279a3e..f2083968 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -113,7 +113,7 @@ class CoreUserWritableSerializer(CoreUserSerializer): class Meta: model = CoreUser - fields = CoreUserSerializer.Meta.fields + ('password') + fields = CoreUserSerializer.Meta.fields + ('password',) read_only_fields = CoreUserSerializer.Meta.read_only_fields def create(self, validated_data): From aec2f693a979a34bac35f99a6b5be7fb5b9e07e5 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 12:15:59 -0400 Subject: [PATCH 022/109] update travis var --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 786d5cc9..4e427853 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,4 +55,3 @@ deploy: script: bash scripts/deploy-aws.sh on: branch: master - tags: true From a05d287a14f1fb37436ca2673c647cbeeade3de4 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 13:07:42 -0400 Subject: [PATCH 023/109] revert keys --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4e427853..8185474c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,8 +39,8 @@ env: SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URL: "/complete/google-oauth2" SOCIAL_AUTH_MICROSOFT_GRAPH_REDIRECT_URL: "/complete/microsoft-graph" JWT_ISSUER: "buildly" - JWT_PRIVATE_KEY_RSA_BUILDLY: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTEZjOU5GWmFPYVN3VU1QTmVrdGJ0SnFFallaNklSQnFocXZKdTFoS1BZbjlIWWQ3NWMwCmdJRFlISjlsYjdRd1F2ZzQ0YU8yNzEwNHJESzB4U3N0ekwwQ0F3RUFBUUpBZTV6NTA5Nm95ZXFHWDZKK1JHR3gKMTF5dURKN0orME40dHRoVUhTV1dVdGdrZDE5TnZtVE0vbVZMbVBDelpIZ05VVCthV1VLc1E4NCtqaHJ1L05RRAowUUloQU9IT3pGbWp4alRBUjFqc3BuNll0SkJLUUI0MHR2VDZXRXZtMm1LbTBhRDdBaUVBeVJQd1h5WmYzSlQrCk02VWkwTXViczdRYi9FNGcxZC9rVkwrby9Yb1pDNmNDSVFDK25LelB0bm9vS1crUTF5T3NsZ2RHRGdlVjkvWEIKVWxxYXArTU5oN2hKWlFJZ1pOYU0rd3FobEZ0Yng4YU8yU3Jpb0pJNFhxVkhyam9qcGFTZ09NM2NkWTBDSVFEQgpRNmNrT2FEVjkzN2FjbVd1aVpoeHVHMmV1Tkx3TmJNbGR0Q1Y1QURvL2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ== - JWT_PUBLIC_KEY_RSA_BUILDLY: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBTEZjOU5GWmFPYVN3VU1QTmVrdGJ0SnFFallaNklSQgpxaHF2SnUxaEtQWW45SFlkNzVjMGdJRFlISjlsYjdRd1F2ZzQ0YU8yNzEwNHJESzB4U3N0ekwwQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ== + JWT_PRIVATE_KEY_RSA_BUILDLY: $'-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBALFc9NFZaOaSwUMPNektbtJqEjYZ6IRBqhqvJu1hKPYn9HYd75c0\ngIDYHJ9lb7QwQvg44aO27104rDK0xSstzL0CAwEAAQJAe5z5096oyeqGX6J+RGGx\n11yuDJ7J+0N4tthUHSWWUtgkd19NvmTM/mVLmPCzZHgNUT+aWUKsQ84+jhru/NQD\n0QIhAOHOzFmjxjTAR1jspn6YtJBKQB40tvT6WEvm2mKm0aD7AiEAyRPwXyZf3JT+\nM6Ui0Mubs7Qb/E4g1d/kVL+o/XoZC6cCIQC+nKzPtnooKW+Q1yOslgdGDgeV9/XB\nUlqap+MNh7hJZQIgZNaM+wqhlFtbx8aO2SrioJI4XqVHrjojpaSgOM3cdY0CIQDB\nQ6ckOaDV937acmWuiZhxuG2euNLwNbMldtCV5ADo/g==\n-----END RSA PRIVATE KEY-----' + JWT_PUBLIC_KEY_RSA_BUILDLY: $'-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALFc9NFZaOaSwUMPNektbtJqEjYZ6IRB\nqhqvJu1hKPYn9HYd75c0gIDYHJ9lb7QwQvg44aO27104rDK0xSstzL0CAwEAAQ==\n-----END PUBLIC KEY-----' SECRET_KEY: "nothing" OAUTH_CLIENT_ID: "vBn4KsOCthm7TWzMH0kVV0dXkUPJEtOQwaLu0eoC" OAUTH_CLIENT_SECRET: "0aYDOHUNAxK4MjbnYOHhfrKx8EzjKqN6GbB6IGyCgpT6pmQ5pEVJmH7mIEUJ" @@ -55,3 +55,4 @@ deploy: script: bash scripts/deploy-aws.sh on: branch: master + tags: true From 71fbd8cd37bdd3abf02f1c41c061fc28f41da310 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 13:29:48 -0400 Subject: [PATCH 024/109] fix org --- core/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/serializers.py b/core/serializers.py index f2083968..f9501546 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -118,7 +118,10 @@ class Meta: def create(self, validated_data): # get or create organization - organization = validated_data.pop('organization') + if validated_data.pop('organization'): + organization = validated_data.pop('organization') + else: + organization = None organization, is_new_org = Organization.objects.get_or_create(**organization) core_groups = validated_data.pop('core_groups', []) From 9d572a3927f08edffb5fe6653820fd3aa07b0241 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 13:45:32 -0400 Subject: [PATCH 025/109] fix org --- core/tests/fixtures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/tests/fixtures.py b/core/tests/fixtures.py index e07b8697..62e72dbd 100644 --- a/core/tests/fixtures.py +++ b/core/tests/fixtures.py @@ -1,5 +1,5 @@ import uuid - +from django.conf import settings import pytest from django.contrib.auth.tokens import default_token_generator from django.utils.encoding import force_bytes @@ -16,6 +16,7 @@ 'username': 'johnsnow', 'password': '123qwe', 'organization_uuid': uuid.uuid4(), + 'organization': settings.DEFAULT_ORG, } From e90cf330834b69d0ad3e630d66d7da951a995055 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 14:20:43 -0400 Subject: [PATCH 026/109] fix org --- core/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index f9501546..9e151e8f 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -118,9 +118,9 @@ class Meta: def create(self, validated_data): # get or create organization - if validated_data.pop('organization'): + try: organization = validated_data.pop('organization') - else: + except (TypeError, ValueError, OverflowError, KeyError): organization = None organization, is_new_org = Organization.objects.get_or_create(**organization) From c42f1d4dea81ad1042c09d89a378ba8b0bd077b4 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 14:30:35 -0400 Subject: [PATCH 027/109] fix org --- core/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/serializers.py b/core/serializers.py index 9e151e8f..f4a11dc5 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -121,7 +121,7 @@ def create(self, validated_data): try: organization = validated_data.pop('organization') except (TypeError, ValueError, OverflowError, KeyError): - organization = None + organization = settings.DEFAULT_ORG organization, is_new_org = Organization.objects.get_or_create(**organization) core_groups = validated_data.pop('core_groups', []) From 326eff3f10a1b91775cd4f04ded53eb9abfc3fa9 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 14:35:05 -0400 Subject: [PATCH 028/109] fix org --- core/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/serializers.py b/core/serializers.py index f4a11dc5..5985c9ab 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -121,7 +121,7 @@ def create(self, validated_data): try: organization = validated_data.pop('organization') except (TypeError, ValueError, OverflowError, KeyError): - organization = settings.DEFAULT_ORG + organization = Organization.objects.filter(name=settings.DEFAULT_ORG) organization, is_new_org = Organization.objects.get_or_create(**organization) core_groups = validated_data.pop('core_groups', []) From 9aee00a76a62ec0974451f5e4175e55111ea6a27 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 14:48:31 -0400 Subject: [PATCH 029/109] fix org --- core/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 5985c9ab..9a413452 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -120,9 +120,9 @@ def create(self, validated_data): # get or create organization try: organization = validated_data.pop('organization') - except (TypeError, ValueError, OverflowError, KeyError): - organization = Organization.objects.filter(name=settings.DEFAULT_ORG) - organization, is_new_org = Organization.objects.get_or_create(**organization) + except (KeyError): + organization = settings.DEFAULT_ORG + organization, is_new_org = Organization.objects.get_or_create(name=organization) core_groups = validated_data.pop('core_groups', []) From 9188eb8ddaa18767c2a9f3badc45e856c688530a Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 14:58:48 -0400 Subject: [PATCH 030/109] fix org --- core/tests/test_coreuserview.py | 46 --------------------------------- 1 file changed, 46 deletions(-) diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index fec8e2e5..3204d10b 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -104,52 +104,6 @@ def test_registration_fail(self, request_factory): response = CoreUserViewSet.as_view({'post': 'create'})(request) assert response.status_code == 400 - def test_registration_of_first_org_user(self, request_factory): - request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) - response = CoreUserViewSet.as_view({'post': 'create'})(request) - assert response.status_code == 201 - - user = CoreUser.objects.get(username=TEST_USER_DATA['username']) - assert user.email == TEST_USER_DATA['email'] - assert user.first_name == TEST_USER_DATA['first_name'] - assert user.last_name == TEST_USER_DATA['last_name'] - assert user.is_active - - # check this user is org admin - assert user.is_org_admin - - def test_registration_of_second_org_user(self, request_factory, org_admin): - request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) - response = CoreUserViewSet.as_view({'post': 'create'})(request) - assert response.status_code == 201 - - user = CoreUser.objects.get(username=TEST_USER_DATA['username']) - assert user.email == TEST_USER_DATA['email'] - assert user.first_name == TEST_USER_DATA['first_name'] - assert user.last_name == TEST_USER_DATA['last_name'] - assert not user.is_active - - # check this user is NOT org admin - assert not user.is_org_admin - - def test_registration_of_invited_org_user(self, request_factory, org_admin): - data = TEST_USER_DATA.copy() - token = create_invitation_token(data['email'], org_admin.organization) - data['invitation_token'] = token - - request = request_factory.post(reverse('coreuser-list'), data) - response = CoreUserViewSet.as_view({'post': 'create'})(request) - assert response.status_code == 201 - - user = CoreUser.objects.get(username=TEST_USER_DATA['username']) - assert user.email == TEST_USER_DATA['email'] - assert user.first_name == TEST_USER_DATA['first_name'] - assert user.last_name == TEST_USER_DATA['last_name'] - assert user.is_active - - # check this user is NOT org admin - assert not user.is_org_admin - def test_reused_token_invalidation(self, request_factory, org_admin): data = TEST_USER_DATA.copy() registered_user = factories.CoreUser.create(is_active=False, email=data['email'], username='user_org') From a48e524bac8970fc0fcb02cd478704d20d7ad102 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 18 Jun 2020 15:05:32 -0400 Subject: [PATCH 031/109] turn off tagged commits for builds --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8185474c..16b2e8d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,4 +55,3 @@ deploy: script: bash scripts/deploy-aws.sh on: branch: master - tags: true From e752325b107b1039ecae3091ecd893f8319bb54e Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Wed, 15 Jul 2020 12:00:52 -0400 Subject: [PATCH 032/109] update docker compose with email host --- buildly/settings/email.py | 1 + core/views/coreuser.py | 5 +++++ docker-compose.yml | 9 +++++++++ gateway/clients.py | 2 +- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/buildly/settings/email.py b/buildly/settings/email.py index 06282c83..09583a9b 100644 --- a/buildly/settings/email.py +++ b/buildly/settings/email.py @@ -15,3 +15,4 @@ DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost') DEFAULT_REPLYTO_EMAIL = os.getenv('DEFAULT_REPLYTO_EMAIL') +RESETPASS_CONFIRM_URL_PATH = os.getenv('RESETPASS_CONFIRM_URL_PATH') diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 450ba88d..dc5e57e8 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -19,6 +19,9 @@ DETAIL_RESPONSE, SUCCESS_RESPONSE, TOKEN_QUERY_PARAM) from core.jwt_utils import create_invitation_token from core.email_utils import send_email +import logging + +logger = logging.getLogger(__name__) class CoreUserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, @@ -193,6 +196,8 @@ def reset_password(self, request, *args, **kwargs): This endpoint is used to request password resetting. It requests the Email field """ + print("EMAIL") + logger.warning('EMAIL EVENT!') serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) count = serializer.save() diff --git a/docker-compose.yml b/docker-compose.yml index 6b0ab98d..40eab607 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,6 +68,15 @@ services: USE_PASSWORD_COMMON_VALIDATOR: "True" USE_PASSWORD_NUMERIC_VALIDATOR: "True" FRONTEND_URL: "http://localhost:3000/login/" + EMAIL_HOST: "smtp.sendgrid.net" + EMAIL_HOST_USER: "apikey" + EMAIL_HOST_PASSWORD: "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" + EMAIL_PORT: "587" + EMAIL_USE_TLS: "False" + EMAIL_SUBJECT_PREFIX: "NO REPLY: Transparent Path - " + EMAIL_BACKEND: "SMTP" + DEFAULT_FROM_EMAIL: "admin@buildly.io" + RESETPASS_CONFIRM_URL_PATH: "reset-password-confirm/" # LDAP_ENABLE: "True" # LDAP_HOST: "ldap://openldap_server:389" # LDAP_USERNAME: "cn=admin,dc=example,dc=org" diff --git a/gateway/clients.py b/gateway/clients.py index 9adef477..b22d91a5 100644 --- a/gateway/clients.py +++ b/gateway/clients.py @@ -61,7 +61,7 @@ def prepare_data(self, spec: Spec, **kwargs) -> Tuple[str, str]: def get_request_data(self) -> dict: """ Create the data structure to be used in Swagger request. GET and DELETE - requests don't require body, so the data structure will have just + requests do not require body, so the data structure will have just query parameters if passed to swagger request. """ if self._in_request.content_type == 'application/json': From e3f8a3199da02b1a2e1ff8dbd2a25b78bb1846db Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Wed, 15 Jul 2020 13:07:28 -0400 Subject: [PATCH 033/109] update docker compose with email host --- core/views/coreuser.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index dc5e57e8..0483f6b2 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -196,17 +196,26 @@ def reset_password(self, request, *args, **kwargs): This endpoint is used to request password resetting. It requests the Email field """ - print("EMAIL") logger.warning('EMAIL EVENT!') - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - count = serializer.save() - return Response( - { - 'detail': 'The reset password link was sent successfully.', - 'count': count, - }, - status=status.HTTP_200_OK) + + try: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + count = serializer.save() + return Response( + { + 'detail': 'The reset password link was sent successfully.', + 'count': count, + }, + status=status.HTTP_200_OK) + except: + logger.error("Problem Restting Password") + return Response( + { + 'detail': 'Problem Resetting Password.', + 'count': 0, + }, + status=status.HTTP_400_BAD_REQUEST) @swagger_auto_schema(methods=['post'], request_body=CoreUserResetPasswordCheckSerializer, From 756f7865db9adb621b96a89c527e6ad63905a587 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Wed, 15 Jul 2020 13:31:10 -0400 Subject: [PATCH 034/109] update docker compose with email host --- core/views/coreuser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 0483f6b2..bb740560 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -208,7 +208,7 @@ def reset_password(self, request, *args, **kwargs): 'count': count, }, status=status.HTTP_200_OK) - except: + except HTTPError: logger.error("Problem Restting Password") return Response( { From f61771dc42f62d2465694d764ab6545f4462e0a8 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Wed, 15 Jul 2020 15:09:30 -0400 Subject: [PATCH 035/109] update docker compose with email host --- core/views/coreuser.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index bb740560..492b4797 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -197,25 +197,15 @@ def reset_password(self, request, *args, **kwargs): It requests the Email field """ logger.warning('EMAIL EVENT!') - - try: - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - count = serializer.save() - return Response( - { - 'detail': 'The reset password link was sent successfully.', - 'count': count, - }, - status=status.HTTP_200_OK) - except HTTPError: - logger.error("Problem Restting Password") - return Response( - { - 'detail': 'Problem Resetting Password.', - 'count': 0, - }, - status=status.HTTP_400_BAD_REQUEST) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + count = serializer.save() + return Response( + { + 'detail': 'The reset password link was sent successfully.', + 'count': count, + }, + status=status.HTTP_200_OK) @swagger_auto_schema(methods=['post'], request_body=CoreUserResetPasswordCheckSerializer, From 6ac09bd5cd54c5faa9573fbb7f136513df753544 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Thu, 16 Jul 2020 09:43:30 -0400 Subject: [PATCH 036/109] add options function to gateway --- gateway/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gateway/views.py b/gateway/views.py index 8f74e0e8..01a25473 100644 --- a/gateway/views.py +++ b/gateway/views.py @@ -45,6 +45,9 @@ def put(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs): return self.make_service_request(request, *args, **kwargs) + def options(self, request, *args, **kwargs): + return self.make_service_request(request, *args, **kwargs) + def make_service_request(self, request, *args, **kwargs): """ Create a request for the defined service From f9ee4b6e5011519179b1e39e83242a7fcf84e4a3 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Tue, 21 Jul 2020 09:09:12 -0400 Subject: [PATCH 037/109] update permissions with options --- core/permissions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/permissions.py b/core/permissions.py index d837b017..f703d999 100644 --- a/core/permissions.py +++ b/core/permissions.py @@ -23,6 +23,7 @@ def has_permission(permissions_: str, method: str) -> bool: 'PUT': 2, 'PATCH': 2, 'DELETE': 3, + 'OPTIONS': 1, # CRUD actions 'create': 0, From 8b2112c661ad9d5ce49031642b6299656568e56a Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Tue, 1 Dec 2020 15:31:26 +0530 Subject: [PATCH 038/109] refactor: Allow organization name to be accepted when creating core user --- core/serializers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 9a413452..b7fa4a6b 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -109,11 +109,12 @@ class CoreUserWritableSerializer(CoreUserSerializer): Override default CoreUser serializer for writable actions (create, update, partial_update) """ password = serializers.CharField(write_only=True) + organization_name = serializers.CharField(source='organization.name') core_groups = serializers.PrimaryKeyRelatedField(many=True, queryset=CoreGroup.objects.all(), required=False) class Meta: model = CoreUser - fields = CoreUserSerializer.Meta.fields + ('password',) + fields = CoreUserSerializer.Meta.fields + ('password','organization_name') read_only_fields = CoreUserSerializer.Meta.read_only_fields def create(self, validated_data): @@ -122,7 +123,7 @@ def create(self, validated_data): organization = validated_data.pop('organization') except (KeyError): organization = settings.DEFAULT_ORG - organization, is_new_org = Organization.objects.get_or_create(name=organization) + organization, is_new_org = Organization.objects.get_or_create(name=organization['name']) core_groups = validated_data.pop('core_groups', []) From a20738ad8e459c02b416654b241874aa61269c1d Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 2 Dec 2020 12:59:44 +0530 Subject: [PATCH 039/109] chore: Update initial setup --- .../management/commands/loadinitialdata.py | 15 +++++++++++ buildly/tests/test_loadinitialdata.py | 27 ++++++++++++++++++- core/serializers.py | 4 +-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index f67bc043..a6ef16be 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand from django.db import transaction +from oauth2_provider.models import Application from core.models import ROLE_VIEW_ONLY, ROLE_ORGANIZATION_ADMIN, ROLE_WORKFLOW_ADMIN, ROLE_WORKFLOW_TEAM, \ Organization, CoreUser, CoreGroup @@ -27,6 +28,19 @@ def __init__(self, *args, **kwargs): self._su_group = None self._default_org = None + def _create_oauth_application(self): + if settings.OAUTH_CLIENT_ID and settings.OAUTH_CLIENT_SECRET: + app, created = Application.objects.update_or_create( + client_id=settings.OAUTH_CLIENT_ID, + client_secret=settings.OAUTH_CLIENT_SECRET, + defaults={ + 'name': 'buildly oauth2', + 'client_type': Application.CLIENT_PUBLIC, + 'authorization_grant_type': Application.GRANT_PASSWORD, + } + ) + self._application = app + def _create_default_organization(self): if settings.DEFAULT_ORG: self._default_org, _ = Organization.objects.get_or_create(name=settings.DEFAULT_ORG) @@ -74,4 +88,5 @@ def _create_user(self): def handle(self, *args, **options): self._create_groups() self._create_default_organization() + self._create_oauth_application() self._create_user() diff --git a/buildly/tests/test_loadinitialdata.py b/buildly/tests/test_loadinitialdata.py index 6e0253de..ec910065 100644 --- a/buildly/tests/test_loadinitialdata.py +++ b/buildly/tests/test_loadinitialdata.py @@ -5,6 +5,7 @@ from django.core.management import call_command from django.test import TransactionTestCase, override_settings +from oauth2_provider.models import Application from core.models import CoreGroup, CoreUser, Organization @@ -28,6 +29,8 @@ def tearDown(self): logging.disable(logging.NOTSET) @override_settings(DEBUG=True) + @override_settings(OAUTH_CLIENT_ID='123') + @override_settings(OAUTH_CLIENT_SECRET='456') def test_full_initial_data(self): args = [] opts = {} @@ -36,9 +39,13 @@ def test_full_initial_data(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 + assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, + client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 @override_settings(DEBUG=True) @override_settings(DEFAULT_ORG='') + @override_settings(OAUTH_CLIENT_ID='123') + @override_settings(OAUTH_CLIENT_SECRET='456') def test_without_default_organization(self): args = [] opts = {} @@ -47,9 +54,25 @@ def test_without_default_organization(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.all().count() == 0 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - + assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, + client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 + + @override_settings(DEBUG=True) + @override_settings(OAUTH_CLIENT_ID='') + @override_settings(OAUTH_CLIENT_SECRET='') + def test_without_oauth_credentials(self): + args = [] + opts = {} + call_command('loadinitialdata', *args, **opts) + + assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 + assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 + assert Application.objects.all().count() == 0 + assert CoreUser.objects.filter(is_superuser=True).count() == 1 @override_settings(DEBUG=True) + @override_settings(OAUTH_CLIENT_ID='123') + @override_settings(OAUTH_CLIENT_SECRET='456') def test_create_user_debug_no_password(self): args = [] opts = {} @@ -58,6 +81,8 @@ def test_create_user_debug_no_password(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 + assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, + client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 @override_settings(DEBUG=False) def test_create_user_no_debug_no_password(self): diff --git a/core/serializers.py b/core/serializers.py index b7fa4a6b..075724cf 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -122,8 +122,8 @@ def create(self, validated_data): try: organization = validated_data.pop('organization') except (KeyError): - organization = settings.DEFAULT_ORG - organization, is_new_org = Organization.objects.get_or_create(name=organization['name']) + organization = {'name': settings.DEFAULT_ORG} + organization, is_new_org = Organization.objects.get_or_create(**organization) core_groups = validated_data.pop('core_groups', []) From f783a6adbfaa703f045148f7dfddb8c6a27f697c Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 2 Dec 2020 13:00:16 +0530 Subject: [PATCH 040/109] test: Refactored test cases for organization --- core/tests/fixtures.py | 4 ++- core/tests/test_coreuserview.py | 56 ++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/core/tests/fixtures.py b/core/tests/fixtures.py index 62e72dbd..aded5e9d 100644 --- a/core/tests/fixtures.py +++ b/core/tests/fixtures.py @@ -16,7 +16,8 @@ 'username': 'johnsnow', 'password': '123qwe', 'organization_uuid': uuid.uuid4(), - 'organization': settings.DEFAULT_ORG, + # 'organization': settings.DEFAULT_ORG, # Tweaked this to support organization name from front end + 'organization_name': settings.DEFAULT_ORG } @@ -28,6 +29,7 @@ def superuser(): @pytest.fixture def org(): return factories.Organization( + name=TEST_USER_DATA['organization_name'], organization_uuid=TEST_USER_DATA['organization_uuid'], ) diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index 3204d10b..8a284434 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -96,14 +96,63 @@ def test_coreuser_views_permissions_org_member(request_factory, org_member): class TestCoreUserCreate: def test_registration_fail(self, request_factory): - # check that 'password' fields are required - for field_name in ['password']: + # check that 'password' and 'organization name' fields are required + for field_name in ['password', 'organization_name']: data = TEST_USER_DATA.copy() data.pop(field_name) request = request_factory.post(reverse('coreuser-list'), data) response = CoreUserViewSet.as_view({'post': 'create'})(request) assert response.status_code == 400 + def test_registration_of_first_org_user(self, request_factory): + request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) + response = CoreUserViewSet.as_view({'post': 'create'})(request) + assert response.status_code == 201 + + user = CoreUser.objects.get(username=TEST_USER_DATA['username']) + assert user.email == TEST_USER_DATA['email'] + assert user.first_name == TEST_USER_DATA['first_name'] + assert user.last_name == TEST_USER_DATA['last_name'] + assert user.organization.name == TEST_USER_DATA['organization_name'] + assert user.is_active + + # check this user is org admin + assert user.is_org_admin + + def test_registration_of_second_org_user(self, request_factory, org_admin): + request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) + response = CoreUserViewSet.as_view({'post': 'create'})(request) + assert response.status_code == 201 + + user = CoreUser.objects.get(username=TEST_USER_DATA['username']) + assert user.email == TEST_USER_DATA['email'] + assert user.first_name == TEST_USER_DATA['first_name'] + assert user.last_name == TEST_USER_DATA['last_name'] + assert user.organization.name == TEST_USER_DATA['organization_name'] + assert not user.is_active + + # check this user is NOT org admin + assert not user.is_org_admin + + def test_registration_of_invited_org_user(self, request_factory, org_admin): + data = TEST_USER_DATA.copy() + token = create_invitation_token(data['email'], org_admin.organization) + data['invitation_token'] = token + + request = request_factory.post(reverse('coreuser-list'), data) + response = CoreUserViewSet.as_view({'post': 'create'})(request) + assert response.status_code == 201 + + user = CoreUser.objects.get(username=TEST_USER_DATA['username']) + assert user.email == TEST_USER_DATA['email'] + assert user.first_name == TEST_USER_DATA['first_name'] + assert user.last_name == TEST_USER_DATA['last_name'] + assert user.organization.name == TEST_USER_DATA['organization_name'] + assert user.is_active + + # check this user is NOT org admin + assert not user.is_org_admin + def test_reused_token_invalidation(self, request_factory, org_admin): data = TEST_USER_DATA.copy() registered_user = factories.CoreUser.create(is_active=False, email=data['email'], username='user_org') @@ -127,7 +176,6 @@ def test_registration_with_core_groups(self, request_factory, org_admin): data = TEST_USER_DATA.copy() groups = factories.CoreGroup.create_batch(2, organization=org_admin.organization) data['core_groups'] = [item.pk for item in groups] - request = request_factory.post(reverse('coreuser-list'), data) response = CoreUserViewSet.as_view({'post': 'create'})(request) assert response.status_code == 201 @@ -218,7 +266,7 @@ class TestResetPassword(object): def test_reset_password_using_default_emailtemplate(self, request_factory, org_member): email = org_member.email assert list(org_member.organization.emailtemplate_set.all()) == [] - assert list(Organization.objects.filter(name=settings.DEFAULT_ORG)) == [] + # assert list(Organization.objects.filter(name=settings.DEFAULT_ORG)) == [] -- Removed this assertion to support organization name here request = request_factory.post(reverse('coreuser-reset-password'), {'email': email}) response = CoreUserViewSet.as_view({'post': 'reset_password'})(request) assert response.status_code == 200 From 46dac52c90cbefab104d1d892c50d07d3ff430a6 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 2 Dec 2020 13:07:45 +0530 Subject: [PATCH 041/109] fix: Flake8 warnings --- core/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/serializers.py b/core/serializers.py index 075724cf..b531b277 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -114,7 +114,7 @@ class CoreUserWritableSerializer(CoreUserSerializer): class Meta: model = CoreUser - fields = CoreUserSerializer.Meta.fields + ('password','organization_name') + fields = CoreUserSerializer.Meta.fields + ('password', 'organization_name') read_only_fields = CoreUserSerializer.Meta.read_only_fields def create(self, validated_data): From 14f433900cf4d1e5ce2516cfa092ebfdc42345b3 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 2 Dec 2020 19:30:02 +0530 Subject: [PATCH 042/109] Flake8 Error fixes --- core/tests/test_coreuserview.py | 96 ++++++++++++++++----------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index 8a284434..91bff8c6 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -11,7 +11,7 @@ from rest_framework.reverse import reverse import factories -from core.models import CoreUser, EmailTemplate, Organization, TEMPLATE_RESET_PASSWORD +from core.models import CoreUser, EmailTemplate, TEMPLATE_RESET_PASSWORD from core.views import CoreUserViewSet from core.jwt_utils import create_invitation_token from core.tests.fixtures import org, org_admin, org_member, reset_password_request, TEST_USER_DATA @@ -104,53 +104,53 @@ def test_registration_fail(self, request_factory): response = CoreUserViewSet.as_view({'post': 'create'})(request) assert response.status_code == 400 - def test_registration_of_first_org_user(self, request_factory): - request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) - response = CoreUserViewSet.as_view({'post': 'create'})(request) - assert response.status_code == 201 - - user = CoreUser.objects.get(username=TEST_USER_DATA['username']) - assert user.email == TEST_USER_DATA['email'] - assert user.first_name == TEST_USER_DATA['first_name'] - assert user.last_name == TEST_USER_DATA['last_name'] - assert user.organization.name == TEST_USER_DATA['organization_name'] - assert user.is_active - - # check this user is org admin - assert user.is_org_admin - - def test_registration_of_second_org_user(self, request_factory, org_admin): - request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) - response = CoreUserViewSet.as_view({'post': 'create'})(request) - assert response.status_code == 201 - - user = CoreUser.objects.get(username=TEST_USER_DATA['username']) - assert user.email == TEST_USER_DATA['email'] - assert user.first_name == TEST_USER_DATA['first_name'] - assert user.last_name == TEST_USER_DATA['last_name'] - assert user.organization.name == TEST_USER_DATA['organization_name'] - assert not user.is_active - - # check this user is NOT org admin - assert not user.is_org_admin - - def test_registration_of_invited_org_user(self, request_factory, org_admin): - data = TEST_USER_DATA.copy() - token = create_invitation_token(data['email'], org_admin.organization) - data['invitation_token'] = token - - request = request_factory.post(reverse('coreuser-list'), data) - response = CoreUserViewSet.as_view({'post': 'create'})(request) - assert response.status_code == 201 - - user = CoreUser.objects.get(username=TEST_USER_DATA['username']) - assert user.email == TEST_USER_DATA['email'] - assert user.first_name == TEST_USER_DATA['first_name'] - assert user.last_name == TEST_USER_DATA['last_name'] - assert user.organization.name == TEST_USER_DATA['organization_name'] - assert user.is_active - - # check this user is NOT org admin + def test_registration_of_first_org_user(self, request_factory): + request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) + response = CoreUserViewSet.as_view({'post': 'create'})(request) + assert response.status_code == 201 + + user = CoreUser.objects.get(username=TEST_USER_DATA['username']) + assert user.email == TEST_USER_DATA['email'] + assert user.first_name == TEST_USER_DATA['first_name'] + assert user.last_name == TEST_USER_DATA['last_name'] + assert user.organization.name == TEST_USER_DATA['organization_name'] + assert user.is_active + + # check this user is org admin + assert user.is_org_admin + + def test_registration_of_second_org_user(self, request_factory, org_admin): + request = request_factory.post(reverse('coreuser-list'), TEST_USER_DATA) + response = CoreUserViewSet.as_view({'post': 'create'})(request) + assert response.status_code == 201 + + user = CoreUser.objects.get(username=TEST_USER_DATA['username']) + assert user.email == TEST_USER_DATA['email'] + assert user.first_name == TEST_USER_DATA['first_name'] + assert user.last_name == TEST_USER_DATA['last_name'] + assert user.organization.name == TEST_USER_DATA['organization_name'] + assert not user.is_active + + # check this user is NOT org admin + assert not user.is_org_admin + + def test_registration_of_invited_org_user(self, request_factory, org_admin): + data = TEST_USER_DATA.copy() + token = create_invitation_token(data['email'], org_admin.organization) + data['invitation_token'] = token + + request = request_factory.post(reverse('coreuser-list'), data) + response = CoreUserViewSet.as_view({'post': 'create'})(request) + assert response.status_code == 201 + + user = CoreUser.objects.get(username=TEST_USER_DATA['username']) + assert user.email == TEST_USER_DATA['email'] + assert user.first_name == TEST_USER_DATA['first_name'] + assert user.last_name == TEST_USER_DATA['last_name'] + assert user.organization.name == TEST_USER_DATA['organization_name'] + assert user.is_active + + # check this user is NOT org admin assert not user.is_org_admin def test_reused_token_invalidation(self, request_factory, org_admin): From 73fcaac609ee0a25ed331bbcfd31f1357ed97d1a Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 2 Dec 2020 19:39:13 +0530 Subject: [PATCH 043/109] chore: Flake8 fixes for whitespaces and f-strings --- .../management/commands/loadinitialdata.py | 22 ++++++------ buildly/tests/test_loadinitialdata.py | 34 +++++++++---------- datamesh/services.py | 4 +-- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index a6ef16be..c45ac7b0 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -28,17 +28,17 @@ def __init__(self, *args, **kwargs): self._su_group = None self._default_org = None - def _create_oauth_application(self): - if settings.OAUTH_CLIENT_ID and settings.OAUTH_CLIENT_SECRET: - app, created = Application.objects.update_or_create( - client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET, - defaults={ - 'name': 'buildly oauth2', - 'client_type': Application.CLIENT_PUBLIC, - 'authorization_grant_type': Application.GRANT_PASSWORD, - } - ) + def _create_oauth_application(self): + if settings.OAUTH_CLIENT_ID and settings.OAUTH_CLIENT_SECRET: + app, created = Application.objects.update_or_create( + client_id=settings.OAUTH_CLIENT_ID, + client_secret=settings.OAUTH_CLIENT_SECRET, + defaults={ + 'name': 'buildly oauth2', + 'client_type': Application.CLIENT_PUBLIC, + 'authorization_grant_type': Application.GRANT_PASSWORD, + } + ) self._application = app def _create_default_organization(self): diff --git a/buildly/tests/test_loadinitialdata.py b/buildly/tests/test_loadinitialdata.py index ec910065..cec3d8cb 100644 --- a/buildly/tests/test_loadinitialdata.py +++ b/buildly/tests/test_loadinitialdata.py @@ -29,7 +29,7 @@ def tearDown(self): logging.disable(logging.NOTSET) @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='123') + @override_settings(OAUTH_CLIENT_ID='123') @override_settings(OAUTH_CLIENT_SECRET='456') def test_full_initial_data(self): args = [] @@ -39,12 +39,12 @@ def test_full_initial_data(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, + assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 @override_settings(DEBUG=True) @override_settings(DEFAULT_ORG='') - @override_settings(OAUTH_CLIENT_ID='123') + @override_settings(OAUTH_CLIENT_ID='123') @override_settings(OAUTH_CLIENT_SECRET='456') def test_without_default_organization(self): args = [] @@ -54,24 +54,24 @@ def test_without_default_organization(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.all().count() == 0 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, + assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 - @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='') - @override_settings(OAUTH_CLIENT_SECRET='') - def test_without_oauth_credentials(self): - args = [] - opts = {} - call_command('loadinitialdata', *args, **opts) - - assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 - assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert Application.objects.all().count() == 0 + @override_settings(DEBUG=True) + @override_settings(OAUTH_CLIENT_ID='') + @override_settings(OAUTH_CLIENT_SECRET='') + def test_without_oauth_credentials(self): + args = [] + opts = {} + call_command('loadinitialdata', *args, **opts) + + assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 + assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 + assert Application.objects.all().count() == 0 assert CoreUser.objects.filter(is_superuser=True).count() == 1 @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='123') + @override_settings(OAUTH_CLIENT_ID='123') @override_settings(OAUTH_CLIENT_SECRET='456') def test_create_user_debug_no_password(self): args = [] @@ -81,7 +81,7 @@ def test_create_user_debug_no_password(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, + assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 @override_settings(DEBUG=False) diff --git a/datamesh/services.py b/datamesh/services.py index fa81abe8..0065b266 100644 --- a/datamesh/services.py +++ b/datamesh/services.py @@ -98,7 +98,7 @@ def _extend_with_local(self, data_item: dict, relationship: Relationship, params if hasattr(self._access_validator, 'validate') and callable(self._access_validator.validate): self._access_validator.validate(obj) else: - raise DatameshConfigurationError(f'DataMesh Error: Access Validator should have validate method') + raise DatameshConfigurationError(f'{"DataMesh Error:Access Validator should have validate method"}') obj_dict = model_to_dict(obj) data_item[relationship.key].append(obj_dict) self._cache[cache_key] = obj_dict @@ -133,7 +133,7 @@ def _add_nested_data(self, data_item: dict, client_map: Dict[str, Any]) -> None: else: logger.error(f'No response data for join record (request params: {params})') else: - raise DatameshConfigurationError(f'DataMesh Error: Client should have request method') + raise DatameshConfigurationError(f'{"DataMesh Error: Client should have request method"}') async def async_extend_data(self, data: Union[dict, list], client_map: Dict[str, Any]): """ From 2454248d238a2177159bbbe6d10980ba33dccf93 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 2 Dec 2020 20:18:09 +0530 Subject: [PATCH 044/109] chore: Resolved ContextualVersionConflict --- requirements/base.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index fe37633f..1d59ba10 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -13,4 +13,5 @@ bravado-core==5.13.1 drf-yasg==1.10.2 requests==2.21.0 aiohttp==3.5.4 -django-auth-ldap==2.1.0 \ No newline at end of file +django-auth-ldap==2.1.0 +urllib3==1.24.3 \ No newline at end of file From 73bffebf4e1bed4311b96e0a7dddea0c734c2e9e Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 2 Dec 2020 20:33:24 +0530 Subject: [PATCH 045/109] chore: requests dependency version --- requirements/base.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 1d59ba10..5176b03a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -11,7 +11,6 @@ django-cors-headers==2.5.3 pyswagger==0.8.39 bravado-core==5.13.1 drf-yasg==1.10.2 -requests==2.21.0 +requests==2.25.0 aiohttp==3.5.4 -django-auth-ldap==2.1.0 -urllib3==1.24.3 \ No newline at end of file +django-auth-ldap==2.1.0 \ No newline at end of file From d517248af0f3fb0170375cab4fe7bbafd1e715a9 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 20 Jan 2021 13:59:42 +0530 Subject: [PATCH 046/109] Changes for Oauth in Initial Script --- .../management/commands/loadinitialdata.py | 15 ----------- buildly/tests/test_loadinitialdata.py | 27 +------------------ 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index c45ac7b0..f67bc043 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand from django.db import transaction -from oauth2_provider.models import Application from core.models import ROLE_VIEW_ONLY, ROLE_ORGANIZATION_ADMIN, ROLE_WORKFLOW_ADMIN, ROLE_WORKFLOW_TEAM, \ Organization, CoreUser, CoreGroup @@ -28,19 +27,6 @@ def __init__(self, *args, **kwargs): self._su_group = None self._default_org = None - def _create_oauth_application(self): - if settings.OAUTH_CLIENT_ID and settings.OAUTH_CLIENT_SECRET: - app, created = Application.objects.update_or_create( - client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET, - defaults={ - 'name': 'buildly oauth2', - 'client_type': Application.CLIENT_PUBLIC, - 'authorization_grant_type': Application.GRANT_PASSWORD, - } - ) - self._application = app - def _create_default_organization(self): if settings.DEFAULT_ORG: self._default_org, _ = Organization.objects.get_or_create(name=settings.DEFAULT_ORG) @@ -88,5 +74,4 @@ def _create_user(self): def handle(self, *args, **options): self._create_groups() self._create_default_organization() - self._create_oauth_application() self._create_user() diff --git a/buildly/tests/test_loadinitialdata.py b/buildly/tests/test_loadinitialdata.py index cec3d8cb..932c7cbc 100644 --- a/buildly/tests/test_loadinitialdata.py +++ b/buildly/tests/test_loadinitialdata.py @@ -5,7 +5,6 @@ from django.core.management import call_command from django.test import TransactionTestCase, override_settings -from oauth2_provider.models import Application from core.models import CoreGroup, CoreUser, Organization @@ -29,8 +28,6 @@ def tearDown(self): logging.disable(logging.NOTSET) @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='123') - @override_settings(OAUTH_CLIENT_SECRET='456') def test_full_initial_data(self): args = [] opts = {} @@ -39,13 +36,9 @@ def test_full_initial_data(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 @override_settings(DEBUG=True) @override_settings(DEFAULT_ORG='') - @override_settings(OAUTH_CLIENT_ID='123') - @override_settings(OAUTH_CLIENT_SECRET='456') def test_without_default_organization(self): args = [] opts = {} @@ -54,25 +47,9 @@ def test_without_default_organization(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.all().count() == 0 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 - @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='') - @override_settings(OAUTH_CLIENT_SECRET='') - def test_without_oauth_credentials(self): - args = [] - opts = {} - call_command('loadinitialdata', *args, **opts) - - assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 - assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert Application.objects.all().count() == 0 - assert CoreUser.objects.filter(is_superuser=True).count() == 1 @override_settings(DEBUG=True) - @override_settings(OAUTH_CLIENT_ID='123') - @override_settings(OAUTH_CLIENT_SECRET='456') def test_create_user_debug_no_password(self): args = [] opts = {} @@ -81,8 +58,6 @@ def test_create_user_debug_no_password(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - assert Application.objects.filter(client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET).count() == 1 @override_settings(DEBUG=False) def test_create_user_no_debug_no_password(self): @@ -92,4 +67,4 @@ def test_create_user_no_debug_no_password(self): assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert CoreUser.objects.filter(is_superuser=True).count() == 0 + assert CoreUser.objects.filter(is_superuser=True).count() == 0 \ No newline at end of file From d418f93e414d1a963ac9c2365c99faa1eba8cff1 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Wed, 27 Jan 2021 19:45:50 +0530 Subject: [PATCH 047/109] removed changed in gateway view to allow options methid from service --- gateway/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gateway/views.py b/gateway/views.py index 01a25473..8f74e0e8 100644 --- a/gateway/views.py +++ b/gateway/views.py @@ -45,9 +45,6 @@ def put(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs): return self.make_service_request(request, *args, **kwargs) - def options(self, request, *args, **kwargs): - return self.make_service_request(request, *args, **kwargs) - def make_service_request(self, request, *args, **kwargs): """ Create a request for the defined service From afc11e5a770e55313dbb17378cc6fdad327d5b93 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Wed, 27 Jan 2021 19:48:01 +0530 Subject: [PATCH 048/109] removed changed in gateway view to allow options methid from service --- gateway/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gateway/views.py b/gateway/views.py index 8f74e0e8..97703d16 100644 --- a/gateway/views.py +++ b/gateway/views.py @@ -45,6 +45,9 @@ def put(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs): return self.make_service_request(request, *args, **kwargs) + # def options(self, request, *args, **kwargs): + # return self.make_service_request(request, *args, **kwargs) + def make_service_request(self, request, *args, **kwargs): """ Create a request for the defined service From 2616b2294cf9a25b63b41b9ab23699e9d86b987a Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Wed, 27 Jan 2021 19:55:04 +0530 Subject: [PATCH 049/109] Commenting out options function for options response of services --- gateway/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gateway/views.py b/gateway/views.py index 97703d16..13671c29 100644 --- a/gateway/views.py +++ b/gateway/views.py @@ -45,6 +45,8 @@ def put(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs): return self.make_service_request(request, *args, **kwargs) + """Commenting out options function to solve error which does not send response, + response for options method""" # def options(self, request, *args, **kwargs): # return self.make_service_request(request, *args, **kwargs) From a454509702654f1425955622015b6c361f459fc8 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Fri, 5 Feb 2021 14:33:02 +0530 Subject: [PATCH 050/109] email alert message for shipment to user --- core/serializers.py | 7 +++ core/views/coreuser.py | 45 +++++++++++++++++++- templates/email/coreuser/shipment_alert.html | 23 ++++++++++ templates/email/coreuser/shipment_alert.txt | 3 ++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 templates/email/coreuser/shipment_alert.html create mode 100644 templates/email/coreuser/shipment_alert.txt diff --git a/core/serializers.py b/core/serializers.py index b531b277..3774e5ce 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -281,3 +281,10 @@ def create(self, validated_data): validated_data['client_id'] = secrets.token_urlsafe(75) validated_data['client_secret'] = secrets.token_urlsafe(190) return super(ApplicationSerializer, self).create(validated_data) + +class CoreUserEmailAlertSerializer(serializers.Serializer): + """ + Serializer for email alert of shipment + """ + user_uuid = serializers.UUIDField() + alert_message = serializers.CharField() \ No newline at end of file diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 492b4797..ca2e23cc 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -13,14 +13,14 @@ from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, - CoreUserResetPasswordConfirmSerializer) + CoreUserResetPasswordConfirmSerializer,CoreUserEmailAlertSerializer) from core.permissions import AllowAuthenticatedRead, AllowOnlyOrgAdmin, IsOrgMember from core.swagger import (COREUSER_INVITE_RESPONSE, COREUSER_INVITE_CHECK_RESPONSE, COREUSER_RESETPASS_RESPONSE, DETAIL_RESPONSE, SUCCESS_RESPONSE, TOKEN_QUERY_PARAM) from core.jwt_utils import create_invitation_token from core.email_utils import send_email import logging - +# from twilio.rest import Client logger = logging.getLogger(__name__) @@ -59,6 +59,7 @@ class CoreUserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, 'reset_password': CoreUserResetPasswordSerializer, 'reset_password_check': CoreUserResetPasswordCheckSerializer, 'reset_password_confirm': CoreUserResetPasswordConfirmSerializer, + 'excursion_alert': CoreUserEmailAlertSerializer, } def list(self, request, *args, **kwargs): @@ -262,3 +263,43 @@ def get_permissions(self): filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) queryset = CoreUser.objects.all() permission_classes = (AllowAuthenticatedRead,) + + @swagger_auto_schema(methods=['post'], + request_body=CoreUserEmailAlertSerializer, + responses=SUCCESS_RESPONSE) + @action(methods=['POST'], detail=False) + def excursion_alert(self, request, *args, **kwargs): + """ + a)Request alert message and uuid of core user + b)Send Email to the user's email with alert message + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user_uuid = request.data['user_uuid'] + alert_message = request.data['alert_message'] + + user = CoreUser.objects.filter(core_user_uuid=user_uuid).first() + email_address = user.email + subject = 'Alert message for shipment' + context = { + 'alert_message': alert_message, + } + template_name = 'email/coreuser/shipment_alert.txt' + html_template_name = 'email/coreuser/shipment_alert.html' + send_email(email_address, subject, context, template_name, html_template_name) + return Response( + { + 'detail': 'The alert messages were sent successfully on email.', + }, status=status.HTTP_200_OK) + + # for phone in phones: + # phone_number = phone + # account_sid = os.environ['TWILIO_ACCOUNT_SID'] + # auth_token = os.environ['TWILIO_AUTH_TOKEN'] + # client = Client(account_sid, auth_token) + # message = client.messages.create( + # body=alert_message, + # from_='+15082068927', + # to=phone_number + # ) + # print(message.sid) \ No newline at end of file diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html new file mode 100644 index 00000000..aba9ea91 --- /dev/null +++ b/templates/email/coreuser/shipment_alert.html @@ -0,0 +1,23 @@ + + + + + + + + +



+ + + + + + +
+

Message from Transparent Path
Admin!

+

Current status of shipment {{ alert_message }}

+
+ + < + + diff --git a/templates/email/coreuser/shipment_alert.txt b/templates/email/coreuser/shipment_alert.txt new file mode 100644 index 00000000..dcbfa46e --- /dev/null +++ b/templates/email/coreuser/shipment_alert.txt @@ -0,0 +1,3 @@ +Alert message from Transparent Path + +Status of shipment {{ alert_message }}! From 32232398d2817d18ef0bf2e458bb8e1b64afcde1 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Mon, 8 Feb 2021 12:15:10 +0530 Subject: [PATCH 051/109] Use generalised function name --- core/serializers.py | 2 +- core/views/coreuser.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 3774e5ce..38a73883 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -287,4 +287,4 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): Serializer for email alert of shipment """ user_uuid = serializers.UUIDField() - alert_message = serializers.CharField() \ No newline at end of file + message = serializers.CharField() \ No newline at end of file diff --git a/core/views/coreuser.py b/core/views/coreuser.py index ca2e23cc..247c2e63 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -59,7 +59,7 @@ class CoreUserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, 'reset_password': CoreUserResetPasswordSerializer, 'reset_password_check': CoreUserResetPasswordCheckSerializer, 'reset_password_confirm': CoreUserResetPasswordConfirmSerializer, - 'excursion_alert': CoreUserEmailAlertSerializer, + 'alert': CoreUserEmailAlertSerializer, } def list(self, request, *args, **kwargs): @@ -268,7 +268,7 @@ def get_permissions(self): request_body=CoreUserEmailAlertSerializer, responses=SUCCESS_RESPONSE) @action(methods=['POST'], detail=False) - def excursion_alert(self, request, *args, **kwargs): + def alert(self, request, *args, **kwargs): """ a)Request alert message and uuid of core user b)Send Email to the user's email with alert message @@ -276,13 +276,13 @@ def excursion_alert(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user_uuid = request.data['user_uuid'] - alert_message = request.data['alert_message'] + message = request.data['message'] user = CoreUser.objects.filter(core_user_uuid=user_uuid).first() email_address = user.email subject = 'Alert message for shipment' context = { - 'alert_message': alert_message, + 'alert_message': message, } template_name = 'email/coreuser/shipment_alert.txt' html_template_name = 'email/coreuser/shipment_alert.html' From c32ca1e82e31a5641b5fd091fc928ef573a2f513 Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Tue, 9 Feb 2021 16:24:16 +0530 Subject: [PATCH 052/109] initial commit --- core/serializers.py | 34 ++++++++++++++++++++++++++++++++++ core/views/coreuser.py | 14 +++++++++----- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index b531b277..a1e1bec4 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -152,6 +152,40 @@ def create(self, validated_data): return coreuser +class CoreUserProfileSerializer(serializers.ModelSerializer): + """ Let's user update his first_name,last_name,title,contact_info, + password and organization_name """ + + first_name = serializers.CharField(required=False) + last_name = serializers.CharField(required=False) + password = serializers.CharField(required=False) + organization_name = serializers.CharField(required=False) + + class Meta: + model = CoreUser + fields = ('first_name', 'last_name', 'password', 'title', 'contact_info', 'organization_name',) + + def update(self, instance, validated_data): + + organization_name = validated_data.pop('organization_name') + + name = Organization.objects.filter(name=organization_name).first() + if name is not None: + instance.organization = name + instance.organization_name = name + + instance.first_name = validated_data.get('first_name', instance.first_name) + instance.last_name = validated_data.get('last_name', instance.last_name) + instance.title = validated_data.get('title', instance.title) + instance.contact_info = validated_data.get('contact_info', instance.contact_info) + password = validated_data.get('password', None) + if password is not None: + instance.set_password(password) + instance.save() + + return instance + + class CoreUserInvitationSerializer(serializers.Serializer): emails = serializers.ListField(child=serializers.EmailField(), min_length=1, max_length=10) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 492b4797..a4fcc6b6 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -13,7 +13,7 @@ from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, - CoreUserResetPasswordConfirmSerializer) + CoreUserResetPasswordConfirmSerializer, CoreUserProfileSerializer) from core.permissions import AllowAuthenticatedRead, AllowOnlyOrgAdmin, IsOrgMember from core.swagger import (COREUSER_INVITE_RESPONSE, COREUSER_INVITE_CHECK_RESPONSE, COREUSER_RESETPASS_RESPONSE, DETAIL_RESPONSE, SUCCESS_RESPONSE, TOKEN_QUERY_PARAM) @@ -53,8 +53,10 @@ class CoreUserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, SERIALIZERS_MAP = { 'default': CoreUserSerializer, 'create': CoreUserWritableSerializer, - 'update': CoreUserWritableSerializer, - 'partial_update': CoreUserWritableSerializer, + # 'update': CoreUserWritableSerializer, + 'update': CoreUserProfileSerializer, + # 'partial_update': CoreUserWritableSerializer, + 'partial_update': CoreUserProfileSerializer, 'invite': CoreUserInvitationSerializer, 'reset_password': CoreUserResetPasswordSerializer, 'reset_password_check': CoreUserResetPasswordCheckSerializer, @@ -250,10 +252,12 @@ def get_permissions(self): 'reset_password', 'reset_password_check', 'reset_password_confirm', - 'invite_check']: + 'invite_check', 'update', 'partial_update']: return [permissions.AllowAny()] - if self.action in ['update', 'partial_update', 'invite']: + # if self.action in ['update', 'partial_update', 'invite']: + # return [AllowOnlyOrgAdmin(), IsOrgMember()] + if self.action in ['invite']: return [AllowOnlyOrgAdmin(), IsOrgMember()] return super(CoreUserViewSet, self).get_permissions() From a5ff0d884adfb6cbd5b0242850807075cbb070fa Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Wed, 10 Feb 2021 13:41:14 +0530 Subject: [PATCH 053/109] added seperate endpoint for update --- core/serializers.py | 4 +++- core/views/coreuser.py | 26 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index a1e1bec4..9f248652 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -152,12 +152,14 @@ def create(self, validated_data): return coreuser -class CoreUserProfileSerializer(serializers.ModelSerializer): +class CoreUserProfileSerializer(serializers.Serializer): """ Let's user update his first_name,last_name,title,contact_info, password and organization_name """ first_name = serializers.CharField(required=False) last_name = serializers.CharField(required=False) + title = serializers.CharField(required=False) + contact_info = serializers.CharField(required=False) password = serializers.CharField(required=False) organization_name = serializers.CharField(required=False) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index a4fcc6b6..acd39073 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -53,10 +53,9 @@ class CoreUserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, SERIALIZERS_MAP = { 'default': CoreUserSerializer, 'create': CoreUserWritableSerializer, - # 'update': CoreUserWritableSerializer, - 'update': CoreUserProfileSerializer, - # 'partial_update': CoreUserWritableSerializer, - 'partial_update': CoreUserProfileSerializer, + 'update': CoreUserWritableSerializer, + 'partial_update': CoreUserWritableSerializer, + 'update_profile': CoreUserProfileSerializer, 'invite': CoreUserInvitationSerializer, 'reset_password': CoreUserResetPasswordSerializer, 'reset_password_check': CoreUserResetPasswordCheckSerializer, @@ -252,11 +251,12 @@ def get_permissions(self): 'reset_password', 'reset_password_check', 'reset_password_confirm', - 'invite_check', 'update', 'partial_update']: + 'invite_check', + 'update_profile']: return [permissions.AllowAny()] - # if self.action in ['update', 'partial_update', 'invite']: - # return [AllowOnlyOrgAdmin(), IsOrgMember()] + if self.action in ['update', 'partial_update', 'invite']: + return [AllowOnlyOrgAdmin(), IsOrgMember()] if self.action in ['invite']: return [AllowOnlyOrgAdmin(), IsOrgMember()] @@ -266,3 +266,15 @@ def get_permissions(self): filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) queryset = CoreUser.objects.all() permission_classes = (AllowAuthenticatedRead,) + + @action(detail=True, methods=['patch'], name='Update Profile') + def update_profile(self, request, pk=None, *args, **kwargs): + """ + Update a user Profile + """ + # the particular user in CoreUser table + user = self.get_object() + serializer = CoreUserProfileSerializer(user, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) From 8daa870dca3e4e7e2dda5abd63f16d34a7467d4d Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Thu, 11 Feb 2021 10:36:23 +0530 Subject: [PATCH 054/109] add more illutratative field in message --- core/serializers.py | 3 +- core/views/coreuser.py | 13 ++++++--- templates/email/coreuser/shipment_alert.html | 29 +++++++++++++------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 38a73883..4fb9d3d2 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -287,4 +287,5 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): Serializer for email alert of shipment """ user_uuid = serializers.UUIDField() - message = serializers.CharField() \ No newline at end of file + messages = serializers.JSONField() + date_time = serializers.DateTimeField() \ No newline at end of file diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 247c2e63..e1b3fce8 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -9,7 +9,8 @@ import django_filters import jwt from drf_yasg.utils import swagger_auto_schema - +import calendar +import time from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, @@ -276,13 +277,17 @@ def alert(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user_uuid = request.data['user_uuid'] - message = request.data['message'] - + date_time = request.data['date_time'] + messages = request.data['messages'] user = CoreUser.objects.filter(core_user_uuid=user_uuid).first() email_address = user.email subject = 'Alert message for shipment' + time_tuple = time.strptime(date_time, "%Y-%m-%dT%H:%M:%S.%f%z") + time_formate = calendar.timegm(time_tuple) + date_time_form = time.ctime(time_formate) context = { - 'alert_message': message, + 'date_time': date_time_form, + 'messages':messages, } template_name = 'email/coreuser/shipment_alert.txt' html_template_name = 'email/coreuser/shipment_alert.html' diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index aba9ea91..d3b6cd37 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -9,15 +9,24 @@



- - - - -
-

Message from Transparent Path
Admin!

-

Current status of shipment {{ alert_message }}

-
- - < + + + +

Message from Transparent Path
Admin!

+ {% for message in messages %} +
+

Shipment Id {{ message.shipment_id }}

+

{{ message.alert_message }} +                                                                                                                                          + {{ date_time }} +

+
+
+ + {% endfor %} + + + + From c01c660baac4e654bf4a9bda918f1f6163723136 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Thu, 11 Feb 2021 10:52:35 +0530 Subject: [PATCH 055/109] remove commented code in html template of shipment alert --- templates/email/coreuser/shipment_alert.html | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index d3b6cd37..87eaf366 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -22,7 +22,6 @@

Shipment Id {{ message.shipment_id }}


- {% endfor %} From 7dd78ae4beca6c1cbd44e51836575b598bea98a5 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Thu, 11 Feb 2021 10:56:26 +0530 Subject: [PATCH 056/109] modifify html template name to send email alert for shipment --- templates/email/coreuser/shipment_alert.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/templates/email/coreuser/shipment_alert.txt b/templates/email/coreuser/shipment_alert.txt index dcbfa46e..76752e62 100644 --- a/templates/email/coreuser/shipment_alert.txt +++ b/templates/email/coreuser/shipment_alert.txt @@ -1,3 +1 @@ Alert message from Transparent Path - -Status of shipment {{ alert_message }}! From e9c305f820c7ba9c5e2040af73fd30f6158747d6 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Thu, 11 Feb 2021 12:45:53 +0530 Subject: [PATCH 057/109] remove conflict --- core/views/coreuser.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 4cfb50f3..842470aa 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -14,11 +14,8 @@ from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, -<<<<<<< HEAD - CoreUserResetPasswordConfirmSerializer,CoreUserEmailAlertSerializer) -======= - CoreUserResetPasswordConfirmSerializer, CoreUserProfileSerializer) ->>>>>>> 341ee5d6a3ac498c8eb2f71bf1ab76d26952c14c + CoreUserResetPasswordConfirmSerializer,CoreUserEmailAlertSerializer,CoreUserProfileSerializer) + from core.permissions import AllowAuthenticatedRead, AllowOnlyOrgAdmin, IsOrgMember from core.swagger import (COREUSER_INVITE_RESPONSE, COREUSER_INVITE_CHECK_RESPONSE, COREUSER_RESETPASS_RESPONSE, DETAIL_RESPONSE, SUCCESS_RESPONSE, TOKEN_QUERY_PARAM) @@ -273,7 +270,6 @@ def get_permissions(self): queryset = CoreUser.objects.all() permission_classes = (AllowAuthenticatedRead,) -<<<<<<< HEAD @swagger_auto_schema(methods=['post'], request_body=CoreUserEmailAlertSerializer, responses=SUCCESS_RESPONSE) @@ -317,7 +313,6 @@ def alert(self, request, *args, **kwargs): # to=phone_number # ) # print(message.sid) -======= @action(detail=True, methods=['patch'], name='Update Profile') def update_profile(self, request, pk=None, *args, **kwargs): """ @@ -329,4 +324,3 @@ def update_profile(self, request, pk=None, *args, **kwargs): serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) ->>>>>>> 341ee5d6a3ac498c8eb2f71bf1ab76d26952c14c From 034dd408c0db6c0d7046a3058ce2557c61dbabdf Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Wed, 17 Feb 2021 17:24:34 +0530 Subject: [PATCH 058/109] Allow user to subscribe to email alert in Profile --- core/admin.py | 2 +- .../0003_coreuser_email_alert_flag.py | 18 ++++++++++++++++++ core/models.py | 1 + core/serializers.py | 5 ++++- scripts/docker-entrypoint.sh | 1 + scripts/run-standalone-dev.sh | 1 + 6 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 core/migrations/0003_coreuser_email_alert_flag.py diff --git a/core/admin.py b/core/admin.py index 91926ce3..58f10d0c 100644 --- a/core/admin.py +++ b/core/admin.py @@ -29,7 +29,7 @@ class CoreGroupAdmin(admin.ModelAdmin): class CoreUserAdmin(UserAdmin): - list_display = ('username', 'first_name', 'last_name', 'organization', 'is_active') + list_display = ('username', 'first_name', 'last_name', 'organization', 'title', 'is_active', 'email_alert_flag',) display = 'Core User' list_filter = ('is_staff', 'organization') search_fields = ('first_name', 'first_name', 'username', 'title', 'organization__name', ) diff --git a/core/migrations/0003_coreuser_email_alert_flag.py b/core/migrations/0003_coreuser_email_alert_flag.py new file mode 100644 index 00000000..757a400d --- /dev/null +++ b/core/migrations/0003_coreuser_email_alert_flag.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2021-02-17 10:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_auto_20200303_1657'), + ] + + operations = [ + migrations.AddField( + model_name='coreuser', + name='email_alert_flag', + field=models.BooleanField(blank=True, default=False, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index 1dda5908..b935f103 100644 --- a/core/models.py +++ b/core/models.py @@ -178,6 +178,7 @@ class CoreUser(AbstractUser): privacy_disclaimer_accepted = models.BooleanField(default=False) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) + email_alert_flag = models.BooleanField(default=False,blank=True,null=True) REQUIRED_FIELDS = [] diff --git a/core/serializers.py b/core/serializers.py index b4f31ccb..4c105810 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -162,10 +162,12 @@ class CoreUserProfileSerializer(serializers.Serializer): contact_info = serializers.CharField(required=False) password = serializers.CharField(required=False) organization_name = serializers.CharField(required=False) + email_alert_flag = serializers.CharField(required=False) class Meta: model = CoreUser - fields = ('first_name', 'last_name', 'password', 'title', 'contact_info', 'organization_name',) + fields = ('first_name', 'last_name', 'password', 'title', + 'contact_info', 'organization_name', 'email_alert_flag',) def update(self, instance, validated_data): @@ -180,6 +182,7 @@ def update(self, instance, validated_data): instance.last_name = validated_data.get('last_name', instance.last_name) instance.title = validated_data.get('title', instance.title) instance.contact_info = validated_data.get('contact_info', instance.contact_info) + instance.email_alert_flag = validated_data.get('email_alert_flag', instance.email_alert_flag) password = validated_data.get('password', None) if password is not None: instance.set_password(password) diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 7771732c..2eaddcd1 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -5,6 +5,7 @@ set -e bash scripts/tcp-port-wait.sh $DATABASE_HOST $DATABASE_PORT echo $(date -u) "- Migrating" +python manage.py makemigrations python manage.py migrate echo $(date -u) "- Load Initial Data" diff --git a/scripts/run-standalone-dev.sh b/scripts/run-standalone-dev.sh index a266e465..7cd07b09 100644 --- a/scripts/run-standalone-dev.sh +++ b/scripts/run-standalone-dev.sh @@ -8,6 +8,7 @@ set -e bash scripts/tcp-port-wait.sh $DATABASE_HOST $DATABASE_PORT echo $(date -u) "- Migrating" +python manage.py makemigrations python manage.py migrate echo $(date -u) "- Load Initial Data" From ede613c33565d1eb59105823d3e1dd223ae76699 Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Wed, 17 Feb 2021 17:55:56 +0530 Subject: [PATCH 059/109] fixed linting --- core/serializers.py | 5 +++-- core/views/coreuser.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 4c105810..25d08471 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -321,10 +321,11 @@ def create(self, validated_data): validated_data['client_secret'] = secrets.token_urlsafe(190) return super(ApplicationSerializer, self).create(validated_data) + class CoreUserEmailAlertSerializer(serializers.Serializer): """ - Serializer for email alert of shipment + Serializer for email alert of shipment """ user_uuid = serializers.UUIDField() messages = serializers.JSONField() - date_time = serializers.DateTimeField() \ No newline at end of file + date_time = serializers.DateTimeField() diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 842470aa..995fedb6 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -14,8 +14,9 @@ from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, - CoreUserResetPasswordConfirmSerializer,CoreUserEmailAlertSerializer,CoreUserProfileSerializer) - + CoreUserResetPasswordConfirmSerializer, CoreUserEmailAlertSerializer, + CoreUserProfileSerializer) + from core.permissions import AllowAuthenticatedRead, AllowOnlyOrgAdmin, IsOrgMember from core.swagger import (COREUSER_INVITE_RESPONSE, COREUSER_INVITE_CHECK_RESPONSE, COREUSER_RESETPASS_RESPONSE, DETAIL_RESPONSE, SUCCESS_RESPONSE, TOKEN_QUERY_PARAM) @@ -292,7 +293,7 @@ def alert(self, request, *args, **kwargs): date_time_form = time.ctime(time_formate) context = { 'date_time': date_time_form, - 'messages':messages, + 'messages': messages, } template_name = 'email/coreuser/shipment_alert.txt' html_template_name = 'email/coreuser/shipment_alert.html' From 8a6cc285eeb36b1bfd6562d49f3da97b58c9532a Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Wed, 17 Feb 2021 19:25:58 +0530 Subject: [PATCH 060/109] changed shipment id to shipment_uuid --- templates/email/coreuser/shipment_alert.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index 2b096c0e..fe3e207c 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -15,7 +15,7 @@

Message from Transparent Path
Admin!

{% for message in messages %}
-

Shipment Id {{ message.shipment_id }}

+

Shipment Id {{ message.shipment_uuid }}

{{ message.alert_message }} {{ date_time }}

From eee0569d8ff323e5ae70fc8454c762b6ef207d10 Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Thu, 18 Feb 2021 18:19:32 +0530 Subject: [PATCH 061/109] added email_alert_flag to CoreUser Serializer --- core/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 25d08471..cfebb565 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -98,7 +98,7 @@ def validate_invitation_token(self, value): class Meta: model = CoreUser fields = ('id', 'core_user_uuid', 'first_name', 'last_name', 'email', 'username', 'is_active', - 'title', 'contact_info', 'privacy_disclaimer_accepted', 'organization', 'core_groups', + 'title', 'email_alert_flag','contact_info', 'privacy_disclaimer_accepted', 'organization', 'core_groups', 'invitation_token') read_only_fields = ('core_user_uuid', 'organization',) depth = 1 @@ -162,7 +162,7 @@ class CoreUserProfileSerializer(serializers.Serializer): contact_info = serializers.CharField(required=False) password = serializers.CharField(required=False) organization_name = serializers.CharField(required=False) - email_alert_flag = serializers.CharField(required=False) + email_alert_flag = serializers.BooleanField(required=False) class Meta: model = CoreUser From 35dc0bb00393343e9ace8ea2f1c9cebe8b454e01 Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Thu, 18 Feb 2021 19:02:32 +0530 Subject: [PATCH 062/109] fixed linting --- core/serializers.py | 4 ++-- core/tests/test_coreuserview.py | 2 +- core/tests/test_serializers.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index cfebb565..ec9494b9 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -98,8 +98,8 @@ def validate_invitation_token(self, value): class Meta: model = CoreUser fields = ('id', 'core_user_uuid', 'first_name', 'last_name', 'email', 'username', 'is_active', - 'title', 'email_alert_flag','contact_info', 'privacy_disclaimer_accepted', 'organization', 'core_groups', - 'invitation_token') + 'title', 'email_alert_flag', 'contact_info', 'privacy_disclaimer_accepted', + 'organization', 'core_groups', 'invitation_token') read_only_fields = ('core_user_uuid', 'organization',) depth = 1 diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index 91bff8c6..77838e34 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -423,7 +423,7 @@ def test_reset_password_confirm_token_expired(self, request_factory, reset_passw class TestCoreUserRead(object): keys = {'id', 'core_user_uuid', 'first_name', 'last_name', 'email', 'username', 'is_active', 'title', - 'contact_info', 'privacy_disclaimer_accepted', 'organization', 'core_groups'} + 'contact_info','email_alert_flag','privacy_disclaimer_accepted', 'organization', 'core_groups'} def test_coreuser_list(self, request_factory, org_member): factories.CoreUser.create(organization=org_member.organization, username='another_user') # 2nd user of the org diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index e924c29d..c810b08a 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -60,6 +60,7 @@ def test_core_user_serializer(request_factory, org_member): 'privacy_disclaimer_accepted', 'organization', 'core_groups', + 'email_alert_flag', ] assert set(data.keys()) == set(keys) assert isinstance(data['organization'], dict) From a81f287587f6166debbab9064e3c715d1a7b3a37 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Thu, 25 Feb 2021 14:42:41 +0530 Subject: [PATCH 063/109] added boolean field in organisation --- .../0004_organization_allow_import_export.py | 18 ++++++++++++++++++ core/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 core/migrations/0004_organization_allow_import_export.py diff --git a/core/migrations/0004_organization_allow_import_export.py b/core/migrations/0004_organization_allow_import_export.py new file mode 100644 index 00000000..884239c0 --- /dev/null +++ b/core/migrations/0004_organization_allow_import_export.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2021-02-25 08:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_coreuser_email_alert_flag'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='allow_import_export', + field=models.BooleanField(default=False, verbose_name='To allow import export functionality'), + ), + ] diff --git a/core/models.py b/core/models.py index b935f103..2676e9ea 100644 --- a/core/models.py +++ b/core/models.py @@ -94,7 +94,7 @@ class Organization(models.Model): oauth_domains = ArrayField(models.CharField("OAuth Domains", max_length=255, null=True, blank=True), null=True, blank=True) date_format = models.CharField("Date Format", max_length=50, blank=True, default="DD.MM.YYYY") phone = models.CharField(max_length=20, blank=True, null=True) - + allow_import_export = models.BooleanField('To allow import export functionality', default=False) class Meta: ordering = ('name',) verbose_name_plural = "Organizations" From 3e9cd8014b7b3838a54fccf9aef3b226df0691ac Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Tue, 9 Mar 2021 13:50:18 +0530 Subject: [PATCH 064/109] Resolved issue in OPTIONS method via Core --- gateway/clients.py | 12 ++++++++---- gateway/views.py | 6 ++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/gateway/clients.py b/gateway/clients.py index b22d91a5..462f59e2 100644 --- a/gateway/clients.py +++ b/gateway/clients.py @@ -45,9 +45,14 @@ def prepare_data(self, spec: Spec, **kwargs) -> Tuple[str, str]: path = f'/{model}/{{{pk_name}}}/' # Check that operation is valid according to spec - operation = spec.get_op_for_request(self._in_request.method, path) + request_method = self._in_request.method + operation = spec.get_op_for_request(request_method, path) if not operation: - raise exceptions.EndpointNotFound(f'Endpoint not found: {self._in_request.method} {path}') + if request_method == 'OPTIONS': + operation = spec.get_op_for_request('GET',path) + operation.http_method = request_method + else: + raise exceptions.EndpointNotFound(f'Endpoint not found: {self._in_request.method} {path}') method = operation.http_method.lower() path_name = operation.path_name @@ -100,7 +105,6 @@ def get_headers(self) -> dict: headers['content-type'] = 'application/json' return headers - class SwaggerClient(BaseSwaggerClient): """ Synchronous implementation of Swagger client using requests lib """ @@ -180,4 +184,4 @@ async def request(self, **kwargs) -> Tuple[Any, int, Dict[str, str]]: if self.is_valid_for_cache(): self._data[url] = return_data - return return_data + return return_data \ No newline at end of file diff --git a/gateway/views.py b/gateway/views.py index 13671c29..01a25473 100644 --- a/gateway/views.py +++ b/gateway/views.py @@ -45,10 +45,8 @@ def put(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs): return self.make_service_request(request, *args, **kwargs) - """Commenting out options function to solve error which does not send response, - response for options method""" - # def options(self, request, *args, **kwargs): - # return self.make_service_request(request, *args, **kwargs) + def options(self, request, *args, **kwargs): + return self.make_service_request(request, *args, **kwargs) def make_service_request(self, request, *args, **kwargs): """ From 456813e29817f8ae0211704ccbddee81c5661f44 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Tue, 16 Mar 2021 18:31:56 +0530 Subject: [PATCH 065/109] TransparentPath/buildly-core/issues/45:add radis field in organization --- core/migrations/0005_organization_radius.py | 18 ++++++++++++++++++ core/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 core/migrations/0005_organization_radius.py diff --git a/core/migrations/0005_organization_radius.py b/core/migrations/0005_organization_radius.py new file mode 100644 index 00000000..e26235e1 --- /dev/null +++ b/core/migrations/0005_organization_radius.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2021-03-16 12:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_organization_allow_import_export'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='radius', + field=models.FloatField(blank=True, max_length=20, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index 2676e9ea..24e1bb28 100644 --- a/core/models.py +++ b/core/models.py @@ -95,6 +95,7 @@ class Organization(models.Model): date_format = models.CharField("Date Format", max_length=50, blank=True, default="DD.MM.YYYY") phone = models.CharField(max_length=20, blank=True, null=True) allow_import_export = models.BooleanField('To allow import export functionality', default=False) + radius = models.FloatField(max_length=20, blank=True, null=True) class Meta: ordering = ('name',) verbose_name_plural = "Organizations" From 666f6567dcecad33661ff9b99e050395a3e2daa6 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Tue, 23 Mar 2021 15:35:45 +0530 Subject: [PATCH 066/109] Added support for multiple email alerts --- core/serializers.py | 1 + core/views/coreuser.py | 17 ++++++++++++----- templates/email/coreuser/shipment_alert.html | 14 +++++++------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index ec9494b9..d1876f9f 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -328,4 +328,5 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): """ user_uuid = serializers.UUIDField() messages = serializers.JSONField() + subject_line = serializers.CharField(max_length=255) date_time = serializers.DateTimeField() diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 995fedb6..43dca009 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -285,14 +285,21 @@ def alert(self, request, *args, **kwargs): user_uuid = request.data['user_uuid'] date_time = request.data['date_time'] messages = request.data['messages'] + subject_line = request.data['subject_line'] user = CoreUser.objects.filter(core_user_uuid=user_uuid).first() email_address = user.email - subject = 'Alert message for shipment' - time_tuple = time.strptime(date_time, "%Y-%m-%dT%H:%M:%S.%f%z") - time_formate = calendar.timegm(time_tuple) - date_time_form = time.ctime(time_formate) + if subject_line is not None: + subject = subject_line + else: + subject = 'Alert message for shipment' + for message in messages: + try: + time_tuple = time.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + time_tuple = time.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S.%f%z") + time_format = calendar.timegm(time_tuple) + message['date_time'] = time.ctime(time_format) context = { - 'date_time': date_time_form, 'messages': messages, } template_name = 'email/coreuser/shipment_alert.txt' diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index fe3e207c..b2d51c3e 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -5,26 +5,26 @@ - +



- + - - -

Message from Transparent Path
Admin!

- {% for message in messages %} + {% for message in messages %}

Shipment Id {{ message.shipment_uuid }}

{{ message.alert_message }} - {{ date_time }} + {{ message.date_time }}


{% endfor %}
+ + + From 7311d30a60db7e28f0427d6e079c3e3e445ac0b6 Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Tue, 11 May 2021 13:14:23 +0530 Subject: [PATCH 067/109] initial commit --- core/views/organization.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/views/organization.py b/core/views/organization.py index e6f81e79..c656604a 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -3,7 +3,7 @@ import django_filters from rest_framework import viewsets from rest_framework.response import Response - +from rest_framework.decorators import action from core.models import Organization from core.serializers import OrganizationSerializer from core.permissions import IsOrgMember @@ -47,3 +47,14 @@ def list(self, request, *args, **kwargs): permission_classes = (IsOrgMember,) queryset = Organization.objects.all() serializer_class = OrganizationSerializer + + @action(detail=False, methods=['get'], name='Fetch Already existing Organization', url_path='fetch_orgs') + def fetch_existing_orgs(self, request, pk=None, *args, **kwargs): + """ + Fetch Already existing Organizations in Buildly Core, + Any logged in user can access this + """ + # all orgs in Buildly Core + queryset = Organization.objects.all() + serializer = OrganizationSerializer(queryset, many=True) + return Response(serializer.data) From 237b2aac70ca73d58332971b34a113f2e05be9f2 Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Tue, 11 May 2021 16:35:08 +0530 Subject: [PATCH 068/109] return only org names --- core/serializers.py | 4 ++++ core/tests/test_serializers.py | 28 ++++++++++++++++------------ core/views/coreuser.py | 2 +- core/views/organization.py | 8 ++++---- gateway/clients.py | 5 +++-- requirements/base.txt | 2 +- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index d1876f9f..f08fcbc0 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -290,6 +290,10 @@ class Meta: fields = '__all__' +class OrganizationNameSerializer(serializers.Serializer): + name = serializers.CharField(required=False) + + class AccessTokenSerializer(serializers.ModelSerializer): user = CoreUserSerializer() diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index c810b08a..45c71bc5 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -9,18 +9,22 @@ def test_org_serializer(request_factory, org): request = request_factory.get('') serializer = OrganizationSerializer(org, context={'request': request}) data = serializer.data - keys = ['id', - 'organization_uuid', - 'name', - 'description', - 'organization_url', - 'create_date', - 'edit_date', - 'oauth_domains', - 'date_format', - 'phone', - 'industries', - ] + print("test org serializer", data) + keys = [ + 'id', + 'organization_uuid', + 'name', + 'description', + 'organization_url', + 'create_date', + 'edit_date', + 'oauth_domains', + 'date_format', + 'phone', + 'industries', + 'allow_import_export', + 'radius', + ] assert set(data.keys()) == set(keys) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 43dca009..6fc04543 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -283,7 +283,7 @@ def alert(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user_uuid = request.data['user_uuid'] - date_time = request.data['date_time'] + # date_time = request.data['date_time'] messages = request.data['messages'] subject_line = request.data['subject_line'] user = CoreUser.objects.filter(core_user_uuid=user_uuid).first() diff --git a/core/views/organization.py b/core/views/organization.py index c656604a..8f655ae9 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from rest_framework.decorators import action from core.models import Organization -from core.serializers import OrganizationSerializer +from core.serializers import OrganizationSerializer, OrganizationNameSerializer from core.permissions import IsOrgMember @@ -54,7 +54,7 @@ def fetch_existing_orgs(self, request, pk=None, *args, **kwargs): Fetch Already existing Organizations in Buildly Core, Any logged in user can access this """ - # all orgs in Buildly Core - queryset = Organization.objects.all() - serializer = OrganizationSerializer(queryset, many=True) + # returns names of existing orgs in Buildly Core + queryset = Organization.objects.values('name') + serializer = OrganizationNameSerializer(queryset, many=True) return Response(serializer.data) diff --git a/gateway/clients.py b/gateway/clients.py index 462f59e2..3e6c5ccb 100644 --- a/gateway/clients.py +++ b/gateway/clients.py @@ -49,7 +49,7 @@ def prepare_data(self, spec: Spec, **kwargs) -> Tuple[str, str]: operation = spec.get_op_for_request(request_method, path) if not operation: if request_method == 'OPTIONS': - operation = spec.get_op_for_request('GET',path) + operation = spec.get_op_for_request('GET', path) operation.http_method = request_method else: raise exceptions.EndpointNotFound(f'Endpoint not found: {self._in_request.method} {path}') @@ -105,6 +105,7 @@ def get_headers(self) -> dict: headers['content-type'] = 'application/json' return headers + class SwaggerClient(BaseSwaggerClient): """ Synchronous implementation of Swagger client using requests lib """ @@ -184,4 +185,4 @@ async def request(self, **kwargs) -> Tuple[Any, int, Dict[str, str]]: if self.is_valid_for_cache(): self._data[url] = return_data - return return_data \ No newline at end of file + return return_data diff --git a/requirements/base.txt b/requirements/base.txt index 5176b03a..3451c7b2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,7 +3,7 @@ django-filter==2.2.0 django-health-check==3.6.1 git+https://github.com/Humanitec/django-oauth-toolkit-jwt@v0.5.2#egg=django-oauth-toolkit-jwt djangorestframework==3.9.4 -psycopg2-binary==2.8.3 +psycopg2-binary==2.8.6 social-auth-app-django==3.1.0 django-oauth-toolkit==1.3.0 futures==3.1.1 From 8b01b8e54ebc5c65a325dd19c973116e73d98844 Mon Sep 17 00:00:00 2001 From: ashishkmishra36 Date: Tue, 11 May 2021 16:36:51 +0530 Subject: [PATCH 069/109] removed debugging info --- core/tests/test_serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index 45c71bc5..f25c38c2 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -9,7 +9,7 @@ def test_org_serializer(request_factory, org): request = request_factory.get('') serializer = OrganizationSerializer(org, context={'request': request}) data = serializer.data - print("test org serializer", data) + keys = [ 'id', 'organization_uuid', From 443c4803f6c82945fff55911cf245afa143b1150 Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Wed, 2 Jun 2021 16:13:08 +0530 Subject: [PATCH 070/109] change in request format of email alert endpoint (#53) * change in request format of email alert endpoint * resolved flake-8 error --- core/admin.py | 3 ++- core/serializers.py | 3 +-- core/views/coreuser.py | 19 +++++++++++-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/core/admin.py b/core/admin.py index 58f10d0c..cca1c643 100644 --- a/core/admin.py +++ b/core/admin.py @@ -35,7 +35,8 @@ class CoreUserAdmin(UserAdmin): search_fields = ('first_name', 'first_name', 'username', 'title', 'organization__name', ) fieldsets = ( (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('title', 'first_name', 'last_name', 'email', 'contact_info', 'organization')}), + (_('Personal info'), {'fields': ('title', 'first_name', 'last_name', 'email', 'contact_info', + 'organization', 'email_alert_flag')}), (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'core_groups', 'user_permissions')}), (_('Important dates'), {'fields': ('last_login', 'date_joined', 'create_date', 'edit_date')}), ) diff --git a/core/serializers.py b/core/serializers.py index f08fcbc0..1d117501 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -330,7 +330,6 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): """ Serializer for email alert of shipment """ - user_uuid = serializers.UUIDField() + organization_uuid = serializers.UUIDField() messages = serializers.JSONField() subject_line = serializers.CharField(max_length=255) - date_time = serializers.DateTimeField() diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 6fc04543..fdc97220 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -277,17 +277,16 @@ def get_permissions(self): @action(methods=['POST'], detail=False) def alert(self, request, *args, **kwargs): """ - a)Request alert message and uuid of core user - b)Send Email to the user's email with alert message + a)Request alert message and uuid of organization + b)Access user uuids for that respective organization + c)Check if opted for email alert service + d)Send Email to the user's email with alert message """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - user_uuid = request.data['user_uuid'] - # date_time = request.data['date_time'] + org_uuid = request.data['organization_uuid'] messages = request.data['messages'] subject_line = request.data['subject_line'] - user = CoreUser.objects.filter(core_user_uuid=user_uuid).first() - email_address = user.email if subject_line is not None: subject = subject_line else: @@ -304,12 +303,15 @@ def alert(self, request, *args, **kwargs): } template_name = 'email/coreuser/shipment_alert.txt' html_template_name = 'email/coreuser/shipment_alert.html' - send_email(email_address, subject, context, template_name, html_template_name) + core_users = CoreUser.objects.filter(organization__organization_uuid=org_uuid, email_alert_flag=True) + for user in core_users: + email_address = user.email + send_email(email_address, subject, context, template_name, html_template_name) return Response( { 'detail': 'The alert messages were sent successfully on email.', }, status=status.HTTP_200_OK) - + # This code is commented out as in future, It will need to impliment message service. # for phone in phones: # phone_number = phone # account_sid = os.environ['TWILIO_ACCOUNT_SID'] @@ -321,6 +323,7 @@ def alert(self, request, *args, **kwargs): # to=phone_number # ) # print(message.sid) + @action(detail=True, methods=['patch'], name='Update Profile') def update_profile(self, request, pk=None, *args, **kwargs): """ From bea2e45c6fbdd054b47ed66cb077af19468491cd Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Fri, 4 Jun 2021 17:15:23 +0530 Subject: [PATCH 071/109] Added consortium table and its endpoint (#50) * added consortium table and its endpoint * formatted as per flake8 * resolved falke8 error --- core/admin.py | 3 +- core/migrations/0006_consortium.py | 31 +++++++++++++ core/models.py | 21 +++++++++ core/serializers.py | 11 ++++- core/urls.py | 3 +- core/views/consortium.py | 70 ++++++++++++++++++++++++++++++ 6 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 core/migrations/0006_consortium.py create mode 100644 core/views/consortium.py diff --git a/core/admin.py b/core/admin.py index cca1c643..aa2960b1 100644 --- a/core/admin.py +++ b/core/admin.py @@ -2,7 +2,7 @@ from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext_lazy as _ -from core.models import CoreUser, CoreGroup, CoreSites, EmailTemplate, Industry, LogicModule, Organization +from core.models import CoreUser, CoreGroup, CoreSites, EmailTemplate, Industry, LogicModule, Organization, Consortium class LogicModuleAdmin(admin.ModelAdmin): @@ -69,3 +69,4 @@ class EmailTemplateAdmin(admin.ModelAdmin): admin.site.register(CoreSites, CoreSitesAdmin) admin.site.register(EmailTemplate, EmailTemplateAdmin) admin.site.register(Industry) +admin.site.register(Consortium) diff --git a/core/migrations/0006_consortium.py b/core/migrations/0006_consortium.py new file mode 100644 index 00000000..616a9503 --- /dev/null +++ b/core/migrations/0006_consortium.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.10 on 2021-05-28 06:46 + +from django.conf import settings +from django.db import migrations, models +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_organization_radius'), + ] + + operations = [ + migrations.CreateModel( + name='Consortium', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('consortium_uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('name', models.CharField(blank=True, max_length=255, null=True)), + ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ('edit_date', models.DateTimeField(blank=True, null=True)), + ('core_users', models.ManyToManyField(blank=True, related_name='consortium_users', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Consortiums', + 'ordering': ('name',), + }, + ), + ] diff --git a/core/models.py b/core/models.py index 24e1bb28..04139e79 100644 --- a/core/models.py +++ b/core/models.py @@ -264,3 +264,24 @@ def save(self, *args, **kwargs): def __str__(self): return str(self.name) + + +class Consortium(models.Model): + consortium_uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + name = models.CharField(blank=True, null=True, max_length=255) + core_users = models.ManyToManyField(CoreUser, blank=True, related_name='consortium_users') + create_date = models.DateTimeField(default=timezone.now) + edit_date = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ('name',) + verbose_name_plural = "Consortiums" + + def save(self, *args, **kwargs): + if self.create_date is None: + self.create_date = timezone.now() + self.edit_date = timezone.now() + super(Consortium, self).save() + + def __str__(self): + return str(self.name) \ No newline at end of file diff --git a/core/serializers.py b/core/serializers.py index 1d117501..4c573a95 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -17,7 +17,7 @@ from core.email_utils import send_email, send_email_body from core.models import CoreUser, CoreGroup, EmailTemplate, LogicModule, Organization, PERMISSIONS_ORG_ADMIN, \ - TEMPLATE_RESET_PASSWORD + TEMPLATE_RESET_PASSWORD, Consortium class LogicModuleSerializer(serializers.ModelSerializer): @@ -333,3 +333,12 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): organization_uuid = serializers.UUIDField() messages = serializers.JSONField() subject_line = serializers.CharField(max_length=255) + + +class ConsortiumSerializer(serializers.ModelSerializer): + id = serializers.ReadOnlyField() + uuid = serializers.ReadOnlyField() + + class Meta: + model = Consortium + fields = '__all__' diff --git a/core/urls.py b/core/urls.py index f6b85954..6469bba1 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,7 @@ from django.urls import include, path, re_path from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns - +from core.views.consortium import ConsortiumViewSet from rest_framework import routers from core import views @@ -21,6 +21,7 @@ router.register(r'oauth/refreshtokens', views.RefreshTokenViewSet) router.register(r'organization', views.OrganizationViewSet) router.register(r'logicmodule', views.LogicModuleViewSet) +router.register(r'consortium', ConsortiumViewSet) urlpatterns = [ diff --git a/core/views/consortium.py b/core/views/consortium.py new file mode 100644 index 00000000..c953aea5 --- /dev/null +++ b/core/views/consortium.py @@ -0,0 +1,70 @@ +import logging + +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from django_filters.rest_framework import DjangoFilterBackend + +from core.permissions import IsSuperUser +from core.models import Consortium +from core.serializers import ConsortiumSerializer + +from gateway import utils + +logger = logging.getLogger(__name__) + + +class ConsortiumViewSet(viewsets.ModelViewSet): + """ + title: + Consortium of the application + + description: + + retrieve: + Return the Consortium. + + list: + Return a list of all the existing Consortiums. + + create: + Create a new Consortium instance. + + update: + Update a Consortium instance. + + delete: + Delete a Consortium instance. + """ + + filter_fields = ('name',) + filter_backends = (DjangoFilterBackend,) + permission_classes = (IsSuperUser,) + queryset = Consortium.objects.all() + serializer_class = ConsortiumSerializer + + @action(methods=['PUT'], url_path='specification', detail=True) + def update_api_specification(self, request, *args, **kwargs): + """ + Updates the API specification of given logic module + """ + instance = self.get_object() + schema_url = utils.get_swagger_url_by_logic_module(instance) + + response = utils.get_swagger_from_url(schema_url) + spec_dict = response.json() + data = { + 'api_specification': spec_dict + } + + serializer = self.get_serializer(instance, data=data, partial=True) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + return Response(serializer.data) From 157350a8d6e7a1da10dd3b5e5dfb7175c96b8109 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Fri, 4 Jun 2021 19:08:16 +0530 Subject: [PATCH 072/109] Added organization types (#56) * Added organization type * Formatting fixes * Corrected test case --- .../management/commands/loadinitialdata.py | 8 ++++- buildly/settings/base.py | 9 ++++++ buildly/tests/test_loadinitialdata.py | 3 +- core/admin.py | 11 +++++-- core/migrations/0007_organization_type.py | 30 +++++++++++++++++++ core/models.py | 21 +++++++++++++ core/tests/test_serializers.py | 1 + 7 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 core/migrations/0007_organization_type.py diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index f67bc043..82578f52 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -6,7 +6,7 @@ from django.db import transaction from core.models import ROLE_VIEW_ONLY, ROLE_ORGANIZATION_ADMIN, ROLE_WORKFLOW_ADMIN, ROLE_WORKFLOW_TEAM, \ - Organization, CoreUser, CoreGroup + Organization, CoreUser, CoreGroup, OrganizationType logger = logging.getLogger(__name__) @@ -27,6 +27,11 @@ def __init__(self, *args, **kwargs): self._su_group = None self._default_org = None + def _create_organization_types(self): + if settings.ORGANIZATION_TYPES: + for organization_type in settings.ORGANIZATION_TYPES: + OrganizationType.objects.get_or_create(name=organization_type) + def _create_default_organization(self): if settings.DEFAULT_ORG: self._default_org, _ = Organization.objects.get_or_create(name=settings.DEFAULT_ORG) @@ -73,5 +78,6 @@ def _create_user(self): @transaction.atomic def handle(self, *args, **options): self._create_groups() + self._create_organization_types() self._create_default_organization() self._create_user() diff --git a/buildly/settings/base.py b/buildly/settings/base.py index 819bc748..d1221d0a 100644 --- a/buildly/settings/base.py +++ b/buildly/settings/base.py @@ -198,3 +198,12 @@ SWAGGER_SETTINGS = { 'DEFAULT_INFO': 'gateway.urls.swagger_info', } + +ORGANIZATION_TYPES = [ + 'Logistics Provider', + 'Packer', + 'Producer', + 'Receiver', + 'Shipper', + 'Warehouse' +] \ No newline at end of file diff --git a/buildly/tests/test_loadinitialdata.py b/buildly/tests/test_loadinitialdata.py index 932c7cbc..849addfb 100644 --- a/buildly/tests/test_loadinitialdata.py +++ b/buildly/tests/test_loadinitialdata.py @@ -5,7 +5,7 @@ from django.core.management import call_command from django.test import TransactionTestCase, override_settings -from core.models import CoreGroup, CoreUser, Organization +from core.models import CoreGroup, CoreUser, Organization, OrganizationType class DevNull(object): @@ -34,6 +34,7 @@ def test_full_initial_data(self): call_command('loadinitialdata', *args, **opts) assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 + assert OrganizationType.objects.filter().count() >= 6 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 diff --git a/core/admin.py b/core/admin.py index aa2960b1..04a061de 100644 --- a/core/admin.py +++ b/core/admin.py @@ -2,7 +2,8 @@ from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext_lazy as _ -from core.models import CoreUser, CoreGroup, CoreSites, EmailTemplate, Industry, LogicModule, Organization, Consortium +from core.models import CoreUser, CoreGroup, CoreSites, EmailTemplate, \ + Industry, LogicModule, Organization, OrganizationType, Consortium class LogicModuleAdmin(admin.ModelAdmin): @@ -17,8 +18,13 @@ class CoreSitesAdmin(admin.ModelAdmin): search_fields = ('name',) +class OrganizationTypeAdmin(admin.ModelAdmin): + list_display = ('name',) + display = 'Organization Type' + + class OrganizationAdmin(admin.ModelAdmin): - list_display = ('name', 'create_date', 'edit_date') + list_display = ('name', 'organization_type', 'create_date', 'edit_date') display = 'Organization' @@ -64,6 +70,7 @@ class EmailTemplateAdmin(admin.ModelAdmin): admin.site.register(LogicModule, LogicModuleAdmin) admin.site.register(Organization, OrganizationAdmin) +admin.site.register(OrganizationType, OrganizationTypeAdmin) admin.site.register(CoreGroup, CoreGroupAdmin) admin.site.register(CoreUser, CoreUserAdmin) admin.site.register(CoreSites, CoreSitesAdmin) diff --git a/core/migrations/0007_organization_type.py b/core/migrations/0007_organization_type.py new file mode 100644 index 00000000..6000103f --- /dev/null +++ b/core/migrations/0007_organization_type.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.10 on 2021-06-04 06:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_consortium'), + ] + + operations = [ + migrations.CreateModel( + name='OrganizationType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, help_text='Organization type', max_length=255, verbose_name='Name')), + ], + options={ + 'verbose_name_plural': 'Organization Types', + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='organization', + name='organization_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.OrganizationType'), + ), + ] diff --git a/core/models.py b/core/models.py index 04139e79..481ea0f5 100644 --- a/core/models.py +++ b/core/models.py @@ -78,6 +78,25 @@ def save(self, *args, **kwargs): def __str__(self): return self.name +class OrganizationType(models.Model): + """ + Allows organization to be of multiple types. + Supported types are: + 1. Logistics Provider + 2. Packer + 3. Producer + 4. Receiver + 5. Shipper + 6. Warehouse + """ + name = models.CharField("Name", max_length=255, blank=True, help_text="Organization type") + + class Meta: + ordering = ('name',) + verbose_name_plural = "Organization Types" + + def __str__(self): + return str(self.name) class Organization(models.Model): """ @@ -96,6 +115,8 @@ class Organization(models.Model): phone = models.CharField(max_length=20, blank=True, null=True) allow_import_export = models.BooleanField('To allow import export functionality', default=False) radius = models.FloatField(max_length=20, blank=True, null=True) + organization_type = models.ForeignKey(OrganizationType,on_delete=models.CASCADE,null=True) + class Meta: ordering = ('name',) verbose_name_plural = "Organizations" diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index f25c38c2..d19bb337 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -24,6 +24,7 @@ def test_org_serializer(request_factory, org): 'industries', 'allow_import_export', 'radius', + 'organization_type' ] assert set(data.keys()) == set(keys) From 774f9085dce22dee6dca60b434740bce31fc97c6 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 9 Jun 2021 13:01:51 +0530 Subject: [PATCH 073/109] Approval email for newly registered users (#57) * Added organization type * Formatting fixes * Corrected test case * Approval email for new users * Resolved flake8 warnings * Fixed issue in tests * Resolved comments --- buildly/settings/base.py | 8 +- buildly/tests/test_admin.py | 2 +- buildly/tests/test_loadinitialdata.py | 2 +- core/serializers.py | 20 ++ core/tests/test_accesstokenview.py | 1 - core/tests/test_applicationview.py | 1 - core/tests/test_coregroupview.py | 2 +- core/tests/test_coreuserview.py | 2 +- core/tests/test_emailtemplates.py | 1 - core/tests/test_logicmoduleview.py | 1 - core/tests/test_refreshtokenview.py | 1 - core/tests/test_serializers.py | 1 - datamesh/tests/test_join.py | 1 - datamesh/tests/test_models.py | 1 - datamesh/tests/test_serializers.py | 1 - datamesh/tests/test_views.py | 1 - gateway/tests/test_permissions.py | 1 - gateway/tests/test_swagger_aggregator.py | 1 - gateway/tests/test_views.py | 1 - templates/email/coreuser/approval.html | 153 +++++++++ templates/email/coreuser/approval.txt | 5 + templates/email/coreuser/invitation.html | 309 +++++++++++++++---- templates/email/coreuser/shipment_alert.html | 65 ++-- workflow/tests/test_serializers.py | 1 - workflow/tests/test_workflowlevelstatus.py | 1 - workflow/tests/test_workflowleveltypeview.py | 1 - 26 files changed, 477 insertions(+), 107 deletions(-) create mode 100644 templates/email/coreuser/approval.html create mode 100644 templates/email/coreuser/approval.txt diff --git a/buildly/settings/base.py b/buildly/settings/base.py index d1221d0a..6b024c33 100644 --- a/buildly/settings/base.py +++ b/buildly/settings/base.py @@ -200,10 +200,6 @@ } ORGANIZATION_TYPES = [ - 'Logistics Provider', - 'Packer', - 'Producer', - 'Receiver', - 'Shipper', - 'Warehouse' + 'Custodian', + 'Producer' ] \ No newline at end of file diff --git a/buildly/tests/test_admin.py b/buildly/tests/test_admin.py index acaa59a1..0c5337a5 100644 --- a/buildly/tests/test_admin.py +++ b/buildly/tests/test_admin.py @@ -8,7 +8,7 @@ class AdminViewTest(TestCase): def test_admin_user_auth_page_with_superuser(self): """Super user should see superuser status field on django admin""" - admin = CoreUser.objects.create_superuser('admin', 'admin@example.com', 'Password123') + CoreUser.objects.create_superuser('admin', 'admin@example.com', 'Password123') another_user = factories.CoreUser(username='another_user') self.client.login(username='admin', password='Password123') diff --git a/buildly/tests/test_loadinitialdata.py b/buildly/tests/test_loadinitialdata.py index 849addfb..4c4ccc66 100644 --- a/buildly/tests/test_loadinitialdata.py +++ b/buildly/tests/test_loadinitialdata.py @@ -34,7 +34,7 @@ def test_full_initial_data(self): call_command('loadinitialdata', *args, **opts) assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 - assert OrganizationType.objects.filter().count() >= 6 + assert OrganizationType.objects.filter().count() >= 2 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 diff --git a/core/serializers.py b/core/serializers.py index 4c573a95..ccb71e4c 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -138,6 +138,26 @@ def create(self, validated_data): coreuser.set_password(validated_data['password']) coreuser.save() + # Triggers an approval email for newly registered user + approval_link = urljoin(settings.FRONTEND_URL, '/app/profile/users/current-users') + subject = 'Approval Request' + template_name = 'email/coreuser/approval.txt' + html_template_name = 'email/coreuser/approval.html' + context = { + 'approval_link': approval_link, + 'coreuser_name': coreuser.first_name + ' ' + coreuser.last_name, + 'organization_name': organization + } + if is_new_org: + admin = CoreUser.objects.filter(is_superuser=True) # Global Admin + else: + org_admin_groups = CoreGroup.objects.filter(permissions=PERMISSIONS_ORG_ADMIN, is_org_level=True) + admin = CoreUser.objects.filter(core_groups__in=org_admin_groups, + organization=organization) # Organization Admin + if admin: + for users in admin: + send_email(users.email, subject, context, template_name, html_template_name) + # add org admin role to the user if org is new if is_new_org: group_org_admin = CoreGroup.objects.get(organization=organization, diff --git a/core/tests/test_accesstokenview.py b/core/tests/test_accesstokenview.py index c95330c3..542b27e7 100644 --- a/core/tests/test_accesstokenview.py +++ b/core/tests/test_accesstokenview.py @@ -4,7 +4,6 @@ from oauth2_provider.models import AccessToken from rest_framework.reverse import reverse - from core.tests.fixtures import auth_api_client, auth_superuser_api_client, oauth_application, oauth_access_token, \ superuser diff --git a/core/tests/test_applicationview.py b/core/tests/test_applicationview.py index c290f543..99d33e96 100644 --- a/core/tests/test_applicationview.py +++ b/core/tests/test_applicationview.py @@ -4,7 +4,6 @@ from oauth2_provider.models import Application from rest_framework.reverse import reverse - from core.tests.fixtures import auth_api_client, auth_superuser_api_client, oauth_application, superuser diff --git a/core/tests/test_coregroupview.py b/core/tests/test_coregroupview.py index 05448cb5..c5ef168b 100644 --- a/core/tests/test_coregroupview.py +++ b/core/tests/test_coregroupview.py @@ -2,9 +2,9 @@ from rest_framework.reverse import reverse import factories +from core.tests.fixtures import org, org_admin, org_member, reset_password_request, TEST_USER_DATA from core.models import CoreGroup from core.views import CoreGroupViewSet -from core.tests.fixtures import org, org_admin, org_member @pytest.mark.django_db() diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index 77838e34..f8f7540c 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -14,7 +14,7 @@ from core.models import CoreUser, EmailTemplate, TEMPLATE_RESET_PASSWORD from core.views import CoreUserViewSet from core.jwt_utils import create_invitation_token -from core.tests.fixtures import org, org_admin, org_member, reset_password_request, TEST_USER_DATA +from core.tests.fixtures import TEST_USER_DATA, org_admin, org_member, org, reset_password_request @pytest.mark.django_db() diff --git a/core/tests/test_emailtemplates.py b/core/tests/test_emailtemplates.py index df22c5ee..f10ff1a5 100644 --- a/core/tests/test_emailtemplates.py +++ b/core/tests/test_emailtemplates.py @@ -5,7 +5,6 @@ from core.models import EmailTemplate, TEMPLATE_RESET_PASSWORD from core.tests.fixtures import org - @pytest.mark.django_db() class TestEmailTemplateModel: diff --git a/core/tests/test_logicmoduleview.py b/core/tests/test_logicmoduleview.py index 99bb89a7..c2984e07 100644 --- a/core/tests/test_logicmoduleview.py +++ b/core/tests/test_logicmoduleview.py @@ -9,7 +9,6 @@ from core.views.logicmodule import LogicModuleViewSet from core.tests.fixtures import logic_module, superuser - from gateway import utils diff --git a/core/tests/test_refreshtokenview.py b/core/tests/test_refreshtokenview.py index d89de745..78d26526 100644 --- a/core/tests/test_refreshtokenview.py +++ b/core/tests/test_refreshtokenview.py @@ -3,7 +3,6 @@ from oauth2_provider.models import RefreshToken from rest_framework.reverse import reverse - from core.tests.fixtures import auth_api_client, auth_superuser_api_client, oauth_application, oauth_access_token, \ oauth_refresh_token, superuser diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index d19bb337..549911c3 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -3,7 +3,6 @@ from core.serializers import OrganizationSerializer, CoreGroupSerializer, CoreUserSerializer from core.tests.fixtures import core_group, org, org_member - @pytest.mark.django_db() def test_org_serializer(request_factory, org): request = request_factory.get('') diff --git a/datamesh/tests/test_join.py b/datamesh/tests/test_join.py index a95982ec..3e076397 100644 --- a/datamesh/tests/test_join.py +++ b/datamesh/tests/test_join.py @@ -8,7 +8,6 @@ from datamesh.tests.fixtures import relationship, relationship2, relationship_with_10_records from core.tests.fixtures import auth_api_client, org - @pytest.mark.django_db() @patch('gateway.request.GatewayRequest._get_swagger_spec') @patch('gateway.request.SwaggerClient.request') diff --git a/datamesh/tests/test_models.py b/datamesh/tests/test_models.py index 1d75d1a4..b3f5b945 100644 --- a/datamesh/tests/test_models.py +++ b/datamesh/tests/test_models.py @@ -5,7 +5,6 @@ from django.db import IntegrityError, transaction from datamesh.models import JoinRecord, Relationship, LogicModuleModel - from core.tests.fixtures import org from .fixtures import relationship, appointment_logic_module_model, document_logic_module_model diff --git a/datamesh/tests/test_serializers.py b/datamesh/tests/test_serializers.py index 3a0e6877..70c50964 100644 --- a/datamesh/tests/test_serializers.py +++ b/datamesh/tests/test_serializers.py @@ -3,7 +3,6 @@ from datamesh.serializers import JoinRecordSerializer from .fixtures import join_record - @pytest.mark.django_db() def test_join_record_serializer_from_instance(request_factory, join_record): request = request_factory.get('') diff --git a/datamesh/tests/test_views.py b/datamesh/tests/test_views.py index 1424fcf5..f182a620 100644 --- a/datamesh/tests/test_views.py +++ b/datamesh/tests/test_views.py @@ -17,7 +17,6 @@ relationship2 ) - @pytest.mark.django_db() class TestJoinRecordBase: diff --git a/gateway/tests/test_permissions.py b/gateway/tests/test_permissions.py index 3a492f6e..04db1609 100644 --- a/gateway/tests/test_permissions.py +++ b/gateway/tests/test_permissions.py @@ -7,7 +7,6 @@ from gateway.exceptions import ServiceDoesNotExist from gateway.permissions import AllowLogicModuleGroup from gateway.views import APIGatewayView - from core.tests.fixtures import auth_api_client, auth_superuser_api_client, core_group, logic_module, org, org_admin,\ superuser diff --git a/gateway/tests/test_swagger_aggregator.py b/gateway/tests/test_swagger_aggregator.py index 32a422a6..fa599ec1 100644 --- a/gateway/tests/test_swagger_aggregator.py +++ b/gateway/tests/test_swagger_aggregator.py @@ -5,7 +5,6 @@ from gateway import utils from gateway.tests.fixtures import aggregator, logic_module - @pytest.mark.django_db() class TestSwaggerAggregator: diff --git a/gateway/tests/test_views.py b/gateway/tests/test_views.py index 7894b373..218a4dfc 100644 --- a/gateway/tests/test_views.py +++ b/gateway/tests/test_views.py @@ -8,7 +8,6 @@ from core.tests.fixtures import auth_api_client, logic_module from .fixtures import datamesh - CURRENT_PATH = os.path.dirname(os.path.abspath(__file__)) diff --git a/templates/email/coreuser/approval.html b/templates/email/coreuser/approval.html new file mode 100644 index 00000000..eff46409 --- /dev/null +++ b/templates/email/coreuser/approval.html @@ -0,0 +1,153 @@ + + + + + + + +
+ Transparent Path spc +
+



+ + + + + + +
+

+ New Approval Request Admin! +

+

+ Approve newly registered user + {{ coreuser_name }} on {{ organization_name }} + +

+
+ + + + + + +
+ Approve +
+






+
+

+ Transparent Path spc
+ 1700 Westlake Avenue North Suite 200 Seattle, WA 98109 +

+

+ Have any questions or thoughts on Transparent Path spc?
+ Reach out to us at anytime at + support@tpath.io +

+
+ + diff --git a/templates/email/coreuser/approval.txt b/templates/email/coreuser/approval.txt new file mode 100644 index 00000000..4224c0e3 --- /dev/null +++ b/templates/email/coreuser/approval.txt @@ -0,0 +1,5 @@ +New Approval Request Admin! + +Approve newly registered user {{ coreuser_name }} on {{ organization_name }} ! +Click the button below to create your user profile. +{{ approval_link }} diff --git a/templates/email/coreuser/invitation.html b/templates/email/coreuser/invitation.html index 074aa623..e6d2d0f3 100644 --- a/templates/email/coreuser/invitation.html +++ b/templates/email/coreuser/invitation.html @@ -1,57 +1,258 @@ - - - - - -
toladata
-



- - - - - - -
-

Invitation from your Org
Admin!

-

You have been invited to join {{ organization_name }} on TolaData!
Click the button below to create your user profile

-
- - - - - - -
My profile
- - - - - - - - - - - - - - - - - - - - - -
-

Next steps

-
Once you have registered you will be within {{ organization_name }}’s portfolio. To see specific programs you must request access.
You can access the TolaData knowledgebase with training materials, FAQs and other useful information at our TolaData
Knowledgebase: https://help.toladata.com
If you have any questions about this invitation please contact {{ org_admin_name }}
from {{ organization_name }}
-






-
-

TolaData GmbH
Wöhlerstraße 12-13, 10115 Berlin, Germany

-

Have any questions or thoughts on TolaData?
Reach out to us at anytime at support@toladata.com

-
- + + + + + +
+ Transparent Path spc +
+



+ + + + + + +
+

+ Invitation from your Org
Admin!
+

+

+ You have been invited to join + {{ organization_name }} + on Transparent Path spc!
Click the button below to create your user + profile
+

+
+ + + + + + +
+ My profile +
+ + + + + + + + + + + + + + + + + + + + + +
+

+ Next steps +

+
+ Once you have registered you will be within + {{ organization_name }}’s portfolio. To see specific programs you must request access. +
+ You can access the Transparent Path spc knowledgebase with training materials, FAQs + and other useful information at our Transparent Path spc +
+ Knowledgebase: + https://help.Transparent Path spc.com +
+ If you have any questions about this invitation please contact + {{ org_admin_name }}
+ from + + {{ organization_name }} + +
+






+
+

+ Transparent Path spc
+ 1700 Westlake Avenue North Suite 200 Seattle, WA 98109 +

+

+ Have any questions or thoughts on Transparent Path spc?
+ Reach out to us at anytime at + support@tpath.io +

+
+ diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index b2d51c3e..e51bb431 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -1,30 +1,41 @@ - - - - - - -



- - - - - - - -
-

Message from Transparent Path
Admin!

- {% for message in messages %} -
-

Shipment Id {{ message.shipment_uuid }}

-

{{ message.alert_message }} - {{ message.date_time }} -

-
-
- {% endfor %} -
- + + + + + +



+ + + + + + +
+

+ Message from Transparent Path
Admin!
+

+ {% for message in messages %} +
+

Shipment Name {{ message.shipment_uuid }}

+

+ {{ message.alert_message }} + {{ message.date_time }} +

+
+
+ {% endfor %} +
+ diff --git a/workflow/tests/test_serializers.py b/workflow/tests/test_serializers.py index ed6dee7c..aa48087d 100644 --- a/workflow/tests/test_serializers.py +++ b/workflow/tests/test_serializers.py @@ -3,7 +3,6 @@ from workflow.serializers import WorkflowLevel2Serializer, WorkflowLevelTypeSerializer from .fixtures import wfl2, wfl_type - @pytest.mark.django_db() def test_workflow_level2_serializer(request_factory, wfl2): request = request_factory.get('') diff --git a/workflow/tests/test_workflowlevelstatus.py b/workflow/tests/test_workflowlevelstatus.py index a527130b..789b6ae7 100644 --- a/workflow/tests/test_workflowlevelstatus.py +++ b/workflow/tests/test_workflowlevelstatus.py @@ -7,7 +7,6 @@ from ..views import WorkflowLevelStatusViewSet from core.tests.fixtures import org_member, org - @pytest.mark.django_db() def test_list_workflowlevelstatus(request_factory, org_member): request = request_factory.get(reverse('workflowlevelstatus-list')) diff --git a/workflow/tests/test_workflowleveltypeview.py b/workflow/tests/test_workflowleveltypeview.py index ffa64d3e..ccf0f901 100644 --- a/workflow/tests/test_workflowleveltypeview.py +++ b/workflow/tests/test_workflowleveltypeview.py @@ -7,7 +7,6 @@ from ..views import WorkflowLevelTypeViewSet from core.tests.fixtures import org_member, org - @pytest.mark.django_db() def test_list_workflowleveltype(request_factory, org_member): request = request_factory.get(reverse('workflowleveltype-list')) From df0d17c993caac6d84774bc0a3674f2230ac73ce Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 9 Jun 2021 20:51:28 +0530 Subject: [PATCH 074/109] Updated email template for alerts --- core/views/coreuser.py | 2 + templates/email/coreuser/shipment_alert.html | 95 ++++++++++++++++++-- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index fdc97220..799b6637 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -298,6 +298,8 @@ def alert(self, request, *args, **kwargs): time_tuple = time.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S.%f%z") time_format = calendar.timegm(time_tuple) message['date_time'] = time.ctime(time_format) + message['shipment_url'] = urljoin(settings.FRONTEND_URL, + '/app/shipment/edit/:'+str(message['shipment_id'])) context = { 'messages': messages, } diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index e51bb431..5e2533f3 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -5,13 +5,34 @@ +
+ Transparent Path spc +



-
+

-

{% for message in messages %}
-

Shipment Name {{ message.shipment_uuid }}

+

+ Shipment: {{ message.shipment_name }} + {{message.alert_message}} +

- {{ message.alert_message }} - {{ message.date_time }} + Captured at: {{ message.date_time }} (UTC) + + Go to Shipment


@@ -37,5 +77,50 @@

Shipment Name {{ message.shipment_uuid }}

+






+
+

+ Transparent Path spc
+ 1700 Westlake Avenue North Suite 200 Seattle, WA 98109 +

+

+ Have any questions or thoughts on Transparent Path spc?
+ Reach out to us at anytime at + support@tpath.io +

+
From b35bf26c5c00e4f588e3f388a28e59c036f521a7 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 9 Jun 2021 20:52:16 +0530 Subject: [PATCH 075/109] Updated email template for alerts --- templates/email/coreuser/shipment_alert.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index 5e2533f3..3f26bb81 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -46,10 +46,8 @@

{% for message in messages %}
-

- Shipment: {{ message.shipment_name }} - {{message.alert_message}} -

+

Shipment: {{ message.shipment_name }}

+

{{message.alert_message}}

Captured at: {{ message.date_time }} (UTC) Date: Thu, 10 Jun 2021 11:18:36 +0530 Subject: [PATCH 076/109] Organization names coming from open API as list of names (#60) --- core/serializers.py | 4 ---- core/tests/test_organizationview.py | 9 +++++++++ core/views/organization.py | 15 ++++++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index ccb71e4c..40bd8566 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -310,10 +310,6 @@ class Meta: fields = '__all__' -class OrganizationNameSerializer(serializers.Serializer): - name = serializers.CharField(required=False) - - class AccessTokenSerializer(serializers.ModelSerializer): user = CoreUserSerializer() diff --git a/core/tests/test_organizationview.py b/core/tests/test_organizationview.py index 4b5fb3de..f5357d8f 100644 --- a/core/tests/test_organizationview.py +++ b/core/tests/test_organizationview.py @@ -26,3 +26,12 @@ def test_list_organization_normaluser_one_result(self): response = view(self.request) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) + + def test_list_organization_names(self): + factory = APIRequestFactory() + self.request = factory.get('/organization/fetch_orgs/') + self.request.user = factories.CoreUser() + + view = OrganizationViewSet.as_view({'get': 'list'}) + response = view(self.request) + self.assertEqual(response.status_code, 200) \ No newline at end of file diff --git a/core/views/organization.py b/core/views/organization.py index 8f655ae9..1d04705d 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -5,8 +5,9 @@ from rest_framework.response import Response from rest_framework.decorators import action from core.models import Organization -from core.serializers import OrganizationSerializer, OrganizationNameSerializer +from core.serializers import OrganizationSerializer from core.permissions import IsOrgMember +from django.views.decorators.csrf import csrf_exempt logger = logging.getLogger(__name__) @@ -48,13 +49,17 @@ def list(self, request, *args, **kwargs): queryset = Organization.objects.all() serializer_class = OrganizationSerializer + @csrf_exempt @action(detail=False, methods=['get'], name='Fetch Already existing Organization', url_path='fetch_orgs') def fetch_existing_orgs(self, request, pk=None, *args, **kwargs): """ Fetch Already existing Organizations in Buildly Core, Any logged in user can access this """ - # returns names of existing orgs in Buildly Core - queryset = Organization.objects.values('name') - serializer = OrganizationNameSerializer(queryset, many=True) - return Response(serializer.data) + # returns names of existing orgs in Buildly Core as a list + queryset = Organization.objects.all() + names = list() + for record in queryset: + names.append(record.name) + + return Response(names) From ad8ab4c0c14387f13fd22ab881901ef12796cb78 Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Fri, 11 Jun 2021 11:57:38 +0530 Subject: [PATCH 077/109] Resolve permission issue for "/organization/fetch_orgs/" endpoint (#64) * resolved permission issue for fetch_org endpoint * resloved flake-8 error --- core/views/organization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/views/organization.py b/core/views/organization.py index 1d04705d..fc309e6e 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -8,7 +8,7 @@ from core.serializers import OrganizationSerializer from core.permissions import IsOrgMember from django.views.decorators.csrf import csrf_exempt - +from rest_framework.permissions import AllowAny logger = logging.getLogger(__name__) @@ -50,7 +50,8 @@ def list(self, request, *args, **kwargs): serializer_class = OrganizationSerializer @csrf_exempt - @action(detail=False, methods=['get'], name='Fetch Already existing Organization', url_path='fetch_orgs') + @action(detail=False, methods=['get'], permission_classes=[AllowAny], + name='Fetch Already existing Organization', url_path='fetch_orgs') def fetch_existing_orgs(self, request, pk=None, *args, **kwargs): """ Fetch Already existing Organizations in Buildly Core, From 703161bac8f0ec7fa1f2f6e86a3010c3e4d75f5c Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Fri, 11 Jun 2021 13:16:03 +0530 Subject: [PATCH 078/109] Add API endpoint for organization type (#65) * resolved permission issue for fetch_org endpoint * resloved flake-8 error * add endpoint for organization type * corrected comments --- core/serializers.py | 10 ++++++++- core/urls.py | 4 ++-- core/views/organization.py | 43 +++++++++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 40bd8566..4234ce59 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -17,7 +17,7 @@ from core.email_utils import send_email, send_email_body from core.models import CoreUser, CoreGroup, EmailTemplate, LogicModule, Organization, PERMISSIONS_ORG_ADMIN, \ - TEMPLATE_RESET_PASSWORD, Consortium + TEMPLATE_RESET_PASSWORD, Consortium, OrganizationType class LogicModuleSerializer(serializers.ModelSerializer): @@ -358,3 +358,11 @@ class ConsortiumSerializer(serializers.ModelSerializer): class Meta: model = Consortium fields = '__all__' + + +class OrganizationTypeSerializer(serializers.ModelSerializer): + id = serializers.ReadOnlyField() + + class Meta: + model = OrganizationType + fields = '__all__' diff --git a/core/urls.py b/core/urls.py index 6469bba1..31e281fd 100644 --- a/core/urls.py +++ b/core/urls.py @@ -3,7 +3,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from core.views.consortium import ConsortiumViewSet from rest_framework import routers - +from core.views.organization import OrganizationTypeViewSet from core import views from core.views.web import IndexView, oauth_complete @@ -22,7 +22,7 @@ router.register(r'organization', views.OrganizationViewSet) router.register(r'logicmodule', views.LogicModuleViewSet) router.register(r'consortium', ConsortiumViewSet) - +router.register(r'organization_type', OrganizationTypeViewSet) urlpatterns = [ path('', IndexView.as_view(), name='index'), diff --git a/core/views/organization.py b/core/views/organization.py index fc309e6e..745062d4 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -1,11 +1,12 @@ import logging - +from django_filters.rest_framework import DjangoFilterBackend +from core.permissions import IsSuperUser import django_filters from rest_framework import viewsets from rest_framework.response import Response from rest_framework.decorators import action -from core.models import Organization -from core.serializers import OrganizationSerializer +from core.models import Organization, OrganizationType +from core.serializers import OrganizationSerializer, OrganizationTypeSerializer from core.permissions import IsOrgMember from django.views.decorators.csrf import csrf_exempt from rest_framework.permissions import AllowAny @@ -64,3 +65,39 @@ def fetch_existing_orgs(self, request, pk=None, *args, **kwargs): names.append(record.name) return Response(names) + + +class OrganizationTypeViewSet(viewsets.ModelViewSet): + """ + Organization type is associated with an organization which defines type of organization. + + title: + Organization Type + + description: + An organization type are custodian and producer + + They are associated with an organization. + Only admin has access to organization type. + + retrieve: + Return the Organization Type. + + list: + Return a list of all the existing Organization Types. + + create: + Create a new Organization Type instance. + + update: + Update a Organization Type instance. + + delete: + Delete a Organization Type instance. + """ + + filter_fields = ('name',) + filter_backends = (DjangoFilterBackend,) + permission_classes = (IsSuperUser,) + queryset = OrganizationType.objects.all() + serializer_class = OrganizationTypeSerializer From 3fe4554ea0a06ae05466d910fdcb77d673c6c302 Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Fri, 11 Jun 2021 14:41:00 +0530 Subject: [PATCH 079/109] change permission level --- core/views/organization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/views/organization.py b/core/views/organization.py index 745062d4..4aa07bf2 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -1,6 +1,6 @@ import logging from django_filters.rest_framework import DjangoFilterBackend -from core.permissions import IsSuperUser + import django_filters from rest_framework import viewsets from rest_framework.response import Response @@ -98,6 +98,6 @@ class OrganizationTypeViewSet(viewsets.ModelViewSet): filter_fields = ('name',) filter_backends = (DjangoFilterBackend,) - permission_classes = (IsSuperUser,) + permission_classes = (IsOrgMember,) queryset = OrganizationType.objects.all() serializer_class = OrganizationTypeSerializer From 2adf39a44b47f112c244073de14829e7ee269c5f Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Fri, 11 Jun 2021 15:04:51 +0530 Subject: [PATCH 080/109] add create and edit date in organization type --- core/migrations/0008_auto_20210611_0921.py | 23 ++++++++++++++++++++++ core/models.py | 8 ++++++++ 2 files changed, 31 insertions(+) create mode 100644 core/migrations/0008_auto_20210611_0921.py diff --git a/core/migrations/0008_auto_20210611_0921.py b/core/migrations/0008_auto_20210611_0921.py new file mode 100644 index 00000000..89afb7f4 --- /dev/null +++ b/core/migrations/0008_auto_20210611_0921.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.10 on 2021-06-11 09:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_organization_type'), + ] + + operations = [ + migrations.AddField( + model_name='organizationtype', + name='create_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='organizationtype', + name='edit_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index 481ea0f5..127e1cd4 100644 --- a/core/models.py +++ b/core/models.py @@ -90,11 +90,19 @@ class OrganizationType(models.Model): 6. Warehouse """ name = models.CharField("Name", max_length=255, blank=True, help_text="Organization type") + create_date = models.DateTimeField(null=True, blank=True) + edit_date = models.DateTimeField(null=True, blank=True) class Meta: ordering = ('name',) verbose_name_plural = "Organization Types" + def save(self, *args, **kwargs): + if self.create_date is None: + self.create_date = timezone.now() + self.edit_date = timezone.now() + super(OrganizationType, self).save(*args, **kwargs) + def __str__(self): return str(self.name) From 8c1ff4dd7625caf1faf67c5ae51e375cc4d786ff Mon Sep 17 00:00:00 2001 From: vishalajackus Date: Fri, 11 Jun 2021 15:31:41 +0530 Subject: [PATCH 081/109] change permission level to only organization admin --- core/views/organization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/views/organization.py b/core/views/organization.py index 4aa07bf2..fb90601e 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -7,7 +7,7 @@ from rest_framework.decorators import action from core.models import Organization, OrganizationType from core.serializers import OrganizationSerializer, OrganizationTypeSerializer -from core.permissions import IsOrgMember +from core.permissions import AllowOnlyOrgAdmin, IsOrgMember from django.views.decorators.csrf import csrf_exempt from rest_framework.permissions import AllowAny @@ -98,6 +98,6 @@ class OrganizationTypeViewSet(viewsets.ModelViewSet): filter_fields = ('name',) filter_backends = (DjangoFilterBackend,) - permission_classes = (IsOrgMember,) + permission_classes = (AllowOnlyOrgAdmin,) queryset = OrganizationType.objects.all() serializer_class = OrganizationTypeSerializer From 42dfbd0b9347afd7b3694790d27aed30250d7db8 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Tue, 29 Jun 2021 15:28:25 +0530 Subject: [PATCH 082/109] Updated configuration for consortium (#70) * Updated configuration for consortium * Flake8 fixes --- core/migrations/0001_initial.py | 164 ++++++++++-------- core/migrations/0002_auto_20200303_1657.py | 23 --- core/migrations/0002_consortium.py | 33 ++++ .../0003_coreuser_email_alert_flag.py | 18 -- .../0004_organization_allow_import_export.py | 18 -- core/migrations/0005_organization_radius.py | 18 -- core/migrations/0006_consortium.py | 31 ---- core/migrations/0007_organization_type.py | 30 ---- core/migrations/0008_auto_20210611_0921.py | 23 --- core/models.py | 9 +- core/serializers.py | 13 +- core/urls.py | 6 +- core/views/__init__.py | 5 +- core/views/consortium.py | 44 +---- gateway/__init__.py | 1 + gateway/utils.py | 6 +- 16 files changed, 154 insertions(+), 288 deletions(-) delete mode 100644 core/migrations/0002_auto_20200303_1657.py create mode 100644 core/migrations/0002_consortium.py delete mode 100644 core/migrations/0003_coreuser_email_alert_flag.py delete mode 100644 core/migrations/0004_organization_allow_import_export.py delete mode 100644 core/migrations/0005_organization_radius.py delete mode 100644 core/migrations/0006_consortium.py delete mode 100644 core/migrations/0007_organization_type.py delete mode 100644 core/migrations/0008_auto_20210611_0921.py diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index fae8db61..46393a34 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,17 +1,17 @@ -# Generated by Django 2.2.4 on 2019-09-18 16:59 +# Generated by Django 2.2.10 on 2021-06-28 15:34 from django.conf import settings import django.contrib.auth.models import django.contrib.auth.validators import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb from django.db import migrations, models +import django.db.models.deletion import django.utils.timezone import uuid class Migration(migrations.Migration): - settings.AUTH_USER_MODEL = 'core.CoreUser' - initial = True dependencies = [ ('auth', '0011_update_proxy_permissions'), @@ -19,51 +19,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='CoreUser', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('core_user_uuid', models.CharField(default=uuid.uuid4, max_length=255, unique=True, verbose_name='CoreUser UUID')), - ('title', models.CharField(blank=True, choices=[('mr', 'Mr.'), ('mrs', 'Mrs.'), ('ms', 'Ms.')], max_length=3, null=True)), - ('contact_info', models.CharField(blank=True, max_length=255, null=True)), - ('privacy_disclaimer_accepted', models.BooleanField(default=False)), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), - ('edit_date', models.DateTimeField(blank=True, null=True)), - ], - options={ - 'ordering': ('first_name',), - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='CoreGroup', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uuid', models.CharField(default=uuid.uuid4, max_length=255, unique=True, verbose_name='CoreGroup UUID')), - ('name', models.CharField(max_length=80, verbose_name='Name of the role')), - ('is_global', models.BooleanField(default=False, verbose_name='Is global group')), - ('is_org_level', models.BooleanField(default=False, verbose_name='Is organization level group')), - ('is_default', models.BooleanField(default=False, verbose_name='Is organization default group')), - ('permissions', models.PositiveSmallIntegerField(default=4, help_text='Decimal integer from 0 to 15 converted from 4-bit binary, each bit indicates permissions for CRUD', verbose_name='Permissions')), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), - ('edit_date', models.DateTimeField(blank=True, null=True)), - ], - options={ - 'ordering': ('name',), - }, - ), migrations.CreateModel( name='Industry', fields=[ @@ -82,7 +37,7 @@ class Migration(migrations.Migration): name='Organization', fields=[ ('organization_uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Organization UUID')), - ('name', models.CharField(blank=True, default='Default Organization', help_text='Each end user must be grouped into an organization', max_length=255, verbose_name='Organization Name')), + ('name', models.CharField(blank=True, help_text='Each end user must be grouped into an organization', max_length=255, verbose_name='Organization Name')), ('description', models.TextField(blank=True, help_text='Description of organization', max_length=765, null=True, verbose_name='Description/Notes')), ('organization_url', models.CharField(blank=True, help_text='Link to organizations external web site', max_length=255, null=True)), ('create_date', models.DateTimeField(blank=True, null=True)), @@ -91,6 +46,8 @@ class Migration(migrations.Migration): ('date_format', models.CharField(blank=True, default='DD.MM.YYYY', max_length=50, verbose_name='Date Format')), ('phone', models.CharField(blank=True, max_length=20, null=True)), ('industries', models.ManyToManyField(blank=True, help_text='Type of Industry the organization belongs to if any', related_name='organizations', to='core.Industry')), + ('allow_import_export', models.BooleanField(default=False, verbose_name='To allow import export functionality')), + ('radius', models.FloatField(blank=True, max_length=20, null=True)), ], options={ 'verbose_name_plural': 'Organizations', @@ -113,30 +70,23 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Core Sites', }, ), - migrations.AddField( - model_name='coregroup', - name='organization', - field=models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization'), - ), - migrations.AddField( - model_name='coreuser', - name='core_groups', - field=models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='core.CoreGroup', verbose_name='User groups'), - ), - migrations.AddField( - model_name='coreuser', - name='groups', - field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), - ), - migrations.AddField( - model_name='coreuser', - name='organization', - field=models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization'), - ), - migrations.AddField( - model_name='coreuser', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), + migrations.CreateModel( + name='CoreGroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(default=uuid.uuid4, max_length=255, unique=True, verbose_name='CoreGroup UUID')), + ('name', models.CharField(max_length=80, verbose_name='Name of the role')), + ('is_global', models.BooleanField(default=False, verbose_name='Is global group')), + ('is_org_level', models.BooleanField(default=False, verbose_name='Is organization level group')), + ('is_default', models.BooleanField(default=False, verbose_name='Is organization default group')), + ('permissions', models.PositiveSmallIntegerField(default=4, help_text='Decimal integer from 0 to 15 converted from 4-bit binary, each bit indicates permissions for CRUD', verbose_name='Permissions')), + ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ('edit_date', models.DateTimeField(blank=True, null=True)), + ('organization', models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization')), + ], + options={ + 'ordering': ('name',), + }, ), migrations.CreateModel( name='EmailTemplate', @@ -163,7 +113,7 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, max_length=765, null=True, verbose_name='Description/Notes')), ('endpoint', models.CharField(blank=True, max_length=255, null=True)), ('endpoint_name', models.CharField(blank=True, max_length=255, null=True)), - ('relationships', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ('api_specification', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), ('docs_endpoint', models.CharField(blank=True, max_length=255, null=True)), ('core_groups', models.ManyToManyField(blank=True, related_name='logic_module_set', related_query_name='logic_module', to='core.CoreGroup', verbose_name='Logic Module groups')), ('create_date', models.DateTimeField(blank=True, null=True)), @@ -175,4 +125,70 @@ class Migration(migrations.Migration): 'unique_together': {('endpoint', 'endpoint_name')}, }, ), + migrations.CreateModel( + name='CoreUser', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('core_user_uuid', models.CharField(default=uuid.uuid4, max_length=255, unique=True, verbose_name='CoreUser UUID')), + ('title', models.CharField(blank=True, choices=[('mr', 'Mr.'), ('mrs', 'Mrs.'), ('ms', 'Ms.')], max_length=3, null=True)), + ('contact_info', models.CharField(blank=True, max_length=255, null=True)), + ('privacy_disclaimer_accepted', models.BooleanField(default=False)), + ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ('edit_date', models.DateTimeField(blank=True, null=True)), + ('core_groups', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='core.CoreGroup', verbose_name='User groups')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('organization', models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ('email_alert_flag', models.BooleanField(blank=True, default=False, null=True)), + ], + options={ + 'ordering': ('first_name',), + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Consortium', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('consortium_uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('name', models.CharField(blank=True, max_length=255, null=True)), + ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ('edit_date', models.DateTimeField(blank=True, null=True)), + ('core_users', models.ManyToManyField(blank=True, related_name='consortium_users', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Consortiums', + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='OrganizationType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, help_text='Organization type', max_length=255, verbose_name='Name')), + ('create_date', models.DateTimeField(blank=True, null=True)), + ('edit_date', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'verbose_name_plural': 'Organization Types', + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='organization', + name='organization_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.OrganizationType'), + ), ] diff --git a/core/migrations/0002_auto_20200303_1657.py b/core/migrations/0002_auto_20200303_1657.py deleted file mode 100644 index b828e4f1..00000000 --- a/core/migrations/0002_auto_20200303_1657.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.9 on 2020-03-03 16:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.RenameField( - model_name='logicmodule', - old_name='relationships', - new_name='api_specification', - ), - migrations.AlterField( - model_name='organization', - name='name', - field=models.CharField(blank=True, help_text='Each end user must be grouped into an organization', max_length=255, verbose_name='Organization Name'), - ), - ] diff --git a/core/migrations/0002_consortium.py b/core/migrations/0002_consortium.py new file mode 100644 index 00000000..7b4e5a46 --- /dev/null +++ b/core/migrations/0002_consortium.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.10 on 2021-06-28 15:35 + +import django.contrib.postgres.fields +from django.db import migrations, models +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='Consortium', + ), + migrations.CreateModel( + name='Consortium', + fields=[ + ('consortium_uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Consortium UUID')), + ('name', models.CharField(blank=True, help_text='Multiple organizations form a consortium together', max_length=255, verbose_name='Consortium Name')), + ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ('edit_date', models.DateTimeField(blank=True, null=True)), + ('custodian_uuids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Custodian UUIDs'), blank=True, null=True, size=None)), + ], + options={ + 'verbose_name_plural': 'Consortiums', + 'ordering': ('name',), + }, + ), + ] diff --git a/core/migrations/0003_coreuser_email_alert_flag.py b/core/migrations/0003_coreuser_email_alert_flag.py deleted file mode 100644 index 757a400d..00000000 --- a/core/migrations/0003_coreuser_email_alert_flag.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.10 on 2021-02-17 10:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0002_auto_20200303_1657'), - ] - - operations = [ - migrations.AddField( - model_name='coreuser', - name='email_alert_flag', - field=models.BooleanField(blank=True, default=False, null=True), - ), - ] diff --git a/core/migrations/0004_organization_allow_import_export.py b/core/migrations/0004_organization_allow_import_export.py deleted file mode 100644 index 884239c0..00000000 --- a/core/migrations/0004_organization_allow_import_export.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.10 on 2021-02-25 08:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_coreuser_email_alert_flag'), - ] - - operations = [ - migrations.AddField( - model_name='organization', - name='allow_import_export', - field=models.BooleanField(default=False, verbose_name='To allow import export functionality'), - ), - ] diff --git a/core/migrations/0005_organization_radius.py b/core/migrations/0005_organization_radius.py deleted file mode 100644 index e26235e1..00000000 --- a/core/migrations/0005_organization_radius.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.10 on 2021-03-16 12:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0004_organization_allow_import_export'), - ] - - operations = [ - migrations.AddField( - model_name='organization', - name='radius', - field=models.FloatField(blank=True, max_length=20, null=True), - ), - ] diff --git a/core/migrations/0006_consortium.py b/core/migrations/0006_consortium.py deleted file mode 100644 index 616a9503..00000000 --- a/core/migrations/0006_consortium.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.2.10 on 2021-05-28 06:46 - -from django.conf import settings -from django.db import migrations, models -import django.utils.timezone -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_organization_radius'), - ] - - operations = [ - migrations.CreateModel( - name='Consortium', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('consortium_uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('name', models.CharField(blank=True, max_length=255, null=True)), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), - ('edit_date', models.DateTimeField(blank=True, null=True)), - ('core_users', models.ManyToManyField(blank=True, related_name='consortium_users', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name_plural': 'Consortiums', - 'ordering': ('name',), - }, - ), - ] diff --git a/core/migrations/0007_organization_type.py b/core/migrations/0007_organization_type.py deleted file mode 100644 index 6000103f..00000000 --- a/core/migrations/0007_organization_type.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.2.10 on 2021-06-04 06:40 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0006_consortium'), - ] - - operations = [ - migrations.CreateModel( - name='OrganizationType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, help_text='Organization type', max_length=255, verbose_name='Name')), - ], - options={ - 'verbose_name_plural': 'Organization Types', - 'ordering': ('name',), - }, - ), - migrations.AddField( - model_name='organization', - name='organization_type', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.OrganizationType'), - ), - ] diff --git a/core/migrations/0008_auto_20210611_0921.py b/core/migrations/0008_auto_20210611_0921.py deleted file mode 100644 index 89afb7f4..00000000 --- a/core/migrations/0008_auto_20210611_0921.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.10 on 2021-06-11 09:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0007_organization_type'), - ] - - operations = [ - migrations.AddField( - model_name='organizationtype', - name='create_date', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='organizationtype', - name='edit_date', - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/core/models.py b/core/models.py index 127e1cd4..e78a3754 100644 --- a/core/models.py +++ b/core/models.py @@ -296,9 +296,12 @@ def __str__(self): class Consortium(models.Model): - consortium_uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) - name = models.CharField(blank=True, null=True, max_length=255) - core_users = models.ManyToManyField(CoreUser, blank=True, related_name='consortium_users') + """ + The consortium instance. Allows sharing of data between 2 or more organizations + """ + consortium_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name='Consortium UUID') + name = models.CharField("Consortium Name", max_length=255, blank=True, help_text="Multiple organizations form a consortium together") + custodian_uuids = ArrayField(models.CharField("Custodian UUIDs", max_length=255, null=True, blank=True), null=True, blank=True) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) diff --git a/core/serializers.py b/core/serializers.py index 4234ce59..e8243818 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -17,7 +17,7 @@ from core.email_utils import send_email, send_email_body from core.models import CoreUser, CoreGroup, EmailTemplate, LogicModule, Organization, PERMISSIONS_ORG_ADMIN, \ - TEMPLATE_RESET_PASSWORD, Consortium, OrganizationType + TEMPLATE_RESET_PASSWORD, OrganizationType, Consortium class LogicModuleSerializer(serializers.ModelSerializer): @@ -351,18 +351,17 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): subject_line = serializers.CharField(max_length=255) -class ConsortiumSerializer(serializers.ModelSerializer): +class OrganizationTypeSerializer(serializers.ModelSerializer): id = serializers.ReadOnlyField() - uuid = serializers.ReadOnlyField() class Meta: - model = Consortium + model = OrganizationType fields = '__all__' -class OrganizationTypeSerializer(serializers.ModelSerializer): - id = serializers.ReadOnlyField() +class ConsortiumSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source='consortium_uuid', read_only=True) class Meta: - model = OrganizationType + model = Consortium fields = '__all__' diff --git a/core/urls.py b/core/urls.py index 31e281fd..91083f1b 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,9 +1,7 @@ from django.urls import include, path, re_path from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from core.views.consortium import ConsortiumViewSet from rest_framework import routers -from core.views.organization import OrganizationTypeViewSet from core import views from core.views.web import IndexView, oauth_complete @@ -21,8 +19,8 @@ router.register(r'oauth/refreshtokens', views.RefreshTokenViewSet) router.register(r'organization', views.OrganizationViewSet) router.register(r'logicmodule', views.LogicModuleViewSet) -router.register(r'consortium', ConsortiumViewSet) -router.register(r'organization_type', OrganizationTypeViewSet) +router.register(r'consortium', views.ConsortiumViewSet) +router.register(r'organization_type', views.OrganizationTypeViewSet) urlpatterns = [ path('', IndexView.as_view(), name='index'), diff --git a/core/views/__init__.py b/core/views/__init__.py index b38e517e..fbe302de 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -1,5 +1,6 @@ from .coregroup import CoreGroupViewSet # noqa from .coreuser import CoreUserViewSet # noqa from .oauth import AccessTokenViewSet, ApplicationViewSet, RefreshTokenViewSet # noqa -from .organization import OrganizationViewSet # noqa -from .logicmodule import LogicModuleViewSet # noqa +from .organization import OrganizationViewSet, OrganizationTypeViewSet # noqa +from .consortium import ConsortiumViewSet # noqa +from .logicmodule import LogicModuleViewSet # noqa \ No newline at end of file diff --git a/core/views/consortium.py b/core/views/consortium.py index c953aea5..59081d5c 100644 --- a/core/views/consortium.py +++ b/core/views/consortium.py @@ -1,26 +1,25 @@ import logging - -from rest_framework import viewsets -from rest_framework.decorators import action -from rest_framework.response import Response - from django_filters.rest_framework import DjangoFilterBackend -from core.permissions import IsSuperUser +from rest_framework import viewsets from core.models import Consortium from core.serializers import ConsortiumSerializer - -from gateway import utils +from core.permissions import AllowAuthenticatedRead logger = logging.getLogger(__name__) class ConsortiumViewSet(viewsets.ModelViewSet): """ + Consortium is group of custodians that enables sharing of data. + title: - Consortium of the application + Consortium description: + A Consortium is collective group of custodians + + which are in turn associated with an organization. retrieve: Return the Consortium. @@ -40,31 +39,6 @@ class ConsortiumViewSet(viewsets.ModelViewSet): filter_fields = ('name',) filter_backends = (DjangoFilterBackend,) - permission_classes = (IsSuperUser,) + permission_classes = (AllowAuthenticatedRead,) queryset = Consortium.objects.all() serializer_class = ConsortiumSerializer - - @action(methods=['PUT'], url_path='specification', detail=True) - def update_api_specification(self, request, *args, **kwargs): - """ - Updates the API specification of given logic module - """ - instance = self.get_object() - schema_url = utils.get_swagger_url_by_logic_module(instance) - - response = utils.get_swagger_from_url(schema_url) - spec_dict = response.json() - data = { - 'api_specification': spec_dict - } - - serializer = self.get_serializer(instance, data=data, partial=True) - serializer.is_valid(raise_exception=True) - self.perform_update(serializer) - - if getattr(instance, '_prefetched_objects_cache', None): - # If 'prefetch_related' has been applied to a queryset, we need to - # forcibly invalidate the prefetch cache on the instance. - instance._prefetched_objects_cache = {} - - return Response(serializer.data) diff --git a/gateway/__init__.py b/gateway/__init__.py index 61a7ff41..b4bb3806 100644 --- a/gateway/__init__.py +++ b/gateway/__init__.py @@ -12,5 +12,6 @@ 'core', 'logicmodule', 'organization', + 'consortium', 'datamesh', ] diff --git a/gateway/utils.py b/gateway/utils.py index 8a78bf9a..eb8c6666 100644 --- a/gateway/utils.py +++ b/gateway/utils.py @@ -14,8 +14,8 @@ from workflow import models as wfm from . import exceptions -from core.models import CoreUser, LogicModule, Organization -from core.views import CoreUserViewSet, OrganizationViewSet +from core.models import CoreUser, LogicModule, Organization, OrganizationType, Consortium +from core.views import CoreUserViewSet, OrganizationViewSet, OrganizationTypeViewSet, ConsortiumViewSet SWAGGER_LOOKUP_FIELD = 'swagger' @@ -27,6 +27,8 @@ wfm.WorkflowLevel1: wfv.WorkflowLevel1ViewSet, CoreUser: CoreUserViewSet, Organization: OrganizationViewSet, + OrganizationType: OrganizationTypeViewSet, + Consortium: ConsortiumViewSet, wfm.WorkflowLevel2Sort: wfv.WorkflowLevel2SortViewSet, } From ac00d2e8477346435481ff92aea5c3c142c52f4a Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 7 Jul 2021 16:00:59 +0530 Subject: [PATCH 083/109] Configuration for user alert preferences (#73) * Configuration for user alert preferences * Flake8 fixes * Fixes in test cases --- core/admin.py | 5 ++- core/migrations/0003_coreuser_preferences.py | 42 ++++++++++++++++++++ core/models.py | 4 +- core/serializers.py | 15 ++++--- core/tests/test_coreuserview.py | 2 +- core/tests/test_serializers.py | 2 +- core/views/coreuser.py | 3 +- 7 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 core/migrations/0003_coreuser_preferences.py diff --git a/core/admin.py b/core/admin.py index 04a061de..66037fb3 100644 --- a/core/admin.py +++ b/core/admin.py @@ -35,15 +35,16 @@ class CoreGroupAdmin(admin.ModelAdmin): class CoreUserAdmin(UserAdmin): - list_display = ('username', 'first_name', 'last_name', 'organization', 'title', 'is_active', 'email_alert_flag',) + list_display = ('username', 'first_name', 'last_name', 'organization', 'title', 'is_active', 'user_timezone') display = 'Core User' list_filter = ('is_staff', 'organization') search_fields = ('first_name', 'first_name', 'username', 'title', 'organization__name', ) fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), {'fields': ('title', 'first_name', 'last_name', 'email', 'contact_info', - 'organization', 'email_alert_flag')}), + 'organization', 'user_timezone')}), (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'core_groups', 'user_permissions')}), + (_('Preferences'), {'fields': ('email_preferences', 'push_preferences')}), (_('Important dates'), {'fields': ('last_login', 'date_joined', 'create_date', 'edit_date')}), ) filter_horizontal = ('core_groups', 'user_permissions', ) diff --git a/core/migrations/0003_coreuser_preferences.py b/core/migrations/0003_coreuser_preferences.py new file mode 100644 index 00000000..adf45395 --- /dev/null +++ b/core/migrations/0003_coreuser_preferences.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2.10 on 2021-07-07 07:46 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.migrations.operations.special + + + +def migrate_email_alert(apps, schema_editor): + CoreUser = apps.get_model("core","CoreUser") + for record in CoreUser.objects.all(): + record.email_preferences = {'environmental': record.email_alert_flag} + record.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_consortium'), + ] + + operations = [ + migrations.AddField( + model_name='coreuser', + name='email_preferences', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='coreuser', + name='push_preferences', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='coreuser', + name='user_timezone', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.RunPython(migrate_email_alert,migrations.RunPython.noop), + migrations.RemoveField( + model_name='coreuser', + name='email_alert_flag', + ), + ] diff --git a/core/models.py b/core/models.py index e78a3754..a7e63ed9 100644 --- a/core/models.py +++ b/core/models.py @@ -208,7 +208,9 @@ class CoreUser(AbstractUser): privacy_disclaimer_accepted = models.BooleanField(default=False) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) - email_alert_flag = models.BooleanField(default=False,blank=True,null=True) + email_preferences = JSONField(blank=True, null=True) + push_preferences = JSONField(blank=True, null=True) + user_timezone = models.CharField(blank=True,null=True,max_length=255) REQUIRED_FIELDS = [] diff --git a/core/serializers.py b/core/serializers.py index e8243818..6ec8ce1b 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -98,8 +98,9 @@ def validate_invitation_token(self, value): class Meta: model = CoreUser fields = ('id', 'core_user_uuid', 'first_name', 'last_name', 'email', 'username', 'is_active', - 'title', 'email_alert_flag', 'contact_info', 'privacy_disclaimer_accepted', - 'organization', 'core_groups', 'invitation_token') + 'title', 'contact_info', 'privacy_disclaimer_accepted', + 'organization', 'core_groups', 'invitation_token', 'email_preferences', + 'push_preferences', 'user_timezone') read_only_fields = ('core_user_uuid', 'organization',) depth = 1 @@ -182,12 +183,14 @@ class CoreUserProfileSerializer(serializers.Serializer): contact_info = serializers.CharField(required=False) password = serializers.CharField(required=False) organization_name = serializers.CharField(required=False) - email_alert_flag = serializers.BooleanField(required=False) + email_preferences = serializers.JSONField(required=False) + push_preferences = serializers.JSONField(required=False) + user_timezone = serializers.CharField(required=False) class Meta: model = CoreUser fields = ('first_name', 'last_name', 'password', 'title', - 'contact_info', 'organization_name', 'email_alert_flag',) + 'contact_info', 'organization_name', 'email_preferences', 'push_preferences', 'user_timezone') def update(self, instance, validated_data): @@ -202,7 +205,9 @@ def update(self, instance, validated_data): instance.last_name = validated_data.get('last_name', instance.last_name) instance.title = validated_data.get('title', instance.title) instance.contact_info = validated_data.get('contact_info', instance.contact_info) - instance.email_alert_flag = validated_data.get('email_alert_flag', instance.email_alert_flag) + instance.email_preferences = validated_data.get('email_preferences', instance.email_preferences) + instance.push_preferences = validated_data.get('push_preferences', instance.push_preferences) + instance.user_timezone = validated_data.get('user_timezone', instance.user_timezone) password = validated_data.get('password', None) if password is not None: instance.set_password(password) diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index f8f7540c..27039869 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -423,7 +423,7 @@ def test_reset_password_confirm_token_expired(self, request_factory, reset_passw class TestCoreUserRead(object): keys = {'id', 'core_user_uuid', 'first_name', 'last_name', 'email', 'username', 'is_active', 'title', - 'contact_info','email_alert_flag','privacy_disclaimer_accepted', 'organization', 'core_groups'} + 'contact_info','privacy_disclaimer_accepted', 'organization', 'core_groups', 'email_preferences', 'push_preferences', 'user_timezone'} def test_coreuser_list(self, request_factory, org_member): factories.CoreUser.create(organization=org_member.organization, username='another_user') # 2nd user of the org diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index 549911c3..338eadb4 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -64,7 +64,7 @@ def test_core_user_serializer(request_factory, org_member): 'privacy_disclaimer_accepted', 'organization', 'core_groups', - 'email_alert_flag', + 'email_preferences', 'push_preferences', 'user_timezone', ] assert set(data.keys()) == set(keys) assert isinstance(data['organization'], dict) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 799b6637..a7fbdef7 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -305,7 +305,8 @@ def alert(self, request, *args, **kwargs): } template_name = 'email/coreuser/shipment_alert.txt' html_template_name = 'email/coreuser/shipment_alert.html' - core_users = CoreUser.objects.filter(organization__organization_uuid=org_uuid, email_alert_flag=True) + # TODO send email via preferences + core_users = CoreUser.objects.filter(organization__organization_uuid=org_uuid) for user in core_users: email_address = user.email send_email(email_address, subject, context, template_name, html_template_name) From 493b62c18c6e3c8d6faebf0d1c7d2941193f9170 Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Mon, 12 Jul 2021 14:29:04 +0530 Subject: [PATCH 084/109] update consortium table for organization uuid (#75) --- core/migrations/0004_auto_20210712_0641.py | 23 ++++++++++++++++++++++ core/models.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 core/migrations/0004_auto_20210712_0641.py diff --git a/core/migrations/0004_auto_20210712_0641.py b/core/migrations/0004_auto_20210712_0641.py new file mode 100644 index 00000000..0608f21d --- /dev/null +++ b/core/migrations/0004_auto_20210712_0641.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.10 on 2021-07-12 06:41 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_coreuser_preferences'), + ] + + operations = [ + migrations.RemoveField( + model_name='consortium', + name='custodian_uuids', + ), + migrations.AddField( + model_name='consortium', + name='organization_uuids', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Organization UUIDs'), blank=True, null=True, size=None), + ), + ] diff --git a/core/models.py b/core/models.py index a7e63ed9..4dfa679b 100644 --- a/core/models.py +++ b/core/models.py @@ -303,7 +303,7 @@ class Consortium(models.Model): """ consortium_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name='Consortium UUID') name = models.CharField("Consortium Name", max_length=255, blank=True, help_text="Multiple organizations form a consortium together") - custodian_uuids = ArrayField(models.CharField("Custodian UUIDs", max_length=255, null=True, blank=True), null=True, blank=True) + organization_uuids = ArrayField(models.CharField("Organization UUIDs", max_length=255, null=True, blank=True), null=True, blank=True) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) From a5faff123bd149cd01bc1b5ff8b92d6bc5f05170 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Tue, 13 Jul 2021 16:08:59 +0530 Subject: [PATCH 085/109] Modifications in email templates (#77) * Changes in email template for environmental alerts * Preferences for email alerts * Updated super admin credentials --- .../management/commands/loadinitialdata.py | 4 +- core/serializers.py | 1 - core/views/coreuser.py | 60 +++++++++++-------- templates/email/coreuser/shipment_alert.html | 16 +++-- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index 82578f52..8fc53890 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -56,7 +56,7 @@ def _create_user(self): logger.info("Creating Super User") user_password = None if settings.DEBUG: - user_password = settings.SUPER_USER_PASSWORD if settings.SUPER_USER_PASSWORD else 'admin' + user_password = settings.SUPER_USER_PASSWORD if settings.SUPER_USER_PASSWORD else 'zGtkgLvmNiKm' elif settings.SUPER_USER_PASSWORD: user_password = settings.SUPER_USER_PASSWORD else: @@ -68,7 +68,7 @@ def _create_user(self): su = CoreUser.objects.create_superuser( first_name='System', last_name='Admin', - username='admin', + username='67OAI8DD5I1O', email='admin@example.com', password=user_password, organization=self._default_org, diff --git a/core/serializers.py b/core/serializers.py index 6ec8ce1b..8266bfc7 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -353,7 +353,6 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): """ organization_uuid = serializers.UUIDField() messages = serializers.JSONField() - subject_line = serializers.CharField(max_length=255) class OrganizationTypeSerializer(serializers.ModelSerializer): diff --git a/core/views/coreuser.py b/core/views/coreuser.py index a7fbdef7..eda41ba4 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -10,7 +10,7 @@ import jwt from drf_yasg.utils import swagger_auto_schema import calendar -import time +from datetime import datetime from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, @@ -23,6 +23,7 @@ from core.jwt_utils import create_invitation_token from core.email_utils import send_email import logging +from dateutil import tz # from twilio.rest import Client logger = logging.getLogger(__name__) @@ -286,30 +287,33 @@ def alert(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) org_uuid = request.data['organization_uuid'] messages = request.data['messages'] - subject_line = request.data['subject_line'] - if subject_line is not None: - subject = subject_line - else: - subject = 'Alert message for shipment' - for message in messages: - try: - time_tuple = time.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S%z") - except ValueError: - time_tuple = time.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S.%f%z") - time_format = calendar.timegm(time_tuple) - message['date_time'] = time.ctime(time_format) - message['shipment_url'] = urljoin(settings.FRONTEND_URL, - '/app/shipment/edit/:'+str(message['shipment_id'])) - context = { - 'messages': messages, - } - template_name = 'email/coreuser/shipment_alert.txt' - html_template_name = 'email/coreuser/shipment_alert.html' - # TODO send email via preferences - core_users = CoreUser.objects.filter(organization__organization_uuid=org_uuid) - for user in core_users: - email_address = user.email - send_email(email_address, subject, context, template_name, html_template_name) + try: + for message in messages: + try: + time_tuple = datetime.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + time_tuple = datetime.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S.%f%z") + subject = '{} Alert'.format(message['parameter'].capitalize()) + message['date_time'] = time_tuple.replace(tzinfo=tz.gettz('UTC')) + message['shipment_url'] = urljoin(settings.FRONTEND_URL, + '/app/shipment/edit/:'+str(message['shipment_id'])) + message['color'] = color_codes.get(message['severity']) + context = { + 'message': message, + } + template_name = 'email/coreuser/shipment_alert.txt' + html_template_name = 'email/coreuser/shipment_alert.html' + # TODO send email via preferences + core_users = CoreUser.objects.filter(organization__organization_uuid=org_uuid) + for user in core_users: + email_address = user.email + preferences = user.email_preferences + if preferences and (preferences.get('environmental',None) or preferences.get('geofence',None)): + local_zone = tz.gettz(user.user_timezone) + message['date_time'] = message['date_time'].astimezone(local_zone) + send_email(email_address, subject, context, template_name, html_template_name) + except Exception as ex: + print('Exception: ',ex) return Response( { 'detail': 'The alert messages were sent successfully on email.', @@ -338,3 +342,9 @@ def update_profile(self, request, pk=None, *args, **kwargs): serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) + +color_codes = { + 'error':'#cc3300', + 'info':'#2196F3', + 'success':'#339900' +} \ No newline at end of file diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index 3f26bb81..63c3ca7f 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -32,24 +32,24 @@

- Message from Transparent Path
Admin!
{{message.alert_message}} +

- {% for message in messages %} -
- {% endfor %} From eec3ef930a81db6078004d48eaf09704c577ed30 Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Thu, 15 Jul 2021 10:37:58 +0530 Subject: [PATCH 086/109] change permission level for consortium table (#79) --- core/views/consortium.py | 4 ++-- core/views/coreuser.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/views/consortium.py b/core/views/consortium.py index 59081d5c..c46ddcad 100644 --- a/core/views/consortium.py +++ b/core/views/consortium.py @@ -4,7 +4,7 @@ from rest_framework import viewsets from core.models import Consortium from core.serializers import ConsortiumSerializer -from core.permissions import AllowAuthenticatedRead +from core.permissions import AllowOnlyOrgAdmin logger = logging.getLogger(__name__) @@ -39,6 +39,6 @@ class ConsortiumViewSet(viewsets.ModelViewSet): filter_fields = ('name',) filter_backends = (DjangoFilterBackend,) - permission_classes = (AllowAuthenticatedRead,) + permission_classes = (AllowOnlyOrgAdmin,) queryset = Consortium.objects.all() serializer_class = ConsortiumSerializer diff --git a/core/views/coreuser.py b/core/views/coreuser.py index eda41ba4..83135950 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -9,7 +9,6 @@ import django_filters import jwt from drf_yasg.utils import swagger_auto_schema -import calendar from datetime import datetime from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, @@ -296,7 +295,7 @@ def alert(self, request, *args, **kwargs): subject = '{} Alert'.format(message['parameter'].capitalize()) message['date_time'] = time_tuple.replace(tzinfo=tz.gettz('UTC')) message['shipment_url'] = urljoin(settings.FRONTEND_URL, - '/app/shipment/edit/:'+str(message['shipment_id'])) + '/app/shipment/edit/:'+str(message['shipment_id'])) message['color'] = color_codes.get(message['severity']) context = { 'message': message, @@ -308,12 +307,12 @@ def alert(self, request, *args, **kwargs): for user in core_users: email_address = user.email preferences = user.email_preferences - if preferences and (preferences.get('environmental',None) or preferences.get('geofence',None)): + if preferences and (preferences.get('environmental', None) or preferences.get('geofence', None)): local_zone = tz.gettz(user.user_timezone) message['date_time'] = message['date_time'].astimezone(local_zone) send_email(email_address, subject, context, template_name, html_template_name) except Exception as ex: - print('Exception: ',ex) + print('Exception: ', ex) return Response( { 'detail': 'The alert messages were sent successfully on email.', @@ -343,8 +342,9 @@ def update_profile(self, request, pk=None, *args, **kwargs): serializer.save() return Response(serializer.data) + color_codes = { - 'error':'#cc3300', - 'info':'#2196F3', - 'success':'#339900' -} \ No newline at end of file + 'error': '#cc3300', + 'info': '#2196F3', + 'success': '#339900' +} From 1d4ccc165b5b5259632a02558908a3271fd5f7b2 Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Fri, 16 Jul 2021 12:26:12 +0530 Subject: [PATCH 087/109] filter consortium by organization (#81) --- core/views/consortium.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/views/consortium.py b/core/views/consortium.py index c46ddcad..ce66ffac 100644 --- a/core/views/consortium.py +++ b/core/views/consortium.py @@ -1,11 +1,10 @@ import logging from django_filters.rest_framework import DjangoFilterBackend - +from rest_framework.response import Response from rest_framework import viewsets from core.models import Consortium from core.serializers import ConsortiumSerializer -from core.permissions import AllowOnlyOrgAdmin - +from core.permissions import IsSuperUser logger = logging.getLogger(__name__) @@ -36,9 +35,17 @@ class ConsortiumViewSet(viewsets.ModelViewSet): delete: Delete a Consortium instance. """ + def list(self, request): + queryset = self.filter_queryset(self.get_queryset()) + organization_uuid = self.request.query_params.get('organization_uuid', None) + # It will check if organization uuid in query param + if organization_uuid is not None: + queryset = queryset.filter(organization_uuids__contains=[organization_uuid]) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) filter_fields = ('name',) filter_backends = (DjangoFilterBackend,) - permission_classes = (AllowOnlyOrgAdmin,) + permission_classes = (IsSuperUser,) queryset = Consortium.objects.all() serializer_class = ConsortiumSerializer From 53796ea9708ffe365f60aa98db07b55ae70f04cd Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Tue, 20 Jul 2021 11:44:49 +0530 Subject: [PATCH 088/109] update consortium array field (#82) --- core/migrations/0005_auto_20210720_0543.py | 19 +++++++++++++++++++ core/models.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 core/migrations/0005_auto_20210720_0543.py diff --git a/core/migrations/0005_auto_20210720_0543.py b/core/migrations/0005_auto_20210720_0543.py new file mode 100644 index 00000000..29a2db05 --- /dev/null +++ b/core/migrations/0005_auto_20210720_0543.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.10 on 2021-07-20 05:43 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20210712_0641'), + ] + + operations = [ + migrations.AlterField( + model_name='consortium', + name='organization_uuids', + field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True, null=True, verbose_name='Organization UUIDs'), blank=True, null=True, size=None), + ), + ] diff --git a/core/models.py b/core/models.py index 4dfa679b..c41c24b5 100644 --- a/core/models.py +++ b/core/models.py @@ -303,7 +303,7 @@ class Consortium(models.Model): """ consortium_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name='Consortium UUID') name = models.CharField("Consortium Name", max_length=255, blank=True, help_text="Multiple organizations form a consortium together") - organization_uuids = ArrayField(models.CharField("Organization UUIDs", max_length=255, null=True, blank=True), null=True, blank=True) + organization_uuids = ArrayField(models.UUIDField("Organization UUIDs", max_length=255, null=True, blank=True), null=True, blank=True) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) From 2ee19dd7341fc8df4fe1013e6c9a298ef6bd730a Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Thu, 22 Jul 2021 15:05:31 +0530 Subject: [PATCH 089/109] Changed permission level for consortium (#85) * Changed permission level for consortium * Updated flake8 fixes --- core/views/consortium.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/views/consortium.py b/core/views/consortium.py index ce66ffac..9ca2cc18 100644 --- a/core/views/consortium.py +++ b/core/views/consortium.py @@ -4,7 +4,7 @@ from rest_framework import viewsets from core.models import Consortium from core.serializers import ConsortiumSerializer -from core.permissions import IsSuperUser +from core.permissions import IsSuperUser, AllowAuthenticatedRead logger = logging.getLogger(__name__) @@ -35,6 +35,8 @@ class ConsortiumViewSet(viewsets.ModelViewSet): delete: Delete a Consortium instance. """ + permission_classes_by_action = {'list': [AllowAuthenticatedRead]} + def list(self, request): queryset = self.filter_queryset(self.get_queryset()) organization_uuid = self.request.query_params.get('organization_uuid', None) @@ -44,6 +46,14 @@ def list(self, request): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + def get_permissions(self): + try: + # return permission_classes depending on `action` + return [permission() for permission in self.permission_classes_by_action[self.action]] + except KeyError: + # action is not set return default permission_classes + return [permission() for permission in self.permission_classes] + filter_fields = ('name',) filter_backends = (DjangoFilterBackend,) permission_classes = (IsSuperUser,) From 2db3290ce2ba0956f007d58bb8988e44f383ef1c Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Thu, 29 Jul 2021 15:43:17 +0530 Subject: [PATCH 090/109] create consortium if custody create (#87) * create consortium if custody create * resolved flake8 error --- gateway/request.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/gateway/request.py b/gateway/request.py index 5d46933d..a33b717a 100644 --- a/gateway/request.py +++ b/gateway/request.py @@ -11,7 +11,7 @@ from gateway import exceptions from gateway import utils -from core.models import LogicModule +from core.models import LogicModule, Consortium from gateway.clients import SwaggerClient, AsyncSwaggerClient from datamesh.services import DataMesh @@ -104,6 +104,30 @@ def perform(self) -> GatewayResponse: except exceptions.ServiceDoesNotExist as e: logger.error(e.content) + path_url = self.request.path # Get request path + list_string_path = path_url.split("/") # Split the request path to check if custody include in it + if ('join' not in self.request.query_params and 'custody' in list_string_path and + status_code == 201 and type(content) in [dict, list] and self.request.method == 'POST'): + # This functionality will execute only when request include custody with post request, + # It will not execute if its join request + related_organization = content.get('organization_uuid') + shipment_name = content.get('shipment') + # Check if consortium already present for respective shipment + consortium = Consortium.objects.filter(name=shipment_name).first() + if consortium: + # if consortium exist,update consortium for organization uuid + organization_list = consortium.organization_uuids + import uuid + org_uuid = uuid.UUID(related_organization) + if org_uuid not in organization_list: + # To avoid repeated organization uuid adding in consortium organization uuid + organization_list.append(related_organization) + consortium.organization_uuids = organization_list + consortium.save() + else: + # If consortium does not exists for shipment name, then create consortium + Consortium.objects.create(name=shipment_name, organization_uuids=[related_organization]) + if type(content) in [dict, list]: content = json.dumps(content, cls=utils.GatewayJSONEncoder) From d2f78ef4dcf3fc723cffbf08fcac6c6d1d8a214a Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Thu, 12 Aug 2021 19:26:53 +0530 Subject: [PATCH 091/109] Fix issue for retrieve query by uuid (#88) --- gateway/clients.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gateway/clients.py b/gateway/clients.py index 3e6c5ccb..eba55c65 100644 --- a/gateway/clients.py +++ b/gateway/clients.py @@ -40,7 +40,13 @@ def prepare_data(self, spec: Spec, **kwargs) -> Tuple[str, str]: if kwargs.get('pk') is None: path = f'/{model}/' else: - pk_name = 'uuid' if utils.valid_uuid4(pk) else 'id' + # It was checking shipment/{uuid} in API specification when pk is of uuid type, + # as TP services are mainly using + # shipment/{id} + # Commenting code to make it work for id, as TP services don't have endpoint to + # make request by uuid + # pk_name = 'uuid' if utils.valid_uuid4(pk) else 'id' + pk_name = 'id' path_kwargs = {pk_name: pk} path = f'/{model}/{{{pk_name}}}/' From 823eedd55b33853dd6dcab91b8239b4b10629211 Mon Sep 17 00:00:00 2001 From: vishalajackus <73515569+vishalajackus@users.noreply.github.com> Date: Mon, 16 Aug 2021 12:19:31 +0530 Subject: [PATCH 092/109] Revert "Fix issue for retrieve query by uuid (#88)" This reverts commit d2f78ef4dcf3fc723cffbf08fcac6c6d1d8a214a. --- gateway/clients.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/gateway/clients.py b/gateway/clients.py index eba55c65..3e6c5ccb 100644 --- a/gateway/clients.py +++ b/gateway/clients.py @@ -40,13 +40,7 @@ def prepare_data(self, spec: Spec, **kwargs) -> Tuple[str, str]: if kwargs.get('pk') is None: path = f'/{model}/' else: - # It was checking shipment/{uuid} in API specification when pk is of uuid type, - # as TP services are mainly using - # shipment/{id} - # Commenting code to make it work for id, as TP services don't have endpoint to - # make request by uuid - # pk_name = 'uuid' if utils.valid_uuid4(pk) else 'id' - pk_name = 'id' + pk_name = 'uuid' if utils.valid_uuid4(pk) else 'id' path_kwargs = {pk_name: pk} path = f'/{model}/{{{pk_name}}}/' From 269d76b51ad59400328f2e04d0d040de3f84374e Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Fri, 19 Nov 2021 19:55:36 +0530 Subject: [PATCH 093/109] Handled boolean for CORS_ORIGIN_ALLOW_ALL --- buildly/settings/production.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildly/settings/production.py b/buildly/settings/production.py index 531c381f..1ba41c5a 100644 --- a/buildly/settings/production.py +++ b/buildly/settings/production.py @@ -16,7 +16,7 @@ MIDDLEWARE = MIDDLEWARE_CORS + MIDDLEWARE -CORS_ORIGIN_ALLOW_ALL = os.getenv('CORS_ORIGIN_ALLOW_ALL', False) +CORS_ORIGIN_ALLOW_ALL = bool(os.getenv('CORS_ORIGIN_ALLOW_ALL', False)) CORS_ORIGIN_WHITELIST = os.environ['CORS_ORIGIN_WHITELIST'].split(',') From e471e849a32d51543f8e5b0b8035f756268e52ae Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Wed, 24 Nov 2021 19:11:07 +0530 Subject: [PATCH 094/109] Updated Bravado Core version --- requirements/base.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 3451c7b2..f3c1820f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,7 +1,8 @@ Django==2.2.10 django-filter==2.2.0 +jsonschema==3.2.0 django-health-check==3.6.1 -git+https://github.com/Humanitec/django-oauth-toolkit-jwt@v0.5.2#egg=django-oauth-toolkit-jwt +git+https://github.com/buildlyio/django-oauth-toolkit-jwt@v0.5.2#egg=django-oauth-toolkit-jwt djangorestframework==3.9.4 psycopg2-binary==2.8.6 social-auth-app-django==3.1.0 @@ -9,7 +10,7 @@ django-oauth-toolkit==1.3.0 futures==3.1.1 django-cors-headers==2.5.3 pyswagger==0.8.39 -bravado-core==5.13.1 +bravado-core==5.17.0 drf-yasg==1.10.2 requests==2.25.0 aiohttp==3.5.4 From 9df8e88809342b5f5d2fb3cf4f67b4a037c33615 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Fri, 26 Nov 2021 20:09:33 +0530 Subject: [PATCH 095/109] Return response data only for PUT, POST, DELETE (#97) --- gateway/clients.py | 10 ++++++---- requirements/base.txt | 2 +- scripts/docker-entrypoint.sh | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/gateway/clients.py b/gateway/clients.py index 3e6c5ccb..4af27982 100644 --- a/gateway/clients.py +++ b/gateway/clients.py @@ -73,12 +73,14 @@ def get_request_data(self) -> dict: return json.dumps(self._in_request.data) method = self._in_request.META['REQUEST_METHOD'].lower() - data = self._in_request.query_params.dict() - - data.pop('aggregate', None) - data.pop('join', None) + data = {} if method in ['post', 'put', 'patch']: + data = self._in_request.query_params.dict() + + data.pop('aggregate', None) + data.pop('join', None) + query_dict_body = self._in_request.data if hasattr(self._in_request, 'data') else dict() body = query_dict_body.dict() if isinstance(query_dict_body, QueryDict) else query_dict_body data.update(body) diff --git a/requirements/base.txt b/requirements/base.txt index f3c1820f..7de01526 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,7 +10,7 @@ django-oauth-toolkit==1.3.0 futures==3.1.1 django-cors-headers==2.5.3 pyswagger==0.8.39 -bravado-core==5.17.0 +bravado-core==5.13.1 drf-yasg==1.10.2 requests==2.25.0 aiohttp==3.5.4 diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 2eaddcd1..1035f135 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -9,7 +9,7 @@ python manage.py makemigrations python manage.py migrate echo $(date -u) "- Load Initial Data" -python manage.py loadinitialdata +# python manage.py loadinitialdata echo $(date -u) "- Collect Static" python manage.py collectstatic --no-input From dfb361ef7c555c566b5569ce2085c0acc29c63f6 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Thu, 10 Feb 2022 18:09:48 +0530 Subject: [PATCH 096/109] Added default radius for organization --- core/migrations/0006_auto_20220210_1102.py | 18 ++++++++++++++++++ core/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 core/migrations/0006_auto_20220210_1102.py diff --git a/core/migrations/0006_auto_20220210_1102.py b/core/migrations/0006_auto_20220210_1102.py new file mode 100644 index 00000000..9e3ddc45 --- /dev/null +++ b/core/migrations/0006_auto_20220210_1102.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2022-02-10 11:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_auto_20210720_0543'), + ] + + operations = [ + migrations.AlterField( + model_name='organization', + name='radius', + field=models.FloatField(blank=True, default=0.0, max_length=20, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index c41c24b5..99e3c6ab 100644 --- a/core/models.py +++ b/core/models.py @@ -122,7 +122,7 @@ class Organization(models.Model): date_format = models.CharField("Date Format", max_length=50, blank=True, default="DD.MM.YYYY") phone = models.CharField(max_length=20, blank=True, null=True) allow_import_export = models.BooleanField('To allow import export functionality', default=False) - radius = models.FloatField(max_length=20, blank=True, null=True) + radius = models.FloatField(max_length=20, blank=True, null=True, default = 0.0) organization_type = models.ForeignKey(OrganizationType,on_delete=models.CASCADE,null=True) class Meta: From 3b258203f97b5594d4b619d0728ada312def30bc Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Tue, 1 Mar 2022 20:51:40 +0530 Subject: [PATCH 097/109] Allow unlimited line size for request --- buildly/gunicorn_conf.py | 3 +++ scripts/docker-entrypoint.sh | 2 +- scripts/run-standalone-dev.sh | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 buildly/gunicorn_conf.py diff --git a/buildly/gunicorn_conf.py b/buildly/gunicorn_conf.py new file mode 100644 index 00000000..f45ad8e7 --- /dev/null +++ b/buildly/gunicorn_conf.py @@ -0,0 +1,3 @@ +bind = '0.0.0.0:8080' +limit_request_field_size = 0 +limit_request_line = 0 diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 1035f135..a5f172f7 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -15,4 +15,4 @@ echo $(date -u) "- Collect Static" python manage.py collectstatic --no-input echo $(date -u) "- Running the server" -gunicorn -b 0.0.0.0:8080 buildly.wsgi +gunicorn -b 0.0.0.0:8080 buildly.wsgi --config buildly/gunicorn_conf.py diff --git a/scripts/run-standalone-dev.sh b/scripts/run-standalone-dev.sh index 7cd07b09..b2e357b3 100644 --- a/scripts/run-standalone-dev.sh +++ b/scripts/run-standalone-dev.sh @@ -15,4 +15,4 @@ echo $(date -u) "- Load Initial Data" python manage.py loadinitialdata echo $(date -u) "- Running the server" -gunicorn -b 0.0.0.0:8080 --reload buildly.wsgi -w 2 --timeout 120 +gunicorn -b 0.0.0.0:8080 --reload buildly.wsgi -w 2 --timeout 120 --config buildly/gunicorn_conf.py From 87c45d2ba6e84341687e3b45cb1fb7cc1db3a2ea Mon Sep 17 00:00:00 2001 From: abhishek-kumar-piyush <97152893+abhishek-kumar-piyush@users.noreply.github.com> Date: Fri, 25 Mar 2022 13:19:31 +0530 Subject: [PATCH 098/109] sensor service email alert for unassigned moving sensor (#105) * squashed migration files * construct shipment_url only when shipment_id is present * remove shipment related from email if shipment is not available --- core/migrations/0001_initial.py | 61 ++++++++++++++------ core/migrations/0002_consortium.py | 33 ----------- core/migrations/0003_coreuser_preferences.py | 42 -------------- core/migrations/0004_auto_20210712_0641.py | 23 -------- core/migrations/0005_auto_20210720_0543.py | 19 ------ core/migrations/0006_auto_20220210_1102.py | 18 ------ core/views/coreuser.py | 5 +- templates/email/coreuser/shipment_alert.html | 46 ++++++++------- 8 files changed, 73 insertions(+), 174 deletions(-) delete mode 100644 core/migrations/0002_consortium.py delete mode 100644 core/migrations/0003_coreuser_preferences.py delete mode 100644 core/migrations/0004_auto_20210712_0641.py delete mode 100644 core/migrations/0005_auto_20210720_0543.py delete mode 100644 core/migrations/0006_auto_20220210_1102.py diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 46393a34..5929e4c6 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,21 +1,25 @@ -# Generated by Django 2.2.10 on 2021-06-28 15:34 +# Generated by Django 2.2.10 on 2022-03-24 11:57 -from django.conf import settings import django.contrib.auth.models import django.contrib.auth.validators import django.contrib.postgres.fields import django.contrib.postgres.fields.jsonb from django.db import migrations, models +import django.db.migrations.operations.special import django.db.models.deletion import django.utils.timezone import uuid - +def migrate_email_alert(apps, schema_editor): + CoreUser = apps.get_model("core","CoreUser") + for record in CoreUser.objects.all(): + record.email_preferences = {'environmental': record.email_alert_flag} + record.save() class Migration(migrations.Migration): dependencies = [ - ('auth', '0011_update_proxy_permissions'), ('sites', '0002_alter_domain_unique'), + ('auth', '0011_update_proxy_permissions'), ] operations = [ @@ -150,6 +154,9 @@ class Migration(migrations.Migration): ('organization', models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ('email_alert_flag', models.BooleanField(blank=True, default=False, null=True)), + ('email_preferences', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ('push_preferences', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ('user_timezone', models.CharField(blank=True, max_length=255, null=True)), ], options={ 'ordering': ('first_name',), @@ -159,36 +166,56 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Consortium', + name='OrganizationType', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('consortium_uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('name', models.CharField(blank=True, max_length=255, null=True)), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ('name', models.CharField(blank=True, help_text='Organization type', max_length=255, verbose_name='Name')), + ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), - ('core_users', models.ManyToManyField(blank=True, related_name='consortium_users', to=settings.AUTH_USER_MODEL)), ], options={ - 'verbose_name_plural': 'Consortiums', + 'verbose_name_plural': 'Organization Types', 'ordering': ('name',), }, ), + migrations.AddField( + model_name='organization', + name='organization_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.OrganizationType'), + ), migrations.CreateModel( - name='OrganizationType', + name='Consortium', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, help_text='Organization type', max_length=255, verbose_name='Name')), - ('create_date', models.DateTimeField(blank=True, null=True)), + ('consortium_uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Consortium UUID')), + ('name', models.CharField(blank=True, help_text='Multiple organizations form a consortium together', max_length=255, verbose_name='Consortium Name')), + ('create_date', models.DateTimeField(default=django.utils.timezone.now)), ('edit_date', models.DateTimeField(blank=True, null=True)), + ('custodian_uuids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Custodian UUIDs'), blank=True, null=True, size=None)), ], options={ - 'verbose_name_plural': 'Organization Types', + 'verbose_name_plural': 'Consortiums', 'ordering': ('name',), }, ), + migrations.RunPython(migrate_email_alert, + migrations.RunPython.noop, + ), + migrations.RemoveField( + model_name='coreuser', + name='email_alert_flag', + ), + migrations.RemoveField( + model_name='consortium', + name='custodian_uuids', + ), migrations.AddField( + model_name='consortium', + name='organization_uuids', + field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True, null=True, verbose_name='Organization UUIDs'), blank=True, null=True, size=None), + ), + migrations.AlterField( model_name='organization', - name='organization_type', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.OrganizationType'), + name='radius', + field=models.FloatField(blank=True, default=0.0, max_length=20, null=True), ), ] diff --git a/core/migrations/0002_consortium.py b/core/migrations/0002_consortium.py deleted file mode 100644 index 7b4e5a46..00000000 --- a/core/migrations/0002_consortium.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 2.2.10 on 2021-06-28 15:35 - -import django.contrib.postgres.fields -from django.db import migrations, models -import django.utils.timezone -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='Consortium', - ), - migrations.CreateModel( - name='Consortium', - fields=[ - ('consortium_uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Consortium UUID')), - ('name', models.CharField(blank=True, help_text='Multiple organizations form a consortium together', max_length=255, verbose_name='Consortium Name')), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), - ('edit_date', models.DateTimeField(blank=True, null=True)), - ('custodian_uuids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Custodian UUIDs'), blank=True, null=True, size=None)), - ], - options={ - 'verbose_name_plural': 'Consortiums', - 'ordering': ('name',), - }, - ), - ] diff --git a/core/migrations/0003_coreuser_preferences.py b/core/migrations/0003_coreuser_preferences.py deleted file mode 100644 index adf45395..00000000 --- a/core/migrations/0003_coreuser_preferences.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 2.2.10 on 2021-07-07 07:46 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.migrations.operations.special - - - -def migrate_email_alert(apps, schema_editor): - CoreUser = apps.get_model("core","CoreUser") - for record in CoreUser.objects.all(): - record.email_preferences = {'environmental': record.email_alert_flag} - record.save() - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0002_consortium'), - ] - - operations = [ - migrations.AddField( - model_name='coreuser', - name='email_preferences', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), - ), - migrations.AddField( - model_name='coreuser', - name='push_preferences', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), - ), - migrations.AddField( - model_name='coreuser', - name='user_timezone', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.RunPython(migrate_email_alert,migrations.RunPython.noop), - migrations.RemoveField( - model_name='coreuser', - name='email_alert_flag', - ), - ] diff --git a/core/migrations/0004_auto_20210712_0641.py b/core/migrations/0004_auto_20210712_0641.py deleted file mode 100644 index 0608f21d..00000000 --- a/core/migrations/0004_auto_20210712_0641.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.10 on 2021-07-12 06:41 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_coreuser_preferences'), - ] - - operations = [ - migrations.RemoveField( - model_name='consortium', - name='custodian_uuids', - ), - migrations.AddField( - model_name='consortium', - name='organization_uuids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Organization UUIDs'), blank=True, null=True, size=None), - ), - ] diff --git a/core/migrations/0005_auto_20210720_0543.py b/core/migrations/0005_auto_20210720_0543.py deleted file mode 100644 index 29a2db05..00000000 --- a/core/migrations/0005_auto_20210720_0543.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.10 on 2021-07-20 05:43 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0004_auto_20210712_0641'), - ] - - operations = [ - migrations.AlterField( - model_name='consortium', - name='organization_uuids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True, null=True, verbose_name='Organization UUIDs'), blank=True, null=True, size=None), - ), - ] diff --git a/core/migrations/0006_auto_20220210_1102.py b/core/migrations/0006_auto_20220210_1102.py deleted file mode 100644 index 9e3ddc45..00000000 --- a/core/migrations/0006_auto_20220210_1102.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.10 on 2022-02-10 11:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_auto_20210720_0543'), - ] - - operations = [ - migrations.AlterField( - model_name='organization', - name='radius', - field=models.FloatField(blank=True, default=0.0, max_length=20, null=True), - ), - ] diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 83135950..9f210f9d 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -294,8 +294,11 @@ def alert(self, request, *args, **kwargs): time_tuple = datetime.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S.%f%z") subject = '{} Alert'.format(message['parameter'].capitalize()) message['date_time'] = time_tuple.replace(tzinfo=tz.gettz('UTC')) - message['shipment_url'] = urljoin(settings.FRONTEND_URL, + if message.get('shipment_id'): + message['shipment_url'] = urljoin(settings.FRONTEND_URL, '/app/shipment/edit/:'+str(message['shipment_id'])) + else: + message['shipment_url'] = None message['color'] = color_codes.get(message['severity']) context = { 'message': message, diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index 63c3ca7f..bc3bc873 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -45,28 +45,32 @@

>

From f6ab18524c2463ebee552b44ae29424310dc1ce3 Mon Sep 17 00:00:00 2001 From: Yasmin Ansari Date: Mon, 28 Mar 2022 20:28:35 +0530 Subject: [PATCH 099/109] Handle when no custody organization mapped to custodian --- gateway/request.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gateway/request.py b/gateway/request.py index a33b717a..a7c40ae8 100644 --- a/gateway/request.py +++ b/gateway/request.py @@ -117,13 +117,14 @@ def perform(self) -> GatewayResponse: if consortium: # if consortium exist,update consortium for organization uuid organization_list = consortium.organization_uuids - import uuid - org_uuid = uuid.UUID(related_organization) - if org_uuid not in organization_list: - # To avoid repeated organization uuid adding in consortium organization uuid - organization_list.append(related_organization) - consortium.organization_uuids = organization_list - consortium.save() + if related_organization: + import uuid + org_uuid = uuid.UUID(related_organization) + if org_uuid not in organization_list: + # To avoid repeated organization uuid adding in consortium organization uuid + organization_list.append(related_organization) + consortium.organization_uuids = organization_list + consortium.save() else: # If consortium does not exists for shipment name, then create consortium Consortium.objects.create(name=shipment_name, organization_uuids=[related_organization]) From 2492d6608b1cd7313b813c73d82d046db89b1d56 Mon Sep 17 00:00:00 2001 From: abhishek-kumar-piyush <97152893+abhishek-kumar-piyush@users.noreply.github.com> Date: Wed, 6 Apr 2022 16:49:38 +0530 Subject: [PATCH 100/109] Environmental warning timezone. (#108) * Change warning timezone to user's timezone only when core user has timezone * append ('UTC') for UTC timezone --- core/views/coreuser.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 9f210f9d..db1711eb 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -311,8 +311,12 @@ def alert(self, request, *args, **kwargs): email_address = user.email preferences = user.email_preferences if preferences and (preferences.get('environmental', None) or preferences.get('geofence', None)): - local_zone = tz.gettz(user.user_timezone) - message['date_time'] = message['date_time'].astimezone(local_zone) + user_timezone = user.user_timezone + if user_timezone: + local_zone = tz.gettz(user_timezone) + message['date_time'] = message['date_time'].astimezone(local_zone) + else: + message['date_time'] = time_tuple.strftime("%B %d, %Y, %I:%M %p")+" (UTC)" send_email(email_address, subject, context, template_name, html_template_name) except Exception as ex: print('Exception: ', ex) From c79fba85d8dcaeb91daf308ace9962b2930353cd Mon Sep 17 00:00:00 2001 From: RadhikaPPatel Date: Tue, 12 Apr 2022 15:11:43 +0530 Subject: [PATCH 101/109] Remove Travis --- .travis.yml | 57 ----------------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 16b2e8d8..00000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -language: python -cache: pip -dist: xenial -python: - - "3.7" -services: - - docker - - postgresql -addons: - postgresql: "9.6" -before_script: - - sleep 10 - - docker run -p 389:389 -p 636:636 --name openldap_server -d osixia/openldap:1.3.0 - - psql -c 'create database buildly_api;' -U postgres - - sudo touch /var/log/buildly.log - - sudo chown travis /var/log/buildly.log -install: - - cat requirements/base.txt | grep "^Django==\|^psycopg2" | xargs pip install - - pip install -r requirements/ci.txt - - pip install awscli -script: - - flake8 - - bandit -r . -ll - - pytest --cov-config=.coveragerc --cache-clear - - docker build -t $DOCKER_REPO . -env: - global: - ALLOWED_HOSTS: "*" - CORS_ORIGIN_WHITELIST: "*" - DATABASE_ENGINE: "postgresql" - DATABASE_NAME: "buildly_api" - DATABASE_USER: "postgres" - DATABASE_PASSWORD: "" - DATABASE_HOST: "localhost" - DATABASE_PORT: "5432" - DEFAULT_ORG: "Default Organization" - DJANGO_SETTINGS_MODULE: "buildly.settings.production" - SOCIAL_AUTH_GITHUB_REDIRECT_URL: "/complete/github" - SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URL: "/complete/google-oauth2" - SOCIAL_AUTH_MICROSOFT_GRAPH_REDIRECT_URL: "/complete/microsoft-graph" - JWT_ISSUER: "buildly" - JWT_PRIVATE_KEY_RSA_BUILDLY: $'-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBALFc9NFZaOaSwUMPNektbtJqEjYZ6IRBqhqvJu1hKPYn9HYd75c0\ngIDYHJ9lb7QwQvg44aO27104rDK0xSstzL0CAwEAAQJAe5z5096oyeqGX6J+RGGx\n11yuDJ7J+0N4tthUHSWWUtgkd19NvmTM/mVLmPCzZHgNUT+aWUKsQ84+jhru/NQD\n0QIhAOHOzFmjxjTAR1jspn6YtJBKQB40tvT6WEvm2mKm0aD7AiEAyRPwXyZf3JT+\nM6Ui0Mubs7Qb/E4g1d/kVL+o/XoZC6cCIQC+nKzPtnooKW+Q1yOslgdGDgeV9/XB\nUlqap+MNh7hJZQIgZNaM+wqhlFtbx8aO2SrioJI4XqVHrjojpaSgOM3cdY0CIQDB\nQ6ckOaDV937acmWuiZhxuG2euNLwNbMldtCV5ADo/g==\n-----END RSA PRIVATE KEY-----' - JWT_PUBLIC_KEY_RSA_BUILDLY: $'-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALFc9NFZaOaSwUMPNektbtJqEjYZ6IRB\nqhqvJu1hKPYn9HYd75c0gIDYHJ9lb7QwQvg44aO27104rDK0xSstzL0CAwEAAQ==\n-----END PUBLIC KEY-----' - SECRET_KEY: "nothing" - OAUTH_CLIENT_ID: "vBn4KsOCthm7TWzMH0kVV0dXkUPJEtOQwaLu0eoC" - OAUTH_CLIENT_SECRET: "0aYDOHUNAxK4MjbnYOHhfrKx8EzjKqN6GbB6IGyCgpT6pmQ5pEVJmH7mIEUJ" - DOCKER_REPO: "transparent-path/buildly-core" - LDAP_ENABLE: "True" - LDAP_HOST: "ldap://localhost:389" - LDAP_USERNAME: "cn=admin,dc=example,dc=org" - LDAP_PASSWORD: "admin" - LDAP_BASE_DN: "dc=example,dc=org" -deploy: - provider: script - script: bash scripts/deploy-aws.sh - on: - branch: master From b444bd172bea19c7382ef2fc4f85150cce262598 Mon Sep 17 00:00:00 2001 From: abhishek-kumar-piyush <97152893+abhishek-kumar-piyush@users.noreply.github.com> Date: Wed, 20 Apr 2022 18:31:19 +0530 Subject: [PATCH 102/109] Remove timestamp from alert messages (#110) * removed 'Captured at' from alert message * stop sending datetime in alert message --- core/views/coreuser.py | 26 ++++++++++---------- templates/email/coreuser/shipment_alert.html | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index db1711eb..064ccb3d 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -9,7 +9,6 @@ import django_filters import jwt from drf_yasg.utils import swagger_auto_schema -from datetime import datetime from core.models import CoreUser, Organization from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, @@ -22,7 +21,8 @@ from core.jwt_utils import create_invitation_token from core.email_utils import send_email import logging -from dateutil import tz +# from datetime import datetime +# from dateutil import tz # from twilio.rest import Client logger = logging.getLogger(__name__) @@ -288,12 +288,12 @@ def alert(self, request, *args, **kwargs): messages = request.data['messages'] try: for message in messages: - try: - time_tuple = datetime.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S%z") - except ValueError: - time_tuple = datetime.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S.%f%z") + # try: + # time_tuple = datetime.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S%z") + # except ValueError: + # time_tuple = datetime.strptime(message['date_time'], "%Y-%m-%dT%H:%M:%S.%f%z") + # message['date_time'] = time_tuple.replace(tzinfo=tz.gettz('UTC')) subject = '{} Alert'.format(message['parameter'].capitalize()) - message['date_time'] = time_tuple.replace(tzinfo=tz.gettz('UTC')) if message.get('shipment_id'): message['shipment_url'] = urljoin(settings.FRONTEND_URL, '/app/shipment/edit/:'+str(message['shipment_id'])) @@ -311,12 +311,12 @@ def alert(self, request, *args, **kwargs): email_address = user.email preferences = user.email_preferences if preferences and (preferences.get('environmental', None) or preferences.get('geofence', None)): - user_timezone = user.user_timezone - if user_timezone: - local_zone = tz.gettz(user_timezone) - message['date_time'] = message['date_time'].astimezone(local_zone) - else: - message['date_time'] = time_tuple.strftime("%B %d, %Y, %I:%M %p")+" (UTC)" + # user_timezone = user.user_timezone + # if user_timezone: + # local_zone = tz.gettz(user_timezone) + # message['date_time'] = message['date_time'].astimezone(local_zone) + # else: + # message['date_time'] = time_tuple.strftime("%B %d, %Y, %I:%M %p")+" (UTC)" send_email(email_address, subject, context, template_name, html_template_name) except Exception as ex: print('Exception: ', ex) diff --git a/templates/email/coreuser/shipment_alert.html b/templates/email/coreuser/shipment_alert.html index bc3bc873..d67f9d20 100644 --- a/templates/email/coreuser/shipment_alert.html +++ b/templates/email/coreuser/shipment_alert.html @@ -51,7 +51,7 @@

Shipment: {{ message.shipment_name }}

{{message.alert_message}}

Alert for {{message.parameter}} : {{message.recorded_value}}

- Captured at: {{ message.date_time }} + {% if message.shipment_id != None %} Date: Wed, 27 Apr 2022 20:32:31 +0530 Subject: [PATCH 103/109] Gunicorn timeout configuration --- buildly/gunicorn_conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/buildly/gunicorn_conf.py b/buildly/gunicorn_conf.py index f45ad8e7..2f7dc5ca 100644 --- a/buildly/gunicorn_conf.py +++ b/buildly/gunicorn_conf.py @@ -1,3 +1,4 @@ bind = '0.0.0.0:8080' limit_request_field_size = 0 limit_request_line = 0 +timeout = 90 \ No newline at end of file From f70809cce56ae85f83f780815c54b4f2bf95fe37 Mon Sep 17 00:00:00 2001 From: abhishek-kumar-piyush <97152893+abhishek-kumar-piyush@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:24:10 +0530 Subject: [PATCH 104/109] buildly-core gitHub actions (#112) * github action for unit test * update triggers for unit test github action * github action for dev docker image build and push * github action for demo docker image build and push * github action for production docker image build and push * added pre-commit hooks for dev, demo and prod branch * update name for unit_test * Github Actions for Unit Test (#114) * Gunicorn timeout configuration * Fixes done for flake --- .github/workflows/demo-build.yml | 52 ++ .github/workflows/dev-build.yml | 28 + .github/workflows/prod-build.yml | 89 +++ .github/workflows/unit_test.yml | 26 + .pre-commit-config.yaml | 6 + buildly/gunicorn_conf.py | 2 +- .../management/commands/loadinitialdata.py | 30 +- buildly/settings/authentication.py | 63 +- buildly/settings/base.py | 60 +- buildly/settings/production.py | 12 +- buildly/tests/test_admin.py | 10 +- buildly/tests/test_loadinitialdata.py | 31 +- buildly/wsgi.py | 3 +- conftest.py | 1 - core/admin.py | 86 ++- core/auth_pipeline.py | 34 +- core/email_utils.py | 19 +- core/jwt_utils.py | 9 +- core/middleware.py | 7 +- core/migrations/0001_initial.py | 632 +++++++++++++++--- core/models.py | 188 ++++-- core/permissions.py | 10 +- core/serializers.py | 179 +++-- core/swagger.py | 62 +- core/tests/fixtures.py | 52 +- core/tests/test_accesstokenview.py | 43 +- core/tests/test_applicationview.py | 31 +- core/tests/test_auth_pipeline.py | 87 ++- core/tests/test_coregroupview.py | 103 ++- core/tests/test_coreuserview.py | 211 ++++-- core/tests/test_emailtemplates.py | 21 +- core/tests/test_jwt_utils.py | 14 +- core/tests/test_logicmoduleview.py | 12 +- core/tests/test_organizationview.py | 2 +- core/tests/test_permissions.py | 2 - core/tests/test_refreshtokenview.py | 53 +- core/tests/test_serializers.py | 61 +- core/tests/test_utils.py | 27 +- core/tests/test_views.py | 24 +- core/urls.py | 9 +- core/utils.py | 3 +- core/views/__init__.py | 2 +- core/views/consortium.py | 7 +- core/views/coregroup.py | 1 + core/views/coreuser.py | 228 ++++--- core/views/logicmodule.py | 4 +- core/views/oauth.py | 24 +- core/views/organization.py | 9 +- core/views/web.py | 16 +- datamesh/exceptions.py | 1 - datamesh/filters.py | 7 +- .../management/commands/loadrelationships.py | 12 +- datamesh/managers.py | 23 +- datamesh/migrations/0001_initial.py | 100 ++- .../migrations/0002_auto_20190918_1659.py | 73 +- datamesh/mixins.py | 3 +- datamesh/models.py | 116 ++-- datamesh/serializers.py | 50 +- datamesh/services.py | 92 ++- datamesh/tests/fixtures.py | 117 ++-- datamesh/tests/test_datamesh_service.py | 227 ++++--- datamesh/tests/test_join.py | 135 ++-- datamesh/tests/test_models.py | 78 +-- datamesh/tests/test_serializers.py | 8 +- datamesh/tests/test_views.py | 266 +++++--- datamesh/utils.py | 16 +- datamesh/views.py | 21 +- docs/conf.py | 29 +- factories/core_models.py | 2 +- factories/datamesh_models.py | 11 +- factories/oauth2_models.py | 3 +- factories/workflow_models.py | 3 +- gateway/aggregator.py | 41 +- gateway/clients.py | 45 +- gateway/generator.py | 25 +- gateway/permissions.py | 28 +- gateway/request.py | 62 +- gateway/tests/fixtures.py | 38 +- gateway/tests/test_permissions.py | 93 +-- gateway/tests/test_swagger_aggregator.py | 22 +- gateway/tests/test_urls.py | 55 +- gateway/tests/test_utils.py | 39 +- gateway/tests/test_views.py | 88 ++- gateway/tests/test_views_async.py | 215 ++++-- gateway/tests/utils.py | 13 +- gateway/urls.py | 25 +- gateway/utils.py | 40 +- gateway/views.py | 17 +- manage.py | 3 +- workflow/admin.py | 15 +- workflow/migrations/0001_initial.py | 423 ++++++++++-- workflow/models.py | 209 +++++- workflow/pagination.py | 6 +- workflow/permissions.py | 59 +- workflow/serializers.py | 9 +- .../tests/test_internationalizationview.py | 24 +- workflow/tests/test_serializers.py | 43 +- workflow/tests/test_workflowlevel1view.py | 272 +++++--- .../tests/test_workflowlevel2serializers.py | 5 +- workflow/tests/test_workflowlevel2sortview.py | 140 ++-- workflow/tests/test_workflowlevel2view.py | 469 +++++++------ workflow/tests/test_workflowlevelstatus.py | 4 +- workflow/tests/test_workflowleveltypeview.py | 1 + workflow/views/workflowlevel1.py | 11 +- workflow/views/workflowlevel2.py | 17 +- workflow/views/workflowlevelstatus.py | 5 +- workflow/views/workflowleveltype.py | 5 +- workflow/views/workflowteam.py | 10 +- 108 files changed, 4640 insertions(+), 2014 deletions(-) create mode 100644 .github/workflows/demo-build.yml create mode 100644 .github/workflows/dev-build.yml create mode 100644 .github/workflows/prod-build.yml create mode 100644 .github/workflows/unit_test.yml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/demo-build.yml b/.github/workflows/demo-build.yml new file mode 100644 index 00000000..ecd8ade6 --- /dev/null +++ b/.github/workflows/demo-build.yml @@ -0,0 +1,52 @@ +name: Build and Push to Demo + +on: + push: + branches: + - demo + +jobs: + build: + name: Build and Push to GCR + runs-on: ubuntu-latest + env: + IMAGE_NAME: gcr.io/spry-bricolage-298920/transparent-path/buildly-core + steps: + - uses: actions/checkout@v2 + + # Login to docker + - name: Docker login + uses: docker/login-action@v1 + with: + registry: gcr.io + username: _json_key + password: ${{ secrets.DEMO_GCR_JSON_KEY }} + + # Build docker image + - name: Build docker image + run: docker build -t $IMAGE_NAME:latest . + + # Push docker image to GCR + - name: Push to Google Container Registry + run: docker push $IMAGE_NAME:latest + + # Send message on Slack + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_MESSAGE: 'Demo Docker Image of Transparent Path buildly-core pushed to Google Container Registry Successfully' + MSG_MINIMAL: true + + # Send email alert + - name: Email Alert + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 465 + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + subject: Github Actions Build and Push job alert + to: ${{ secrets.RECIPIENT_EMAIL }} + from: ${{ secrets.SENDER_EMAIL }} + body: Demo Docker Image of Transparent Path buildly-core pushed to Google Container Registry Successfully \ No newline at end of file diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml new file mode 100644 index 00000000..3bf2cabe --- /dev/null +++ b/.github/workflows/dev-build.yml @@ -0,0 +1,28 @@ +name: Build and Push to Development + +on: + push: + branches: + - dev + +jobs: + build: + name: Build and Push to GCR + runs-on: ubuntu-latest + env: + IMAGE_NAME: gcr.io/dev-buildly/transparent-path/buildly-core + steps: + - uses: actions/checkout@v2 + + - name: Docker login + uses: docker/login-action@v1 + with: + registry: gcr.io + username: _json_key + password: ${{ secrets.DEV_GCR_JSON_KEY }} + + - name: Build docker image + run: docker build -t $IMAGE_NAME:latest . + + - name: Push to Google Container Registry + run: docker push $IMAGE_NAME:latest diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml new file mode 100644 index 00000000..c06f913e --- /dev/null +++ b/.github/workflows/prod-build.yml @@ -0,0 +1,89 @@ +name: Build and Push to Production + +on: + push: + branches: + - prod + +jobs: + build: + name: Build and Push to AWS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + # auto generate tag from label defined in Dockerfile + - uses: butlerlogic/action-autotag@stable + id: tag_version + with: + GITHUB_TOKEN: "${{ secrets.RELEASE_TOKEN }}" + strategy: docker + tag_prefix: "v" + + # Create release notes + - name: Build changelog + id: build_changelog + uses: mikepenz/release-changelog-builder-action@main + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + + # Create release + - name: Create Release + id: create_release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + with: + tag_name: ${{ steps.tag_version.outputs.tagname }} + release_name: Release ${{ steps.tag_version.outputs.tagname }} + body: ${{ steps.build_changelog.outputs.changelog }} + draft: false + prerelease: false + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + # Login to Amazon ECR + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + # Build, tag and push docker image + - name: Build, tag, and push image to Amazon ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: transparent-path/buildly_core + + IMAGE_TAG: latest + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + # Send message on Slack + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_MESSAGE: 'Production Release for Transparent Path buildly-core created and Docker Image pushed to AWS ECR Successfully' + MSG_MINIMAL: true + + # Send email alert + - name: Email Alert + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 465 + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + subject: Github Actions Build and Push job alert + to: ${{ secrets.RECIPIENT_EMAIL }} + from: ${{ secrets.SENDER_EMAIL }} + body: Production Release for Transparent Path buildly-core created and Docker Image pushed to AWS ECR Successfully + +# Reference : https://towardsaws.com/build-push-docker-image-to-aws-ecr-using-github-actions-8396888a8f9e \ No newline at end of file diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml new file mode 100644 index 00000000..123434e5 --- /dev/null +++ b/.github/workflows/unit_test.yml @@ -0,0 +1,26 @@ +name: Buildly Core Unit Test + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + unit_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Build the docker-compose stack + run: docker-compose build + + - name: Setup docker containers + run: docker-compose up -d + + - name: Check running containers + run: docker ps -a + + - name: Run unit test case + run: docker-compose run --entrypoint '/usr/bin/env' --rm buildly bash scripts/run-tests.sh --keepdb + + - name: Stop docker container + run: docker-compose down diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b55d74ea --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: no-commit-to-branch + args: [--branch, prod, --branch, demo, --branch, dev] diff --git a/buildly/gunicorn_conf.py b/buildly/gunicorn_conf.py index 2f7dc5ca..066c541f 100644 --- a/buildly/gunicorn_conf.py +++ b/buildly/gunicorn_conf.py @@ -1,4 +1,4 @@ bind = '0.0.0.0:8080' limit_request_field_size = 0 limit_request_line = 0 -timeout = 90 \ No newline at end of file +timeout = 90 diff --git a/buildly/management/commands/loadinitialdata.py b/buildly/management/commands/loadinitialdata.py index 8fc53890..b2540dd6 100644 --- a/buildly/management/commands/loadinitialdata.py +++ b/buildly/management/commands/loadinitialdata.py @@ -5,8 +5,16 @@ from django.core.management.base import BaseCommand from django.db import transaction -from core.models import ROLE_VIEW_ONLY, ROLE_ORGANIZATION_ADMIN, ROLE_WORKFLOW_ADMIN, ROLE_WORKFLOW_TEAM, \ - Organization, CoreUser, CoreGroup, OrganizationType +from core.models import ( + ROLE_VIEW_ONLY, + ROLE_ORGANIZATION_ADMIN, + ROLE_WORKFLOW_ADMIN, + ROLE_WORKFLOW_TEAM, + Organization, + CoreUser, + CoreGroup, + OrganizationType, +) logger = logging.getLogger(__name__) @@ -34,13 +42,19 @@ def _create_organization_types(self): def _create_default_organization(self): if settings.DEFAULT_ORG: - self._default_org, _ = Organization.objects.get_or_create(name=settings.DEFAULT_ORG) + self._default_org, _ = Organization.objects.get_or_create( + name=settings.DEFAULT_ORG + ) def _create_groups(self): - self._su_group = CoreGroup.objects.filter(is_global=True, permissions=15).first() + self._su_group = CoreGroup.objects.filter( + is_global=True, permissions=15 + ).first() if not self._su_group: logger.info("Creating global CoreGroup") - self._su_group = CoreGroup.objects.create(name='Global Admin', is_global=True, permissions=15) + self._su_group = CoreGroup.objects.create( + name='Global Admin', is_global=True, permissions=15 + ) # TODO: remove this after full Group -> CoreGroup refactoring self._groups.append(Group.objects.get_or_create(name=ROLE_VIEW_ONLY)) @@ -56,7 +70,11 @@ def _create_user(self): logger.info("Creating Super User") user_password = None if settings.DEBUG: - user_password = settings.SUPER_USER_PASSWORD if settings.SUPER_USER_PASSWORD else 'zGtkgLvmNiKm' + user_password = ( + settings.SUPER_USER_PASSWORD + if settings.SUPER_USER_PASSWORD + else 'zGtkgLvmNiKm' + ) elif settings.SUPER_USER_PASSWORD: user_password = settings.SUPER_USER_PASSWORD else: diff --git a/buildly/settings/authentication.py b/buildly/settings/authentication.py index ff4a7cf3..23b21f26 100644 --- a/buildly/settings/authentication.py +++ b/buildly/settings/authentication.py @@ -31,7 +31,7 @@ # Rest Framework OAuth2 and JWT REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] += [ 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', - 'oauth2_provider_jwt.authentication.JWTAuthentication' + 'oauth2_provider_jwt.authentication.JWTAuthentication', ] # Auth Application @@ -50,28 +50,25 @@ AUTH_PASSWORD_VALIDATORS = [] AUTH_PASSWORD_VALIDATORS_MAP = { - 'USE_PASSWORD_USER_ATTRIBUTE_SIMILARITY_VALIDATOR': - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - 'USE_PASSWORD_MINIMUM_LENGTH_VALIDATOR': - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - 'OPTIONS': { - 'min_length': int(os.getenv('PASSWORD_MINIMUM_LENGTH', 6)), - } - }, - 'USE_PASSWORD_COMMON_VALIDATOR': - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - 'USE_PASSWORD_NUMERIC_VALIDATOR': - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + 'USE_PASSWORD_USER_ATTRIBUTE_SIMILARITY_VALIDATOR': { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator' + }, + 'USE_PASSWORD_MINIMUM_LENGTH_VALIDATOR': { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'OPTIONS': {'min_length': int(os.getenv('PASSWORD_MINIMUM_LENGTH', 6))}, + }, + 'USE_PASSWORD_COMMON_VALIDATOR': { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator' + }, + 'USE_PASSWORD_NUMERIC_VALIDATOR': { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator' + }, } -for password_validator_env_var, password_validator in AUTH_PASSWORD_VALIDATORS_MAP.items(): +for ( + password_validator_env_var, + password_validator, +) in AUTH_PASSWORD_VALIDATORS_MAP.items(): if os.getenv(password_validator_env_var, 'True') == 'True': AUTH_PASSWORD_VALIDATORS.append(password_validator) @@ -81,11 +78,13 @@ SOCIAL_AUTH_URL_NAMESPACE = 'social' SOCIAL_AUTH_POSTGRES_JSONFIELD = True -SOCIAL_AUTH_REDIRECT_IS_HTTPS = True if os.getenv('SOCIAL_AUTH_REDIRECT_IS_HTTPS') == 'True' else False +SOCIAL_AUTH_REDIRECT_IS_HTTPS = ( + True if os.getenv('SOCIAL_AUTH_REDIRECT_IS_HTTPS') == 'True' else False +) SOCIAL_AUTH_LOGIN_REDIRECT_URLS = { 'github': os.getenv('SOCIAL_AUTH_GITHUB_REDIRECT_URL', None), 'google-oauth2': os.getenv('SOCIAL_AUTH_GOOGLE_OAUTH2_REDIRECT_URL', None), - 'microsoft-graph': os.getenv('SOCIAL_AUTH_MICROSOFT_GRAPH_REDIRECT_URL', None) + 'microsoft-graph': os.getenv('SOCIAL_AUTH_MICROSOFT_GRAPH_REDIRECT_URL', None), } SOCIAL_AUTH_PIPELINE = ( @@ -115,9 +114,13 @@ # Whitelist of domains allowed to login via social auths # i.e. ['example.com', 'buildly.io','treeaid.org'] if os.getenv('SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS'): - SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = os.getenv('SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS').split(',') + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = os.getenv( + 'SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS' + ).split(',') if os.getenv('SOCIAL_AUTH_MICROSOFT_WHITELISTED_DOMAINS'): - SOCIAL_AUTH_GOOGLE_MICROSOFT_DOMAINS = os.getenv('SOCIAL_AUTH_MICROSOFT_WHITELISTED_DOMAINS').split(',') + SOCIAL_AUTH_GOOGLE_MICROSOFT_DOMAINS = os.getenv( + 'SOCIAL_AUTH_MICROSOFT_WHITELISTED_DOMAINS' + ).split(',') # oauth2 settings OAUTH2_PROVIDER = { @@ -130,7 +133,9 @@ } DEFAULT_OAUTH_DOMAINS = os.getenv('DEFAULT_OAUTH_DOMAINS', '') -CREATE_DEFAULT_PROGRAM = True if os.getenv('CREATE_DEFAULT_PROGRAM') == 'True' else False +CREATE_DEFAULT_PROGRAM = ( + True if os.getenv('CREATE_DEFAULT_PROGRAM') == 'True' else False +) # LDAP configuration # https://django-auth-ldap.readthedocs.io/en/latest/reference.html#settings @@ -145,7 +150,7 @@ AUTH_LDAP_USER_SEARCH = LDAPSearch( AUTH_LDAP_BASE_DN, ldap.SCOPE_SUBTREE, - f'{AUTH_LDAP_USERNAME_FIELD_SEARCH}=%(user)s' + f'{AUTH_LDAP_USERNAME_FIELD_SEARCH}=%(user)s', ) AUTH_LDAP_USER_ATTR_MAP = { @@ -155,4 +160,6 @@ 'email': 'mail', } AUTH_LDAP_ALWAYS_UPDATE_USER = True - AUTH_LDAP_CACHE_TIMEOUT = 3600 # Cache distinguished names and group memberships for an hour to minimize + AUTH_LDAP_CACHE_TIMEOUT = ( + 3600 + ) # Cache distinguished names and group memberships for an hour to minimize diff --git a/buildly/settings/base.py b/buildly/settings/base.py index 6b024c33..b660fc94 100644 --- a/buildly/settings/base.py +++ b/buildly/settings/base.py @@ -1,8 +1,7 @@ import os # Base dir path -BASE_DIR = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) SECRET_KEY = os.environ['SECRET_KEY'] @@ -20,9 +19,7 @@ STATIC_URL = '/static/' -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "static"), -] +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] INSTALLED_APPS_DJANGO = [ @@ -40,31 +37,23 @@ 'django_filters', 'rest_framework', 'rest_framework.authtoken', - # Social auth 'social_django', - # OAuth2 'oauth2_provider', 'oauth2_provider_jwt', - # swagger 'drf_yasg', - # health check - 'health_check', # required - 'health_check.db', # stock Django health checkers + 'health_check', # required + 'health_check.db', # stock Django health checkers ] -INSTALLED_APPS_LOCAL = [ - 'buildly', - 'gateway', - 'core', - 'workflow', - 'datamesh', -] +INSTALLED_APPS_LOCAL = ['buildly', 'gateway', 'core', 'workflow', 'datamesh'] -INSTALLED_APPS = INSTALLED_APPS_DJANGO + INSTALLED_APPS_THIRD_PARTIES + INSTALLED_APPS_LOCAL +INSTALLED_APPS = ( + INSTALLED_APPS_DJANGO + INSTALLED_APPS_THIRD_PARTIES + INSTALLED_APPS_LOCAL +) MIDDLEWARE_DJANGO = [ 'django.middleware.security.SecurityMiddleware', @@ -76,13 +65,9 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -MIDDLEWARE_CSRF = [ - 'core.middleware.DisableCsrfCheck', -] +MIDDLEWARE_CSRF = ['core.middleware.DisableCsrfCheck'] -EXCEPTION_MIDDLEWARE = [ - 'core.middleware.ExceptionMiddleware' -] +EXCEPTION_MIDDLEWARE = ['core.middleware.ExceptionMiddleware'] MIDDLEWARE = MIDDLEWARE_DJANGO + MIDDLEWARE_CSRF + EXCEPTION_MIDDLEWARE @@ -91,9 +76,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - 'templates', - ], + 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -106,10 +89,10 @@ 'social_django.context_processors.login_redirect', ], 'builtins': [ # TODO to delete? - 'django.contrib.staticfiles.templatetags.staticfiles', + 'django.contrib.staticfiles.templatetags.staticfiles' ], }, - }, + } ] WSGI_APPLICATION = 'buildly.wsgi.application' @@ -171,9 +154,7 @@ 'rest_framework.authentication.SessionAuthentication', # TODO check if disable, and also delete CSRF 'rest_framework.authentication.TokenAuthentication', ], - 'DEFAULT_PERMISSION_CLASSES': ( - 'core.permissions.IsSuperUserBrowseableAPI', - ) + 'DEFAULT_PERMISSION_CLASSES': ('core.permissions.IsSuperUserBrowseableAPI',) # ToDo: Think about `DEFAULT_PAGINATION_CLASS as env variable and # customizable values with reasonable defaults } @@ -181,7 +162,9 @@ # Front-end application URL FRONTEND_URL = os.getenv('FRONTEND_URL', 'http://www.example.com/') REGISTRATION_URL_PATH = os.getenv('REGISTRATION_URL_PATH', 'register/') -RESETPASS_CONFIRM_URL_PATH = os.getenv('RESETPASS_CONFIRM_URL_PATH', 'reset_password_confirm/') +RESETPASS_CONFIRM_URL_PATH = os.getenv( + 'RESETPASS_CONFIRM_URL_PATH', 'reset_password_confirm/' +) PASSWORD_RESET_TIMEOUT_DAYS = 1 @@ -195,11 +178,6 @@ # Swagger settings - for generate_swagger management command -SWAGGER_SETTINGS = { - 'DEFAULT_INFO': 'gateway.urls.swagger_info', -} +SWAGGER_SETTINGS = {'DEFAULT_INFO': 'gateway.urls.swagger_info'} -ORGANIZATION_TYPES = [ - 'Custodian', - 'Producer' -] \ No newline at end of file +ORGANIZATION_TYPES = ['Custodian', 'Producer'] diff --git a/buildly/settings/production.py b/buildly/settings/production.py index 1ba41c5a..184eb65e 100644 --- a/buildly/settings/production.py +++ b/buildly/settings/production.py @@ -5,13 +5,9 @@ # CORS to allow external apps auth through OAuth 2 # https://github.com/ottoyiu/django-cors-headers -INSTALLED_APPS += ( - 'corsheaders', -) +INSTALLED_APPS += ('corsheaders',) -MIDDLEWARE_CORS = [ - 'corsheaders.middleware.CorsMiddleware', -] +MIDDLEWARE_CORS = ['corsheaders.middleware.CorsMiddleware'] MIDDLEWARE = MIDDLEWARE_CORS + MIDDLEWARE @@ -42,13 +38,13 @@ 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 'class': 'logging.FileHandler', 'filename': '/var/log/buildly.log', - }, + } }, 'loggers': { 'django': { 'handlers': ['file'], 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 'propagate': True, - }, + } }, } diff --git a/buildly/tests/test_admin.py b/buildly/tests/test_admin.py index 0c5337a5..1a34931e 100644 --- a/buildly/tests/test_admin.py +++ b/buildly/tests/test_admin.py @@ -19,9 +19,9 @@ def test_admin_user_auth_page_with_superuser(self): def test_admin_user_auth_page_with_staff_user(self): """Staff user shouldn't see superuser status field on django admin""" - staff_user = CoreUser.objects.create_user('staff_user', - 'staffuser@example.com', - 'Password123') + staff_user = CoreUser.objects.create_user( + 'staff_user', 'staffuser@example.com', 'Password123' + ) permission = Permission.objects.get(name='Can change core user') staff_user.user_permissions.add(permission) staff_user.is_staff = True @@ -50,7 +50,9 @@ def test_admin_user_permissions_section_with_superuser(self): def test_admin_user_permissions_section_with_staff_user(self): """Staff user shouldn't see user permissions section field on django admin""" - staff_user = CoreUser.objects.create_user('staff_user', 'staffuser@example.com', 'Password123') + staff_user = CoreUser.objects.create_user( + 'staff_user', 'staffuser@example.com', 'Password123' + ) permission = Permission.objects.get(name='Can change core user') staff_user.user_permissions.add(permission) staff_user.is_staff = True diff --git a/buildly/tests/test_loadinitialdata.py b/buildly/tests/test_loadinitialdata.py index 4c4ccc66..7e2e51c2 100644 --- a/buildly/tests/test_loadinitialdata.py +++ b/buildly/tests/test_loadinitialdata.py @@ -33,7 +33,12 @@ def test_full_initial_data(self): opts = {} call_command('loadinitialdata', *args, **opts) - assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 + assert ( + CoreGroup.objects.filter( + name='Global Admin', is_global=True, permissions=15 + ).count() + == 1 + ) assert OrganizationType.objects.filter().count() >= 2 assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 @@ -45,18 +50,27 @@ def test_without_default_organization(self): opts = {} call_command('loadinitialdata', *args, **opts) - assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 + assert ( + CoreGroup.objects.filter( + name='Global Admin', is_global=True, permissions=15 + ).count() + == 1 + ) assert Organization.objects.all().count() == 0 assert CoreUser.objects.filter(is_superuser=True).count() == 1 - @override_settings(DEBUG=True) def test_create_user_debug_no_password(self): args = [] opts = {} call_command('loadinitialdata', *args, **opts) - assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 + assert ( + CoreGroup.objects.filter( + name='Global Admin', is_global=True, permissions=15 + ).count() + == 1 + ) assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 assert CoreUser.objects.filter(is_superuser=True).count() == 1 @@ -66,6 +80,11 @@ def test_create_user_no_debug_no_password(self): opts = {} call_command('loadinitialdata', *args, **opts) - assert CoreGroup.objects.filter(name='Global Admin', is_global=True, permissions=15).count() == 1 + assert ( + CoreGroup.objects.filter( + name='Global Admin', is_global=True, permissions=15 + ).count() + == 1 + ) assert Organization.objects.filter(name=settings.DEFAULT_ORG).count() == 1 - assert CoreUser.objects.filter(is_superuser=True).count() == 0 \ No newline at end of file + assert CoreUser.objects.filter(is_superuser=True).count() == 0 diff --git a/buildly/wsgi.py b/buildly/wsgi.py index e3a545c4..92eb60cc 100644 --- a/buildly/wsgi.py +++ b/buildly/wsgi.py @@ -11,7 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", - "buildly.settings.production") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "buildly.settings.production") application = get_wsgi_application() diff --git a/conftest.py b/conftest.py index f725c4e0..6e084174 100644 --- a/conftest.py +++ b/conftest.py @@ -11,7 +11,6 @@ def request_factory(): @pytest.fixture(scope='session') def wsgi_request_factory(): - def _make_wsgi_request(data: dict = None): environ = { 'REQUEST_METHOD': 'get', diff --git a/core/admin.py b/core/admin.py index 66037fb3..2b78b2a1 100644 --- a/core/admin.py +++ b/core/admin.py @@ -2,8 +2,17 @@ from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext_lazy as _ -from core.models import CoreUser, CoreGroup, CoreSites, EmailTemplate, \ - Industry, LogicModule, Organization, OrganizationType, Consortium +from core.models import ( + CoreUser, + CoreGroup, + CoreSites, + EmailTemplate, + Industry, + LogicModule, + Organization, + OrganizationType, + Consortium, +) class LogicModuleAdmin(admin.ModelAdmin): @@ -29,25 +38,72 @@ class OrganizationAdmin(admin.ModelAdmin): class CoreGroupAdmin(admin.ModelAdmin): - list_display = ('name', 'organization', 'is_global', 'is_org_level', 'is_default', 'permissions') + list_display = ( + 'name', + 'organization', + 'is_global', + 'is_org_level', + 'is_default', + 'permissions', + ) display = 'Core Group' - search_fields = ('name', 'organization__name', ) + search_fields = ('name', 'organization__name') class CoreUserAdmin(UserAdmin): - list_display = ('username', 'first_name', 'last_name', 'organization', 'title', 'is_active', 'user_timezone') + list_display = ( + 'username', + 'first_name', + 'last_name', + 'organization', + 'title', + 'is_active', + 'user_timezone', + ) display = 'Core User' list_filter = ('is_staff', 'organization') - search_fields = ('first_name', 'first_name', 'username', 'title', 'organization__name', ) + search_fields = ( + 'first_name', + 'first_name', + 'username', + 'title', + 'organization__name', + ) fieldsets = ( (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('title', 'first_name', 'last_name', 'email', 'contact_info', - 'organization', 'user_timezone')}), - (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'core_groups', 'user_permissions')}), + ( + _('Personal info'), + { + 'fields': ( + 'title', + 'first_name', + 'last_name', + 'email', + 'contact_info', + 'organization', + 'user_timezone', + ) + }, + ), + ( + _('Permissions'), + { + 'fields': ( + 'is_active', + 'is_staff', + 'is_superuser', + 'core_groups', + 'user_permissions', + ) + }, + ), (_('Preferences'), {'fields': ('email_preferences', 'push_preferences')}), - (_('Important dates'), {'fields': ('last_login', 'date_joined', 'create_date', 'edit_date')}), + ( + _('Important dates'), + {'fields': ('last_login', 'date_joined', 'create_date', 'edit_date')}, + ), ) - filter_horizontal = ('core_groups', 'user_permissions', ) + filter_horizontal = ('core_groups', 'user_permissions') def get_fieldsets(self, request, obj=None): @@ -59,7 +115,13 @@ def get_fieldsets(self, request, obj=None): if not request.user.is_superuser: fieldsets[2][1]['fields'] = ('is_active', 'is_staff') else: - fieldsets[2][1]['fields'] = ('is_active', 'is_staff', 'is_superuser', 'core_groups', 'user_permissions') + fieldsets[2][1]['fields'] = ( + 'is_active', + 'is_staff', + 'is_superuser', + 'core_groups', + 'user_permissions', + ) return fieldsets diff --git a/core/auth_pipeline.py b/core/auth_pipeline.py index 39ab6282..e68f1fff 100644 --- a/core/auth_pipeline.py +++ b/core/auth_pipeline.py @@ -18,17 +18,18 @@ def create_organization(core_user=None, *args, **kwargs): # create or get an organization and associate it to the core user if settings.DEFAULT_ORG: - organization, created = Organization.objects.get_or_create(name=settings.DEFAULT_ORG) + organization, created = Organization.objects.get_or_create( + name=settings.DEFAULT_ORG + ) else: - organization, created = Organization.objects.get_or_create(name=core_user.username) + organization, created = Organization.objects.get_or_create( + name=core_user.username + ) core_user.organization = organization core_user.save() - return { - 'is_new_org': created, - 'organization': organization - } + return {'is_new_org': created, 'organization': organization} def auth_allowed(backend, details, response, *args, **kwargs): @@ -60,28 +61,31 @@ def auth_allowed(backend, details, response, *args, **kwargs): else: domain = email.split('@', 1)[1] if whitelisted_emails or whitelisted_domains: - allowed = (email in whitelisted_emails or domain in - whitelisted_domains) + allowed = email in whitelisted_emails or domain in whitelisted_domains # Check if the user email domain matches with one of the org oauth # domains and add the organization uuid in the details if allowed: org_uuid = Organization.objects.values_list( - 'organization_uuid', flat=True).get(name=settings.DEFAULT_ORG) + 'organization_uuid', flat=True + ).get(name=settings.DEFAULT_ORG) details.update({'organization_uuid': org_uuid}) else: try: org_uuid = Organization.objects.values_list( - 'organization_uuid', flat=True).get( - oauth_domains__contains=[domain]) + 'organization_uuid', flat=True + ).get(oauth_domains__contains=[domain]) details.update({'organization_uuid': org_uuid}) allowed = True except Organization.DoesNotExist: pass except Organization.MultipleObjectsReturned as e: - logger.warning('There is more than one Organization with ' - 'the domain {}.\n{}'.format(domain, e)) + logger.warning( + 'There is more than one Organization with ' + 'the domain {}.\n{}'.format(domain, e) + ) if not allowed: - return render_to_response('unauthorized.html', - context={'STATIC_URL': static_url}) + return render_to_response( + 'unauthorized.html', context={'STATIC_URL': static_url} + ) diff --git a/core/email_utils.py b/core/email_utils.py index b9b7404c..3ad65397 100644 --- a/core/email_utils.py +++ b/core/email_utils.py @@ -3,14 +3,25 @@ from django.conf import settings -def send_email(email_address: str, subject: str, context: dict, template_name: str, - html_template_name: str = None) -> int: +def send_email( + email_address: str, + subject: str, + context: dict, + template_name: str, + html_template_name: str = None, +) -> int: text_content = loader.render_to_string(template_name, context, using=None) - html_content = loader.render_to_string(html_template_name, context, using=None) if html_template_name else None + html_content = ( + loader.render_to_string(html_template_name, context, using=None) + if html_template_name + else None + ) return send_email_body(email_address, subject, text_content, html_content) -def send_email_body(email_address: str, subject: str, text_content: str, html_content: str = None) -> int: +def send_email_body( + email_address: str, subject: str, text_content: str, html_content: str = None +) -> int: msg = EmailMultiAlternatives( from_email=settings.DEFAULT_FROM_EMAIL, subject=subject, diff --git a/core/jwt_utils.py b/core/jwt_utils.py index 64c430ef..499432f0 100644 --- a/core/jwt_utils.py +++ b/core/jwt_utils.py @@ -16,7 +16,8 @@ def payload_enricher(request): username = request.POST.get('username') try: user = CoreUser.objects.values( - 'core_user_uuid', 'organization__organization_uuid').get(username=username) + 'core_user_uuid', 'organization__organization_uuid' + ).get(username=username) except CoreUser.DoesNotExist: logger.error('No matching CoreUser found.') raise PermissionDenied('No matching CoreUser found.') @@ -26,7 +27,9 @@ def payload_enricher(request): } elif request.POST.get('refresh_token'): try: - refresh_token = RefreshToken.objects.get(token=request.POST.get('refresh_token')) + refresh_token = RefreshToken.objects.get( + token=request.POST.get('refresh_token') + ) user = refresh_token.user return { 'core_user_uuid': user.core_user_uuid, @@ -43,6 +46,6 @@ def create_invitation_token(email_address: str, organization: Organization): payload = { 'email': email_address, 'org_uuid': str(organization.organization_uuid) if organization else None, - 'exp': datetime.datetime.utcnow() + exp_hours + 'exp': datetime.datetime.utcnow() + exp_hours, } return jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256').decode('utf-8') diff --git a/core/middleware.py b/core/middleware.py index e0604ea3..86251fe7 100644 --- a/core/middleware.py +++ b/core/middleware.py @@ -11,7 +11,6 @@ class DisableCsrfCheck(MiddlewareMixin): - def process_request(self, req): attr = '_dont_enforce_csrf_checks' if not getattr(req, attr, False): @@ -29,10 +28,10 @@ def process_request(self, req): class ExceptionMiddleware(MiddlewareMixin): - @staticmethod def process_exception(request, exception): if isinstance(exception, MIDDLEWARE_EXCEPTIONS): - return JsonResponse(data=json.loads(exception.content), - status=exception.status) + return JsonResponse( + data=json.loads(exception.content), status=exception.status + ) return None diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 5929e4c6..e8a565b4 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -10,11 +10,14 @@ import django.utils.timezone import uuid + def migrate_email_alert(apps, schema_editor): - CoreUser = apps.get_model("core","CoreUser") + CoreUser = apps.get_model("core", "CoreUser") for record in CoreUser.objects.all(): record.email_preferences = {'environmental': record.email_alert_flag} record.save() + + class Migration(migrations.Migration): dependencies = [ @@ -26,81 +29,267 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Industry', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, default='Tech', max_length=255, verbose_name='Industry Name')), - ('description', models.TextField(blank=True, max_length=765, null=True, verbose_name='Description/Notes')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'name', + models.CharField( + blank=True, + default='Tech', + max_length=255, + verbose_name='Industry Name', + ), + ), + ( + 'description', + models.TextField( + blank=True, + max_length=765, + null=True, + verbose_name='Description/Notes', + ), + ), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), ], - options={ - 'verbose_name_plural': 'Industries', - 'ordering': ('name',), - }, + options={'verbose_name_plural': 'Industries', 'ordering': ('name',)}, ), migrations.CreateModel( name='Organization', fields=[ - ('organization_uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Organization UUID')), - ('name', models.CharField(blank=True, help_text='Each end user must be grouped into an organization', max_length=255, verbose_name='Organization Name')), - ('description', models.TextField(blank=True, help_text='Description of organization', max_length=765, null=True, verbose_name='Description/Notes')), - ('organization_url', models.CharField(blank=True, help_text='Link to organizations external web site', max_length=255, null=True)), + ( + 'organization_uuid', + models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + verbose_name='Organization UUID', + ), + ), + ( + 'name', + models.CharField( + blank=True, + help_text='Each end user must be grouped into an organization', + max_length=255, + verbose_name='Organization Name', + ), + ), + ( + 'description', + models.TextField( + blank=True, + help_text='Description of organization', + max_length=765, + null=True, + verbose_name='Description/Notes', + ), + ), + ( + 'organization_url', + models.CharField( + blank=True, + help_text='Link to organizations external web site', + max_length=255, + null=True, + ), + ), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), - ('oauth_domains', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True, verbose_name='OAuth Domains'), blank=True, null=True, size=None)), - ('date_format', models.CharField(blank=True, default='DD.MM.YYYY', max_length=50, verbose_name='Date Format')), + ( + 'oauth_domains', + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name='OAuth Domains', + ), + blank=True, + null=True, + size=None, + ), + ), + ( + 'date_format', + models.CharField( + blank=True, + default='DD.MM.YYYY', + max_length=50, + verbose_name='Date Format', + ), + ), ('phone', models.CharField(blank=True, max_length=20, null=True)), - ('industries', models.ManyToManyField(blank=True, help_text='Type of Industry the organization belongs to if any', related_name='organizations', to='core.Industry')), - ('allow_import_export', models.BooleanField(default=False, verbose_name='To allow import export functionality')), + ( + 'industries', + models.ManyToManyField( + blank=True, + help_text='Type of Industry the organization belongs to if any', + related_name='organizations', + to='core.Industry', + ), + ), + ( + 'allow_import_export', + models.BooleanField( + default=False, + verbose_name='To allow import export functionality', + ), + ), ('radius', models.FloatField(blank=True, max_length=20, null=True)), ], - options={ - 'verbose_name_plural': 'Organizations', - 'ordering': ('name',), - }, + options={'verbose_name_plural': 'Organizations', 'ordering': ('name',)}, ), migrations.CreateModel( name='CoreSites', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('name', models.CharField(blank=True, max_length=255, null=True)), ('privacy_disclaimer', models.TextField(blank=True, null=True)), ('created', models.DateTimeField(blank=True, null=True)), ('updated', models.DateTimeField(blank=True, null=True)), - ('whitelisted_domains', models.TextField(blank=True, null=True, verbose_name='Whitelisted Domains')), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), + ( + 'whitelisted_domains', + models.TextField( + blank=True, null=True, verbose_name='Whitelisted Domains' + ), + ), + ( + 'site', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to='sites.Site' + ), + ), ], - options={ - 'verbose_name': 'Core Site', - 'verbose_name_plural': 'Core Sites', - }, + options={'verbose_name': 'Core Site', 'verbose_name_plural': 'Core Sites'}, ), migrations.CreateModel( name='CoreGroup', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uuid', models.CharField(default=uuid.uuid4, max_length=255, unique=True, verbose_name='CoreGroup UUID')), - ('name', models.CharField(max_length=80, verbose_name='Name of the role')), - ('is_global', models.BooleanField(default=False, verbose_name='Is global group')), - ('is_org_level', models.BooleanField(default=False, verbose_name='Is organization level group')), - ('is_default', models.BooleanField(default=False, verbose_name='Is organization default group')), - ('permissions', models.PositiveSmallIntegerField(default=4, help_text='Decimal integer from 0 to 15 converted from 4-bit binary, each bit indicates permissions for CRUD', verbose_name='Permissions')), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'uuid', + models.CharField( + default=uuid.uuid4, + max_length=255, + unique=True, + verbose_name='CoreGroup UUID', + ), + ), + ( + 'name', + models.CharField(max_length=80, verbose_name='Name of the role'), + ), + ( + 'is_global', + models.BooleanField(default=False, verbose_name='Is global group'), + ), + ( + 'is_org_level', + models.BooleanField( + default=False, verbose_name='Is organization level group' + ), + ), + ( + 'is_default', + models.BooleanField( + default=False, verbose_name='Is organization default group' + ), + ), + ( + 'permissions', + models.PositiveSmallIntegerField( + default=4, + help_text='Decimal integer from 0 to 15 converted from 4-bit binary, each bit indicates permissions for CRUD', + verbose_name='Permissions', + ), + ), + ( + 'create_date', + models.DateTimeField(default=django.utils.timezone.now), + ), ('edit_date', models.DateTimeField(blank=True, null=True)), - ('organization', models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization')), + ( + 'organization', + models.ForeignKey( + blank=True, + help_text='Related Org to associate with', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='core.Organization', + ), + ), ], - options={ - 'ordering': ('name',), - }, + options={'ordering': ('name',)}, ), migrations.CreateModel( name='EmailTemplate', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('subject', models.CharField(max_length=255, verbose_name='Subject')), - ('type', models.PositiveSmallIntegerField(choices=[(1, 'Password resetting'), (2, 'Invitation')], verbose_name='Type of template')), - ('template', models.TextField(blank=True, null=True, verbose_name='Reset password e-mail template (text)')), - ('template_html', models.TextField(blank=True, null=True, verbose_name='Reset password e-mail template (HTML)')), - ('organization', models.ForeignKey(help_text='Related Org to associate with', on_delete=django.db.models.deletion.CASCADE, to='core.Organization', verbose_name='Organization')), + ( + 'type', + models.PositiveSmallIntegerField( + choices=[(1, 'Password resetting'), (2, 'Invitation')], + verbose_name='Type of template', + ), + ), + ( + 'template', + models.TextField( + blank=True, + null=True, + verbose_name='Reset password e-mail template (text)', + ), + ), + ( + 'template_html', + models.TextField( + blank=True, + null=True, + verbose_name='Reset password e-mail template (HTML)', + ), + ), + ( + 'organization', + models.ForeignKey( + help_text='Related Org to associate with', + on_delete=django.db.models.deletion.CASCADE, + to='core.Organization', + verbose_name='Organization', + ), + ), ], options={ 'verbose_name': 'Email Template', @@ -111,15 +300,64 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LogicModule', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('module_uuid', models.CharField(default=uuid.uuid4, max_length=255, unique=True, verbose_name='Logic Module UUID')), - ('name', models.CharField(blank=True, max_length=255, verbose_name='Logic Module Name')), - ('description', models.TextField(blank=True, max_length=765, null=True, verbose_name='Description/Notes')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'module_uuid', + models.CharField( + default=uuid.uuid4, + max_length=255, + unique=True, + verbose_name='Logic Module UUID', + ), + ), + ( + 'name', + models.CharField( + blank=True, max_length=255, verbose_name='Logic Module Name' + ), + ), + ( + 'description', + models.TextField( + blank=True, + max_length=765, + null=True, + verbose_name='Description/Notes', + ), + ), ('endpoint', models.CharField(blank=True, max_length=255, null=True)), - ('endpoint_name', models.CharField(blank=True, max_length=255, null=True)), - ('api_specification', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), - ('docs_endpoint', models.CharField(blank=True, max_length=255, null=True)), - ('core_groups', models.ManyToManyField(blank=True, related_name='logic_module_set', related_query_name='logic_module', to='core.CoreGroup', verbose_name='Logic Module groups')), + ( + 'endpoint_name', + models.CharField(blank=True, max_length=255, null=True), + ), + ( + 'api_specification', + django.contrib.postgres.fields.jsonb.JSONField( + blank=True, null=True + ), + ), + ( + 'docs_endpoint', + models.CharField(blank=True, max_length=255, null=True), + ), + ( + 'core_groups', + models.ManyToManyField( + blank=True, + related_name='logic_module_set', + related_query_name='logic_module', + to='core.CoreGroup', + verbose_name='Logic Module groups', + ), + ), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), ], @@ -132,44 +370,200 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CoreUser', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('core_user_uuid', models.CharField(default=uuid.uuid4, max_length=255, unique=True, verbose_name='CoreUser UUID')), - ('title', models.CharField(blank=True, choices=[('mr', 'Mr.'), ('mrs', 'Mrs.'), ('ms', 'Ms.')], max_length=3, null=True)), - ('contact_info', models.CharField(blank=True, max_length=255, null=True)), + ( + 'last_login', + models.DateTimeField( + blank=True, null=True, verbose_name='last login' + ), + ), + ( + 'is_superuser', + models.BooleanField( + default=False, + help_text='Designates that this user has all permissions without explicitly assigning them.', + verbose_name='superuser status', + ), + ), + ( + 'username', + models.CharField( + error_messages={ + 'unique': 'A user with that username already exists.' + }, + help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name='username', + ), + ), + ( + 'first_name', + models.CharField( + blank=True, max_length=30, verbose_name='first name' + ), + ), + ( + 'last_name', + models.CharField( + blank=True, max_length=150, verbose_name='last name' + ), + ), + ( + 'email', + models.EmailField( + blank=True, max_length=254, verbose_name='email address' + ), + ), + ( + 'is_staff', + models.BooleanField( + default=False, + help_text='Designates whether the user can log into this admin site.', + verbose_name='staff status', + ), + ), + ( + 'is_active', + models.BooleanField( + default=True, + help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', + verbose_name='active', + ), + ), + ( + 'date_joined', + models.DateTimeField( + default=django.utils.timezone.now, verbose_name='date joined' + ), + ), + ( + 'core_user_uuid', + models.CharField( + default=uuid.uuid4, + max_length=255, + unique=True, + verbose_name='CoreUser UUID', + ), + ), + ( + 'title', + models.CharField( + blank=True, + choices=[('mr', 'Mr.'), ('mrs', 'Mrs.'), ('ms', 'Ms.')], + max_length=3, + null=True, + ), + ), + ( + 'contact_info', + models.CharField(blank=True, max_length=255, null=True), + ), ('privacy_disclaimer_accepted', models.BooleanField(default=False)), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ( + 'create_date', + models.DateTimeField(default=django.utils.timezone.now), + ), ('edit_date', models.DateTimeField(blank=True, null=True)), - ('core_groups', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='core.CoreGroup', verbose_name='User groups')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('organization', models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), - ('email_alert_flag', models.BooleanField(blank=True, default=False, null=True)), - ('email_preferences', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), - ('push_preferences', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), - ('user_timezone', models.CharField(blank=True, max_length=255, null=True)), - ], - options={ - 'ordering': ('first_name',), - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ( + 'core_groups', + models.ManyToManyField( + blank=True, + related_name='user_set', + related_query_name='user', + to='core.CoreGroup', + verbose_name='User groups', + ), + ), + ( + 'groups', + models.ManyToManyField( + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', + related_name='user_set', + related_query_name='user', + to='auth.Group', + verbose_name='groups', + ), + ), + ( + 'organization', + models.ForeignKey( + blank=True, + help_text='Related Org to associate with', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='core.Organization', + ), + ), + ( + 'user_permissions', + models.ManyToManyField( + blank=True, + help_text='Specific permissions for this user.', + related_name='user_set', + related_query_name='user', + to='auth.Permission', + verbose_name='user permissions', + ), + ), + ( + 'email_alert_flag', + models.BooleanField(blank=True, default=False, null=True), + ), + ( + 'email_preferences', + django.contrib.postgres.fields.jsonb.JSONField( + blank=True, null=True + ), + ), + ( + 'push_preferences', + django.contrib.postgres.fields.jsonb.JSONField( + blank=True, null=True + ), + ), + ( + 'user_timezone', + models.CharField(blank=True, max_length=255, null=True), + ), ], + options={'ordering': ('first_name',)}, + managers=[('objects', django.contrib.auth.models.UserManager())], ), migrations.CreateModel( name='OrganizationType', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, help_text='Organization type', max_length=255, verbose_name='Name')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'name', + models.CharField( + blank=True, + help_text='Organization type', + max_length=255, + verbose_name='Name', + ), + ), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), ], @@ -181,37 +575,69 @@ class Migration(migrations.Migration): migrations.AddField( model_name='organization', name='organization_type', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.OrganizationType'), + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='core.OrganizationType', + ), ), migrations.CreateModel( name='Consortium', fields=[ - ('consortium_uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Consortium UUID')), - ('name', models.CharField(blank=True, help_text='Multiple organizations form a consortium together', max_length=255, verbose_name='Consortium Name')), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ( + 'consortium_uuid', + models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + verbose_name='Consortium UUID', + ), + ), + ( + 'name', + models.CharField( + blank=True, + help_text='Multiple organizations form a consortium together', + max_length=255, + verbose_name='Consortium Name', + ), + ), + ( + 'create_date', + models.DateTimeField(default=django.utils.timezone.now), + ), ('edit_date', models.DateTimeField(blank=True, null=True)), - ('custodian_uuids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Custodian UUIDs'), blank=True, null=True, size=None)), + ( + 'custodian_uuids', + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name='Custodian UUIDs', + ), + blank=True, + null=True, + size=None, + ), + ), ], - options={ - 'verbose_name_plural': 'Consortiums', - 'ordering': ('name',), - }, - ), - migrations.RunPython(migrate_email_alert, - migrations.RunPython.noop, - ), - migrations.RemoveField( - model_name='coreuser', - name='email_alert_flag', - ), - migrations.RemoveField( - model_name='consortium', - name='custodian_uuids', + options={'verbose_name_plural': 'Consortiums', 'ordering': ('name',)}, ), + migrations.RunPython(migrate_email_alert, migrations.RunPython.noop), + migrations.RemoveField(model_name='coreuser', name='email_alert_flag'), + migrations.RemoveField(model_name='consortium', name='custodian_uuids'), migrations.AddField( model_name='consortium', name='organization_uuids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True, null=True, verbose_name='Organization UUIDs'), blank=True, null=True, size=None), + field=django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField( + blank=True, null=True, verbose_name='Organization UUIDs' + ), + blank=True, + null=True, + size=None, + ), ), migrations.AlterField( model_name='organization', diff --git a/core/models.py b/core/models.py index 99e3c6ab..382a04a4 100644 --- a/core/models.py +++ b/core/models.py @@ -61,7 +61,9 @@ def save(self, *args, **kwargs): class Industry(models.Model): name = models.CharField("Industry Name", max_length=255, blank=True, default="Tech") - description = models.TextField("Description/Notes", max_length=765, null=True, blank=True) + description = models.TextField( + "Description/Notes", max_length=765, null=True, blank=True + ) create_date = models.DateTimeField(null=True, blank=True) edit_date = models.DateTimeField(null=True, blank=True) @@ -78,6 +80,7 @@ def save(self, *args, **kwargs): def __str__(self): return self.name + class OrganizationType(models.Model): """ Allows organization to be of multiple types. @@ -89,7 +92,10 @@ class OrganizationType(models.Model): 5. Shipper 6. Warehouse """ - name = models.CharField("Name", max_length=255, blank=True, help_text="Organization type") + + name = models.CharField( + "Name", max_length=255, blank=True, help_text="Organization type" + ) create_date = models.DateTimeField(null=True, blank=True) edit_date = models.DateTimeField(null=True, blank=True) @@ -106,24 +112,59 @@ def save(self, *args, **kwargs): def __str__(self): return str(self.name) + class Organization(models.Model): """ The organization instance. There could be multiple organizations inside one application. When organization is created two CoreGroups are created automatically: Admins group and default Users group. """ - organization_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name='Organization UUID') - name = models.CharField("Organization Name", max_length=255, blank=True, help_text="Each end user must be grouped into an organization") - description = models.TextField("Description/Notes", max_length=765, null=True, blank=True, help_text="Description of organization") - organization_url = models.CharField(blank=True, null=True, max_length=255, help_text="Link to organizations external web site") - industries = models.ManyToManyField(Industry, blank=True, related_name='organizations', help_text="Type of Industry the organization belongs to if any") + + organization_uuid = models.UUIDField( + primary_key=True, default=uuid.uuid4, verbose_name='Organization UUID' + ) + name = models.CharField( + "Organization Name", + max_length=255, + blank=True, + help_text="Each end user must be grouped into an organization", + ) + description = models.TextField( + "Description/Notes", + max_length=765, + null=True, + blank=True, + help_text="Description of organization", + ) + organization_url = models.CharField( + blank=True, + null=True, + max_length=255, + help_text="Link to organizations external web site", + ) + industries = models.ManyToManyField( + Industry, + blank=True, + related_name='organizations', + help_text="Type of Industry the organization belongs to if any", + ) create_date = models.DateTimeField(null=True, blank=True) edit_date = models.DateTimeField(null=True, blank=True) - oauth_domains = ArrayField(models.CharField("OAuth Domains", max_length=255, null=True, blank=True), null=True, blank=True) - date_format = models.CharField("Date Format", max_length=50, blank=True, default="DD.MM.YYYY") + oauth_domains = ArrayField( + models.CharField("OAuth Domains", max_length=255, null=True, blank=True), + null=True, + blank=True, + ) + date_format = models.CharField( + "Date Format", max_length=50, blank=True, default="DD.MM.YYYY" + ) phone = models.CharField(max_length=20, blank=True, null=True) - allow_import_export = models.BooleanField('To allow import export functionality', default=False) - radius = models.FloatField(max_length=20, blank=True, null=True, default = 0.0) - organization_type = models.ForeignKey(OrganizationType,on_delete=models.CASCADE,null=True) + allow_import_export = models.BooleanField( + 'To allow import export functionality', default=False + ) + radius = models.FloatField(max_length=20, blank=True, null=True, default=0.0) + organization_type = models.ForeignKey( + OrganizationType, on_delete=models.CASCADE, null=True + ) class Meta: ordering = ('name',) @@ -146,7 +187,7 @@ def _create_initial_groups(self): organization=self, is_org_level=True, name='Admins', - permissions=PERMISSIONS_ORG_ADMIN + permissions=PERMISSIONS_ORG_ADMIN, ) CoreGroup.objects.create( @@ -154,7 +195,7 @@ def _create_initial_groups(self): is_org_level=True, is_default=True, name='Users', - permissions=PERMISSIONS_VIEW_ONLY + permissions=PERMISSIONS_VIEW_ONLY, ) @@ -165,13 +206,26 @@ class CoreGroup(models.Model): Permissions field is the decimal integer from 0 to 15 converted from 4-bit binary, each bit indicates permissions for CRUD. For example: 12 -> 1100 -> CR__ (allowed to Create and Read). """ - uuid = models.CharField('CoreGroup UUID', max_length=255, default=uuid.uuid4, unique=True) + + uuid = models.CharField( + 'CoreGroup UUID', max_length=255, default=uuid.uuid4, unique=True + ) name = models.CharField('Name of the role', max_length=80) - organization = models.ForeignKey(Organization, blank=True, null=True, on_delete=models.CASCADE, help_text='Related Org to associate with') + organization = models.ForeignKey( + Organization, + blank=True, + null=True, + on_delete=models.CASCADE, + help_text='Related Org to associate with', + ) is_global = models.BooleanField('Is global group', default=False) is_org_level = models.BooleanField('Is organization level group', default=False) is_default = models.BooleanField('Is organization default group', default=False) - permissions = models.PositiveSmallIntegerField('Permissions', default=PERMISSIONS_VIEW_ONLY, help_text='Decimal integer from 0 to 15 converted from 4-bit binary, each bit indicates permissions for CRUD') + permissions = models.PositiveSmallIntegerField( + 'Permissions', + default=PERMISSIONS_VIEW_ONLY, + help_text='Decimal integer from 0 to 15 converted from 4-bit binary, each bit indicates permissions for CRUD', + ) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) @@ -194,23 +248,34 @@ class CoreUser(AbstractUser): """ CoreUser is the registered user who belongs to some organization and can manage its projects. """ - TITLE_CHOICES = ( - ('mr', 'Mr.'), - ('mrs', 'Mrs.'), - ('ms', 'Ms.'), - ) - core_user_uuid = models.CharField(max_length=255, verbose_name='CoreUser UUID', default=uuid.uuid4, unique=True) + TITLE_CHOICES = (('mr', 'Mr.'), ('mrs', 'Mrs.'), ('ms', 'Ms.')) + + core_user_uuid = models.CharField( + max_length=255, verbose_name='CoreUser UUID', default=uuid.uuid4, unique=True + ) title = models.CharField(blank=True, null=True, max_length=3, choices=TITLE_CHOICES) contact_info = models.CharField(blank=True, null=True, max_length=255) - organization = models.ForeignKey(Organization, blank=True, null=True, on_delete=models.CASCADE, help_text='Related Org to associate with') - core_groups = models.ManyToManyField(CoreGroup, verbose_name='User groups', blank=True, related_name='user_set', related_query_name='user') + organization = models.ForeignKey( + Organization, + blank=True, + null=True, + on_delete=models.CASCADE, + help_text='Related Org to associate with', + ) + core_groups = models.ManyToManyField( + CoreGroup, + verbose_name='User groups', + blank=True, + related_name='user_set', + related_query_name='user', + ) privacy_disclaimer_accepted = models.BooleanField(default=False) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) email_preferences = JSONField(blank=True, null=True) push_preferences = JSONField(blank=True, null=True) - user_timezone = models.CharField(blank=True,null=True,max_length=255) + user_timezone = models.CharField(blank=True, null=True, max_length=255) REQUIRED_FIELDS = [] @@ -228,7 +293,11 @@ def save(self, *args, **kwargs): super(CoreUser, self).save() if is_new: # Add default groups - self.core_groups.add(*CoreGroup.objects.filter(organization=self.organization, is_default=True)) + self.core_groups.add( + *CoreGroup.objects.filter( + organization=self.organization, is_default=True + ) + ) @property def is_org_admin(self) -> bool: @@ -236,7 +305,9 @@ def is_org_admin(self) -> bool: Check if user has organization level admin permissions """ if not hasattr(self, '_is_org_admin'): - self._is_org_admin = self.core_groups.filter(permissions=PERMISSIONS_ORG_ADMIN, is_org_level=True).exists() + self._is_org_admin = self.core_groups.filter( + permissions=PERMISSIONS_ORG_ADMIN, is_org_level=True + ).exists() return self._is_org_admin @property @@ -247,7 +318,9 @@ def is_global_admin(self) -> bool: if self.is_superuser: return True if not hasattr(self, '_is_global_admin'): - self._is_global_admin = self.core_groups.filter(permissions=PERMISSIONS_ADMIN, is_global=True).exists() + self._is_global_admin = self.core_groups.filter( + permissions=PERMISSIONS_ADMIN, is_global=True + ).exists() return self._is_global_admin @@ -255,14 +328,24 @@ class EmailTemplate(models.Model): """ Stores e-mail templates specific to organization """ - organization = models.ForeignKey(Organization, on_delete=models.CASCADE, verbose_name='Organization', help_text='Related Org to associate with') + + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + verbose_name='Organization', + help_text='Related Org to associate with', + ) subject = models.CharField('Subject', max_length=255) type = models.PositiveSmallIntegerField('Type of template', choices=TEMPLATE_TYPES) - template = models.TextField("Reset password e-mail template (text)", null=True, blank=True) - template_html = models.TextField("Reset password e-mail template (HTML)", null=True, blank=True) + template = models.TextField( + "Reset password e-mail template (text)", null=True, blank=True + ) + template_html = models.TextField( + "Reset password e-mail template (HTML)", null=True, blank=True + ) class Meta: - unique_together = ('organization', 'type', ) + unique_together = ('organization', 'type') verbose_name = "Email Template" verbose_name_plural = "Email Templates" @@ -271,14 +354,27 @@ def __str__(self): class LogicModule(models.Model): - module_uuid = models.CharField(max_length=255, verbose_name='Logic Module UUID', default=uuid.uuid4, unique=True) + module_uuid = models.CharField( + max_length=255, + verbose_name='Logic Module UUID', + default=uuid.uuid4, + unique=True, + ) name = models.CharField("Logic Module Name", max_length=255, blank=True) - description = models.TextField("Description/Notes", max_length=765, null=True, blank=True) + description = models.TextField( + "Description/Notes", max_length=765, null=True, blank=True + ) endpoint = models.CharField(blank=True, null=True, max_length=255) endpoint_name = models.CharField(blank=True, null=True, max_length=255) docs_endpoint = models.CharField(blank=True, null=True, max_length=255) api_specification = JSONField(blank=True, null=True) - core_groups = models.ManyToManyField(CoreGroup, verbose_name='Logic Module groups', blank=True, related_name='logic_module_set', related_query_name='logic_module') + core_groups = models.ManyToManyField( + CoreGroup, + verbose_name='Logic Module groups', + blank=True, + related_name='logic_module_set', + related_query_name='logic_module', + ) create_date = models.DateTimeField(null=True, blank=True) edit_date = models.DateTimeField(null=True, blank=True) @@ -301,9 +397,21 @@ class Consortium(models.Model): """ The consortium instance. Allows sharing of data between 2 or more organizations """ - consortium_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name='Consortium UUID') - name = models.CharField("Consortium Name", max_length=255, blank=True, help_text="Multiple organizations form a consortium together") - organization_uuids = ArrayField(models.UUIDField("Organization UUIDs", max_length=255, null=True, blank=True), null=True, blank=True) + + consortium_uuid = models.UUIDField( + primary_key=True, default=uuid.uuid4, verbose_name='Consortium UUID' + ) + name = models.CharField( + "Consortium Name", + max_length=255, + blank=True, + help_text="Multiple organizations form a consortium together", + ) + organization_uuids = ArrayField( + models.UUIDField("Organization UUIDs", max_length=255, null=True, blank=True), + null=True, + blank=True, + ) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(null=True, blank=True) @@ -318,4 +426,4 @@ def save(self, *args, **kwargs): super(Consortium, self).save() def __str__(self): - return str(self.name) \ No newline at end of file + return str(self.name) diff --git a/core/permissions.py b/core/permissions.py index f703d999..ab720ffa 100644 --- a/core/permissions.py +++ b/core/permissions.py @@ -10,7 +10,9 @@ def merge_permissions(permissions1: str, permissions2: str) -> str: """ Merge two CRUD permissions string representations""" - return ''.join(map(str, [max(int(i), int(j)) for i, j in zip(permissions1, permissions2)])) + return ''.join( + map(str, [max(int(i), int(j)) for i, j in zip(permissions1, permissions2)]) + ) def has_permission(permissions_: str, method: str) -> bool: @@ -24,7 +26,6 @@ def has_permission(permissions_: str, method: str) -> bool: 'PATCH': 2, 'DELETE': 3, 'OPTIONS': 1, - # CRUD actions 'create': 0, 'list': 1, @@ -42,7 +43,6 @@ def has_permission(permissions_: str, method: str) -> bool: class IsSuperUserBrowseableAPI(permissions.BasePermission): - def has_permission(self, request, view): if request.user.is_authenticated: if view.__class__.__name__ == 'SchemaView': @@ -95,7 +95,9 @@ def has_permission(self, request, view): user_org = request.user.organization_id if 'organization' in request.data: - org_serializer = view.get_serializer_class()().get_fields()['organization'] + org_serializer = view.get_serializer_class()().get_fields()[ + 'organization' + ] primitive_value = request.data.get('organization') org = org_serializer.run_validation(primitive_value) return org.pk == user_org diff --git a/core/serializers.py b/core/serializers.py index 8266bfc7..f2f1e2be 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -16,8 +16,17 @@ from core.email_utils import send_email, send_email_body -from core.models import CoreUser, CoreGroup, EmailTemplate, LogicModule, Organization, PERMISSIONS_ORG_ADMIN, \ - TEMPLATE_RESET_PASSWORD, OrganizationType, Consortium +from core.models import ( + CoreUser, + CoreGroup, + EmailTemplate, + LogicModule, + Organization, + PERMISSIONS_ORG_ADMIN, + TEMPLATE_RESET_PASSWORD, + OrganizationType, + Consortium, +) class LogicModuleSerializer(serializers.ModelSerializer): @@ -35,6 +44,7 @@ class PermissionsField(serializers.DictField): For example: 9 -> '1001' (binary representation) -> `{'create': True, 'read': False, 'update': False, 'delete': True}` """ + _keys = ('create', 'read', 'update', 'delete') def __init__(self, *args, **kwargs): @@ -49,14 +59,15 @@ def to_internal_value(self, data): data = super().to_internal_value(data) keys = data.keys() if not set(keys) == set(self._keys): - raise serializers.ValidationError("Permissions field: incorrect keys format") + raise serializers.ValidationError( + "Permissions field: incorrect keys format" + ) permissions = ''.join([str(int(data[key])) for key in self._keys]) return int(permissions, 2) class UUIDPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): - def to_representation(self, value): return str(super().to_representation(value)) @@ -64,21 +75,33 @@ def to_representation(self, value): class CoreGroupSerializer(serializers.ModelSerializer): permissions = PermissionsField(required=False) - organization = UUIDPrimaryKeyRelatedField(required=False, - queryset=Organization.objects.all(), - help_text="Related Org to associate with") + organization = UUIDPrimaryKeyRelatedField( + required=False, + queryset=Organization.objects.all(), + help_text="Related Org to associate with", + ) class Meta: model = CoreGroup read_only_fields = ('uuid', 'workflowlevel1s', 'workflowlevel2s') - fields = ('id', 'uuid', 'name', 'is_global', 'is_org_level', 'permissions', 'organization', 'workflowlevel1s', - 'workflowlevel2s') + fields = ( + 'id', + 'uuid', + 'name', + 'is_global', + 'is_org_level', + 'permissions', + 'organization', + 'workflowlevel1s', + 'workflowlevel2s', + ) class CoreUserSerializer(serializers.ModelSerializer): """ Default CoreUser serializer """ + is_active = serializers.BooleanField(required=False) core_groups = CoreGroupSerializer(read_only=True, many=True) invitation_token = serializers.CharField(required=False) @@ -97,11 +120,25 @@ def validate_invitation_token(self, value): class Meta: model = CoreUser - fields = ('id', 'core_user_uuid', 'first_name', 'last_name', 'email', 'username', 'is_active', - 'title', 'contact_info', 'privacy_disclaimer_accepted', - 'organization', 'core_groups', 'invitation_token', 'email_preferences', - 'push_preferences', 'user_timezone') - read_only_fields = ('core_user_uuid', 'organization',) + fields = ( + 'id', + 'core_user_uuid', + 'first_name', + 'last_name', + 'email', + 'username', + 'is_active', + 'title', + 'contact_info', + 'privacy_disclaimer_accepted', + 'organization', + 'core_groups', + 'invitation_token', + 'email_preferences', + 'push_preferences', + 'user_timezone', + ) + read_only_fields = ('core_user_uuid', 'organization') depth = 1 @@ -109,9 +146,12 @@ class CoreUserWritableSerializer(CoreUserSerializer): """ Override default CoreUser serializer for writable actions (create, update, partial_update) """ + password = serializers.CharField(write_only=True) organization_name = serializers.CharField(source='organization.name') - core_groups = serializers.PrimaryKeyRelatedField(many=True, queryset=CoreGroup.objects.all(), required=False) + core_groups = serializers.PrimaryKeyRelatedField( + many=True, queryset=CoreGroup.objects.all(), required=False + ) class Meta: model = CoreUser @@ -131,39 +171,45 @@ def create(self, validated_data): # create core user invitation_token = validated_data.pop('invitation_token', None) validated_data['is_active'] = is_new_org or bool(invitation_token) - coreuser = CoreUser.objects.create( - organization=organization, - **validated_data - ) + coreuser = CoreUser.objects.create(organization=organization, **validated_data) # set user password coreuser.set_password(validated_data['password']) coreuser.save() # Triggers an approval email for newly registered user - approval_link = urljoin(settings.FRONTEND_URL, '/app/profile/users/current-users') + approval_link = urljoin( + settings.FRONTEND_URL, '/app/profile/users/current-users' + ) subject = 'Approval Request' template_name = 'email/coreuser/approval.txt' html_template_name = 'email/coreuser/approval.html' context = { - 'approval_link': approval_link, - 'coreuser_name': coreuser.first_name + ' ' + coreuser.last_name, - 'organization_name': organization + 'approval_link': approval_link, + 'coreuser_name': coreuser.first_name + ' ' + coreuser.last_name, + 'organization_name': organization, } if is_new_org: admin = CoreUser.objects.filter(is_superuser=True) # Global Admin else: - org_admin_groups = CoreGroup.objects.filter(permissions=PERMISSIONS_ORG_ADMIN, is_org_level=True) - admin = CoreUser.objects.filter(core_groups__in=org_admin_groups, - organization=organization) # Organization Admin + org_admin_groups = CoreGroup.objects.filter( + permissions=PERMISSIONS_ORG_ADMIN, is_org_level=True + ) + admin = CoreUser.objects.filter( + core_groups__in=org_admin_groups, organization=organization + ) # Organization Admin if admin: for users in admin: - send_email(users.email, subject, context, template_name, html_template_name) + send_email( + users.email, subject, context, template_name, html_template_name + ) # add org admin role to the user if org is new if is_new_org: - group_org_admin = CoreGroup.objects.get(organization=organization, - is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN) + group_org_admin = CoreGroup.objects.get( + organization=organization, + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + ) coreuser.core_groups.add(group_org_admin) # add requested groups to the user @@ -189,8 +235,17 @@ class CoreUserProfileSerializer(serializers.Serializer): class Meta: model = CoreUser - fields = ('first_name', 'last_name', 'password', 'title', - 'contact_info', 'organization_name', 'email_preferences', 'push_preferences', 'user_timezone') + fields = ( + 'first_name', + 'last_name', + 'password', + 'title', + 'contact_info', + 'organization_name', + 'email_preferences', + 'push_preferences', + 'user_timezone', + ) def update(self, instance, validated_data): @@ -204,10 +259,18 @@ def update(self, instance, validated_data): instance.first_name = validated_data.get('first_name', instance.first_name) instance.last_name = validated_data.get('last_name', instance.last_name) instance.title = validated_data.get('title', instance.title) - instance.contact_info = validated_data.get('contact_info', instance.contact_info) - instance.email_preferences = validated_data.get('email_preferences', instance.email_preferences) - instance.push_preferences = validated_data.get('push_preferences', instance.push_preferences) - instance.user_timezone = validated_data.get('user_timezone', instance.user_timezone) + instance.contact_info = validated_data.get( + 'contact_info', instance.contact_info + ) + instance.email_preferences = validated_data.get( + 'email_preferences', instance.email_preferences + ) + instance.push_preferences = validated_data.get( + 'push_preferences', instance.push_preferences + ) + instance.user_timezone = validated_data.get( + 'user_timezone', instance.user_timezone + ) password = validated_data.get('password', None) if password is not None: instance.set_password(password) @@ -217,17 +280,21 @@ def update(self, instance, validated_data): class CoreUserInvitationSerializer(serializers.Serializer): - emails = serializers.ListField(child=serializers.EmailField(), - min_length=1, max_length=10) + emails = serializers.ListField( + child=serializers.EmailField(), min_length=1, max_length=10 + ) class CoreUserResetPasswordSerializer(serializers.Serializer): """Serializer for reset password request data """ + email = serializers.EmailField() def save(self, **kwargs): - resetpass_url = urljoin(settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH) + resetpass_url = urljoin( + settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH + ) resetpass_url = resetpass_url + '{uid}/{token}/' email = self.validated_data["email"] @@ -242,14 +309,22 @@ def save(self, **kwargs): } # get specific subj and templates for user's organization - tpl = EmailTemplate.objects.filter(organization=user.organization, type=TEMPLATE_RESET_PASSWORD).first() + tpl = EmailTemplate.objects.filter( + organization=user.organization, type=TEMPLATE_RESET_PASSWORD + ).first() if not tpl: - tpl = EmailTemplate.objects.filter(organization__name=settings.DEFAULT_ORG, - type=TEMPLATE_RESET_PASSWORD).first() + tpl = EmailTemplate.objects.filter( + organization__name=settings.DEFAULT_ORG, + type=TEMPLATE_RESET_PASSWORD, + ).first() if tpl and tpl.template: context = Context(context) text_content = Template(tpl.template).render(context) - html_content = Template(tpl.template_html).render(context) if tpl.template_html else None + html_content = ( + Template(tpl.template_html).render(context) + if tpl.template_html + else None + ) count += send_email_body(email, tpl.subject, text_content, html_content) continue @@ -257,7 +332,9 @@ def save(self, **kwargs): subject = 'Reset your password' template_name = 'email/coreuser/password_reset.txt' html_template_name = 'email/coreuser/password_reset.html' - count += send_email(email, subject, context, template_name, html_template_name) + count += send_email( + email, subject, context, template_name, html_template_name + ) return count @@ -265,6 +342,7 @@ def save(self, **kwargs): class CoreUserResetPasswordCheckSerializer(serializers.Serializer): """Serializer for checking token for resetting password """ + uid = serializers.CharField() token = serializers.CharField() @@ -286,6 +364,7 @@ def validate(self, attrs): class CoreUserResetPasswordConfirmSerializer(CoreUserResetPasswordCheckSerializer): """Serializer for reset password data """ + new_password1 = serializers.CharField(max_length=128) new_password2 = serializers.CharField(max_length=128) @@ -338,8 +417,15 @@ class ApplicationSerializer(serializers.ModelSerializer): class Meta: model = Application - fields = ('id', 'authorization_grant_type', 'client_id', 'client_secret', 'client_type', 'name', - 'redirect_uris') + fields = ( + 'id', + 'authorization_grant_type', + 'client_id', + 'client_secret', + 'client_type', + 'name', + 'redirect_uris', + ) def create(self, validated_data): validated_data['client_id'] = secrets.token_urlsafe(75) @@ -351,6 +437,7 @@ class CoreUserEmailAlertSerializer(serializers.Serializer): """ Serializer for email alert of shipment """ + organization_uuid = serializers.UUIDField() messages = serializers.JSONField() diff --git a/core/swagger.py b/core/swagger.py index d3a07859..c7a4b664 100644 --- a/core/swagger.py +++ b/core/swagger.py @@ -3,43 +3,43 @@ TOKEN_QUERY_PARAM = Parameter('token', IN_QUERY, type='string', required=True) -DETAIL_RESPONSE = {200: Schema( - type='object', - properties={ - 'detail': Schema(type='string') - }) +DETAIL_RESPONSE = { + 200: Schema(type='object', properties={'detail': Schema(type='string')}) } -SUCCESS_RESPONSE = {200: Schema( - type='object', - properties={ - 'success': Schema(type='boolean') - }) +SUCCESS_RESPONSE = { + 200: Schema(type='object', properties={'success': Schema(type='boolean')}) } -COREUSER_INVITE_RESPONSE = {200: Schema( - type='object', - properties={ - 'detail': Schema(type='string'), - 'invitations': Schema(type='array', items=Schema(type='string')) - }) +COREUSER_INVITE_RESPONSE = { + 200: Schema( + type='object', + properties={ + 'detail': Schema(type='string'), + 'invitations': Schema(type='array', items=Schema(type='string')), + }, + ) } -COREUSER_INVITE_CHECK_RESPONSE = {200: Schema( - type='object', - properties={ - 'email': Schema(type='string'), - 'organization': Schema(type='object', properties={ - 'organization_uuid': Schema(type='string'), - 'name': Schema(type='string'), - }) - }) +COREUSER_INVITE_CHECK_RESPONSE = { + 200: Schema( + type='object', + properties={ + 'email': Schema(type='string'), + 'organization': Schema( + type='object', + properties={ + 'organization_uuid': Schema(type='string'), + 'name': Schema(type='string'), + }, + ), + }, + ) } -COREUSER_RESETPASS_RESPONSE = {200: Schema( - type='object', - properties={ - 'detail': Schema(type='string'), - 'count': Schema(type='number') - }) +COREUSER_RESETPASS_RESPONSE = { + 200: Schema( + type='object', + properties={'detail': Schema(type='string'), 'count': Schema(type='number')}, + ) } diff --git a/core/tests/fixtures.py b/core/tests/fixtures.py index aded5e9d..bf2b3bce 100644 --- a/core/tests/fixtures.py +++ b/core/tests/fixtures.py @@ -17,7 +17,7 @@ 'password': '123qwe', 'organization_uuid': uuid.uuid4(), # 'organization': settings.DEFAULT_ORG, # Tweaked this to support organization name from front end - 'organization_name': settings.DEFAULT_ORG + 'organization_name': settings.DEFAULT_ORG, } @@ -46,8 +46,12 @@ def core_group(org): @pytest.fixture def org_admin(org): - group_org_admin = factories.CoreGroup(name='Org Admin', organization=org, is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN) + group_org_admin = factories.CoreGroup( + name='Org Admin', + organization=org, + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + ) coreuser = factories.CoreUser.create(organization=group_org_admin.organization) coreuser.core_groups.add(group_org_admin) return coreuser @@ -91,20 +95,38 @@ def oauth_refresh_token(): @pytest.fixture() def logic_module(): - return factories.LogicModule.create(name='documents', - endpoint_name='documents', - endpoint='http://documentservice:8080') + return factories.LogicModule.create( + name='documents', + endpoint_name='documents', + endpoint='http://documentservice:8080', + ) @pytest.fixture() def datamesh(): - lm1 = factories.LogicModule.create(name='location', endpoint_name='location', - endpoint='http://locationservice:8080') - lm2 = factories.LogicModule.create(name='documents', endpoint_name='documents', - endpoint='http://documentservice:8080') - lmm1 = factories.LogicModuleModel(logic_module_endpoint_name=lm1.endpoint_name, model='SiteProfile', - endpoint='/siteprofiles/', lookup_field_name='uuid') - lmm2 = factories.LogicModuleModel(logic_module_endpoint_name=lm2.endpoint_name, model='Document', - endpoint='/documents/', lookup_field_name='id') - relationship = factories.Relationship(origin_model=lmm1, related_model=lmm2, key='documents') + lm1 = factories.LogicModule.create( + name='location', + endpoint_name='location', + endpoint='http://locationservice:8080', + ) + lm2 = factories.LogicModule.create( + name='documents', + endpoint_name='documents', + endpoint='http://documentservice:8080', + ) + lmm1 = factories.LogicModuleModel( + logic_module_endpoint_name=lm1.endpoint_name, + model='SiteProfile', + endpoint='/siteprofiles/', + lookup_field_name='uuid', + ) + lmm2 = factories.LogicModuleModel( + logic_module_endpoint_name=lm2.endpoint_name, + model='Document', + endpoint='/documents/', + lookup_field_name='id', + ) + relationship = factories.Relationship( + origin_model=lmm1, related_model=lmm2, key='documents' + ) return lm1, lm2, relationship diff --git a/core/tests/test_accesstokenview.py b/core/tests/test_accesstokenview.py index 542b27e7..e172a621 100644 --- a/core/tests/test_accesstokenview.py +++ b/core/tests/test_accesstokenview.py @@ -4,15 +4,22 @@ from oauth2_provider.models import AccessToken from rest_framework.reverse import reverse -from core.tests.fixtures import auth_api_client, auth_superuser_api_client, oauth_application, oauth_access_token, \ - superuser +from core.tests.fixtures import ( + auth_api_client, + auth_superuser_api_client, + oauth_application, + oauth_access_token, + superuser, +) @pytest.mark.django_db() class TestAccessTokenListView: ENDPOINT_BASE_URL = reverse('accesstoken-list') - def test_list_accesstoken_superuser(self, auth_superuser_api_client, oauth_access_token, superuser): + def test_list_accesstoken_superuser( + self, auth_superuser_api_client, oauth_access_token, superuser + ): """ Superusers are able to list all the objects """ @@ -32,7 +39,9 @@ def test_list_accesstoken_normaluser(self, auth_api_client, oauth_access_token): class TestAccessTokenCreateView: ENDPOINT_BASE_URL = reverse('accesstoken-list') - def test_create_accesstoken_superuser(self, auth_superuser_api_client, oauth_application, superuser): + def test_create_accesstoken_superuser( + self, auth_superuser_api_client, oauth_application, superuser + ): """ Nobody is able to create new access tokens """ @@ -41,7 +50,7 @@ def test_create_accesstoken_superuser(self, auth_superuser_api_client, oauth_app 'user': superuser, 'application': oauth_application, 'expires': datetime.datetime.utcnow() + datetime.timedelta(hours=1), - 'scope': 'read write' + 'scope': 'read write', } response = auth_superuser_api_client.post(self.ENDPOINT_BASE_URL, data) @@ -52,13 +61,17 @@ def test_create_accesstoken_superuser(self, auth_superuser_api_client, oauth_app class TestAccessTokenRetrieveViews: ENDPOINT_BASE_URL = reverse('accesstoken-list') - def test_retrieve_unexisting_accesstoken(self, auth_superuser_api_client, superuser): + def test_retrieve_unexisting_accesstoken( + self, auth_superuser_api_client, superuser + ): url = f'{self.ENDPOINT_BASE_URL}1111/' response = auth_superuser_api_client.get(url) assert response.status_code == 404 - def test_retrieve_accesstoken_superuser(self, auth_superuser_api_client, oauth_access_token, superuser): + def test_retrieve_accesstoken_superuser( + self, auth_superuser_api_client, oauth_access_token, superuser + ): """ Superusers are able to retrieve any access token """ @@ -82,15 +95,15 @@ def test_retrieve_accesstoken_normaluser(self, auth_api_client, oauth_access_tok class TestAccessTokenUpdateView: ENDPOINT_BASE_URL = reverse('accesstoken-list') - def test_update_accesstoken_superuser(self, auth_superuser_api_client, oauth_access_token, superuser): + def test_update_accesstoken_superuser( + self, auth_superuser_api_client, oauth_access_token, superuser + ): """ Nobody is able to update access tokens """ url = f'{self.ENDPOINT_BASE_URL}{oauth_access_token.pk}/' - data = { - 'token': secrets.token_urlsafe(8) - } + data = {'token': secrets.token_urlsafe(8)} response = auth_superuser_api_client.put(url, data) assert response.status_code == 405 @@ -105,7 +118,9 @@ def test_delete_unexisting_accesstoken(self, auth_superuser_api_client, superuse response = auth_superuser_api_client.delete(url) assert response.status_code == 404 - def test_delete_accesstoken_superuser(self, auth_superuser_api_client, oauth_access_token, superuser): + def test_delete_accesstoken_superuser( + self, auth_superuser_api_client, oauth_access_token, superuser + ): """ Superusers are able to delete any access token """ @@ -131,7 +146,9 @@ def test_delete_accesstoken_normaluser(self, auth_api_client, oauth_access_token class TestAccessTokenFilterView: ENDPOINT_BASE_URL = reverse('accesstoken-list') - def test_filter_accesstoken_by_user_username(self, auth_superuser_api_client, oauth_access_token, superuser): + def test_filter_accesstoken_by_user_username( + self, auth_superuser_api_client, oauth_access_token, superuser + ): """ Superusers can filter access tokens by users' username """ diff --git a/core/tests/test_applicationview.py b/core/tests/test_applicationview.py index 99d33e96..3e0698c7 100644 --- a/core/tests/test_applicationview.py +++ b/core/tests/test_applicationview.py @@ -4,14 +4,21 @@ from oauth2_provider.models import Application from rest_framework.reverse import reverse -from core.tests.fixtures import auth_api_client, auth_superuser_api_client, oauth_application, superuser +from core.tests.fixtures import ( + auth_api_client, + auth_superuser_api_client, + oauth_application, + superuser, +) @pytest.mark.django_db() class TestApplicationListView: ENDPOINT_BASE_URL = reverse('application-list') - def test_list_application_superuser(self, auth_superuser_api_client, oauth_application, superuser): + def test_list_application_superuser( + self, auth_superuser_api_client, oauth_application, superuser + ): """ Superusers are able to list all the objects """ @@ -63,13 +70,17 @@ def test_create_application_normaluser(self, auth_api_client): class TestApplicationRetrieveViews: ENDPOINT_BASE_URL = reverse('application-list') - def test_retrieve_unexisting_application(self, auth_superuser_api_client, superuser): + def test_retrieve_unexisting_application( + self, auth_superuser_api_client, superuser + ): url = f'{self.ENDPOINT_BASE_URL}1111/' response = auth_superuser_api_client.get(url) assert response.status_code == 404 - def test_retrieve_application_superuser(self, auth_superuser_api_client, oauth_application, superuser): + def test_retrieve_application_superuser( + self, auth_superuser_api_client, oauth_application, superuser + ): """ Superusers are able to retrieve any oauth application """ @@ -93,7 +104,9 @@ def test_retrieve_application_normaluser(self, auth_api_client, oauth_applicatio class TestApplicationUpdateView: ENDPOINT_BASE_URL = reverse('application-list') - def test_update_application_superuser(self, auth_superuser_api_client, oauth_application, superuser): + def test_update_application_superuser( + self, auth_superuser_api_client, oauth_application, superuser + ): """ Superuser is able to update oauth applications """ @@ -102,7 +115,7 @@ def test_update_application_superuser(self, auth_superuser_api_client, oauth_app data = { 'client_type': Application.CLIENT_PUBLIC, 'authorization_grant_type': Application.GRANT_PASSWORD, - 'name': Faker('name').generate() + 'name': Faker('name').generate(), } response = auth_superuser_api_client.put(url, data) assert response.status_code == 200 @@ -117,7 +130,7 @@ def test_update_application_normaluser(self, auth_api_client, oauth_application) data = { 'client_type': Application.CLIENT_PUBLIC, 'authorization_grant_type': Application.GRANT_PASSWORD, - 'name': Faker('name').generate() + 'name': Faker('name').generate(), } response = auth_api_client.put(url, data) assert response.status_code == 403 @@ -133,7 +146,9 @@ def test_delete_unexisting_application(self, auth_superuser_api_client, superuse response = auth_superuser_api_client.delete(url) assert response.status_code == 404 - def test_delete_application_superuser(self, auth_superuser_api_client, oauth_application, superuser): + def test_delete_application_superuser( + self, auth_superuser_api_client, oauth_application, superuser + ): """ Superusers are able to delete any oauth application """ diff --git a/core/tests/test_auth_pipeline.py b/core/tests/test_auth_pipeline.py index 418c6791..2ed7869c 100644 --- a/core/tests/test_auth_pipeline.py +++ b/core/tests/test_auth_pipeline.py @@ -15,6 +15,7 @@ class OAuthTest(TestCase): """ Test cases for OAuth Provider interface """ + # Fake classes for testing class BackendTest(object): def __init__(self): @@ -32,7 +33,7 @@ def setUp(self): logging.disable(logging.WARNING) self.core_user = factories.CoreUser() self.org = factories.Organization(organization_uuid='12345') - self.app = factories.Application(user=self.core_user, ) + self.app = factories.Application(user=self.core_user) def tearDown(self): logging.disable(logging.NOTSET) @@ -48,10 +49,12 @@ def test_authorization_success(self): self.core_user.save() # Get Authorization token - basic_token = base64.b64encode(f'{self.app.client_id}:{self.app.client_secret}'.encode('utf-8')).decode('utf-8') + basic_token = base64.b64encode( + f'{self.app.client_id}:{self.app.client_secret}'.encode('utf-8') + ).decode('utf-8') headers = { 'HTTP_USER_AGENT': 'Test/1.0', - 'HTTP_AUTHORIZATION': f'Basic {basic_token}' + 'HTTP_AUTHORIZATION': f'Basic {basic_token}', } c = APIClient() @@ -61,7 +64,12 @@ def test_authorization_success(self): data = f'username={self.core_user.username}&password=1234&grant_type=password' - response = c.post(authorize_url, data=data, content_type='application/x-www-form-urlencoded', headers=headers) + response = c.post( + authorize_url, + data=data, + content_type='application/x-www-form-urlencoded', + headers=headers, + ) self.assertEqual(response.status_code, 200) self.assertIn('access_token', response.json()) self.assertIn('access_token_jwt', response.json()) @@ -71,9 +79,13 @@ def test_authorization_success(self): def test_create_organization_new_default_org(self): Organization.objects.filter(name=settings.DEFAULT_ORG).delete() - coreuser = factories.CoreUser(first_name='John', last_name='Lennon', organization=None) + coreuser = factories.CoreUser( + first_name='John', last_name='Lennon', organization=None + ) - response = auth_pipeline.create_organization(core_user=coreuser, is_new_core_user=True) + response = auth_pipeline.create_organization( + core_user=coreuser, is_new_core_user=True + ) self.assertIn('is_new_org', response) self.assertTrue(response['is_new_org']) @@ -88,7 +100,9 @@ def test_create_organization_new_default_org(self): def test_create_organization_new_username_org(self): coreuser = factories.CoreUser(first_name='John', last_name='Lennon') - response = auth_pipeline.create_organization(core_user=coreuser, is_new_core_user=True) + response = auth_pipeline.create_organization( + core_user=coreuser, is_new_core_user=True + ) self.assertIn('is_new_org', response) self.assertTrue(response['is_new_org']) @@ -106,7 +120,9 @@ def test_create_organization_org_exists(self): coreuser.organization = org coreuser.save() - response = auth_pipeline.create_organization(core_user=coreuser, is_new_core_user=True) + response = auth_pipeline.create_organization( + core_user=coreuser, is_new_core_user=True + ) self.assertIn('is_new_org', response) self.assertFalse(response['is_new_org']) @@ -120,11 +136,15 @@ def test_create_organization_org_exists(self): def test_create_organization_no_new_coreuser(self): coreuser = factories.CoreUser(first_name='John', last_name='Lennon') - response = auth_pipeline.create_organization(core_user=coreuser, is_new_core_user=False) + response = auth_pipeline.create_organization( + core_user=coreuser, is_new_core_user=False + ) self.assertIsNone(response) def test_create_organization_no_coreuser(self): - response = auth_pipeline.create_organization(core_user=None, is_new_core_user=True) + response = auth_pipeline.create_organization( + core_user=None, is_new_core_user=True + ) self.assertIsNone(response) def test_create_organization_no_is_new_core_user(self): @@ -139,10 +159,13 @@ def test_auth_allowed_not_in_whitelist(self): details = {'email': self.core_user.email} response = auth_pipeline.auth_allowed(backend, details, None) template_content = response.content - self.assertIn(b"You don't appear to have permissions to access " - b"the system.", template_content) - self.assertIn(b"Please check with your organization to have access.", - template_content) + self.assertIn( + b"You don't appear to have permissions to access " b"the system.", + template_content, + ) + self.assertIn( + b"Please check with your organization to have access.", template_content + ) def test_auth_allowed_in_whitelisted_domains_conf(self): factories.Organization(name=settings.DEFAULT_ORG) @@ -157,27 +180,32 @@ def test_auth_allowed_in_whitelisted_domains_conf(self): def test_auth_allowed_multi_oauth_domain(self): self.org.oauth_domains = ['testenv.com'] self.org.save() - factories.Organization(name='Another Org', - oauth_domains=['testenv.com']) + factories.Organization(name='Another Org', oauth_domains=['testenv.com']) backend = self.BackendTest() details = {'email': self.core_user.email} response = auth_pipeline.auth_allowed(backend, details, None) template_content = response.content - self.assertIn(b"You don't appear to have permissions to access " - b"the system.", template_content) - self.assertIn(b"Please check with your organization to have access.", - template_content) + self.assertIn( + b"You don't appear to have permissions to access " b"the system.", + template_content, + ) + self.assertIn( + b"Please check with your organization to have access.", template_content + ) def test_auth_allowed_no_whitelist_oauth_domain(self): backend = self.BackendTest() details = {'email': self.core_user.email} response = auth_pipeline.auth_allowed(backend, details, None) template_content = response.content - self.assertIn(b"You don't appear to have permissions to access " - b"the system.", template_content) - self.assertIn(b"Please check with your organization to have access.", - template_content) + self.assertIn( + b"You don't appear to have permissions to access " b"the system.", + template_content, + ) + self.assertIn( + b"Please check with your organization to have access.", template_content + ) def test_auth_allowed_no_email(self): factories.Organization(name=settings.DEFAULT_ORG) @@ -185,10 +213,13 @@ def test_auth_allowed_no_email(self): details = {} response = auth_pipeline.auth_allowed(backend, details, None) template_content = response.content - self.assertIn(b"You don't appear to have permissions to access " - b"the system.", template_content) - self.assertIn(b"Please check with your organization to have access.", - template_content) + self.assertIn( + b"You don't appear to have permissions to access " b"the system.", + template_content, + ) + self.assertIn( + b"Please check with your organization to have access.", template_content + ) def test_create_organization(self): pass diff --git a/core/tests/test_coregroupview.py b/core/tests/test_coregroupview.py index c5ef168b..63910987 100644 --- a/core/tests/test_coregroupview.py +++ b/core/tests/test_coregroupview.py @@ -2,14 +2,19 @@ from rest_framework.reverse import reverse import factories -from core.tests.fixtures import org, org_admin, org_member, reset_password_request, TEST_USER_DATA +from core.tests.fixtures import ( + org, + org_admin, + org_member, + reset_password_request, + TEST_USER_DATA, +) from core.models import CoreGroup from core.views import CoreGroupViewSet @pytest.mark.django_db() class TestCoreGroupViewsPermissions: - def test_coregroup_views_permissions_unauth(self, request_factory): request = request_factory.get(reverse('coregroup-list')) response = CoreGroupViewSet.as_view({'get': 'list'})(request) @@ -58,13 +63,17 @@ def test_coregroup_views_permissions_org_member(self, request_factory, org_membe request = request_factory.patch(reverse('coregroup-detail', args=(1000,))) request.user = org_member - response = CoreGroupViewSet.as_view({'patch': 'partial_update'})(request, pk=1000) + response = CoreGroupViewSet.as_view({'patch': 'partial_update'})( + request, pk=1000 + ) assert response.status_code == 404 # it's allowed but wf1 is from different org request = request_factory.delete(reverse('coregroup-detail', args=(1000,))) request.user = org_member response = CoreGroupViewSet.as_view({'delete': 'destroy'})(request, pk=1000) - assert response.status_code == 404 # first checks if exists, then checks object permissions + assert ( + response.status_code == 404 + ) # first checks if exists, then checks object permissions def test_coregroup_views_permissions_org_admin(self, request_factory, org_admin): request = request_factory.get(reverse('coregroup-list')) @@ -72,8 +81,11 @@ def test_coregroup_views_permissions_org_admin(self, request_factory, org_admin) response = CoreGroupViewSet.as_view({'get': 'list'})(request) assert response.status_code == 200 - request = request_factory.post(reverse('coregroup-list'), {'organization': org_admin.organization.pk}, - format='json') + request = request_factory.post( + reverse('coregroup-list'), + {'organization': org_admin.organization.pk}, + format='json', + ) request.user = org_admin response = CoreGroupViewSet.as_view({'post': 'create'})(request) assert response.status_code == 400 @@ -101,10 +113,14 @@ def test_coregroup_views_permissions_org_admin(self, request_factory, org_admin) @pytest.mark.django_db() class TestCoreGroupCreateView: - - def test_coregroup_create_fail_missing_required_fields(self, request_factory, org_admin): - request = request_factory.post(reverse('coregroup-list'), {'organization': org_admin.organization.pk}, - format='json') + def test_coregroup_create_fail_missing_required_fields( + self, request_factory, org_admin + ): + request = request_factory.post( + reverse('coregroup-list'), + {'organization': org_admin.organization.pk}, + format='json', + ) request.user = org_admin response = CoreGroupViewSet.as_view({'post': 'create'})(request) assert response.status_code == 400 @@ -123,7 +139,7 @@ def test_coregroup_create_fail(self, request_factory, org_admin): data = { 'name': 'New Group', 'permissions': 9, - 'organization': org_admin.organization.pk + 'organization': org_admin.organization.pk, } request = request_factory.post(reverse('coregroup-list'), data, format='json') request.user = org_admin @@ -134,7 +150,7 @@ def test_coregroup_create_fail_again(self, request_factory, org_admin): data = { 'name': 'New Group', 'permissions': '1001', - 'organization': org_admin.organization.pk + 'organization': org_admin.organization.pk, } request = request_factory.post(reverse('coregroup-list'), data, format='json') request.user = org_admin @@ -144,8 +160,13 @@ def test_coregroup_create_fail_again(self, request_factory, org_admin): def test_coregroup_create(self, request_factory, org_admin): data = { 'name': 'New Group', - 'permissions': {'create': True, 'read': False, 'update': False, 'delete': True}, - 'organization': org_admin.organization.pk + 'permissions': { + 'create': True, + 'read': False, + 'update': False, + 'delete': True, + }, + 'organization': org_admin.organization.pk, } request = request_factory.post(reverse('coregroup-list'), data, format='json') request.user = org_admin @@ -160,7 +181,7 @@ def test_coregroup_create_int(self, request_factory, org_admin): data = { 'name': 'New Group', 'permissions': {'create': 1, 'read': 0, 'update': 0, 'delete': 1}, - 'organization': org_admin.organization.pk + 'organization': org_admin.organization.pk, } request = request_factory.post(reverse('coregroup-list'), data, format='json') request.user = org_admin @@ -173,29 +194,38 @@ def test_coregroup_create_int(self, request_factory, org_admin): @pytest.mark.django_db() class TestCoreGroupUpdateView: - def test_coregroup_update_fail(self, request_factory, org_admin): - coregroup = factories.CoreGroup.create(name='Program Admin', organization=org_admin.organization) + coregroup = factories.CoreGroup.create( + name='Program Admin', organization=org_admin.organization + ) - data = { - 'name': 'Admin of something else', - 'permissions': 9, - } + data = {'name': 'Admin of something else', 'permissions': 9} - request = request_factory.put(reverse('coregroup-detail', args=(coregroup.pk,)), data, format='json') + request = request_factory.put( + reverse('coregroup-detail', args=(coregroup.pk,)), data, format='json' + ) request.user = org_admin response = CoreGroupViewSet.as_view({'put': 'update'})(request, pk=coregroup.pk) assert response.status_code == 400 def test_coregroup_update(self, request_factory, org_admin): - coregroup = factories.CoreGroup.create(name='Program Admin', organization=org_admin.organization) + coregroup = factories.CoreGroup.create( + name='Program Admin', organization=org_admin.organization + ) data = { 'name': 'Admin of something else', - 'permissions': {'create': True, 'read': False, 'update': False, 'delete': True}, + 'permissions': { + 'create': True, + 'read': False, + 'update': False, + 'delete': True, + }, } - request = request_factory.put(reverse('coregroup-detail', args=(coregroup.pk,)), data, format='json') + request = request_factory.put( + reverse('coregroup-detail', args=(coregroup.pk,)), data, format='json' + ) request.user = org_admin response = CoreGroupViewSet.as_view({'put': 'update'})(request, pk=coregroup.pk) assert response.status_code == 200 @@ -206,7 +236,6 @@ def test_coregroup_update(self, request_factory, org_admin): @pytest.mark.django_db() class TestCoreGroupListView: - def test_coregroup_list(self, request_factory, org_admin): factories.CoreGroup.create(name='Group 1', organization=org_admin.organization) factories.CoreGroup.create(name='Group 2', organization=org_admin.organization) @@ -230,7 +259,9 @@ def test_coregroup_list_another_org(self, request_factory, org_admin): def test_coregroup_list_global(self, request_factory, org_admin): factories.CoreGroup.create(name='Group 1', organization=org_admin.organization) - factories.CoreGroup.create(name='Group Global', organization=None, is_global=True) + factories.CoreGroup.create( + name='Group Global', organization=None, is_global=True + ) request = request_factory.get(reverse('coregroup-list')) request.user = org_admin @@ -241,13 +272,14 @@ def test_coregroup_list_global(self, request_factory, org_admin): @pytest.mark.django_db() class TestCoreGroupDetailView: - def test_coregroup_detail(self, request_factory, org_admin): coregroup = factories.CoreGroup.create(organization=org_admin.organization) request = request_factory.get(reverse('coregroup-detail', args=(coregroup.pk,))) request.user = org_admin - response = CoreGroupViewSet.as_view({'get': 'retrieve'})(request, pk=coregroup.pk) + response = CoreGroupViewSet.as_view({'get': 'retrieve'})( + request, pk=coregroup.pk + ) assert response.status_code == 200 def test_coregroup_detail_another_org(self, request_factory, org_admin): @@ -256,19 +288,24 @@ def test_coregroup_detail_another_org(self, request_factory, org_admin): request = request_factory.get(reverse('coregroup-detail', args=(coregroup.pk,))) request.user = org_admin - response = CoreGroupViewSet.as_view({'get': 'retrieve'})(request, pk=coregroup.pk) + response = CoreGroupViewSet.as_view({'get': 'retrieve'})( + request, pk=coregroup.pk + ) assert response.status_code == 403 @pytest.mark.django_db() class TestCoreGroupDeleteView: - def test_coregroup_delete(self, request_factory, org_admin): coregroup = factories.CoreGroup.create(organization=org_admin.organization) - request = request_factory.delete(reverse('coregroup-detail', args=(coregroup.pk,))) + request = request_factory.delete( + reverse('coregroup-detail', args=(coregroup.pk,)) + ) request.user = org_admin - response = CoreGroupViewSet.as_view({'delete': 'destroy'})(request, pk=coregroup.pk) + response = CoreGroupViewSet.as_view({'delete': 'destroy'})( + request, pk=coregroup.pk + ) assert response.status_code == 204 with pytest.raises(CoreGroup.DoesNotExist): diff --git a/core/tests/test_coreuserview.py b/core/tests/test_coreuserview.py index 27039869..a3bde114 100644 --- a/core/tests/test_coreuserview.py +++ b/core/tests/test_coreuserview.py @@ -14,7 +14,13 @@ from core.models import CoreUser, EmailTemplate, TEMPLATE_RESET_PASSWORD from core.views import CoreUserViewSet from core.jwt_utils import create_invitation_token -from core.tests.fixtures import TEST_USER_DATA, org_admin, org_member, org, reset_password_request +from core.tests.fixtures import ( + TEST_USER_DATA, + org_admin, + org_member, + org, + reset_password_request, +) @pytest.mark.django_db() @@ -51,8 +57,7 @@ def test_coreuser_views_permissions_unauth(request_factory): # has no permission request = request_factory.patch(reverse('coreuser-detail', args=(1,))) - response = CoreUserViewSet.as_view({'patch': 'partial_update'})(request, - pk=1) + response = CoreUserViewSet.as_view({'patch': 'partial_update'})(request, pk=1) assert response.status_code == 403 @@ -87,14 +92,12 @@ def test_coreuser_views_permissions_org_member(request_factory, org_member): # has no permission request = request_factory.patch(reverse('coreuser-detail', args=(1,))) request.user = org_member - response = CoreUserViewSet.as_view({'patch': 'partial_update'})(request, - pk=1) + response = CoreUserViewSet.as_view({'patch': 'partial_update'})(request, pk=1) assert response.status_code == 403 @pytest.mark.django_db() class TestCoreUserCreate: - def test_registration_fail(self, request_factory): # check that 'password' and 'organization name' fields are required for field_name in ['password', 'organization_name']: @@ -155,7 +158,9 @@ def test_registration_of_invited_org_user(self, request_factory, org_admin): def test_reused_token_invalidation(self, request_factory, org_admin): data = TEST_USER_DATA.copy() - registered_user = factories.CoreUser.create(is_active=False, email=data['email'], username='user_org') + registered_user = factories.CoreUser.create( + is_active=False, email=data['email'], username='user_org' + ) token = create_invitation_token(data['email'], org_admin.organization) data['invitation_token'] = token @@ -174,7 +179,9 @@ def test_email_mismatch_token_invalidation(self, request_factory, org_admin): def test_registration_with_core_groups(self, request_factory, org_admin): data = TEST_USER_DATA.copy() - groups = factories.CoreGroup.create_batch(2, organization=org_admin.organization) + groups = factories.CoreGroup.create_batch( + 2, organization=org_admin.organization + ) data['core_groups'] = [item.pk for item in groups] request = request_factory.post(reverse('coreuser-list'), data) response = CoreUserViewSet.as_view({'post': 'create'})(request) @@ -187,14 +194,13 @@ def test_registration_with_core_groups(self, request_factory, org_admin): @pytest.mark.django_db() class TestCoreUserUpdate: - def test_coreuser_update(self, request_factory, org_admin): - user = factories.CoreUser.create(is_active=False, organization=org_admin.organization, username='org_user') + user = factories.CoreUser.create( + is_active=False, organization=org_admin.organization, username='org_user' + ) pk = user.pk - data = { - 'is_active': True, - } + data = {'is_active': True} request = request_factory.patch(reverse('coreuser-detail', args=(pk,)), data) request.user = org_admin response = CoreUserViewSet.as_view({'patch': 'partial_update'})(request, pk=pk) @@ -204,27 +210,29 @@ def test_coreuser_update(self, request_factory, org_admin): def test_coreuser_update_dif_org(self, request_factory, org_admin): dif_org = factories.Organization(name='Another Org') - user = factories.CoreUser.create(is_active=False, organization=dif_org, username='another_org_user') + user = factories.CoreUser.create( + is_active=False, organization=dif_org, username='another_org_user' + ) pk = user.pk - data = { - 'is_active': True, - } + data = {'is_active': True} request = request_factory.patch(reverse('coreuser-detail', args=(pk,)), data) request.user = org_admin response = CoreUserViewSet.as_view({'patch': 'partial_update'})(request, pk=pk) assert response.status_code == 403 def test_coreuser_update_groups(self, request_factory, org_admin): - user = factories.CoreUser.create(is_active=False, organization=org_admin.organization, username='user_org') - initial_groups = factories.CoreGroup.create_batch(2, organization=user.organization) + user = factories.CoreUser.create( + is_active=False, organization=org_admin.organization, username='user_org' + ) + initial_groups = factories.CoreGroup.create_batch( + 2, organization=user.organization + ) user.core_groups.add(*initial_groups) pk = user.pk new_groups = factories.CoreGroup.create_batch(2, organization=user.organization) - data = { - 'core_groups': [item.pk for item in new_groups], - } + data = {'core_groups': [item.pk for item in new_groups]} request = request_factory.patch(reverse('coreuser-detail', args=(pk,)), data) request.user = org_admin response = CoreUserViewSet.as_view({'patch': 'partial_update'})(request, pk=pk) @@ -235,7 +243,6 @@ def test_coreuser_update_groups(self, request_factory, org_admin): @pytest.mark.django_db() class TestCoreUserInvite: - def test_invitation(self, request_factory, org_admin): data = {'emails': [TEST_USER_DATA['email']]} request = request_factory.post(reverse('coreuser-invite'), data) @@ -246,28 +253,39 @@ def test_invitation(self, request_factory, org_admin): def test_invitation_check(self, request_factory, org): token = create_invitation_token(TEST_USER_DATA['email'], org) - request = request_factory.get(reverse('coreuser-invite-check'), {'token': token}) + request = request_factory.get( + reverse('coreuser-invite-check'), {'token': token} + ) response = CoreUserViewSet.as_view({'get': 'invite_check'})(request) assert response.status_code == 200 assert response.data['email'] == TEST_USER_DATA['email'] - assert response.data['organization']['organization_uuid'] == org.organization_uuid + assert ( + response.data['organization']['organization_uuid'] == org.organization_uuid + ) def test_prevent_token_reuse(self, request_factory, org): token = create_invitation_token(TEST_USER_DATA['email'], org) - registered_user = factories.CoreUser.create(is_active=False, email=TEST_USER_DATA['email'], username='user_org') - request = request_factory.get(reverse('coreuser-invite-check'), {'token': token}) + registered_user = factories.CoreUser.create( + is_active=False, email=TEST_USER_DATA['email'], username='user_org' + ) + request = request_factory.get( + reverse('coreuser-invite-check'), {'token': token} + ) response = CoreUserViewSet.as_view({'get': 'invite_check'})(request) assert response.status_code == 401 @pytest.mark.django_db() class TestResetPassword(object): - - def test_reset_password_using_default_emailtemplate(self, request_factory, org_member): + def test_reset_password_using_default_emailtemplate( + self, request_factory, org_member + ): email = org_member.email assert list(org_member.organization.emailtemplate_set.all()) == [] # assert list(Organization.objects.filter(name=settings.DEFAULT_ORG)) == [] -- Removed this assertion to support organization name here - request = request_factory.post(reverse('coreuser-reset-password'), {'email': email}) + request = request_factory.post( + reverse('coreuser-reset-password'), {'email': email} + ) response = CoreUserViewSet.as_view({'post': 'reset_password'})(request) assert response.status_code == 200 assert response.data['count'] == 1 @@ -276,7 +294,9 @@ def test_reset_password_using_default_emailtemplate(self, request_factory, org_m message = mail.outbox[0] assert message.to == [email] - resetpass_url = urljoin(settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH) + resetpass_url = urljoin( + settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH + ) uid = urlsafe_base64_encode(force_bytes(org_member.pk)) token = default_token_generator.make_token(org_member) assert f'{resetpass_url}{uid}/{token}/' in message.body @@ -292,10 +312,12 @@ def test_reset_password_using_org_template(self, request_factory, org_member): template=""" Custom template {{ password_reset_link }} - """ + """, ) - request = request_factory.post(reverse('coreuser-reset-password'), {'email': email}) + request = request_factory.post( + reverse('coreuser-reset-password'), {'email': email} + ) response = CoreUserViewSet.as_view({'post': 'reset_password'})(request) assert response.status_code == 200 assert response.data['count'] == 1 @@ -304,14 +326,18 @@ def test_reset_password_using_org_template(self, request_factory, org_member): message = mail.outbox[0] assert message.to == [email] - resetpass_url = urljoin(settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH) + resetpass_url = urljoin( + settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH + ) uid = urlsafe_base64_encode(force_bytes(org_member.pk)) token = default_token_generator.make_token(org_member) assert message.subject == 'Custom subject' assert 'Custom template' in message.body assert f'{resetpass_url}{uid}/{token}/' in message.body - def test_reset_password_using_default_org_template(self, request_factory, org_member): + def test_reset_password_using_default_org_template( + self, request_factory, org_member + ): email = org_member.email default_organization = factories.Organization(name=settings.DEFAULT_ORG) @@ -323,10 +349,12 @@ def test_reset_password_using_default_org_template(self, request_factory, org_me template=""" Custom template {{ password_reset_link }} - """ + """, ) - request = request_factory.post(reverse('coreuser-reset-password'), {'email': email}) + request = request_factory.post( + reverse('coreuser-reset-password'), {'email': email} + ) response = CoreUserViewSet.as_view({'post': 'reset_password'})(request) assert response.status_code == 200 assert response.data['count'] == 1 @@ -335,7 +363,9 @@ def test_reset_password_using_default_org_template(self, request_factory, org_me message = mail.outbox[0] assert message.to == [email] - resetpass_url = urljoin(settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH) + resetpass_url = urljoin( + settings.FRONTEND_URL, settings.RESETPASS_CONFIRM_URL_PATH + ) uid = urlsafe_base64_encode(force_bytes(org_member.pk)) token = default_token_generator.make_token(org_member) assert message.subject == 'Custom subject' @@ -343,32 +373,39 @@ def test_reset_password_using_default_org_template(self, request_factory, org_me assert f'{resetpass_url}{uid}/{token}/' in message.body def test_reset_password_no_user(self, request_factory): - request = request_factory.post(reverse('coreuser-reset-password'), {'email': 'foo@example.com'}) + request = request_factory.post( + reverse('coreuser-reset-password'), {'email': 'foo@example.com'} + ) response = CoreUserViewSet.as_view({'post': 'reset_password'})(request) assert response.status_code == 200 assert response.data['count'] == 0 def test_reset_password_check(self, request_factory, reset_password_request): user, uid, token = reset_password_request - data = { - 'uid': uid, - 'token': token, - } + data = {'uid': uid, 'token': token} request = request_factory.post(reverse('coreuser-reset-password-check'), data) response = CoreUserViewSet.as_view({'post': 'reset_password_check'})(request) assert response.status_code == 200 assert response.data['success'] is True - def test_reset_password_check_expired(self, request_factory, reset_password_request): + def test_reset_password_check_expired( + self, request_factory, reset_password_request + ): user, uid, token = reset_password_request - data = { - 'uid': uid, - 'token': token, - } - mock_date = date.today() + timedelta(int(settings.PASSWORD_RESET_TIMEOUT_DAYS) + 1) - with mock.patch('django.contrib.auth.tokens.PasswordResetTokenGenerator._today', return_value=mock_date): - request = request_factory.post(reverse('coreuser-reset-password-check'), data) - response = CoreUserViewSet.as_view({'post': 'reset_password_check'})(request) + data = {'uid': uid, 'token': token} + mock_date = date.today() + timedelta( + int(settings.PASSWORD_RESET_TIMEOUT_DAYS) + 1 + ) + with mock.patch( + 'django.contrib.auth.tokens.PasswordResetTokenGenerator._today', + return_value=mock_date, + ): + request = request_factory.post( + reverse('coreuser-reset-password-check'), data + ) + response = CoreUserViewSet.as_view({'post': 'reset_password_check'})( + request + ) assert response.status_code == 200 assert response.data['success'] is False @@ -389,7 +426,9 @@ def test_reset_password_confirm(self, request_factory, reset_password_request): updated_user = CoreUser.objects.get(pk=user.pk) assert updated_user.check_password(test_password) - def test_reset_password_confirm_diff_passwords(self, request_factory, reset_password_request): + def test_reset_password_confirm_diff_passwords( + self, request_factory, reset_password_request + ): test_password1 = '5UU74e7nfU' test_password2 = '5UU74e7nfUa' user, uid, token = reset_password_request @@ -401,9 +440,13 @@ def test_reset_password_confirm_diff_passwords(self, request_factory, reset_pass } request = request_factory.post(reverse('coreuser-reset-password-confirm'), data) response = CoreUserViewSet.as_view({'post': 'reset_password_confirm'})(request) - assert response.status_code == 400 # validation error (password fields didn't match) + assert ( + response.status_code == 400 + ) # validation error (password fields didn't match) - def test_reset_password_confirm_token_expired(self, request_factory, reset_password_request): + def test_reset_password_confirm_token_expired( + self, request_factory, reset_password_request + ): test_password = '5UU74e7nfU' user, uid, token = reset_password_request data = { @@ -412,23 +455,53 @@ def test_reset_password_confirm_token_expired(self, request_factory, reset_passw 'uid': uid, 'token': token, } - mock_date = date.today() + timedelta(int(settings.PASSWORD_RESET_TIMEOUT_DAYS) + 1) - with mock.patch('django.contrib.auth.tokens.PasswordResetTokenGenerator._today', return_value=mock_date): - request = request_factory.post(reverse('coreuser-reset-password-confirm'), data) - response = CoreUserViewSet.as_view({'post': 'reset_password_confirm'})(request) - assert response.status_code == 400 # validation error (the token is expired) + mock_date = date.today() + timedelta( + int(settings.PASSWORD_RESET_TIMEOUT_DAYS) + 1 + ) + with mock.patch( + 'django.contrib.auth.tokens.PasswordResetTokenGenerator._today', + return_value=mock_date, + ): + request = request_factory.post( + reverse('coreuser-reset-password-confirm'), data + ) + response = CoreUserViewSet.as_view({'post': 'reset_password_confirm'})( + request + ) + assert ( + response.status_code == 400 + ) # validation error (the token is expired) @pytest.mark.django_db() class TestCoreUserRead(object): - keys = {'id', 'core_user_uuid', 'first_name', 'last_name', 'email', 'username', 'is_active', 'title', - 'contact_info','privacy_disclaimer_accepted', 'organization', 'core_groups', 'email_preferences', 'push_preferences', 'user_timezone'} + keys = { + 'id', + 'core_user_uuid', + 'first_name', + 'last_name', + 'email', + 'username', + 'is_active', + 'title', + 'contact_info', + 'privacy_disclaimer_accepted', + 'organization', + 'core_groups', + 'email_preferences', + 'push_preferences', + 'user_timezone', + } def test_coreuser_list(self, request_factory, org_member): - factories.CoreUser.create(organization=org_member.organization, username='another_user') # 2nd user of the org - factories.CoreUser.create(organization=factories.Organization(name='another otg'), - username='yet_another_user') # user of the different org + factories.CoreUser.create( + organization=org_member.organization, username='another_user' + ) # 2nd user of the org + factories.CoreUser.create( + organization=factories.Organization(name='another otg'), + username='yet_another_user', + ) # user of the different org request = request_factory.get(reverse('coreuser-list')) request.user = org_member response = CoreUserViewSet.as_view({'get': 'list'})(request) @@ -438,11 +511,15 @@ def test_coreuser_list(self, request_factory, org_member): assert set(data[0].keys()) == self.keys def test_coreuser_retrieve(self, request_factory, org_member): - core_user = factories.CoreUser.create(organization=org_member.organization, username='another_user') + core_user = factories.CoreUser.create( + organization=org_member.organization, username='another_user' + ) request = request_factory.get(reverse('coreuser-detail', args=(core_user.pk,))) request.user = org_member - response = CoreUserViewSet.as_view({'get': 'retrieve'})(request, pk=core_user.pk) + response = CoreUserViewSet.as_view({'get': 'retrieve'})( + request, pk=core_user.pk + ) assert response.status_code == 200 assert set(response.data.keys()) == self.keys diff --git a/core/tests/test_emailtemplates.py b/core/tests/test_emailtemplates.py index f10ff1a5..3110ce23 100644 --- a/core/tests/test_emailtemplates.py +++ b/core/tests/test_emailtemplates.py @@ -5,9 +5,9 @@ from core.models import EmailTemplate, TEMPLATE_RESET_PASSWORD from core.tests.fixtures import org + @pytest.mark.django_db() class TestEmailTemplateModel: - def test_create(self, org): tpl = EmailTemplate( organization=org, @@ -16,18 +16,19 @@ def test_create(self, org): template=""" Custom template {{ password_reset_link }} - """ + """, ) tpl.save() - updated = EmailTemplate.objects.get(organization=org, type=TEMPLATE_RESET_PASSWORD) + updated = EmailTemplate.objects.get( + organization=org, type=TEMPLATE_RESET_PASSWORD + ) assert updated.subject == tpl.subject assert updated.template == tpl.template def test_create_fail_without_subj(self, org): tpl = EmailTemplate.objects.create( - organization=org, - type=TEMPLATE_RESET_PASSWORD + organization=org, type=TEMPLATE_RESET_PASSWORD ) with pytest.raises(ValidationError): tpl.full_clean() @@ -41,7 +42,7 @@ def test_create_fail_unique_constraint(self, org): template=""" Custom template {{ password_reset_link }} - """ + """, ) tpl.save() tpl = EmailTemplate( @@ -51,7 +52,7 @@ def test_create_fail_unique_constraint(self, org): template=""" Another custom template {{ password_reset_link }} - """ + """, ) with pytest.raises(IntegrityError): tpl.save() @@ -64,7 +65,7 @@ def test_update(self, org): template=""" Custom template {{ password_reset_link }} - """ + """, ) tpl.subject = 'Updated custom subject' @@ -74,6 +75,8 @@ def test_update(self, org): """ tpl.save() - updated = EmailTemplate.objects.get(organization=org, type=TEMPLATE_RESET_PASSWORD) + updated = EmailTemplate.objects.get( + organization=org, type=TEMPLATE_RESET_PASSWORD + ) assert updated.subject == tpl.subject assert updated.template == tpl.template diff --git a/core/tests/test_jwt_utils.py b/core/tests/test_jwt_utils.py index 4135b20c..0a86b104 100644 --- a/core/tests/test_jwt_utils.py +++ b/core/tests/test_jwt_utils.py @@ -5,7 +5,11 @@ from django.utils import timezone import factories -from oauth2_provider.models import get_application_model, get_access_token_model, get_refresh_token_model +from oauth2_provider.models import ( + get_application_model, + get_access_token_model, + get_refresh_token_model, +) from core.jwt_utils import payload_enricher from core.models import ROLE_ORGANIZATION_ADMIN @@ -17,7 +21,6 @@ class JWTUtilsTest(TestCase): - def setUp(self) -> None: self.rf = RequestFactory() self.core_user = factories.CoreUser() @@ -31,16 +34,17 @@ def setUp(self) -> None: ) application.save() access_token = AccessToken.objects.create( - user=self.core_user, token="1234567890", + user=self.core_user, + token="1234567890", application=application, expires=timezone.now() + datetime.timedelta(days=1), - scope="read write" + scope="read write", ) RefreshToken.objects.create( access_token=access_token, user=self.core_user, application=application, - token="007" + token="007", ) def test_jwt_payload_enricher(self): diff --git a/core/tests/test_logicmoduleview.py b/core/tests/test_logicmoduleview.py index c2984e07..685e2074 100644 --- a/core/tests/test_logicmoduleview.py +++ b/core/tests/test_logicmoduleview.py @@ -13,7 +13,6 @@ class LogicModuleViewsPermissionTest(TestCase): - def setUp(self): self.client = APIClient() self.core_user = factories.CoreUser() @@ -24,7 +23,7 @@ def setUp(self): 'id': 1, 'workflowlevel2_uuid': 1, 'name': 'test', - 'contact_uuid': 1 + 'contact_uuid': 1, } def make_logicmodule_request(self): @@ -50,8 +49,9 @@ def make_logicmodule_request_superuser(self): @pytest.mark.django_db() class TestLogicModuleUpdate: - - def test_logic_module_update_api_specification(self, request_factory, superuser, logic_module, monkeypatch): + def test_logic_module_update_api_specification( + self, request_factory, superuser, logic_module, monkeypatch + ): mocked_url = Mock(return_value='example.com') test_swagger = {'name': 'example'} mocked_swagger = Mock() @@ -60,7 +60,9 @@ def test_logic_module_update_api_specification(self, request_factory, superuser, monkeypatch.setattr(utils, 'get_swagger_url_by_logic_module', mocked_url) monkeypatch.setattr(utils, 'get_swagger_from_url', mocked_swagger) - request = request_factory.put(reverse('logicmodule-update-api-specification', args=(logic_module.pk,))) + request = request_factory.put( + reverse('logicmodule-update-api-specification', args=(logic_module.pk,)) + ) request.user = superuser view = LogicModuleViewSet.as_view({'put': 'update_api_specification'}) response = view(request, pk=logic_module.pk) diff --git a/core/tests/test_organizationview.py b/core/tests/test_organizationview.py index f5357d8f..a6e51aa1 100644 --- a/core/tests/test_organizationview.py +++ b/core/tests/test_organizationview.py @@ -34,4 +34,4 @@ def test_list_organization_names(self): view = OrganizationViewSet.as_view({'get': 'list'}) response = view(self.request) - self.assertEqual(response.status_code, 200) \ No newline at end of file + self.assertEqual(response.status_code, 200) diff --git a/core/tests/test_permissions.py b/core/tests/test_permissions.py index b8b27884..aa56f723 100644 --- a/core/tests/test_permissions.py +++ b/core/tests/test_permissions.py @@ -2,7 +2,6 @@ class TestMergePermissions: - def test_merge_permissions(self): """ Merge no access to view only permission will result in view only @@ -12,7 +11,6 @@ def test_merge_permissions(self): class TestHasPermission: - def test_has_permission_success(self): result = has_permission('0100', 'GET') assert result diff --git a/core/tests/test_refreshtokenview.py b/core/tests/test_refreshtokenview.py index 78d26526..2a25a282 100644 --- a/core/tests/test_refreshtokenview.py +++ b/core/tests/test_refreshtokenview.py @@ -3,15 +3,23 @@ from oauth2_provider.models import RefreshToken from rest_framework.reverse import reverse -from core.tests.fixtures import auth_api_client, auth_superuser_api_client, oauth_application, oauth_access_token, \ - oauth_refresh_token, superuser +from core.tests.fixtures import ( + auth_api_client, + auth_superuser_api_client, + oauth_application, + oauth_access_token, + oauth_refresh_token, + superuser, +) @pytest.mark.django_db() class TestRefreshTokenListView: ENDPOINT_BASE_URL = reverse('refreshtoken-list') - def test_list_refreshtoken_superuser(self, auth_superuser_api_client, oauth_refresh_token, superuser): + def test_list_refreshtoken_superuser( + self, auth_superuser_api_client, oauth_refresh_token, superuser + ): """ Superusers are able to list all the objects """ @@ -31,8 +39,13 @@ def test_list_refreshtoken_normaluser(self, auth_api_client, oauth_refresh_token class TestRefreshTokenCreateView: ENDPOINT_BASE_URL = reverse('refreshtoken-list') - def test_create_refreshtoken_superuser(self, auth_superuser_api_client, oauth_application, oauth_access_token, - superuser): + def test_create_refreshtoken_superuser( + self, + auth_superuser_api_client, + oauth_application, + oauth_access_token, + superuser, + ): """ Nobody is able to create new access tokens """ @@ -40,7 +53,7 @@ def test_create_refreshtoken_superuser(self, auth_superuser_api_client, oauth_ap 'token': secrets.token_urlsafe(8), 'user': superuser, 'application': oauth_application, - 'access_token': oauth_access_token + 'access_token': oauth_access_token, } response = auth_superuser_api_client.post(self.ENDPOINT_BASE_URL, data) @@ -51,13 +64,17 @@ def test_create_refreshtoken_superuser(self, auth_superuser_api_client, oauth_ap class TestRefreshTokenRetrieveViews: ENDPOINT_BASE_URL = reverse('refreshtoken-list') - def test_retrieve_unexisting_refreshtoken(self, auth_superuser_api_client, superuser): + def test_retrieve_unexisting_refreshtoken( + self, auth_superuser_api_client, superuser + ): url = f'{self.ENDPOINT_BASE_URL}1111/' response = auth_superuser_api_client.get(url) assert response.status_code == 404 - def test_retrieve_refreshtoken_superuser(self, auth_superuser_api_client, oauth_refresh_token, superuser): + def test_retrieve_refreshtoken_superuser( + self, auth_superuser_api_client, oauth_refresh_token, superuser + ): """ Superusers are able to retrieve any access token """ @@ -67,7 +84,9 @@ def test_retrieve_refreshtoken_superuser(self, auth_superuser_api_client, oauth_ assert response.status_code == 200 assert response.data['token'] == oauth_refresh_token.token - def test_retrieve_refreshtoken_normaluser(self, auth_api_client, oauth_refresh_token): + def test_retrieve_refreshtoken_normaluser( + self, auth_api_client, oauth_refresh_token + ): """ Normal users are not able to retrieve any access token """ @@ -81,15 +100,15 @@ def test_retrieve_refreshtoken_normaluser(self, auth_api_client, oauth_refresh_t class TestRefreshTokenUpdateView: ENDPOINT_BASE_URL = reverse('refreshtoken-list') - def test_update_refreshtoken_superuser(self, auth_superuser_api_client, oauth_refresh_token, superuser): + def test_update_refreshtoken_superuser( + self, auth_superuser_api_client, oauth_refresh_token, superuser + ): """ Nobody is able to update access tokens """ url = f'{self.ENDPOINT_BASE_URL}{oauth_refresh_token.pk}/' - data = { - 'token': secrets.token_urlsafe(8) - } + data = {'token': secrets.token_urlsafe(8)} response = auth_superuser_api_client.put(url, data) assert response.status_code == 405 @@ -104,7 +123,9 @@ def test_delete_unexisting_refreshtoken(self, auth_superuser_api_client, superus response = auth_superuser_api_client.delete(url) assert response.status_code == 404 - def test_delete_refreshtoken_superuser(self, auth_superuser_api_client, oauth_refresh_token, superuser): + def test_delete_refreshtoken_superuser( + self, auth_superuser_api_client, oauth_refresh_token, superuser + ): """ Superusers are able to delete any access token """ @@ -130,7 +151,9 @@ def test_delete_refreshtoken_normaluser(self, auth_api_client, oauth_refresh_tok class TestRefreshTokenFilterView: ENDPOINT_BASE_URL = reverse('refreshtoken-list') - def test_filter_refreshtoken_by_user_username(self, auth_superuser_api_client, oauth_refresh_token, superuser): + def test_filter_refreshtoken_by_user_username( + self, auth_superuser_api_client, oauth_refresh_token, superuser + ): """ Superusers can filter access tokens by users' username """ diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index 338eadb4..97dfab13 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -1,8 +1,13 @@ import pytest -from core.serializers import OrganizationSerializer, CoreGroupSerializer, CoreUserSerializer +from core.serializers import ( + OrganizationSerializer, + CoreGroupSerializer, + CoreUserSerializer, +) from core.tests.fixtures import core_group, org, org_member + @pytest.mark.django_db() def test_org_serializer(request_factory, org): request = request_factory.get('') @@ -23,7 +28,7 @@ def test_org_serializer(request_factory, org): 'industries', 'allow_import_export', 'radius', - 'organization_type' + 'organization_type', ] assert set(data.keys()) == set(keys) @@ -33,16 +38,17 @@ def test_core_groups_serializer(request_factory, core_group): request = request_factory.get('') serializer = CoreGroupSerializer(core_group, context={'request': request}) data = serializer.data - keys = ['id', - 'uuid', - 'name', - 'is_global', - 'is_org_level', - 'permissions', - 'organization', - 'workflowlevel1s', - 'workflowlevel2s', - ] + keys = [ + 'id', + 'uuid', + 'name', + 'is_global', + 'is_org_level', + 'permissions', + 'organization', + 'workflowlevel1s', + 'workflowlevel2s', + ] assert set(data.keys()) == set(keys) assert isinstance(data['organization'], str) @@ -52,19 +58,22 @@ def test_core_user_serializer(request_factory, org_member): request = request_factory.get('') serializer = CoreUserSerializer(org_member, context={'request': request}) data = serializer.data - keys = ['id', - 'core_user_uuid', - 'first_name', - 'last_name', - 'email', - 'username', - 'is_active', - 'title', - 'contact_info', - 'privacy_disclaimer_accepted', - 'organization', - 'core_groups', - 'email_preferences', 'push_preferences', 'user_timezone', - ] + keys = [ + 'id', + 'core_user_uuid', + 'first_name', + 'last_name', + 'email', + 'username', + 'is_active', + 'title', + 'contact_info', + 'privacy_disclaimer_accepted', + 'organization', + 'core_groups', + 'email_preferences', + 'push_preferences', + 'user_timezone', + ] assert set(data.keys()) == set(keys) assert isinstance(data['organization'], dict) diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py index 161b61ad..440e7786 100644 --- a/core/tests/test_utils.py +++ b/core/tests/test_utils.py @@ -25,19 +25,16 @@ def core_user(org): @pytest.fixture def application(org): return factories.Application.create( - client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET + client_id=settings.OAUTH_CLIENT_ID, client_secret=settings.OAUTH_CLIENT_SECRET ) + # ------------ Tests ------------------ class TestGenerateTokens(object): - @pytest.mark.django_db() - def test_success(self, wsgi_request_factory, core_user, - application, monkeypatch): - + def test_success(self, wsgi_request_factory, core_user, application, monkeypatch): def mock_create_token(*args, **kwargs): return { 'access_token': 'bZr9TVYykJnbVL1gAjq4Xhn3x1SY91', @@ -63,7 +60,7 @@ def mock_encode_jwt(*args, **kwargs): 'token_type': 'Bearer', 'scope': 'read write', 'refresh_token': 'UDJsQxZpjVxhuOgrCMLHnc79NI5ZpU', - 'access_token_jwt': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9' + 'access_token_jwt': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9', } # remove jwt enricher @@ -81,9 +78,9 @@ def mock_encode_jwt(*args, **kwargs): assert result == final_token @pytest.mark.django_db() - def test_success_without_mocking_token_creation(self, wsgi_request_factory, core_user, - application, monkeypatch): - + def test_success_without_mocking_token_creation( + self, wsgi_request_factory, core_user, application, monkeypatch + ): def mock_generate_payload(*args, **kwargs): return { 'iss': 'BuildlyTest', @@ -114,8 +111,9 @@ def mock_encode_jwt(*args, **kwargs): assert refresh_token.token == result['refresh_token'] @pytest.mark.django_db() - def test_without_encode_mock_success(self, wsgi_request_factory, core_user, - application, monkeypatch): + def test_without_encode_mock_success( + self, wsgi_request_factory, core_user, application, monkeypatch + ): def mock_create_token(*args, **kwargs): return { 'access_token': 'bZr9TVYykJnbVL1gAjq4Xhn3x1SY91', @@ -148,8 +146,9 @@ def mock_generate_payload(*args, **kwargs): utils.generate_access_tokens(request, core_user) @pytest.mark.django_db() - def test_no_func_mocks_success(self, wsgi_request_factory, core_user, - application, monkeypatch): + def test_no_func_mocks_success( + self, wsgi_request_factory, core_user, application, monkeypatch + ): # remove jwt enricher monkeypatch.delattr('django.conf.settings.JWT_PAYLOAD_ENRICHER') diff --git a/core/tests/test_views.py b/core/tests/test_views.py index 05024840..d24c826e 100644 --- a/core/tests/test_views.py +++ b/core/tests/test_views.py @@ -32,6 +32,7 @@ def core_user(org): # ------------ Tests ------------------ + class IndexViewTest(TestCase): def setUp(self): self.factory = RequestFactory() @@ -54,7 +55,6 @@ def test_health_check_success(self): class TestOAuthComplete(object): - def test_no_code_fail(self, client): oauth_url = reverse('oauth_complete', args=('github',)) expected_data = {'detail': 'Authorization code has to be provided.'} @@ -66,8 +66,9 @@ def test_no_code_fail(self, client): assert data == expected_data @pytest.mark.django_db() - def test_is_authenticated_success(self, wsgi_request_factory, core_user, monkeypatch): - + def test_is_authenticated_success( + self, wsgi_request_factory, core_user, monkeypatch + ): def mock_auth_complete(*args, **kwargs): return core_user @@ -77,15 +78,14 @@ def mock_auth_complete(*args, **kwargs): 'token_type': 'Bearer', 'scope': 'read write', 'refresh_token': 'UDJsQxZpjVxhuOgrCMLHnc79NI5ZpU', - 'access_token_jwt': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9' + 'access_token_jwt': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9', } # mock functions in order as in the code monkeypatch.setattr(web, 'user_is_authenticated', lambda x: True) monkeypatch.setattr(web, 'partial_pipeline_data', lambda x, y: None) monkeypatch.setattr(oauth.BaseOAuth2, 'auth_complete', mock_auth_complete) - monkeypatch.setattr(web, 'generate_access_tokens', - lambda x, y: tokens) + monkeypatch.setattr(web, 'generate_access_tokens', lambda x, y: tokens) # mock request object request = wsgi_request_factory() @@ -100,7 +100,6 @@ def mock_auth_complete(*args, **kwargs): @pytest.mark.django_db() def test_user_success(self, wsgi_request_factory, core_user, monkeypatch): - def mock_auth_complete(*args, **kwargs): return core_user @@ -110,7 +109,7 @@ def mock_auth_complete(*args, **kwargs): 'token_type': 'Bearer', 'scope': 'read write', 'refresh_token': 'UDJsQxZpjVxhuOgrCMLHnc79NI5ZpU', - 'access_token_jwt': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9' + 'access_token_jwt': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9', } # mock functions in order as in the code @@ -154,7 +153,6 @@ def mock_auth_complete(*args, **kwargs): assert response.url == settings.LOGIN_URL def test_no_auth_no_user(self, wsgi_request_factory, monkeypatch): - def mock_auth_complete(*args, **kwargs): return None @@ -199,7 +197,9 @@ def test_without_code_xfail(self, wsgi_request_factory): web.oauth_complete(request, 'github') @pytest.mark.xfail(raises=SocialAuthNotConfigured) - def test_no_social_auth_redirect_url_xfail(self, settings, wsgi_request_factory, monkeypatch): + def test_no_social_auth_redirect_url_xfail( + self, settings, wsgi_request_factory, monkeypatch + ): settings.SOCIAL_AUTH_LOGIN_REDIRECT_URLS['github'] = None # mock functions @@ -215,7 +215,9 @@ def test_no_social_auth_redirect_url_xfail(self, settings, wsgi_request_factory, web.oauth_complete(request, 'github') @pytest.mark.xfail(raises=SocialAuthNotConfigured) - def test_no_support_social_auth_backend_xfail(self, settings, wsgi_request_factory, monkeypatch): + def test_no_support_social_auth_backend_xfail( + self, settings, wsgi_request_factory, monkeypatch + ): del settings.SOCIAL_AUTH_LOGIN_REDIRECT_URLS['github'] # mock functions diff --git a/core/urls.py b/core/urls.py index 91083f1b..f13aa9de 100644 --- a/core/urls.py +++ b/core/urls.py @@ -29,10 +29,13 @@ path('datamesh/', include('datamesh.urls')), path('', include('gateway.urls')), path('', include('workflow.urls')), - # Auth backend URL's - path('oauth/', include('oauth2_provider_jwt.urls', namespace='oauth2_provider_jwt')), - re_path(r'^oauth/complete/(?P[^/]+)/$', oauth_complete, name='oauth_complete'), + path( + 'oauth/', include('oauth2_provider_jwt.urls', namespace='oauth2_provider_jwt') + ), + re_path( + r'^oauth/complete/(?P[^/]+)/$', oauth_complete, name='oauth_complete' + ), ] urlpatterns += staticfiles_urlpatterns() + router.urls diff --git a/core/utils.py b/core/utils.py index 5a2661ba..b650020f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -20,8 +20,7 @@ def generate_access_tokens(request: WSGIRequest, user: User): request.user = user request.grant_type = '' request.client = Application.objects.get( - client_id=settings.OAUTH_CLIENT_ID, - client_secret=settings.OAUTH_CLIENT_SECRET + client_id=settings.OAUTH_CLIENT_ID, client_secret=settings.OAUTH_CLIENT_SECRET ) token = bearer_token.create_token(request, refresh_token=True) bearer_token.request_validator.save_bearer_token(token, request) diff --git a/core/views/__init__.py b/core/views/__init__.py index fbe302de..8b234c77 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -3,4 +3,4 @@ from .oauth import AccessTokenViewSet, ApplicationViewSet, RefreshTokenViewSet # noqa from .organization import OrganizationViewSet, OrganizationTypeViewSet # noqa from .consortium import ConsortiumViewSet # noqa -from .logicmodule import LogicModuleViewSet # noqa \ No newline at end of file +from .logicmodule import LogicModuleViewSet # noqa diff --git a/core/views/consortium.py b/core/views/consortium.py index 9ca2cc18..902f3909 100644 --- a/core/views/consortium.py +++ b/core/views/consortium.py @@ -5,6 +5,7 @@ from core.models import Consortium from core.serializers import ConsortiumSerializer from core.permissions import IsSuperUser, AllowAuthenticatedRead + logger = logging.getLogger(__name__) @@ -35,6 +36,7 @@ class ConsortiumViewSet(viewsets.ModelViewSet): delete: Delete a Consortium instance. """ + permission_classes_by_action = {'list': [AllowAuthenticatedRead]} def list(self, request): @@ -49,7 +51,10 @@ def list(self, request): def get_permissions(self): try: # return permission_classes depending on `action` - return [permission() for permission in self.permission_classes_by_action[self.action]] + return [ + permission() + for permission in self.permission_classes_by_action[self.action] + ] except KeyError: # action is not set return default permission_classes return [permission() for permission in self.permission_classes] diff --git a/core/views/coregroup.py b/core/views/coregroup.py index 59bc2738..fe33612d 100644 --- a/core/views/coregroup.py +++ b/core/views/coregroup.py @@ -12,6 +12,7 @@ class CoreGroupViewSet(viewsets.ModelViewSet): It's used for creating groups of Core Users inside an organization and defining model level permissions for this group """ + queryset = CoreGroup.objects.all() serializer_class = CoreGroupSerializer permission_classes = (IsOrgMember,) diff --git a/core/views/coreuser.py b/core/views/coreuser.py index 064ccb3d..f19960e6 100644 --- a/core/views/coreuser.py +++ b/core/views/coreuser.py @@ -10,26 +10,43 @@ import jwt from drf_yasg.utils import swagger_auto_schema from core.models import CoreUser, Organization -from core.serializers import (CoreUserSerializer, CoreUserWritableSerializer, CoreUserInvitationSerializer, - CoreUserResetPasswordSerializer, CoreUserResetPasswordCheckSerializer, - CoreUserResetPasswordConfirmSerializer, CoreUserEmailAlertSerializer, - CoreUserProfileSerializer) +from core.serializers import ( + CoreUserSerializer, + CoreUserWritableSerializer, + CoreUserInvitationSerializer, + CoreUserResetPasswordSerializer, + CoreUserResetPasswordCheckSerializer, + CoreUserResetPasswordConfirmSerializer, + CoreUserEmailAlertSerializer, + CoreUserProfileSerializer, +) from core.permissions import AllowAuthenticatedRead, AllowOnlyOrgAdmin, IsOrgMember -from core.swagger import (COREUSER_INVITE_RESPONSE, COREUSER_INVITE_CHECK_RESPONSE, COREUSER_RESETPASS_RESPONSE, - DETAIL_RESPONSE, SUCCESS_RESPONSE, TOKEN_QUERY_PARAM) +from core.swagger import ( + COREUSER_INVITE_RESPONSE, + COREUSER_INVITE_CHECK_RESPONSE, + COREUSER_RESETPASS_RESPONSE, + DETAIL_RESPONSE, + SUCCESS_RESPONSE, + TOKEN_QUERY_PARAM, +) from core.jwt_utils import create_invitation_token from core.email_utils import send_email import logging + # from datetime import datetime # from dateutil import tz # from twilio.rest import Client logger = logging.getLogger(__name__) -class CoreUserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, - mixins.CreateModelMixin, mixins.UpdateModelMixin, - viewsets.GenericViewSet): +class CoreUserViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, +): """ A core user is an extension of the default User object. A core user is also the primary relationship for identity and access to a logged in user. They are associated with an organization, Group (for permission though WorkflowTeam) @@ -73,7 +90,8 @@ def list(self, request, *args, **kwargs): organization_id = request.user.organization_id queryset = queryset.filter(organization_id=organization_id) serializer = self.get_serializer( - instance=queryset, context={'request': request}, many=True) + instance=queryset, context={'request': request}, many=True + ) return Response(serializer.data) def retrieve(self, request, *args, **kwargs): @@ -91,9 +109,11 @@ def me(self, request, *args, **kwargs): serializer = self.get_serializer(instance=user, context={'request': request}) return Response(serializer.data) - @swagger_auto_schema(methods=['post'], - request_body=CoreUserInvitationSerializer, - responses=COREUSER_INVITE_RESPONSE) + @swagger_auto_schema( + methods=['post'], + request_body=CoreUserInvitationSerializer, + responses=COREUSER_INVITE_RESPONSE, + ) @action(methods=['POST'], detail=False) def invite(self, request, *args, **kwargs): """ @@ -108,15 +128,15 @@ def invite(self, request, *args, **kwargs): links = self.perform_invite(serializer) return Response( - { - 'detail': 'The invitations were sent successfully.', - 'invitations': links, - }, - status=status.HTTP_200_OK) - - @swagger_auto_schema(methods=['get'], - responses=COREUSER_INVITE_CHECK_RESPONSE, - manual_parameters=[TOKEN_QUERY_PARAM]) + {'detail': 'The invitations were sent successfully.', 'invitations': links}, + status=status.HTTP_200_OK, + ) + + @swagger_auto_schema( + methods=['get'], + responses=COREUSER_INVITE_CHECK_RESPONSE, + manual_parameters=[TOKEN_QUERY_PARAM], + ) @action(methods=['GET'], detail=False) def invite_check(self, request, *args, **kwargs): """ @@ -126,43 +146,50 @@ def invite_check(self, request, *args, **kwargs): try: token = self.request.query_params['token'] except KeyError: - return Response({'detail': 'No token is provided.'}, - status.HTTP_401_UNAUTHORIZED) + return Response( + {'detail': 'No token is provided.'}, status.HTTP_401_UNAUTHORIZED + ) try: - decoded = jwt.decode(token, settings.SECRET_KEY, - algorithms='HS256') + decoded = jwt.decode(token, settings.SECRET_KEY, algorithms='HS256') except jwt.DecodeError: - return Response({'detail': 'Token is not valid.'}, - status.HTTP_401_UNAUTHORIZED) + return Response( + {'detail': 'Token is not valid.'}, status.HTTP_401_UNAUTHORIZED + ) except jwt.ExpiredSignatureError: - return Response({'detail': 'Token is expired.'}, - status.HTTP_401_UNAUTHORIZED) + return Response( + {'detail': 'Token is expired.'}, status.HTTP_401_UNAUTHORIZED + ) if CoreUser.objects.filter(email=decoded['email']).exists(): - return Response({'detail': 'Token has been used.'}, - status.HTTP_401_UNAUTHORIZED) - - organization = Organization.objects\ - .values('organization_uuid', 'name')\ - .get(organization_uuid=decoded['org_uuid']) \ - if decoded['org_uuid'] else None + return Response( + {'detail': 'Token has been used.'}, status.HTTP_401_UNAUTHORIZED + ) + + organization = ( + Organization.objects.values('organization_uuid', 'name').get( + organization_uuid=decoded['org_uuid'] + ) + if decoded['org_uuid'] + else None + ) - return Response({ - 'email': decoded['email'], - 'organization': organization - }, status=status.HTTP_200_OK) + return Response( + {'email': decoded['email'], 'organization': organization}, + status=status.HTTP_200_OK, + ) @transaction.atomic def perform_invite(self, serializer): - reg_location = urljoin(settings.FRONTEND_URL, - settings.REGISTRATION_URL_PATH) + reg_location = urljoin(settings.FRONTEND_URL, settings.REGISTRATION_URL_PATH) reg_location = reg_location + '?token={}' email_addresses = serializer.validated_data.get('emails') user = self.request.user organization = user.organization - registered_emails = CoreUser.objects.filter(email__in=email_addresses).values_list('email', flat=True) + registered_emails = CoreUser.objects.filter( + email__in=email_addresses + ).values_list('email', flat=True) links = [] for email_address in email_addresses: @@ -180,21 +207,23 @@ def perform_invite(self, serializer): # create the used context for the E-mail templates context = { 'invitation_link': invitation_link, - 'org_admin_name': user.name - if hasattr(user, 'coreuser') else '', - 'organization_name': organization.name - if organization else '' + 'org_admin_name': user.name if hasattr(user, 'coreuser') else '', + 'organization_name': organization.name if organization else '', } subject = 'Application Access' # TODO we need to make this dynamic template_name = 'email/coreuser/invitation.txt' html_template_name = 'email/coreuser/invitation.html' - send_email(email_address, subject, context, template_name, html_template_name) + send_email( + email_address, subject, context, template_name, html_template_name + ) return links - @swagger_auto_schema(methods=['post'], - request_body=CoreUserResetPasswordSerializer, - responses=COREUSER_RESETPASS_RESPONSE) + @swagger_auto_schema( + methods=['post'], + request_body=CoreUserResetPasswordSerializer, + responses=COREUSER_RESETPASS_RESPONSE, + ) @action(methods=['POST'], detail=False) def reset_password(self, request, *args, **kwargs): """ @@ -210,26 +239,27 @@ def reset_password(self, request, *args, **kwargs): 'detail': 'The reset password link was sent successfully.', 'count': count, }, - status=status.HTTP_200_OK) - - @swagger_auto_schema(methods=['post'], - request_body=CoreUserResetPasswordCheckSerializer, - responses=SUCCESS_RESPONSE) + status=status.HTTP_200_OK, + ) + + @swagger_auto_schema( + methods=['post'], + request_body=CoreUserResetPasswordCheckSerializer, + responses=SUCCESS_RESPONSE, + ) @action(methods=['POST'], detail=False) def reset_password_check(self, request, *args, **kwargs): """ This endpoint is used to check that token is valid. """ serializer = self.get_serializer(data=request.data) - return Response( - { - 'success': serializer.is_valid(), - }, - status=status.HTTP_200_OK) + return Response({'success': serializer.is_valid()}, status=status.HTTP_200_OK) - @swagger_auto_schema(methods=['post'], - request_body=CoreUserResetPasswordConfirmSerializer, - responses=DETAIL_RESPONSE) + @swagger_auto_schema( + methods=['post'], + request_body=CoreUserResetPasswordConfirmSerializer, + responses=DETAIL_RESPONSE, + ) @action(methods=['POST'], detail=False) def reset_password_confirm(self, request, *args, **kwargs): """ @@ -239,10 +269,9 @@ def reset_password_confirm(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) serializer.save() return Response( - { - 'detail': 'The password was changed successfully.', - }, - status=status.HTTP_200_OK) + {'detail': 'The password was changed successfully.'}, + status=status.HTTP_200_OK, + ) def get_serializer_class(self): action_ = getattr(self, 'action', 'default') @@ -251,12 +280,14 @@ def get_serializer_class(self): def get_permissions(self): if hasattr(self, 'action'): # different permissions when creating a new user or resetting password - if self.action in ['create', - 'reset_password', - 'reset_password_check', - 'reset_password_confirm', - 'invite_check', - 'update_profile']: + if self.action in [ + 'create', + 'reset_password', + 'reset_password_check', + 'reset_password_confirm', + 'invite_check', + 'update_profile', + ]: return [permissions.AllowAny()] if self.action in ['update', 'partial_update', 'invite']: @@ -271,9 +302,11 @@ def get_permissions(self): queryset = CoreUser.objects.all() permission_classes = (AllowAuthenticatedRead,) - @swagger_auto_schema(methods=['post'], - request_body=CoreUserEmailAlertSerializer, - responses=SUCCESS_RESPONSE) + @swagger_auto_schema( + methods=['post'], + request_body=CoreUserEmailAlertSerializer, + responses=SUCCESS_RESPONSE, + ) @action(methods=['POST'], detail=False) def alert(self, request, *args, **kwargs): """ @@ -295,35 +328,46 @@ def alert(self, request, *args, **kwargs): # message['date_time'] = time_tuple.replace(tzinfo=tz.gettz('UTC')) subject = '{} Alert'.format(message['parameter'].capitalize()) if message.get('shipment_id'): - message['shipment_url'] = urljoin(settings.FRONTEND_URL, - '/app/shipment/edit/:'+str(message['shipment_id'])) + message['shipment_url'] = urljoin( + settings.FRONTEND_URL, + '/app/shipment/edit/:' + str(message['shipment_id']), + ) else: message['shipment_url'] = None message['color'] = color_codes.get(message['severity']) - context = { - 'message': message, - } + context = {'message': message} template_name = 'email/coreuser/shipment_alert.txt' html_template_name = 'email/coreuser/shipment_alert.html' # TODO send email via preferences - core_users = CoreUser.objects.filter(organization__organization_uuid=org_uuid) + core_users = CoreUser.objects.filter( + organization__organization_uuid=org_uuid + ) for user in core_users: email_address = user.email preferences = user.email_preferences - if preferences and (preferences.get('environmental', None) or preferences.get('geofence', None)): + if preferences and ( + preferences.get('environmental', None) + or preferences.get('geofence', None) + ): # user_timezone = user.user_timezone # if user_timezone: # local_zone = tz.gettz(user_timezone) # message['date_time'] = message['date_time'].astimezone(local_zone) # else: # message['date_time'] = time_tuple.strftime("%B %d, %Y, %I:%M %p")+" (UTC)" - send_email(email_address, subject, context, template_name, html_template_name) + send_email( + email_address, + subject, + context, + template_name, + html_template_name, + ) except Exception as ex: print('Exception: ', ex) return Response( - { - 'detail': 'The alert messages were sent successfully on email.', - }, status=status.HTTP_200_OK) + {'detail': 'The alert messages were sent successfully on email.'}, + status=status.HTTP_200_OK, + ) # This code is commented out as in future, It will need to impliment message service. # for phone in phones: # phone_number = phone @@ -350,8 +394,4 @@ def update_profile(self, request, pk=None, *args, **kwargs): return Response(serializer.data) -color_codes = { - 'error': '#cc3300', - 'info': '#2196F3', - 'success': '#339900' -} +color_codes = {'error': '#cc3300', 'info': '#2196F3', 'success': '#339900'} diff --git a/core/views/logicmodule.py b/core/views/logicmodule.py index a6df9fa0..3427d345 100644 --- a/core/views/logicmodule.py +++ b/core/views/logicmodule.py @@ -54,9 +54,7 @@ def update_api_specification(self, request, *args, **kwargs): response = utils.get_swagger_from_url(schema_url) spec_dict = response.json() - data = { - 'api_specification': spec_dict - } + data = {'api_specification': spec_dict} serializer = self.get_serializer(instance, data=data, partial=True) serializer.is_valid(raise_exception=True) diff --git a/core/views/oauth.py b/core/views/oauth.py index d21e4b6f..d0108243 100644 --- a/core/views/oauth.py +++ b/core/views/oauth.py @@ -3,13 +3,20 @@ from oauth2_provider.models import AccessToken, Application, RefreshToken -from core.serializers import AccessTokenSerializer, ApplicationSerializer, RefreshTokenSerializer +from core.serializers import ( + AccessTokenSerializer, + ApplicationSerializer, + RefreshTokenSerializer, +) from core.permissions import IsSuperUser -class AccessTokenViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet): +class AccessTokenViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): """ title: Users' access tokens @@ -79,9 +86,12 @@ class ApplicationViewSet(viewsets.ModelViewSet): serializer_class = ApplicationSerializer -class RefreshTokenViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet): +class RefreshTokenViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): """ title: Users' refresh tokens diff --git a/core/views/organization.py b/core/views/organization.py index fb90601e..493ee50c 100644 --- a/core/views/organization.py +++ b/core/views/organization.py @@ -51,8 +51,13 @@ def list(self, request, *args, **kwargs): serializer_class = OrganizationSerializer @csrf_exempt - @action(detail=False, methods=['get'], permission_classes=[AllowAny], - name='Fetch Already existing Organization', url_path='fetch_orgs') + @action( + detail=False, + methods=['get'], + permission_classes=[AllowAny], + name='Fetch Already existing Organization', + url_path='fetch_orgs', + ) def fetch_existing_orgs(self, request, pk=None, *args, **kwargs): """ Fetch Already existing Organizations in Buildly Core, diff --git a/core/views/web.py b/core/views/web.py index 3755754e..6b81b2a4 100644 --- a/core/views/web.py +++ b/core/views/web.py @@ -11,8 +11,12 @@ from rest_framework.reverse import reverse from social_core.exceptions import AuthFailed -from social_core.utils import (partial_pipeline_data, setting_url, - user_is_active, user_is_authenticated) +from social_core.utils import ( + partial_pipeline_data, + setting_url, + user_is_active, + user_is_authenticated, +) from social_django.utils import psa from core.exceptions import SocialAuthFailed, SocialAuthNotConfigured @@ -60,7 +64,9 @@ def oauth_complete(request, backend, *args, **kwargs): if backend not in settings.SOCIAL_AUTH_LOGIN_REDIRECT_URLS: raise SocialAuthNotConfigured(f'The backend {backend} is not supported.') elif not settings.SOCIAL_AUTH_LOGIN_REDIRECT_URLS.get(backend): - raise SocialAuthNotConfigured(f'A redirect URL for the backend {backend} was not defined.') + raise SocialAuthNotConfigured( + f'A redirect URL for the backend {backend} was not defined.' + ) # prepare request to validate code data = request.backend.strategy.request_data() @@ -86,7 +92,9 @@ def oauth_complete(request, backend, *args, **kwargs): tokens = generate_access_tokens(request, user) return JsonResponse(data=tokens, status=200) else: - url = setting_url(request.backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', 'LOGIN_URL') + url = setting_url( + request.backend, 'INACTIVE_USER_URL', 'LOGIN_ERROR_URL', 'LOGIN_URL' + ) else: url = setting_url(request.backend, 'LOGIN_ERROR_URL', 'LOGIN_URL') diff --git a/datamesh/exceptions.py b/datamesh/exceptions.py index 03814915..749ab229 100644 --- a/datamesh/exceptions.py +++ b/datamesh/exceptions.py @@ -1,3 +1,2 @@ - class DatameshConfigurationError(BaseException): pass diff --git a/datamesh/filters.py b/datamesh/filters.py index 38d43773..35b28554 100644 --- a/datamesh/filters.py +++ b/datamesh/filters.py @@ -12,4 +12,9 @@ class JoinRecordFilter(django_filters.FilterSet): class Meta: model = JoinRecord - fields = ('record_id', 'record_uuid', 'related_record_id', 'related_record_uuid') + fields = ( + 'record_id', + 'record_uuid', + 'related_record_id', + 'related_record_uuid', + ) diff --git a/datamesh/management/commands/loadrelationships.py b/datamesh/management/commands/loadrelationships.py index 1901f446..2592b473 100644 --- a/datamesh/management/commands/loadrelationships.py +++ b/datamesh/management/commands/loadrelationships.py @@ -27,7 +27,7 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add --file argument to Command.""" parser.add_argument( - '--file', default=None, nargs='?', help='Path of file to import.', + '--file', default=None, nargs='?', help='Path of file to import.' ) def handle(self, *args, **options): @@ -58,7 +58,7 @@ def handle(self, *args, **options): relationship, _ = Relationship.objects.get_or_create( origin_model=origin_model, related_model=related_model, - key='contact_siteprofile_relationship' + key='contact_siteprofile_relationship', ) eligible_join_records = [] # create JoinRecords with contact.id and siteprofile_uuid for all contacts @@ -78,11 +78,15 @@ def handle(self, *args, **options): relationship=relationship, record_uuid=contact['pk'], related_record_uuid=siteprofile_uuid, - defaults={'organization': organization} + defaults={'organization': organization}, ) print(join_record) eligible_join_records.append(join_record.pk) print(f'{self.counter} Contacts parsed and written to the JoinRecords.') # delete not eligible JoinRecords in this relationship - deleted, _ = JoinRecord.objects.exclude(pk__in=eligible_join_records).filter(relationship=relationship).delete() + deleted, _ = ( + JoinRecord.objects.exclude(pk__in=eligible_join_records) + .filter(relationship=relationship) + .delete() + ) print(f'{deleted} JoinRecord(s) deleted.') diff --git a/datamesh/managers.py b/datamesh/managers.py index 132734eb..6ae46a71 100644 --- a/datamesh/managers.py +++ b/datamesh/managers.py @@ -7,19 +7,20 @@ class LogicModuleModelManager(Manager): - def get_by_concatenated_model_name(self, concatenated_model_name: str) -> Model: - return self.annotate(swagger_model_name=Concat( - 'logic_module_endpoint_name', 'model')).filter( - swagger_model_name=concatenated_model_name).first() + return ( + self.annotate( + swagger_model_name=Concat('logic_module_endpoint_name', 'model') + ) + .filter(swagger_model_name=concatenated_model_name) + .first() + ) class JoinRecordManager(Manager): - - def get_join_records(self, - origin_pk: Any, - relationship: Model, - is_forward_relationship: bool) -> QuerySet: + def get_join_records( + self, origin_pk: Any, relationship: Model, is_forward_relationship: bool + ) -> QuerySet: """Get JoinRecords for relation on origin_pk in a certain direction.""" if utils.valid_uuid4(str(origin_pk)): pk_field = 'record_uuid' @@ -28,4 +29,6 @@ def get_join_records(self, if not is_forward_relationship: pk_field = 'related_' + pk_field - return self.filter(relationship=relationship).filter(**{pk_field: str(origin_pk)}) + return self.filter(relationship=relationship).filter( + **{pk_field: str(origin_pk)} + ) diff --git a/datamesh/migrations/0001_initial.py b/datamesh/migrations/0001_initial.py index 39d95848..efd991b5 100644 --- a/datamesh/migrations/0001_initial.py +++ b/datamesh/migrations/0001_initial.py @@ -9,41 +9,113 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( name='JoinRecord', fields=[ - ('join_record_uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + 'join_record_uuid', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), ('record_id', models.PositiveIntegerField(blank=True, null=True)), ('record_uuid', models.UUIDField(blank=True, null=True)), - ('related_record_id', models.PositiveIntegerField(blank=True, null=True)), + ( + 'related_record_id', + models.PositiveIntegerField(blank=True, null=True), + ), ('related_record_uuid', models.UUIDField(blank=True, null=True)), ], ), migrations.CreateModel( name='LogicModuleModel', fields=[ - ('logic_module_model_uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('logic_module_endpoint_name', models.CharField(help_text='Without leading and trailing slashes', max_length=255)), + ( + 'logic_module_model_uuid', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + 'logic_module_endpoint_name', + models.CharField( + help_text='Without leading and trailing slashes', max_length=255 + ), + ), ('model', models.CharField(max_length=128)), - ('endpoint', models.CharField(help_text="Endpoint of the model with leading and trailing slashs, p.e.: '/siteprofiles/'", max_length=255)), - ('lookup_field_name', models.SlugField(default='id', help_text="Name of the field in the model for detail methods, p.e.: 'id' or 'uuid'", max_length=64)), - ('is_local', models.BooleanField(default=False, help_text='Local model is taken from Buildly')), + ( + 'endpoint', + models.CharField( + help_text="Endpoint of the model with leading and trailing slashs, p.e.: '/siteprofiles/'", + max_length=255, + ), + ), + ( + 'lookup_field_name', + models.SlugField( + default='id', + help_text="Name of the field in the model for detail methods, p.e.: 'id' or 'uuid'", + max_length=64, + ), + ), + ( + 'is_local', + models.BooleanField( + default=False, help_text='Local model is taken from Buildly' + ), + ), ], options={ - 'unique_together': {('logic_module_endpoint_name', 'model'), ('logic_module_endpoint_name', 'endpoint')}, + 'unique_together': { + ('logic_module_endpoint_name', 'model'), + ('logic_module_endpoint_name', 'endpoint'), + } }, ), migrations.CreateModel( name='Relationship', fields=[ - ('key', models.SlugField(help_text="The key in the response body, where the related object data will be saved into, p.e.: 'contact_siteprofile_relationship'.", max_length=64)), - ('relationship_uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('origin_model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='joins_origins', to='datamesh.LogicModuleModel')), - ('related_model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='joins_relateds', to='datamesh.LogicModuleModel')), + ( + 'key', + models.SlugField( + help_text="The key in the response body, where the related object data will be saved into, p.e.: 'contact_siteprofile_relationship'.", + max_length=64, + ), + ), + ( + 'relationship_uuid', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + 'origin_model', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='joins_origins', + to='datamesh.LogicModuleModel', + ), + ), + ( + 'related_model', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='joins_relateds', + to='datamesh.LogicModuleModel', + ), + ), ], ), ] diff --git a/datamesh/migrations/0002_auto_20190918_1659.py b/datamesh/migrations/0002_auto_20190918_1659.py index 83c0a747..040ddb46 100644 --- a/datamesh/migrations/0002_auto_20190918_1659.py +++ b/datamesh/migrations/0002_auto_20190918_1659.py @@ -8,21 +8,28 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('core', '0001_initial'), - ('datamesh', '0001_initial'), - ] + dependencies = [('core', '0001_initial'), ('datamesh', '0001_initial')] operations = [ migrations.AddField( model_name='joinrecord', name='organization', - field=models.ForeignKey(blank=True, help_text='Related Organization with access', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization'), + field=models.ForeignKey( + blank=True, + help_text='Related Organization with access', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='core.Organization', + ), ), migrations.AddField( model_name='joinrecord', name='relationship', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='joinrecords', to='datamesh.Relationship'), + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='joinrecords', + to='datamesh.Relationship', + ), ), migrations.AlterUniqueTogether( name='relationship', @@ -30,26 +37,68 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='joinrecord', - constraint=models.UniqueConstraint(condition=models.Q(('record_uuid', None), ('related_record_uuid', None)), fields=('relationship', 'record_id', 'related_record_id'), name='unique_join_record_ids'), + constraint=models.UniqueConstraint( + condition=models.Q( + ('record_uuid', None), ('related_record_uuid', None) + ), + fields=('relationship', 'record_id', 'related_record_id'), + name='unique_join_record_ids', + ), ), migrations.AddConstraint( model_name='joinrecord', - constraint=models.UniqueConstraint(condition=models.Q(('record_id', None), ('related_record_id', None)), fields=('relationship', 'record_uuid', 'related_record_uuid'), name='unique_join_record_uuids'), + constraint=models.UniqueConstraint( + condition=models.Q(('record_id', None), ('related_record_id', None)), + fields=('relationship', 'record_uuid', 'related_record_uuid'), + name='unique_join_record_uuids', + ), ), migrations.AddConstraint( model_name='joinrecord', - constraint=models.UniqueConstraint(condition=models.Q(('record_uuid', None), ('related_record_id', None)), fields=('relationship', 'record_id', 'related_record_uuid'), name='unique_join_record_id_uuid'), + constraint=models.UniqueConstraint( + condition=models.Q(('record_uuid', None), ('related_record_id', None)), + fields=('relationship', 'record_id', 'related_record_uuid'), + name='unique_join_record_id_uuid', + ), ), migrations.AddConstraint( model_name='joinrecord', - constraint=models.UniqueConstraint(condition=models.Q(('record_id', None), ('related_record_uuid', None)), fields=('relationship', 'record_uuid', 'related_record_id'), name='unique_join_record_uuid_id'), + constraint=models.UniqueConstraint( + condition=models.Q(('record_id', None), ('related_record_uuid', None)), + fields=('relationship', 'record_uuid', 'related_record_id'), + name='unique_join_record_uuid_id', + ), ), migrations.AddConstraint( model_name='joinrecord', - constraint=models.CheckConstraint(check=models.Q(models.Q(('record_id', None), ('record_uuid', None), _negated=True), models.Q(models.Q(_negated=True, record_id=None), models.Q(_negated=True, record_uuid=None), _negated=True)), name='one_record_primary_key'), + constraint=models.CheckConstraint( + check=models.Q( + models.Q(('record_id', None), ('record_uuid', None), _negated=True), + models.Q( + models.Q(_negated=True, record_id=None), + models.Q(_negated=True, record_uuid=None), + _negated=True, + ), + ), + name='one_record_primary_key', + ), ), migrations.AddConstraint( model_name='joinrecord', - constraint=models.CheckConstraint(check=models.Q(models.Q(('related_record_id', None), ('related_record_uuid', None), _negated=True), models.Q(models.Q(_negated=True, related_record_id=None), models.Q(_negated=True, related_record_uuid=None), _negated=True)), name='one_related_record_primary_key'), + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ('related_record_id', None), + ('related_record_uuid', None), + _negated=True, + ), + models.Q( + models.Q(_negated=True, related_record_id=None), + models.Q(_negated=True, related_record_uuid=None), + _negated=True, + ), + ), + name='one_related_record_primary_key', + ), ), ] diff --git a/datamesh/mixins.py b/datamesh/mixins.py index 1e9b851c..83480f89 100644 --- a/datamesh/mixins.py +++ b/datamesh/mixins.py @@ -1,10 +1,9 @@ - - class OrganizationQuerySetMixin(object): """ Adds functionality to return a queryset filtered by the organization_uuid in the JWT header. If no jwt header is given, an empty queryset will be returned. """ + def get_queryset(self): queryset = super().get_queryset() organization_uuid = self.request.session.get('jwt_organization_uuid', None) diff --git a/datamesh/models.py b/datamesh/models.py index b455b22d..bc19b368 100644 --- a/datamesh/models.py +++ b/datamesh/models.py @@ -10,19 +10,32 @@ class LogicModuleModel(models.Model): - logic_module_model_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - logic_module_endpoint_name = models.CharField(max_length=255, help_text="Without leading and trailing slashes") + logic_module_model_uuid = models.UUIDField( + primary_key=True, default=uuid.uuid4, editable=False + ) + logic_module_endpoint_name = models.CharField( + max_length=255, help_text="Without leading and trailing slashes" + ) model = models.CharField(max_length=128) - endpoint = models.CharField(max_length=255, help_text="Endpoint of the model with leading and trailing slashs, p.e.: '/siteprofiles/'") - lookup_field_name = models.SlugField(max_length=64, default='id', help_text="Name of the field in the model for detail methods, p.e.: 'id' or 'uuid'") - is_local = models.BooleanField(default=False, help_text="Local model is taken from Buildly") + endpoint = models.CharField( + max_length=255, + help_text="Endpoint of the model with leading and trailing slashs, p.e.: '/siteprofiles/'", + ) + lookup_field_name = models.SlugField( + max_length=64, + default='id', + help_text="Name of the field in the model for detail methods, p.e.: 'id' or 'uuid'", + ) + is_local = models.BooleanField( + default=False, help_text="Local model is taken from Buildly" + ) objects = LogicModuleModelManager() class Meta: unique_together = ( ('logic_module_endpoint_name', 'model'), - ('logic_module_endpoint_name', 'endpoint') + ('logic_module_endpoint_name', 'endpoint'), ) def __str__(self): @@ -40,8 +53,9 @@ def get_relationships(self) -> List[Tuple[models.Model, bool]]: ) relationships_with_direction = list() for relationship in relationships: - relationships_with_direction.append((relationship, - relationship.origin_model == self)) + relationships_with_direction.append( + (relationship, relationship.origin_model == self) + ) return relationships_with_direction @@ -55,17 +69,28 @@ def save(self, **kwargs): class Relationship(models.Model): - key = models.SlugField(max_length=64, help_text="The key in the response body, where the related object data will be saved into, p.e.: 'contact_siteprofile_relationship'.") - relationship_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - origin_model = models.ForeignKey(LogicModuleModel, related_name='joins_origins', on_delete=models.CASCADE) - related_model = models.ForeignKey(LogicModuleModel, related_name='joins_relateds', on_delete=models.CASCADE) + key = models.SlugField( + max_length=64, + help_text="The key in the response body, where the related object data will be saved into, p.e.: 'contact_siteprofile_relationship'.", + ) + relationship_uuid = models.UUIDField( + primary_key=True, default=uuid.uuid4, editable=False + ) + origin_model = models.ForeignKey( + LogicModuleModel, related_name='joins_origins', on_delete=models.CASCADE + ) + related_model = models.ForeignKey( + LogicModuleModel, related_name='joins_relateds', on_delete=models.CASCADE + ) def __str__(self): return f'{self.origin_model} -> {self.related_model}' def validate_reverse_relationship_absence(self): """Validate reverse relationship does not exist already.""" - if self.__class__.objects.filter(origin_model=self.related_model, related_model=self.origin_model).count(): + if self.__class__.objects.filter( + origin_model=self.related_model, related_model=self.origin_model + ).count(): raise ValidationError("Reverse relationship already exists.") def save(self, *args, **kwargs): @@ -73,21 +98,27 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) class Meta: - unique_together = ( - 'relationship_uuid', - 'origin_model', - 'related_model', - ) + unique_together = ('relationship_uuid', 'origin_model', 'related_model') class JoinRecord(models.Model): - join_record_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - relationship = models.ForeignKey(Relationship, related_name='joinrecords', on_delete=models.CASCADE) + join_record_uuid = models.UUIDField( + primary_key=True, default=uuid.uuid4, editable=False + ) + relationship = models.ForeignKey( + Relationship, related_name='joinrecords', on_delete=models.CASCADE + ) record_id = models.PositiveIntegerField(blank=True, null=True) record_uuid = models.UUIDField(blank=True, null=True) related_record_id = models.PositiveIntegerField(blank=True, null=True) related_record_uuid = models.UUIDField(blank=True, null=True) - organization = models.ForeignKey(Organization, null=True, blank=True, help_text="Related Organization with access", on_delete=models.CASCADE) + organization = models.ForeignKey( + Organization, + null=True, + blank=True, + help_text="Related Organization with access", + on_delete=models.CASCADE, + ) objects = JoinRecordManager() @@ -96,52 +127,39 @@ class Meta: # 'record_id', 'record_uuid', 'related_record_id', 'related_record_uuid' constraints = [ UniqueConstraint( - fields=( - 'relationship', - 'record_id', - 'related_record_id', - ), + fields=('relationship', 'record_id', 'related_record_id'), name='unique_join_record_ids', - condition=Q(record_uuid=None, related_record_uuid=None) + condition=Q(record_uuid=None, related_record_uuid=None), ), UniqueConstraint( - fields=( - 'relationship', - 'record_uuid', - 'related_record_uuid', - ), + fields=('relationship', 'record_uuid', 'related_record_uuid'), name='unique_join_record_uuids', - condition=Q(record_id=None, related_record_id=None) + condition=Q(record_id=None, related_record_id=None), ), UniqueConstraint( - fields=( - 'relationship', - 'record_id', - 'related_record_uuid', - ), + fields=('relationship', 'record_id', 'related_record_uuid'), name='unique_join_record_id_uuid', - condition=Q(record_uuid=None, related_record_id=None) + condition=Q(record_uuid=None, related_record_id=None), ), UniqueConstraint( - fields=( - 'relationship', - 'record_uuid', - 'related_record_id', - ), + fields=('relationship', 'record_uuid', 'related_record_id'), name='unique_join_record_uuid_id', - condition=Q(record_id=None, related_record_uuid=None) + condition=Q(record_id=None, related_record_uuid=None), ), CheckConstraint( name='one_record_primary_key', - check=~Q(record_id=None, record_uuid=None) & (~(~Q(record_id=None) & ~Q(record_uuid=None))) + check=~Q(record_id=None, record_uuid=None) + & (~(~Q(record_id=None) & ~Q(record_uuid=None))), ), CheckConstraint( name='one_related_record_primary_key', - check=~Q(related_record_id=None, related_record_uuid=None) & ( - ~(~Q(related_record_id=None) & ~Q(related_record_uuid=None))) + check=~Q(related_record_id=None, related_record_uuid=None) + & (~(~Q(related_record_id=None) & ~Q(related_record_uuid=None))), ), ] def __str__(self): - return f'{self.relationship} - ' \ + return ( + f'{self.relationship} - ' f'{self.record_id or self.record_uuid} -> {self.related_record_id or self.related_record_uuid}' + ) diff --git a/datamesh/serializers.py b/datamesh/serializers.py index 6b5b3e36..a3b390ea 100644 --- a/datamesh/serializers.py +++ b/datamesh/serializers.py @@ -6,7 +6,6 @@ class LogicModuleModelSerializer(serializers.ModelSerializer): - class Meta: model = LogicModuleModel fields = '__all__' @@ -34,11 +33,15 @@ class JoinRecordSerializer(serializers.ModelSerializer): _model_choices_map = dict() origin_model_name = serializers.ChoiceField(choices=_model_choices, write_only=True) - related_model_name = serializers.ChoiceField(choices=_model_choices, write_only=True) + related_model_name = serializers.ChoiceField( + choices=_model_choices, write_only=True + ) def __init__(self, *args, **kwargs): """Define the choices for valid LogicModuleModels.""" - for model in LogicModuleModel.objects.all().values('logic_module_endpoint_name', 'model', 'pk'): + for model in LogicModuleModel.objects.all().values( + 'logic_module_endpoint_name', 'model', 'pk' + ): choice = model['logic_module_endpoint_name'] + model['model'] self._model_choices.append(choice) self._model_choices_map.update({choice: model['pk']}) @@ -46,27 +49,35 @@ def __init__(self, *args, **kwargs): def create(self, validated_data: dict) -> JoinRecord: """Get logic_module_models, get_or_create `Relationship`s and save in case it is not already existing.""" - origin_model_pk = self._model_choices_map[validated_data.pop('origin_model_name')] - related_model_pk = self._model_choices_map[validated_data.pop('related_model_name')] + origin_model_pk = self._model_choices_map[ + validated_data.pop('origin_model_name') + ] + related_model_pk = self._model_choices_map[ + validated_data.pop('related_model_name') + ] relationship, _ = Relationship.objects.get_or_create( - origin_model_id=origin_model_pk, - related_model_id=related_model_pk + origin_model_id=origin_model_pk, related_model_id=related_model_pk + ) + organization_uuid = self.context['request'].session.get( + 'jwt_organization_uuid', None ) - organization_uuid = self.context['request'].session.get('jwt_organization_uuid', None) join_record, _ = JoinRecord.objects.get_or_create( relationship=relationship, organization_id=organization_uuid, - **validated_data + **validated_data, ) return join_record def update(self, instance: JoinRecord, validated_data: dict) -> JoinRecord: """Automatically set the relationship from the passed models.""" - origin_model_pk = self._model_choices_map[validated_data.pop('origin_model_name')] - related_model_pk = self._model_choices_map[validated_data.pop('related_model_name')] + origin_model_pk = self._model_choices_map[ + validated_data.pop('origin_model_name') + ] + related_model_pk = self._model_choices_map[ + validated_data.pop('related_model_name') + ] relationship, _ = Relationship.objects.get_or_create( - origin_model_id=origin_model_pk, - related_model_id=related_model_pk + origin_model_id=origin_model_pk, related_model_id=related_model_pk ) instance.relationship = relationship for key, value in validated_data.items(): @@ -78,13 +89,16 @@ def to_representation(self, instance: JoinRecord) -> OrderedDict: """Add origin_model_name and related_model_name.""" ret_repr = super().to_representation(instance) ret_repr.update( - {'origin_model_name': f'{instance.relationship.origin_model.logic_module_endpoint_name}' + { + 'origin_model_name': f'{instance.relationship.origin_model.logic_module_endpoint_name}' f'{instance.relationship.origin_model.model}', - 'related_model_name': f'{instance.relationship.related_model.logic_module_endpoint_name}' - f'{instance.relationship.related_model.model}'}) + 'related_model_name': f'{instance.relationship.related_model.logic_module_endpoint_name}' + f'{instance.relationship.related_model.model}', + } + ) return ret_repr class Meta: model = JoinRecord - exclude = ('relationship', ) - read_only_fields = ('organization', ) + exclude = ('relationship',) + read_only_fields = ('organization',) diff --git a/datamesh/services.py b/datamesh/services.py index 0065b266..ec3dd680 100644 --- a/datamesh/services.py +++ b/datamesh/services.py @@ -18,9 +18,15 @@ class DataMesh: For each model DataMesh object should be created. """ - def __init__(self, logic_module_endpoint: str, model_endpoint: str, access_validator: Any = None): - self._logic_module_model = LogicModuleModel.objects.get(logic_module_endpoint_name=logic_module_endpoint, - endpoint=model_endpoint) + def __init__( + self, + logic_module_endpoint: str, + model_endpoint: str, + access_validator: Any = None, + ): + self._logic_module_model = LogicModuleModel.objects.get( + logic_module_endpoint_name=logic_module_endpoint, endpoint=model_endpoint + ) self._relationships = self._logic_module_model.get_relationships() self._origin_lookup_field = self._logic_module_model.lookup_field_name self._access_validator = access_validator @@ -33,11 +39,16 @@ def related_logic_modules(self) -> list: The origin_model has to be added for the symmetrical/reverse relationships. """ if not hasattr(self, '_related_logic_modules'): - modules_list = [relationship.related_model.logic_module_endpoint_name - for relationship, _ in self._relationships if not relationship.related_model.is_local] + modules_list = [ + relationship.related_model.logic_module_endpoint_name + for relationship, _ in self._relationships + if not relationship.related_model.is_local + ] modules_list_reverse = [ relationship.origin_model.logic_module_endpoint_name - for relationship, _ in self._relationships if not relationship.origin_model.is_local] + for relationship, _ in self._relationships + if not relationship.origin_model.is_local + ] self._related_logic_modules = set(modules_list + modules_list_reverse) return self._related_logic_modules @@ -46,19 +57,22 @@ def get_related_records_meta(self, origin_pk: Any) -> Generator[tuple, None, Non Gets list of related records' META-data that is used for retrieving data for each of these records """ for relationship, is_forward_lookup in self._relationships: - join_records = JoinRecord.objects.get_join_records(origin_pk, relationship, is_forward_lookup) + join_records = JoinRecord.objects.get_join_records( + origin_pk, relationship, is_forward_lookup + ) if join_records: related_model, related_record_field = prepare_lookup_kwargs( - is_forward_lookup, relationship, join_records[0]) + is_forward_lookup, relationship, join_records[0] + ) for join_record in join_records: params = { - 'pk': (str(getattr(join_record, related_record_field))), - 'model': related_model.endpoint.strip('/'), - 'service': related_model.logic_module_endpoint_name, - 'pk_name': related_model.lookup_field_name, - } + 'pk': (str(getattr(join_record, related_record_field))), + 'model': related_model.endpoint.strip('/'), + 'service': related_model.logic_module_endpoint_name, + 'pk_name': related_model.lookup_field_name, + } yield relationship, params @@ -75,19 +89,21 @@ def extend_data(self, data: Union[dict, list], client_map: Dict[str, Any]) -> No for data_item in data: self._add_nested_data(data_item, client_map) - def _extend_with_local(self, data_item: dict, relationship: Relationship, params: dict) -> None: + def _extend_with_local( + self, data_item: dict, relationship: Relationship, params: dict + ) -> None: """ Extend data from local object (via Django ORM query)""" cache_key = f"{params['service']}.{params['model']}.{params['pk']}" if cache_key in self._cache: data_item[relationship.key].append(self._cache[cache_key]) return try: - model = apps.get_model(app_label=params['service'], model_name=params['model']) + model = apps.get_model( + app_label=params['service'], model_name=params['model'] + ) except LookupError as e: raise DatameshConfigurationError(f'Data Mesh configuration error: {e}') - lookup = { - params['pk_name']: params['pk'] - } + lookup = {params['pk_name']: params['pk']} try: obj = model.objects.get(**lookup) except model.DoesNotExist as e: @@ -95,10 +111,14 @@ def _extend_with_local(self, data_item: dict, relationship: Relationship, params else: # TODO: need to validate object access, like utils.validate_object_access(request, obj) if self._access_validator: - if hasattr(self._access_validator, 'validate') and callable(self._access_validator.validate): + if hasattr(self._access_validator, 'validate') and callable( + self._access_validator.validate + ): self._access_validator.validate(obj) else: - raise DatameshConfigurationError(f'{"DataMesh Error:Access Validator should have validate method"}') + raise DatameshConfigurationError( + f'{"DataMesh Error:Access Validator should have validate method"}' + ) obj_dict = model_to_dict(obj) data_item[relationship.key].append(obj_dict) self._cache[cache_key] = obj_dict @@ -126,16 +146,24 @@ def _add_nested_data(self, data_item: dict, client_map: Dict[str, Any]) -> None: if hasattr(client, 'request') and callable(client.request): content = client.request(**params) - if isinstance(content, tuple): # assume that response body is the first returned value + if isinstance( + content, tuple + ): # assume that response body is the first returned value content = content[0] if isinstance(content, dict): data_item[relationship.key].append(dict(content)) else: - logger.error(f'No response data for join record (request params: {params})') + logger.error( + f'No response data for join record (request params: {params})' + ) else: - raise DatameshConfigurationError(f'{"DataMesh Error: Client should have request method"}') + raise DatameshConfigurationError( + f'{"DataMesh Error: Client should have request method"}' + ) - async def async_extend_data(self, data: Union[dict, list], client_map: Dict[str, Any]): + async def async_extend_data( + self, data: Union[dict, list], client_map: Dict[str, Any] + ): """ Async aggregation logic """ @@ -169,17 +197,25 @@ async def _prepare_tasks(self, data_item: dict, client_map: Dict[str, Any]) -> l params['method'] = 'get' client = client_map.get(params['service']) - tasks.append(self._extend_content(client, data_item[relationship.key], **params)) + tasks.append( + self._extend_content(client, data_item[relationship.key], **params) + ) return tasks - async def _extend_content(self, client: Any, placeholder: list, **request_kwargs) -> None: + async def _extend_content( + self, client: Any, placeholder: list, **request_kwargs + ) -> None: """ Performs data request and extends data with received data """ content = await client.request(**request_kwargs) - if isinstance(content, tuple): # assume that response body is the first returned value + if isinstance( + content, tuple + ): # assume that response body is the first returned value content = content[0] if isinstance(content, dict): placeholder.append(dict(content)) else: - logger.error(f'No response data for join record (request params: {request_kwargs})') + logger.error( + f'No response data for join record (request params: {request_kwargs})' + ) diff --git a/datamesh/tests/fixtures.py b/datamesh/tests/fixtures.py index bf2d3b59..bb6251e3 100644 --- a/datamesh/tests/fixtures.py +++ b/datamesh/tests/fixtures.py @@ -13,68 +13,105 @@ def join_record(): @pytest.fixture def relationship(): lm = factories.LogicModule(name='Products Service', endpoint_name='products') - lmm = factories.LogicModuleModel(logic_module_endpoint_name=lm.endpoint_name, - model='Product', endpoint='/products/') - lm_document = factories.LogicModule(name='Document Service', endpoint_name='documents') - lmm_document = factories.LogicModuleModel(logic_module_endpoint_name=lm_document.endpoint_name, - model='Document', endpoint='/documents/') - return factories.Relationship(origin_model=lmm, related_model=lmm_document, key='product_document_relationship') + lmm = factories.LogicModuleModel( + logic_module_endpoint_name=lm.endpoint_name, + model='Product', + endpoint='/products/', + ) + lm_document = factories.LogicModule( + name='Document Service', endpoint_name='documents' + ) + lmm_document = factories.LogicModuleModel( + logic_module_endpoint_name=lm_document.endpoint_name, + model='Document', + endpoint='/documents/', + ) + return factories.Relationship( + origin_model=lmm, + related_model=lmm_document, + key='product_document_relationship', + ) @pytest.fixture def relationship2(relationship): lmm = relationship.origin_model # Products model from 1st relationship - lm_location = factories.LogicModule(name='Location Service', endpoint_name='location') - lmm_location = factories.LogicModuleModel(logic_module_endpoint_name=lm_location.endpoint_name, - model='Location', endpoint='/siteprofile/') - return factories.Relationship(origin_model=lmm, related_model=lmm_location, key='location_relationship') + lm_location = factories.LogicModule( + name='Location Service', endpoint_name='location' + ) + lmm_location = factories.LogicModuleModel( + logic_module_endpoint_name=lm_location.endpoint_name, + model='Location', + endpoint='/siteprofile/', + ) + return factories.Relationship( + origin_model=lmm, related_model=lmm_location, key='location_relationship' + ) @pytest.fixture def relationship_with_10_records(org): lm = factories.LogicModule(name='Products Service', endpoint_name='products') - lmm = factories.LogicModuleModel(logic_module_endpoint_name=lm.endpoint_name, - model='Product', endpoint='/products/', lookup_field_name='uuid') - lm_document = factories.LogicModule(name='Document Service', endpoint_name='documents') - lmm_document = factories.LogicModuleModel(logic_module_endpoint_name=lm_document.endpoint_name, - model='Document', endpoint='/documents/', - lookup_field_name='uuid') - relationship = factories.Relationship(origin_model=lmm, related_model=lmm_document, key='product_document_relationship') + lmm = factories.LogicModuleModel( + logic_module_endpoint_name=lm.endpoint_name, + model='Product', + endpoint='/products/', + lookup_field_name='uuid', + ) + lm_document = factories.LogicModule( + name='Document Service', endpoint_name='documents' + ) + lmm_document = factories.LogicModuleModel( + logic_module_endpoint_name=lm_document.endpoint_name, + model='Document', + endpoint='/documents/', + lookup_field_name='uuid', + ) + relationship = factories.Relationship( + origin_model=lmm, + related_model=lmm_document, + key='product_document_relationship', + ) for _ in range(10): - factories.JoinRecord.create(relationship=relationship, - record_uuid=uuid.uuid4(), record_id=None, - related_record_uuid=uuid.uuid4(), related_record_id=None, - organization=org) + factories.JoinRecord.create( + relationship=relationship, + record_uuid=uuid.uuid4(), + record_id=None, + related_record_uuid=uuid.uuid4(), + related_record_id=None, + organization=org, + ) return relationship @pytest.fixture def relationship_with_local(): lm = factories.LogicModule(name='Products Service', endpoint_name='products') - lmm = factories.LogicModuleModel(logic_module_endpoint_name=lm.endpoint_name, - model='Product', endpoint='/products/') - lmm_org = factories.LogicModuleModel(logic_module_endpoint_name='core', - model='Organization', - endpoint='/organization/', - lookup_field_name='organization_uuid', - is_local=True) - return factories.Relationship(origin_model=lmm, related_model=lmm_org, key='product_document_relationship') + lmm = factories.LogicModuleModel( + logic_module_endpoint_name=lm.endpoint_name, + model='Product', + endpoint='/products/', + ) + lmm_org = factories.LogicModuleModel( + logic_module_endpoint_name='core', + model='Organization', + endpoint='/organization/', + lookup_field_name='organization_uuid', + is_local=True, + ) + return factories.Relationship( + origin_model=lmm, related_model=lmm_org, key='product_document_relationship' + ) @pytest.fixture def document_logic_module(): - return factories.LogicModule( - name='document', - endpoint_name='document' - ) + return factories.LogicModule(name='document', endpoint_name='document') @pytest.fixture def crm_logic_module(): - return factories.LogicModule( - name='crm', - endpoint_name='crm' - ) + return factories.LogicModule(name='crm', endpoint_name='crm') @pytest.fixture @@ -85,14 +122,12 @@ def logic_module_model(): @pytest.fixture def document_logic_module_model(): return factories.LogicModuleModel( - logic_module_endpoint_name='document', - model='Document' + logic_module_endpoint_name='document', model='Document' ) @pytest.fixture def appointment_logic_module_model(): return factories.LogicModuleModel( - logic_module_endpoint_name='crm', - model='Appointment' + logic_module_endpoint_name='crm', model='Appointment' ) diff --git a/datamesh/tests/test_datamesh_service.py b/datamesh/tests/test_datamesh_service.py index f644121d..d88bb5b5 100644 --- a/datamesh/tests/test_datamesh_service.py +++ b/datamesh/tests/test_datamesh_service.py @@ -5,16 +5,25 @@ import factories from core.tests.fixtures import org -from datamesh.tests.fixtures import relationship, relationship2, relationship_with_10_records, relationship_with_local +from datamesh.tests.fixtures import ( + relationship, + relationship2, + relationship_with_10_records, + relationship_with_local, +) from datamesh.services import DataMesh @pytest.mark.django_db() class TestSyncDataMesh: - def test_join_data_one_obj_w_relationships(self, relationship): - factories.JoinRecord(relationship=relationship, record_id=1, related_record_id=2, - record_uuid=None, related_record_uuid=None) + factories.JoinRecord( + relationship=relationship, + record_id=1, + related_record_id=2, + record_uuid=None, + related_record_uuid=None, + ) logic_module_model = relationship.origin_model data = {'id': 1, 'name': 'test', 'contact_uuid': 1} @@ -22,11 +31,16 @@ def test_join_data_one_obj_w_relationships(self, relationship): # mock client for related logic module class ClientMock: def request(self, **kwargs): - return {'id': 1, 'file': '/somewhere/128/',} - client_map = {relationship.related_model.logic_module_endpoint_name: ClientMock()} + return {'id': 1, 'file': '/somewhere/128/'} - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + client_map = { + relationship.related_model.logic_module_endpoint_name: ClientMock() + } + + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) datamesh.extend_data(data, client_map) # validate result @@ -34,19 +48,28 @@ def request(self, **kwargs): 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship.key: [{ - 'id': 1, - 'file': '/somewhere/128/', - }] + relationship.key: [{'id': 1, 'file': '/somewhere/128/'}], } assert data == expected_data - def test_join_data_one_obj_w_two_relationships(self, relationship, relationship2, org): - factories.JoinRecord(relationship=relationship, record_id=1, related_record_id=2, - record_uuid=None, related_record_uuid=None) - factories.JoinRecord(relationship=relationship2, record_id=1, related_record_id=10, - record_uuid=None, related_record_uuid=None) + def test_join_data_one_obj_w_two_relationships( + self, relationship, relationship2, org + ): + factories.JoinRecord( + relationship=relationship, + record_id=1, + related_record_id=2, + record_uuid=None, + related_record_uuid=None, + ) + factories.JoinRecord( + relationship=relationship2, + record_id=1, + related_record_id=10, + record_uuid=None, + related_record_uuid=None, + ) logic_module_model = relationship.origin_model data = {'id': 1, 'name': 'test', 'contact_uuid': 1} @@ -59,13 +82,16 @@ def request(self, **kwargs): if kwargs['model'] == 'siteprofile': return {'id': 10, 'city': 'New York'} return {} + client_map = { relationship.related_model.logic_module_endpoint_name: ClientMock(), relationship2.related_model.logic_module_endpoint_name: ClientMock(), } - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) datamesh.extend_data(data, client_map) # validate result @@ -73,14 +99,8 @@ def request(self, **kwargs): 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship.key: [{ - 'id': 2, - 'file': '/documents/128/', - }], - relationship2.key: [{ - 'id': 10, - 'city': 'New York', - }] + relationship.key: [{'id': 2, 'file': '/documents/128/'}], + relationship2.key: [{'id': 10, 'city': 'New York'}], } assert data == expected_data @@ -90,18 +110,31 @@ def test_join_data_list(self, relationship_with_10_records): logic_module_model = relationship_with_10_records.origin_model - data = [{'uuid': str(item.record_uuid), 'name': f'Boiler #{i}'} for i, item in enumerate(join_records)] + data = [ + {'uuid': str(item.record_uuid), 'name': f'Boiler #{i}'} + for i, item in enumerate(join_records) + ] # mock client for related logic module - mocked_response_data = {str(item.related_record_uuid): '/documents/128/' for item in join_records} + mocked_response_data = { + str(item.related_record_uuid): '/documents/128/' for item in join_records + } class ClientMock: def request(self, **kwargs): - return {'uuid': kwargs['pk'], 'file': mocked_response_data[kwargs['pk']]} - client_map = {relationship_with_10_records.related_model.logic_module_endpoint_name: ClientMock()} + return { + 'uuid': kwargs['pk'], + 'file': mocked_response_data[kwargs['pk']], + } + + client_map = { + relationship_with_10_records.related_model.logic_module_endpoint_name: ClientMock() + } - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) datamesh.extend_data(data, client_map) for i, item in enumerate(data): @@ -112,15 +145,21 @@ def request(self, **kwargs): assert nested[0]['uuid'] == str(join_records[i].related_record_uuid) def test_relationship_with_local_lm(self, relationship_with_local, org): - factories.JoinRecord(relationship=relationship_with_local, record_id=1, - related_record_uuid=org.organization_uuid, - record_uuid=None, related_record_id=None) + factories.JoinRecord( + relationship=relationship_with_local, + record_id=1, + related_record_uuid=org.organization_uuid, + record_uuid=None, + related_record_id=None, + ) logic_module_model = relationship_with_local.origin_model data = {'id': 1, 'name': 'test', 'contact_uuid': 1} - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) datamesh.extend_data(data, {}) # validate result @@ -128,7 +167,7 @@ def test_relationship_with_local_lm(self, relationship_with_local, org): 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship_with_local.key: [model_to_dict(org)] + relationship_with_local.key: [model_to_dict(org)], } assert data == expected_data @@ -136,10 +175,14 @@ def test_relationship_with_local_lm(self, relationship_with_local, org): @pytest.mark.django_db() class TestAsyncDataMesh: - def test_join_data_one_obj_w_relationships(self, relationship): - factories.JoinRecord(relationship=relationship, record_id=1, related_record_id=2, - record_uuid=None, related_record_uuid=None) + factories.JoinRecord( + relationship=relationship, + record_id=1, + related_record_id=2, + record_uuid=None, + related_record_uuid=None, + ) logic_module_model = relationship.origin_model data = {'id': 1, 'name': 'test', 'contact_uuid': 1} @@ -147,11 +190,16 @@ def test_join_data_one_obj_w_relationships(self, relationship): # mock client for related logic module class ClientMock: async def request(self, **kwargs): - return {'id': 1, 'file': '/somewhere/128/',} - client_map = {relationship.related_model.logic_module_endpoint_name: ClientMock()} + return {'id': 1, 'file': '/somewhere/128/'} - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + client_map = { + relationship.related_model.logic_module_endpoint_name: ClientMock() + } + + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) asyncio.run(datamesh.async_extend_data(data, client_map)) @@ -160,19 +208,28 @@ async def request(self, **kwargs): 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship.key: [{ - 'id': 1, - 'file': '/somewhere/128/', - }] + relationship.key: [{'id': 1, 'file': '/somewhere/128/'}], } assert data == expected_data - def test_join_data_one_obj_w_two_relationships(self, relationship, relationship2, org): - factories.JoinRecord(relationship=relationship, record_id=1, related_record_id=2, - record_uuid=None, related_record_uuid=None) - factories.JoinRecord(relationship=relationship2, record_id=1, related_record_id=10, - record_uuid=None, related_record_uuid=None) + def test_join_data_one_obj_w_two_relationships( + self, relationship, relationship2, org + ): + factories.JoinRecord( + relationship=relationship, + record_id=1, + related_record_id=2, + record_uuid=None, + related_record_uuid=None, + ) + factories.JoinRecord( + relationship=relationship2, + record_id=1, + related_record_id=10, + record_uuid=None, + related_record_uuid=None, + ) logic_module_model = relationship.origin_model data = {'id': 1, 'name': 'test', 'contact_uuid': 1} @@ -185,13 +242,16 @@ async def request(self, **kwargs): if kwargs['model'] == 'siteprofile': return {'id': 10, 'city': 'New York'} return {} + client_map = { relationship.related_model.logic_module_endpoint_name: ClientMock(), relationship2.related_model.logic_module_endpoint_name: ClientMock(), } - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) asyncio.run(datamesh.async_extend_data(data, client_map)) # validate result @@ -199,14 +259,8 @@ async def request(self, **kwargs): 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship.key: [{ - 'id': 2, - 'file': '/documents/128/', - }], - relationship2.key: [{ - 'id': 10, - 'city': 'New York', - }] + relationship.key: [{'id': 2, 'file': '/documents/128/'}], + relationship2.key: [{'id': 10, 'city': 'New York'}], } assert data == expected_data @@ -216,18 +270,31 @@ def test_join_data_list(self, relationship_with_10_records): logic_module_model = relationship_with_10_records.origin_model - data = [{'uuid': str(item.record_uuid), 'name': f'Boiler #{i}'} for i, item in enumerate(join_records)] + data = [ + {'uuid': str(item.record_uuid), 'name': f'Boiler #{i}'} + for i, item in enumerate(join_records) + ] # mock client for related logic module - mocked_response_data = {str(item.related_record_uuid): '/documents/128/' for item in join_records} + mocked_response_data = { + str(item.related_record_uuid): '/documents/128/' for item in join_records + } class ClientMock: async def request(self, **kwargs): - return {'uuid': kwargs['pk'], 'file': mocked_response_data[kwargs['pk']]} - client_map = {relationship_with_10_records.related_model.logic_module_endpoint_name: ClientMock()} + return { + 'uuid': kwargs['pk'], + 'file': mocked_response_data[kwargs['pk']], + } + + client_map = { + relationship_with_10_records.related_model.logic_module_endpoint_name: ClientMock() + } - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) asyncio.run(datamesh.async_extend_data(data, client_map)) for i, item in enumerate(data): @@ -238,15 +305,21 @@ async def request(self, **kwargs): assert nested[0]['uuid'] == str(join_records[i].related_record_uuid) def test_relationship_with_local_lm(self, relationship_with_local, org): - factories.JoinRecord(relationship=relationship_with_local, record_id=1, - related_record_uuid=org.organization_uuid, - record_uuid=None, related_record_id=None) + factories.JoinRecord( + relationship=relationship_with_local, + record_id=1, + related_record_uuid=org.organization_uuid, + record_uuid=None, + related_record_id=None, + ) logic_module_model = relationship_with_local.origin_model data = {'id': 1, 'name': 'test', 'contact_uuid': 1} - datamesh = DataMesh(logic_module_endpoint=logic_module_model.logic_module_endpoint_name, - model_endpoint=logic_module_model.endpoint) + datamesh = DataMesh( + logic_module_endpoint=logic_module_model.logic_module_endpoint_name, + model_endpoint=logic_module_model.endpoint, + ) asyncio.run(datamesh.async_extend_data(data, {})) # validate result @@ -254,7 +327,7 @@ def test_relationship_with_local_lm(self, relationship_with_local, org): 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship_with_local.key: [model_to_dict(org)] + relationship_with_local.key: [model_to_dict(org)], } assert data == expected_data diff --git a/datamesh/tests/test_join.py b/datamesh/tests/test_join.py index 3e076397..1cd42b89 100644 --- a/datamesh/tests/test_join.py +++ b/datamesh/tests/test_join.py @@ -5,28 +5,48 @@ from bravado_core.spec import Spec import factories -from datamesh.tests.fixtures import relationship, relationship2, relationship_with_10_records +from datamesh.tests.fixtures import ( + relationship, + relationship2, + relationship_with_10_records, +) from core.tests.fixtures import auth_api_client, org + @pytest.mark.django_db() @patch('gateway.request.GatewayRequest._get_swagger_spec') @patch('gateway.request.SwaggerClient.request') -def test_join_data_one_obj_w_relationships(mock_perform_request, mock_spec, auth_api_client, relationship): - factories.JoinRecord(relationship=relationship, record_id=1, related_record_id=2, - record_uuid=None, related_record_uuid=None) +def test_join_data_one_obj_w_relationships( + mock_perform_request, mock_spec, auth_api_client, relationship +): + factories.JoinRecord( + relationship=relationship, + record_id=1, + related_record_id=2, + record_uuid=None, + related_record_uuid=None, + ) # mock app mock_spec.return_value = Mock(Spec) # mock first response - service_response = ({'id': 1, 'name': 'test', 'contact_uuid': 1}, - 200, {'Content-Type': ['application/json']}) + service_response = ( + {'id': 1, 'name': 'test', 'contact_uuid': 1}, + 200, + {'Content-Type': ['application/json']}, + ) # mock second response - expand_response = ({'id': 1, 'file': '/somewhere/128/',}, - 200, {'Content-Type': ['application/json']}) + expand_response = ( + {'id': 1, 'file': '/somewhere/128/'}, + 200, + {'Content-Type': ['application/json']}, + ) mock_perform_request.side_effect = [service_response, expand_response] # make api request - path = '/{}/{}/'.format(relationship.origin_model.logic_module_endpoint_name, 'products') + path = '/{}/{}/'.format( + relationship.origin_model.logic_module_endpoint_name, 'products' + ) response = auth_api_client.get(path, {'join': ''}) # validate result @@ -34,10 +54,7 @@ def test_join_data_one_obj_w_relationships(mock_perform_request, mock_spec, auth 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship.key: [{ - 'id': 1, - 'file': '/somewhere/128/', - }] + relationship.key: [{'id': 1, 'file': '/somewhere/128/'}], } assert response.status_code == 200 @@ -47,27 +64,53 @@ def test_join_data_one_obj_w_relationships(mock_perform_request, mock_spec, auth @pytest.mark.django_db() @patch('gateway.request.GatewayRequest._get_swagger_spec') @patch('gateway.request.SwaggerClient.request') -def test_join_data_one_obj_w_two_relationships(mock_perform_request, mock_spec, auth_api_client, - relationship, relationship2): - factories.JoinRecord(relationship=relationship, record_id=1, related_record_id=2, - record_uuid=None, related_record_uuid=None) - factories.JoinRecord(relationship=relationship2, record_id=1, related_record_id=10, - record_uuid=None, related_record_uuid=None) +def test_join_data_one_obj_w_two_relationships( + mock_perform_request, mock_spec, auth_api_client, relationship, relationship2 +): + factories.JoinRecord( + relationship=relationship, + record_id=1, + related_record_id=2, + record_uuid=None, + related_record_uuid=None, + ) + factories.JoinRecord( + relationship=relationship2, + record_id=1, + related_record_id=10, + record_uuid=None, + related_record_uuid=None, + ) mock_spec.return_value = Mock(Spec) # mock first response - service_response = ({'id': 1, 'name': 'test', 'contact_uuid': 1}, - 200, {'Content-Type': ['application/json']}) + service_response = ( + {'id': 1, 'name': 'test', 'contact_uuid': 1}, + 200, + {'Content-Type': ['application/json']}, + ) # mock second response - expand_response1 = ({'id': 2, 'file': '/documents/128/'}, - 200, {'Content-Type': ['application/json']}) + expand_response1 = ( + {'id': 2, 'file': '/documents/128/'}, + 200, + {'Content-Type': ['application/json']}, + ) # mock third response - expand_response2 = ({'id': 10, 'city': 'New York'}, - 200, {'Content-Type': ['application/json']}) - mock_perform_request.side_effect = [service_response, expand_response1, expand_response2] + expand_response2 = ( + {'id': 10, 'city': 'New York'}, + 200, + {'Content-Type': ['application/json']}, + ) + mock_perform_request.side_effect = [ + service_response, + expand_response1, + expand_response2, + ] # make api request - path = '/{}/{}/'.format(relationship.origin_model.logic_module_endpoint_name, 'products') + path = '/{}/{}/'.format( + relationship.origin_model.logic_module_endpoint_name, 'products' + ) response = auth_api_client.get(path, {'join': ''}) # validate result @@ -75,14 +118,8 @@ def test_join_data_one_obj_w_two_relationships(mock_perform_request, mock_spec, 'id': 1, 'name': 'test', 'contact_uuid': 1, - relationship.key: [{ - 'id': 2, - 'file': '/documents/128/', - }], - relationship2.key: [{ - 'id': 10, - 'city': 'New York', - }] + relationship.key: [{'id': 2, 'file': '/documents/128/'}], + relationship2.key: [{'id': 10, 'city': 'New York'}], } assert response.status_code == 200 @@ -92,25 +129,37 @@ def test_join_data_one_obj_w_two_relationships(mock_perform_request, mock_spec, @pytest.mark.django_db() @patch('gateway.request.GatewayRequest._get_swagger_spec') @patch('gateway.request.SwaggerClient.request') -def test_join_data_list(mock_perform_request, mock_spec, auth_api_client, relationship_with_10_records, org): +def test_join_data_list( + mock_perform_request, mock_spec, auth_api_client, relationship_with_10_records, org +): mock_spec.return_value = Mock(Spec) join_records = relationship_with_10_records.joinrecords.all() - main_service_data = [{'uuid': item.record_uuid, 'name': f'Boiler #{i}'} - for i, item in enumerate(join_records)] - main_service_response = (main_service_data, - 200, {'Content-Type': ['application/json']}) + main_service_data = [ + {'uuid': item.record_uuid, 'name': f'Boiler #{i}'} + for i, item in enumerate(join_records) + ] + main_service_response = ( + main_service_data, + 200, + {'Content-Type': ['application/json']}, + ) expand_responses = [] for item in join_records: expand_responses.append( - ({'uuid': item.related_record_uuid, 'file': '/documents/128/'}, - 200, {'Content-Type': ['application/json']}) + ( + {'uuid': item.related_record_uuid, 'file': '/documents/128/'}, + 200, + {'Content-Type': ['application/json']}, + ) ) mock_perform_request.side_effect = [main_service_response] + expand_responses # make api request - path = '/{}/{}/'.format(relationship_with_10_records.origin_model.logic_module_endpoint_name, 'products') + path = '/{}/{}/'.format( + relationship_with_10_records.origin_model.logic_module_endpoint_name, 'products' + ) response = auth_api_client.get(path, {'join': ''}) assert response.status_code == 200 diff --git a/datamesh/tests/test_models.py b/datamesh/tests/test_models.py index b3f5b945..b79b23b9 100644 --- a/datamesh/tests/test_models.py +++ b/datamesh/tests/test_models.py @@ -6,7 +6,11 @@ from datamesh.models import JoinRecord, Relationship, LogicModuleModel from core.tests.fixtures import org -from .fixtures import relationship, appointment_logic_module_model, document_logic_module_model +from .fixtures import ( + relationship, + appointment_logic_module_model, + document_logic_module_model, +) @pytest.mark.django_db() @@ -14,12 +18,14 @@ def test_fail_create_reverse_relationship(relationship): with pytest.raises(ValidationError): Relationship.objects.create( related_model=relationship.origin_model, - origin_model=relationship.related_model + origin_model=relationship.related_model, ) @pytest.mark.django_db() -def test_get_by_concatenated_model_name(appointment_logic_module_model, document_logic_module_model): +def test_get_by_concatenated_model_name( + appointment_logic_module_model, document_logic_module_model +): lmm = LogicModuleModel.objects.get_by_concatenated_model_name("crmAppointment") assert lmm == appointment_logic_module_model assert None == LogicModuleModel.objects.get_by_concatenated_model_name("nothing") @@ -28,10 +34,7 @@ def test_get_by_concatenated_model_name(appointment_logic_module_model, document @pytest.mark.django_db() def test_create_join_record(relationship, org): JoinRecord.objects.create( - relationship=relationship, - record_id=1, - related_record_id=2, - organization=org, + relationship=relationship, record_id=1, related_record_id=2, organization=org ) JoinRecord.objects.create( relationship=relationship, @@ -43,44 +46,45 @@ def test_create_join_record(relationship, org): @pytest.mark.django_db() -def test_one_record_primary_key_check_constraint_fail_empty_record_id_and_record_uuid(relationship, org): +def test_one_record_primary_key_check_constraint_fail_empty_record_id_and_record_uuid( + relationship, org +): with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( - related_record_id=1, - relationship=relationship, - organization=org, + related_record_id=1, relationship=relationship, organization=org ) assert JoinRecord.objects.count() == 0 @pytest.mark.django_db() -def test_one_record_primary_key_check_constraint_fail_filled_id_and_uuid(relationship, org): +def test_one_record_primary_key_check_constraint_fail_filled_id_and_uuid( + relationship, org +): with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( - record_id=1, - record_uuid=1, - relationship=relationship, - organization=org, + record_id=1, record_uuid=1, relationship=relationship, organization=org ) assert JoinRecord.objects.count() == 0 @pytest.mark.django_db() -def test_one_record_primary_key_check_constraint_fail_empty_related_id_and_related_uuid(relationship, org): +def test_one_record_primary_key_check_constraint_fail_empty_related_id_and_related_uuid( + relationship, org +): with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( - record_id=1, - relationship=relationship, - organization=org, + record_id=1, relationship=relationship, organization=org ) assert JoinRecord.objects.count() == 0 @pytest.mark.django_db() -def test_one_record_primary_key_check_constraint_fail_filled_related_id_and_related_uuid(relationship, org): +def test_one_record_primary_key_check_constraint_fail_filled_related_id_and_related_uuid( + relationship, org +): with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( @@ -95,16 +99,12 @@ def test_one_record_primary_key_check_constraint_fail_filled_related_id_and_rela @pytest.mark.django_db() def test_unique_together_join_record_id_id(relationship, org): JoinRecord.objects.create( - record_id=1, - related_record_id=1, - relationship=relationship, + record_id=1, related_record_id=1, relationship=relationship ) with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( - record_id=1, - related_record_id=1, - relationship=relationship, + record_id=1, related_record_id=1, relationship=relationship ) assert JoinRecord.objects.count() == 1 @@ -112,16 +112,12 @@ def test_unique_together_join_record_id_id(relationship, org): @pytest.mark.django_db() def test_unique_together_join_record_uuid_uuid(relationship, org): JoinRecord.objects.create( - record_uuid=1, - related_record_uuid=1, - relationship=relationship, + record_uuid=1, related_record_uuid=1, relationship=relationship ) with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( - record_uuid=1, - related_record_uuid=1, - relationship=relationship, + record_uuid=1, related_record_uuid=1, relationship=relationship ) assert JoinRecord.objects.count() == 1 @@ -129,16 +125,12 @@ def test_unique_together_join_record_uuid_uuid(relationship, org): @pytest.mark.django_db() def test_unique_together_join_record_id_uuid(relationship, org): JoinRecord.objects.create( - record_id=1, - related_record_uuid=1, - relationship=relationship, + record_id=1, related_record_uuid=1, relationship=relationship ) with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( - record_id=1, - related_record_uuid=1, - relationship=relationship, + record_id=1, related_record_uuid=1, relationship=relationship ) assert JoinRecord.objects.count() == 1 @@ -146,15 +138,11 @@ def test_unique_together_join_record_id_uuid(relationship, org): @pytest.mark.django_db() def test_unique_together_join_record_uuid_id(relationship, org): JoinRecord.objects.create( - record_uuid=1, - related_record_id=1, - relationship=relationship, + record_uuid=1, related_record_id=1, relationship=relationship ) with pytest.raises(IntegrityError): with transaction.atomic(): JoinRecord.objects.create( - record_uuid=1, - related_record_id=1, - relationship=relationship, + record_uuid=1, related_record_id=1, relationship=relationship ) assert JoinRecord.objects.count() == 1 diff --git a/datamesh/tests/test_serializers.py b/datamesh/tests/test_serializers.py index 70c50964..787a33e1 100644 --- a/datamesh/tests/test_serializers.py +++ b/datamesh/tests/test_serializers.py @@ -3,14 +3,16 @@ from datamesh.serializers import JoinRecordSerializer from .fixtures import join_record + @pytest.mark.django_db() def test_join_record_serializer_from_instance(request_factory, join_record): request = request_factory.get('') request.session = { - 'jwt_organization_uuid': join_record.organization.organization_uuid, + 'jwt_organization_uuid': join_record.organization.organization_uuid } - serializer = JoinRecordSerializer(instance=join_record, - context={'request': request}) + serializer = JoinRecordSerializer( + instance=join_record, context={'request': request} + ) keys = [ "join_record_uuid", "record_id", diff --git a/datamesh/tests/test_views.py b/datamesh/tests/test_views.py index f182a620..3510f73e 100644 --- a/datamesh/tests/test_views.py +++ b/datamesh/tests/test_views.py @@ -14,9 +14,10 @@ appointment_logic_module_model, join_record, relationship, - relationship2 + relationship2, ) + @pytest.mark.django_db() class TestJoinRecordBase: @@ -36,7 +37,7 @@ def test_join_record_list_view_minimal( ): join_records = factories.JoinRecord.create_batch( size=5, - **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]} + **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]}, ) request = request_factory.get("") request.user = org_admin @@ -44,8 +45,9 @@ def test_join_record_list_view_minimal( response = views.JoinRecordViewSet.as_view({"get": "list"})(request) assert response.status_code == 200 assert len(response.data) == 5 - assert set([str(jr.join_record_uuid) for jr in join_records]) == \ - set([jr['join_record_uuid'] for jr in response.data]) + assert set([str(jr.join_record_uuid) for jr in join_records]) == set( + [jr['join_record_uuid'] for jr in response.data] + ) def test_join_record_list_view_organization_only( self, @@ -58,12 +60,12 @@ def test_join_record_list_view_organization_only( ): join_records = factories.JoinRecord.create_batch( size=5, - **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]} + **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]}, ) - factories.JoinRecord.create(organization=factories.Organization( - name='Another Organization' - )) + factories.JoinRecord.create( + organization=factories.Organization(name='Another Organization') + ) request = request_factory.get("") request.user = org_admin @@ -76,11 +78,17 @@ def test_join_record_list_view_organization_only( ) def test_join_record_detail_fail_organization_permission( - self, request_factory, org_admin, document_logic_module, - crm_logic_module, document_logic_module_model, appointment_logic_module_model,): - join_record = factories.JoinRecord.create(organization=factories.Organization( - name='Another Organization' - )) + self, + request_factory, + org_admin, + document_logic_module, + crm_logic_module, + document_logic_module_model, + appointment_logic_module_model, + ): + join_record = factories.JoinRecord.create( + organization=factories.Organization(name='Another Organization') + ) request = request_factory.get(str(join_record.join_record_uuid)) request.user = org_admin request.session = self.session @@ -88,17 +96,17 @@ def test_join_record_detail_fail_organization_permission( assert response.status_code == 200 def test_join_record_list_filter_one_record_id( - self, - request_factory, - org_admin, - document_logic_module, - crm_logic_module, - document_logic_module_model, - appointment_logic_module_model, + self, + request_factory, + org_admin, + document_logic_module, + crm_logic_module, + document_logic_module_model, + appointment_logic_module_model, ): join_records = factories.JoinRecord.create_batch( size=5, - **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]} + **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]}, ) query_params = { 'related_record_uuid': join_records[0].related_record_uuid, @@ -110,23 +118,26 @@ def test_join_record_list_filter_one_record_id( response = views.JoinRecordViewSet.as_view({"get": "list"})(request) assert response.status_code == 200 assert len(response.data) == 1 - assert str(join_records[0].join_record_uuid) == response.data[0]["join_record_uuid"] + assert ( + str(join_records[0].join_record_uuid) + == response.data[0]["join_record_uuid"] + ) def test_join_record_list_filter_several_record_uuids( - self, - request_factory, - org_admin, - document_logic_module, - crm_logic_module, - document_logic_module_model, - appointment_logic_module_model, + self, + request_factory, + org_admin, + document_logic_module, + crm_logic_module, + document_logic_module_model, + appointment_logic_module_model, ): join_records = factories.JoinRecord.create_batch( size=5, - **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]} + **{"organization__organization_uuid": TEST_USER_DATA["organization_uuid"]}, ) query_params = { - 'related_record_uuid': f'{join_records[0].related_record_uuid},{join_records[1].related_record_uuid}', + 'related_record_uuid': f'{join_records[0].related_record_uuid},{join_records[1].related_record_uuid}' } request = request_factory.get(f'?{urlencode(query_params)}') request.user = org_admin @@ -134,8 +145,12 @@ def test_join_record_list_filter_several_record_uuids( response = views.JoinRecordViewSet.as_view({"get": "list"})(request) assert response.status_code == 200 assert len(response.data) == 2 - assert set((str(join_records[0].join_record_uuid), str(join_records[1].join_record_uuid))) == set( - [jr["join_record_uuid"] for jr in response.data]) + assert set( + ( + str(join_records[0].join_record_uuid), + str(join_records[1].join_record_uuid), + ) + ) == set([jr["join_record_uuid"] for jr in response.data]) @pytest.mark.django_db() @@ -169,8 +184,12 @@ def test_join_record_create_view( def test_join_record_detail_view(request_factory, join_record, org_admin): request = request_factory.get("") request.user = org_admin - request.session = {"jwt_organization_uuid": str(join_record.organization.organization_uuid)} - response = views.JoinRecordViewSet.as_view({"get": "retrieve"})(request, pk=str(join_record.pk)) + request.session = { + "jwt_organization_uuid": str(join_record.organization.organization_uuid) + } + response = views.JoinRecordViewSet.as_view({"get": "retrieve"})( + request, pk=str(join_record.pk) + ) assert response.status_code == 200 assert str(join_record.pk) == response.data["join_record_uuid"] @@ -180,7 +199,9 @@ def test_join_record_detail_view_no_access(request_factory, join_record, org_adm request = request_factory.get("") request.user = org_admin request.session = {"jwt_organization_uuid": str(uuid.uuid4())} - response = views.JoinRecordViewSet.as_view({"get": "retrieve"})(request, pk=str(join_record.pk)) + response = views.JoinRecordViewSet.as_view({"get": "retrieve"})( + request, pk=str(join_record.pk) + ) assert response.status_code == 404 @@ -188,18 +209,20 @@ def test_join_record_detail_view_no_access(request_factory, join_record, org_adm class TestLogicModuleModelView: expected_keys = { - 'logic_module_model_uuid', - 'logic_module_endpoint_name', - 'model', - 'endpoint', - 'lookup_field_name', - 'is_local', - } - - def test_list_logic_module_models(self, - request_factory, - document_logic_module_model, - appointment_logic_module_model): + 'logic_module_model_uuid', + 'logic_module_endpoint_name', + 'model', + 'endpoint', + 'lookup_field_name', + 'is_local', + } + + def test_list_logic_module_models( + self, + request_factory, + document_logic_module_model, + appointment_logic_module_model, + ): request = request_factory.get(reverse('logicmodulemodel-list')) user = factories.CoreUser() request.user = user @@ -213,7 +236,7 @@ def test_create_logic_module_model(self, request_factory): "logic_module_endpoint_name": "location", "model": "siteprofile", "endpoint": "/siteprofiles/", - "lookup_field_name": "uuid" + "lookup_field_name": "uuid", } request = request_factory.post(reverse('logicmodulemodel-list'), data) user = factories.CoreUser(is_superuser=True) @@ -227,7 +250,7 @@ def test_create_logic_module_model_no_access(self, request_factory): "logic_module_endpoint_name": "location", "model": "siteprofile", "endpoint": "/siteprofiles/", - "lookup_field_name": "uuid" + "lookup_field_name": "uuid", } request = request_factory.post(reverse('logicmodulemodel-list'), data) @@ -236,84 +259,107 @@ def test_create_logic_module_model_no_access(self, request_factory): response = views.LogicModuleModelViewSet.as_view({"post": "create"})(request) assert response.status_code == 403 - def test_detail_logic_module_models(self, request_factory, document_logic_module_model): + def test_detail_logic_module_models( + self, request_factory, document_logic_module_model + ): pk = str(document_logic_module_model.pk) request = request_factory.get(reverse('logicmodulemodel-detail', args=(pk,))) user = factories.CoreUser() request.user = user - response = views.LogicModuleModelViewSet.as_view({"get": "retrieve"})(request, pk=pk) + response = views.LogicModuleModelViewSet.as_view({"get": "retrieve"})( + request, pk=pk + ) assert response.status_code == 200 assert self.expected_keys == set(response.data.keys()) - def test_update_logic_module_model(self, request_factory, document_logic_module_model): + def test_update_logic_module_model( + self, request_factory, document_logic_module_model + ): pk = str(document_logic_module_model.pk) data = { "logic_module_endpoint_name": "document", "model": "document", "endpoint": "/documents/", - "lookup_field_name": "another_lookup_field" + "lookup_field_name": "another_lookup_field", } - request = request_factory.put(reverse('logicmodulemodel-detail', args=(pk,)), data) + request = request_factory.put( + reverse('logicmodulemodel-detail', args=(pk,)), data + ) user = factories.CoreUser(is_superuser=True) request.user = user - response = views.LogicModuleModelViewSet.as_view({"put": "update"})(request, pk=pk) + response = views.LogicModuleModelViewSet.as_view({"put": "update"})( + request, pk=pk + ) assert response.status_code == 200 assert self.expected_keys == set(response.data.keys()) assert response.data['lookup_field_name'] == 'another_lookup_field' - def test_update_logic_module_model_no_access(self, request_factory, document_logic_module_model): + def test_update_logic_module_model_no_access( + self, request_factory, document_logic_module_model + ): pk = str(document_logic_module_model.pk) data = { "logic_module_endpoint_name": "document", "model": "document", "endpoint": "/documents/", - "lookup_field_name": "another_lookup_field" + "lookup_field_name": "another_lookup_field", } - request = request_factory.put(reverse('logicmodulemodel-detail', args=(pk,)), data) + request = request_factory.put( + reverse('logicmodulemodel-detail', args=(pk,)), data + ) user = factories.CoreUser() request.user = user - response = views.LogicModuleModelViewSet.as_view({"put": "update"})(request, pk=pk) + response = views.LogicModuleModelViewSet.as_view({"put": "update"})( + request, pk=pk + ) assert response.status_code == 403 - def test_patch_logic_module_model(self, request_factory, document_logic_module_model): + def test_patch_logic_module_model( + self, request_factory, document_logic_module_model + ): pk = str(document_logic_module_model.pk) - data = { - "lookup_field_name": "another_lookup_field" - } - request = request_factory.patch(reverse('logicmodulemodel-detail', args=(pk,)), data) + data = {"lookup_field_name": "another_lookup_field"} + request = request_factory.patch( + reverse('logicmodulemodel-detail', args=(pk,)), data + ) user = factories.CoreUser(is_superuser=True) request.user = user - response = views.LogicModuleModelViewSet.as_view({"patch": "partial_update"})(request, pk=pk) + response = views.LogicModuleModelViewSet.as_view({"patch": "partial_update"})( + request, pk=pk + ) assert response.status_code == 200 assert self.expected_keys == set(response.data.keys()) assert response.data['lookup_field_name'] == 'another_lookup_field' - def test_delete_logic_module_models(self, request_factory, document_logic_module_model): + def test_delete_logic_module_models( + self, request_factory, document_logic_module_model + ): pk = str(document_logic_module_model.pk) request = request_factory.delete(reverse('logicmodulemodel-detail', args=(pk,))) user = factories.CoreUser(is_superuser=True) request.user = user - response = views.LogicModuleModelViewSet.as_view({"delete": "destroy"})(request, pk=pk) + response = views.LogicModuleModelViewSet.as_view({"delete": "destroy"})( + request, pk=pk + ) assert response.status_code == 204 - def test_delete_logic_module_models_no_access(self, request_factory, document_logic_module_model): + def test_delete_logic_module_models_no_access( + self, request_factory, document_logic_module_model + ): pk = str(document_logic_module_model.pk) request = request_factory.delete(reverse('logicmodulemodel-detail', args=(pk,))) user = factories.CoreUser() request.user = user - response = views.LogicModuleModelViewSet.as_view({"delete": "destroy"})(request, pk=pk) + response = views.LogicModuleModelViewSet.as_view({"delete": "destroy"})( + request, pk=pk + ) assert response.status_code == 403 @pytest.mark.django_db() class TestRelationshipView: - expected_keys = { - 'relationship_uuid', - 'origin_model', - 'related_model', - 'key', - } + expected_keys = {'relationship_uuid', 'origin_model', 'related_model', 'key'} def test_list_relationships(self, request_factory, relationship, relationship2): request = request_factory.get(reverse('relationship-list')) @@ -323,17 +369,25 @@ def test_list_relationships(self, request_factory, relationship, relationship2): assert response.status_code == 200 assert len(response.data) == 2 assert set(response.data[0].keys()) == self.expected_keys - assert set(response.data[0]['origin_model'].keys()) == TestLogicModuleModelView.expected_keys - assert set(response.data[0]['related_model'].keys()) == TestLogicModuleModelView.expected_keys - - def test_create_relationship(self, - request_factory, - document_logic_module_model, - appointment_logic_module_model): - data= { + assert ( + set(response.data[0]['origin_model'].keys()) + == TestLogicModuleModelView.expected_keys + ) + assert ( + set(response.data[0]['related_model'].keys()) + == TestLogicModuleModelView.expected_keys + ) + + def test_create_relationship( + self, + request_factory, + document_logic_module_model, + appointment_logic_module_model, + ): + data = { "origin_model_id": document_logic_module_model.pk, "related_model_id": appointment_logic_module_model.pk, - "key": "document_appointment_rel" + "key": "document_appointment_rel", } request = request_factory.post(reverse('relationship-list'), data) user = factories.CoreUser(is_superuser=True) @@ -342,14 +396,16 @@ def test_create_relationship(self, assert response.status_code == 201 assert self.expected_keys == set(response.data.keys()) - def test_create_relationship_no_access(self, - request_factory, - document_logic_module_model, - appointment_logic_module_model): - data= { + def test_create_relationship_no_access( + self, + request_factory, + document_logic_module_model, + appointment_logic_module_model, + ): + data = { "origin_model_id": document_logic_module_model.pk, "related_model_id": appointment_logic_module_model.pk, - "key": "document_appointment_rel" + "key": "document_appointment_rel", } request = request_factory.post(reverse('relationship-list'), data) user = factories.CoreUser() @@ -362,7 +418,9 @@ def test_detail_relationship(self, request_factory, relationship): request = request_factory.get(reverse('relationship-detail', args=(pk,))) user = factories.CoreUser() request.user = user - response = views.RelationshiplViewSet.as_view({"get": "retrieve"})(request, pk=pk) + response = views.RelationshiplViewSet.as_view({"get": "retrieve"})( + request, pk=pk + ) assert response.status_code == 200 assert self.expected_keys == set(response.data.keys()) @@ -371,7 +429,7 @@ def test_update_relationship(self, request_factory, relationship): data = { "origin_model_id": relationship.origin_model.pk, "related_model_id": relationship.related_model.pk, - "key": "another_key_for_this_rel" + "key": "another_key_for_this_rel", } request = request_factory.put(reverse('relationship-detail', args=(pk,)), data) user = factories.CoreUser(is_superuser=True) @@ -386,7 +444,7 @@ def test_update_relationship_no_access(self, request_factory, relationship): data = { "origin_model_id": relationship.origin_model.pk, "related_model_id": relationship.related_model.pk, - "key": "another_key_for_this_rel" + "key": "another_key_for_this_rel", } request = request_factory.put(reverse('relationship-detail', args=(pk,)), data) user = factories.CoreUser() @@ -396,13 +454,15 @@ def test_update_relationship_no_access(self, request_factory, relationship): def test_patch_relationship(self, request_factory, relationship): pk = str(relationship.pk) - data = { - "key": "another_key_for_this_rel" - } - request = request_factory.patch(reverse('relationship-detail', args=(pk,)), data) + data = {"key": "another_key_for_this_rel"} + request = request_factory.patch( + reverse('relationship-detail', args=(pk,)), data + ) user = factories.CoreUser(is_superuser=True) request.user = user - response = views.RelationshiplViewSet.as_view({"patch": "partial_update"})(request, pk=pk) + response = views.RelationshiplViewSet.as_view({"patch": "partial_update"})( + request, pk=pk + ) assert response.status_code == 200 assert self.expected_keys == set(response.data.keys()) assert response.data['key'] == 'another_key_for_this_rel' @@ -412,7 +472,9 @@ def test_delete_relationship(self, request_factory, relationship): request = request_factory.delete(reverse('relationship-detail', args=(pk,))) user = factories.CoreUser(is_superuser=True) request.user = user - response = views.RelationshiplViewSet.as_view({"delete": "destroy"})(request, pk=pk) + response = views.RelationshiplViewSet.as_view({"delete": "destroy"})( + request, pk=pk + ) assert response.status_code == 204 def test_delete_relationship_no_access(self, request_factory, relationship): @@ -420,5 +482,7 @@ def test_delete_relationship_no_access(self, request_factory, relationship): request = request_factory.delete(reverse('relationship-detail', args=(pk,))) user = factories.CoreUser() request.user = user - response = views.RelationshiplViewSet.as_view({"delete": "destroy"})(request, pk=pk) + response = views.RelationshiplViewSet.as_view({"delete": "destroy"})( + request, pk=pk + ) assert response.status_code == 403 diff --git a/datamesh/utils.py b/datamesh/utils.py index 3ae80d51..90c57693 100644 --- a/datamesh/utils.py +++ b/datamesh/utils.py @@ -3,17 +3,21 @@ from datamesh.models import Relationship, JoinRecord, LogicModuleModel -def prepare_lookup_kwargs(is_forward_lookup: bool, - relationship: Relationship, - join_record: JoinRecord) -> Tuple[LogicModuleModel, str]: +def prepare_lookup_kwargs( + is_forward_lookup: bool, relationship: Relationship, join_record: JoinRecord +) -> Tuple[LogicModuleModel, str]: """Find out if pk is id or uuid and prepare lookup according to direction.""" if is_forward_lookup: related_model = relationship.related_model - related_record_field = 'related_record_id' if join_record.related_record_id is not None\ + related_record_field = ( + 'related_record_id' + if join_record.related_record_id is not None else 'related_record_uuid' + ) else: related_model = relationship.origin_model - related_record_field = 'record_id' if join_record.record_id is not None\ - else 'record_uuid' + related_record_field = ( + 'record_id' if join_record.record_id is not None else 'record_uuid' + ) return related_model, related_record_field diff --git a/datamesh/views.py b/datamesh/views.py index 737b7bed..eb62a093 100644 --- a/datamesh/views.py +++ b/datamesh/views.py @@ -4,7 +4,11 @@ from .filters import JoinRecordFilter from .mixins import OrganizationQuerySetMixin from .models import JoinRecord, LogicModuleModel, Relationship -from .serializers import JoinRecordSerializer, LogicModuleModelSerializer, RelationshipSerializer +from .serializers import ( + JoinRecordSerializer, + LogicModuleModelSerializer, + RelationshipSerializer, +) from workflow.permissions import IsSuperUserOrReadOnly @@ -20,15 +24,16 @@ class RelationshiplViewSet(viewsets.ModelViewSet): permission_classes = (IsSuperUserOrReadOnly,) -class JoinRecordViewSet(OrganizationQuerySetMixin, - viewsets.ModelViewSet): +class JoinRecordViewSet(OrganizationQuerySetMixin, viewsets.ModelViewSet): queryset = JoinRecord.objects.all() serializer_class = JoinRecordSerializer filter_backends = (DjangoFilterBackend,) filter_class = JoinRecordFilter - filter_fields = ('relationship__key', - 'record_id', - 'record_uuid', - 'related_record_id', - 'related_record_uuid',) + filter_fields = ( + 'relationship__key', + 'record_id', + 'record_uuid', + 'related_record_id', + 'related_record_uuid', + ) diff --git a/docs/conf.py b/docs/conf.py index e685783f..aec4c1fe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -109,15 +109,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -127,8 +124,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'BuildlyCore.tex', u'Buildly Core Documentation', - u'Buildly.io', 'manual'), + ( + master_doc, + 'BuildlyCore.tex', + u'Buildly Core Documentation', + u'Buildly.io', + 'manual', + ) ] @@ -136,10 +138,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'buildlycore', u'Buildly Core Documentation', - [author], 1) -] +man_pages = [(master_doc, 'buildlycore', u'Buildly Core Documentation', [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -148,9 +147,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'BuildlyCore', u'Buildly Core Documentation', - author, 'BuildlyCore', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + 'BuildlyCore', + u'Buildly Core Documentation', + author, + 'BuildlyCore', + 'One line description of project.', + 'Miscellaneous', + ) ] diff --git a/factories/core_models.py b/factories/core_models.py index 3aae9e14..b2f25897 100644 --- a/factories/core_models.py +++ b/factories/core_models.py @@ -5,7 +5,7 @@ CoreUser as CoreUserM, CoreGroup as CoreGroupM, LogicModule as LogicModuleM, - Organization as OrganizationM + Organization as OrganizationM, ) diff --git a/factories/datamesh_models.py b/factories/datamesh_models.py index d67f4ce9..1ce8badb 100644 --- a/factories/datamesh_models.py +++ b/factories/datamesh_models.py @@ -4,15 +4,18 @@ from factory import DjangoModelFactory, SubFactory, LazyAttribute -from datamesh.models import (LogicModuleModel as LogicModulModelM, - Relationship as RelationshipM, - JoinRecord as JoinRecordM) +from datamesh.models import ( + LogicModuleModel as LogicModulModelM, + Relationship as RelationshipM, + JoinRecord as JoinRecordM, +) from factories import Organization class LogicModuleModel(DjangoModelFactory): logic_module_endpoint_name = LazyAttribute( - lambda o: ''.join(random.choices(string.ascii_uppercase + string.digits, k=16))) + lambda o: ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) + ) lookup_field_name = 'id' class Meta: diff --git a/factories/oauth2_models.py b/factories/oauth2_models.py index b4d3db89..1d6fadab 100644 --- a/factories/oauth2_models.py +++ b/factories/oauth2_models.py @@ -4,7 +4,8 @@ from oauth2_provider.models import ( Application as ApplicationM, AccessToken as AccessTokenM, - RefreshToken as RefreshTokenM) + RefreshToken as RefreshTokenM, +) from .workflow_models import CoreUser diff --git a/factories/workflow_models.py b/factories/workflow_models.py index b3f0071d..b989b054 100644 --- a/factories/workflow_models.py +++ b/factories/workflow_models.py @@ -7,7 +7,8 @@ WorkflowLevel2Sort as WorkflowLevel2SortM, Internationalization as InternationalizationM, WorkflowLevelType as WorkflowLevelTypeM, - WorkflowLevelStatus as WorkflowLevelStatusM) + WorkflowLevelStatus as WorkflowLevelStatusM, +) from .django_models import Group from .core_models import CoreUser, Organization diff --git a/gateway/aggregator.py b/gateway/aggregator.py index f74e7329..0fb723d7 100644 --- a/gateway/aggregator.py +++ b/gateway/aggregator.py @@ -28,19 +28,25 @@ def get_aggregate_swagger(self) -> dict: # Get the swagger.json try: # Use stored specification of the module - spec_dict = LogicModule.objects.values_list( - 'api_specification', flat=True).filter(endpoint_name=endpoint_name).first() + spec_dict = ( + LogicModule.objects.values_list( + 'api_specification', flat=True + ) + .filter(endpoint_name=endpoint_name) + .first() + ) # Pull specification of the module from its service and store it if spec_dict is None: response = utils.get_swagger_from_url(schema_url) spec_dict = response.json() - LogicModule.objects.filter(endpoint_name=endpoint_name).update( - api_specification=spec_dict) + LogicModule.objects.filter( + endpoint_name=endpoint_name + ).update(api_specification=spec_dict) swagger_apis[endpoint_name] = { 'spec': spec_dict, - 'url': schema_url + 'url': schema_url, } except ConnectionError as error: logger.warning(error) @@ -50,8 +56,7 @@ def get_aggregate_swagger(self) -> dict: logger.info('Cannot remove {} from errors'.format(schema_url)) return swagger_apis - def _update_specification(self, name: str, api_name: str, - api_spec: dict) -> dict: + def _update_specification(self, name: str, api_name: str, api_spec: dict) -> dict: """ Update names of the specification of a service's API @@ -83,19 +88,23 @@ def merge_aggregates(self) -> dict: 'consumes': self.configuration.get('consumes', None), 'produces': self.configuration.get('produces', None), 'definitions': {}, - 'paths': {} + 'paths': {}, } swagger_apis = self.get_aggregate_swagger() for api, api_spec in swagger_apis.items(): # Rename definition to avoid collision. - api_spec['spec'] = json.loads(json.dumps(api_spec['spec']).replace( - '#/definitions/', u'#/definitions/{}'.format(api))) + api_spec['spec'] = json.loads( + json.dumps(api_spec['spec']).replace( + '#/definitions/', u'#/definitions/{}'.format(api) + ) + ) # update the definitions if 'definitions' in api_spec['spec']: - basic_swagger['definitions'].update(self._update_specification( - 'definitions', api, api_spec)) + basic_swagger['definitions'].update( + self._update_specification('definitions', api, api_spec) + ) # update the paths to match with the gateway if 'paths' in api_spec['spec']: @@ -103,8 +112,9 @@ def merge_aggregates(self) -> dict: basic_swagger['paths'].update(api_spec['spec']['paths']) else: api_name = '/{}'.format(api) - basic_swagger['paths'].update(self._update_specification( - 'paths', api_name, api_spec)) + basic_swagger['paths'].update( + self._update_specification('paths', api_name, api_spec) + ) return basic_swagger @@ -121,7 +131,8 @@ def generate_operation_id(self, swagger): if 'operationId' in action_spec: current_op_id = action_spec['operationId'] action_spec['operationId'] = '{}.{}'.format( - service_name, current_op_id) + service_name, current_op_id + ) def generate_swagger(self): """ diff --git a/gateway/clients.py b/gateway/clients.py index 4af27982..255af314 100644 --- a/gateway/clients.py +++ b/gateway/clients.py @@ -28,7 +28,10 @@ def request(self, **kwargs): def is_valid_for_cache(self) -> bool: """ Checks if request is valid for caching operations """ - return self._in_request.method.lower() == 'get' and not self._in_request.query_params + return ( + self._in_request.method.lower() == 'get' + and not self._in_request.query_params + ) def prepare_data(self, spec: Spec, **kwargs) -> Tuple[str, str]: """ Parse request URL, validates operation, and returns method and URL for outgoing request""" @@ -52,7 +55,9 @@ def prepare_data(self, spec: Spec, **kwargs) -> Tuple[str, str]: operation = spec.get_op_for_request('GET', path) operation.http_method = request_method else: - raise exceptions.EndpointNotFound(f'Endpoint not found: {self._in_request.method} {path}') + raise exceptions.EndpointNotFound( + f'Endpoint not found: {self._in_request.method} {path}' + ) method = operation.http_method.lower() path_name = operation.path_name @@ -81,17 +86,21 @@ def get_request_data(self) -> dict: data.pop('aggregate', None) data.pop('join', None) - query_dict_body = self._in_request.data if hasattr(self._in_request, 'data') else dict() - body = query_dict_body.dict() if isinstance(query_dict_body, QueryDict) else query_dict_body + query_dict_body = ( + self._in_request.data if hasattr(self._in_request, 'data') else dict() + ) + body = ( + query_dict_body.dict() + if isinstance(query_dict_body, QueryDict) + else query_dict_body + ) data.update(body) # handle uploaded files if self._in_request.FILES: for key, value in self._in_request.FILES.items(): data[key] = { - 'header': { - 'Content-Type': value.content_type, - }, + 'header': {'Content-Type': value.content_type}, 'data': value, 'filename': value.name, } @@ -101,7 +110,7 @@ def get_request_data(self) -> dict: def get_headers(self) -> dict: """Get data and headers from the incoming request.""" headers = { - 'Authorization': get_authorization_header(self._in_request).decode('utf-8'), + 'Authorization': get_authorization_header(self._in_request).decode('utf-8') } if self._in_request.content_type == 'application/json': headers['content-type'] = 'application/json' @@ -126,15 +135,19 @@ def request(self, **kwargs) -> Tuple[Any, int, Dict[str, str]]: # Make request to the service method = getattr(requests, method) try: - response = method(url, - headers=self.get_headers(), - params=self._in_request.query_params, - data=self.get_request_data(), - files=self._in_request.FILES) + response = method( + url, + headers=self.get_headers(), + params=self._in_request.query_params, + data=self.get_request_data(), + files=self._in_request.FILES, + ) except Exception as e: - error_msg = (f'An error occurred when redirecting the request to ' - f'or receiving the response from the service.\n' - f'Origin: ({e.__class__.__name__}: {e})') + error_msg = ( + f'An error occurred when redirecting the request to ' + f'or receiving the response from the service.\n' + f'Origin: ({e.__class__.__name__}: {e})' + ) raise exceptions.GatewayError(error_msg) try: diff --git a/gateway/generator.py b/gateway/generator.py index 9bbeb725..d1b2d630 100644 --- a/gateway/generator.py +++ b/gateway/generator.py @@ -9,18 +9,18 @@ class OpenAPISchemaGenerator(drf_gen.OpenAPISchemaGenerator): def get_schema(self, request=None, public=False): schema_urls = utils.get_swagger_urls() config_aggregator = { - 'info': { - 'title': 'API Gateway', - 'description': '', - 'version': '1.0' - }, + 'info': {'title': 'API Gateway', 'description': '', 'version': '1.0'}, 'apis': schema_urls, - 'produces': ['application/json', - 'application/x-www-form-urlencoded', - 'multipart/form-data'], - 'consumes': ['application/json', - 'application/x-www-form-urlencoded', - 'multipart/form-data'], + 'produces': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data', + ], + 'consumes': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data', + ], } sw_aggregator = SwaggerAggregator(config_aggregator) swagger_spec = sw_aggregator.generate_swagger() @@ -40,8 +40,7 @@ def get_schema(self, request=None, public=False): paths=paths, consumes=swagger_spec['consumes'], produces=swagger_spec['produces'], - security_definitions=swagger_spec.get( - 'security_definitions', None), + security_definitions=swagger_spec.get('security_definitions', None), security=swagger_spec.get('security', None), _url=url, _version=self.version, diff --git a/gateway/permissions.py b/gateway/permissions.py index 2bb27951..6d8861b8 100644 --- a/gateway/permissions.py +++ b/gateway/permissions.py @@ -15,7 +15,9 @@ class AllowLogicModuleGroup(permissions.BasePermission): @staticmethod def _get_logic_module(service_name: str) -> LogicModule: try: - return LogicModule.objects.prefetch_related('core_groups').get(endpoint_name=service_name) + return LogicModule.objects.prefetch_related('core_groups').get( + endpoint_name=service_name + ) except LogicModule.DoesNotExist: raise ServiceDoesNotExist(f'Service "{service_name}" not found.') @@ -28,19 +30,31 @@ def has_permission(self, request, view): service_name = view.kwargs['service'] logic_module = self._get_logic_module(service_name=service_name) - logic_module_group = logic_module.core_groups.filter(Q(is_global=True) | - Q(organization=request.user.organization, - is_global=False, is_org_level=True)) + logic_module_group = logic_module.core_groups.filter( + Q(is_global=True) + | Q( + organization=request.user.organization, + is_global=False, + is_org_level=True, + ) + ) if logic_module_group: # default permission is no access '0000' viewonly_display_permissions = '{0:04b}'.format(PERMISSIONS_NO_ACCESS) - global_permissions, org_permissions = viewonly_display_permissions, viewonly_display_permissions + global_permissions, org_permissions = ( + viewonly_display_permissions, + viewonly_display_permissions, + ) for group in logic_module_group: if group.is_global: - global_permissions = merge_permissions(global_permissions, group.display_permissions) + global_permissions = merge_permissions( + global_permissions, group.display_permissions + ) elif group.is_org_level: - org_permissions = merge_permissions(org_permissions, group.display_permissions) + org_permissions = merge_permissions( + org_permissions, group.display_permissions + ) method = request.META['REQUEST_METHOD'] if has_permission(global_permissions, method): diff --git a/gateway/request.py b/gateway/request.py index a7c40ae8..c947292a 100644 --- a/gateway/request.py +++ b/gateway/request.py @@ -57,9 +57,13 @@ def _get_logic_module(self, service_name: str) -> LogicModule: """ Retrieve LogicModule by service name. """ if service_name not in self._logic_modules: try: - self._logic_modules[service_name] = LogicModule.objects.get(endpoint_name=service_name) + self._logic_modules[service_name] = LogicModule.objects.get( + endpoint_name=service_name + ) except LogicModule.DoesNotExist: - raise exceptions.ServiceDoesNotExist(f'Service "{service_name}" not found.') + raise exceptions.ServiceDoesNotExist( + f'Service "{service_name}" not found.' + ) return self._logic_modules[service_name] def get_datamesh(self) -> DataMesh: @@ -69,11 +73,13 @@ def get_datamesh(self) -> DataMesh: # find out forwards relations through logic module from request as origin padding = self.request.path.index(f'/{logic_module.endpoint_name}') - endpoint = self.request.path[len(f'/{logic_module.endpoint_name}')+padding:] - endpoint = endpoint[:endpoint.index('/', 1) + 1] - return DataMesh(logic_module_endpoint=logic_module.endpoint_name, - model_endpoint=endpoint, - access_validator=utils.ObjectAccessValidator(self.request)) + endpoint = self.request.path[len(f'/{logic_module.endpoint_name}') + padding:] + endpoint = endpoint[: endpoint.index('/', 1) + 1] + return DataMesh( + logic_module_endpoint=logic_module.endpoint_name, + model_endpoint=endpoint, + access_validator=utils.ObjectAccessValidator(self.request), + ) class GatewayRequest(BaseGatewayRequest): @@ -89,7 +95,9 @@ def perform(self) -> GatewayResponse: try: spec = self._get_swagger_spec(self.url_kwargs['service']) except exceptions.ServiceDoesNotExist as e: - return GatewayResponse(e.content, e.status, {'Content-Type': e.content_type}) + return GatewayResponse( + e.content, e.status, {'Content-Type': e.content_type} + ) # create a client for performing data requests client = SwaggerClient(spec, self.request) @@ -98,16 +106,27 @@ def perform(self) -> GatewayResponse: content, status_code, headers = client.request(**self.url_kwargs) # aggregate/join with the JoinRecord-models - if 'join' in self.request.query_params and status_code == 200 and type(content) in [dict, list]: + if ( + 'join' in self.request.query_params + and status_code == 200 + and type(content) in [dict, list] + ): try: self._join_response_data(resp_data=content) except exceptions.ServiceDoesNotExist as e: logger.error(e.content) path_url = self.request.path # Get request path - list_string_path = path_url.split("/") # Split the request path to check if custody include in it - if ('join' not in self.request.query_params and 'custody' in list_string_path and - status_code == 201 and type(content) in [dict, list] and self.request.method == 'POST'): + list_string_path = path_url.split( + "/" + ) # Split the request path to check if custody include in it + if ( + 'join' not in self.request.query_params + and 'custody' in list_string_path + and status_code == 201 + and type(content) in [dict, list] + and self.request.method == 'POST' + ): # This functionality will execute only when request include custody with post request, # It will not execute if its join request related_organization = content.get('organization_uuid') @@ -119,6 +138,7 @@ def perform(self) -> GatewayResponse: organization_list = consortium.organization_uuids if related_organization: import uuid + org_uuid = uuid.UUID(related_organization) if org_uuid not in organization_list: # To avoid repeated organization uuid adding in consortium organization uuid @@ -127,7 +147,9 @@ def perform(self) -> GatewayResponse: consortium.save() else: # If consortium does not exists for shipment name, then create consortium - Consortium.objects.create(name=shipment_name, organization_uuids=[related_organization]) + Consortium.objects.create( + name=shipment_name, organization_uuids=[related_organization] + ) if type(content) in [dict, list]: content = json.dumps(content, cls=utils.GatewayJSONEncoder) @@ -191,14 +213,18 @@ def perform(self) -> GatewayResponse: result = {} asyncio.run(self.async_perform(result)) if 'response' not in result: - raise exceptions.GatewayError('Error performing asynchronous gateway request') + raise exceptions.GatewayError( + 'Error performing asynchronous gateway request' + ) return result['response'] async def async_perform(self, result: dict): try: spec = await self._get_swagger_spec(self.url_kwargs['service']) except exceptions.ServiceDoesNotExist as e: - return GatewayResponse(e.content, e.status, {'Content-Type': e.content_type}) + return GatewayResponse( + e.content, e.status, {'Content-Type': e.content_type} + ) # create a client for performing data requests client = AsyncSwaggerClient(spec, self.request) @@ -207,7 +233,11 @@ async def async_perform(self, result: dict): content, status_code, headers = await client.request(**self.url_kwargs) # aggregate/join with the JoinRecord-models - if 'join' in self.request.query_params and status_code == 200 and type(content) in [dict, list]: + if ( + 'join' in self.request.query_params + and status_code == 200 + and type(content) in [dict, list] + ): try: await self._join_response_data(resp_data=content) except exceptions.ServiceDoesNotExist as e: diff --git a/gateway/tests/fixtures.py b/gateway/tests/fixtures.py index 2c668397..dc243a8c 100644 --- a/gateway/tests/fixtures.py +++ b/gateway/tests/fixtures.py @@ -8,23 +8,37 @@ @pytest.fixture() def datamesh(): - lm1 = factories.LogicModule.create(name='location', endpoint_name='location', - endpoint='http://locationservice:8080') - lm2 = factories.LogicModule.create(name='documents', endpoint_name='documents', - endpoint='http://documentservice:8080') - lmm1 = factories.LogicModuleModel(logic_module_endpoint_name=lm1.endpoint_name, model='SiteProfile', - endpoint='/siteprofiles/', lookup_field_name='uuid') - lmm2 = factories.LogicModuleModel(logic_module_endpoint_name=lm2.endpoint_name, model='Document', - endpoint='/documents/', lookup_field_name='id') - relationship = factories.Relationship(origin_model=lmm1, related_model=lmm2, key='documents') + lm1 = factories.LogicModule.create( + name='location', + endpoint_name='location', + endpoint='http://locationservice:8080', + ) + lm2 = factories.LogicModule.create( + name='documents', + endpoint_name='documents', + endpoint='http://documentservice:8080', + ) + lmm1 = factories.LogicModuleModel( + logic_module_endpoint_name=lm1.endpoint_name, + model='SiteProfile', + endpoint='/siteprofiles/', + lookup_field_name='uuid', + ) + lmm2 = factories.LogicModuleModel( + logic_module_endpoint_name=lm2.endpoint_name, + model='Document', + endpoint='/documents/', + lookup_field_name='id', + ) + relationship = factories.Relationship( + origin_model=lmm1, related_model=lmm2, key='documents' + ) return lm1, lm2, relationship @pytest.fixture def aggregator(logic_module): configuration = { - 'apis': { - logic_module.endpoint_name: f'{logic_module.endpoint}/swagger.json' - } + 'apis': {logic_module.endpoint_name: f'{logic_module.endpoint}/swagger.json'} } return SwaggerAggregator(configuration) diff --git a/gateway/tests/test_permissions.py b/gateway/tests/test_permissions.py index 04db1609..d5a6f23b 100644 --- a/gateway/tests/test_permissions.py +++ b/gateway/tests/test_permissions.py @@ -7,59 +7,72 @@ from gateway.exceptions import ServiceDoesNotExist from gateway.permissions import AllowLogicModuleGroup from gateway.views import APIGatewayView -from core.tests.fixtures import auth_api_client, auth_superuser_api_client, core_group, logic_module, org, org_admin,\ - superuser +from core.tests.fixtures import ( + auth_api_client, + auth_superuser_api_client, + core_group, + logic_module, + org, + org_admin, + superuser, +) @pytest.mark.django_db() class TestAllowLogicModuleGroup: - - def test_has_permission_superuser_success(self, auth_superuser_api_client, superuser): + def test_has_permission_superuser_success( + self, auth_superuser_api_client, superuser + ): """ Superusers are able to access all services """ - request = WSGIRequest(auth_superuser_api_client._base_environ(**auth_superuser_api_client._credentials)) + request = WSGIRequest( + auth_superuser_api_client._base_environ( + **auth_superuser_api_client._credentials + ) + ) request.user = superuser - kwargs = { - 'kwargs': {'service': 'test'}, - 'request': request - } + kwargs = {'kwargs': {'service': 'test'}, 'request': request} permission_obj = AllowLogicModuleGroup() view = APIGatewayView(**kwargs) result = permission_obj.has_permission(kwargs['request'], view) assert result - def test_has_permission_normal_user_service_doesnt_exist(self, auth_api_client, org_admin, org): - request = WSGIRequest(auth_api_client._base_environ(**auth_api_client._credentials)) + def test_has_permission_normal_user_service_doesnt_exist( + self, auth_api_client, org_admin, org + ): + request = WSGIRequest( + auth_api_client._base_environ(**auth_api_client._credentials) + ) request.user = org_admin - kwargs = { - 'kwargs': {'service': 'test'}, - 'request': request - } + kwargs = {'kwargs': {'service': 'test'}, 'request': request} permission_obj = AllowLogicModuleGroup() view = APIGatewayView(**kwargs) with pytest.raises(ServiceDoesNotExist): permission_obj.has_permission(kwargs['request'], view) - def test_has_permission_normal_user_no_restrictions(self, auth_api_client, org_admin, org, logic_module): + def test_has_permission_normal_user_no_restrictions( + self, auth_api_client, org_admin, org, logic_module + ): """ Services without permission groups can be accessed by everybody """ - request = WSGIRequest(auth_api_client._base_environ(**auth_api_client._credentials)) + request = WSGIRequest( + auth_api_client._base_environ(**auth_api_client._credentials) + ) request.user = org_admin - kwargs = { - 'kwargs': {'service': logic_module.name}, - 'request': request - } + kwargs = {'kwargs': {'service': logic_module.name}, 'request': request} permission_obj = AllowLogicModuleGroup() view = APIGatewayView(**kwargs) result = permission_obj.has_permission(kwargs['request'], view) assert result - def test_has_permission_global_level_permission(self, auth_api_client, org_admin, org, logic_module, core_group): + def test_has_permission_global_level_permission( + self, auth_api_client, org_admin, org, logic_module, core_group + ): """ Global level permissions of a service are applied to all users who try to access it """ @@ -69,39 +82,40 @@ def test_has_permission_global_level_permission(self, auth_api_client, org_admin core_group.save() logic_module.core_groups.add(core_group) - request = WSGIRequest(auth_api_client._base_environ(**auth_api_client._credentials)) + request = WSGIRequest( + auth_api_client._base_environ(**auth_api_client._credentials) + ) request.user = org_admin - kwargs = { - 'kwargs': {'service': logic_module.name}, - 'request': request - } + kwargs = {'kwargs': {'service': logic_module.name}, 'request': request} permission_obj = AllowLogicModuleGroup() view = APIGatewayView(**kwargs) result = permission_obj.has_permission(kwargs['request'], view) assert result - def test_has_permission_normal_user_org_level_permission(self, auth_api_client, org_admin, org, logic_module): + def test_has_permission_normal_user_org_level_permission( + self, auth_api_client, org_admin, org, logic_module + ): """ Organization level permissions of a service are applied to users of a specific org """ org_admin_group = org_admin.core_groups.all().first() logic_module.core_groups.add(org_admin_group) - request = WSGIRequest(auth_api_client._base_environ(**auth_api_client._credentials)) + request = WSGIRequest( + auth_api_client._base_environ(**auth_api_client._credentials) + ) request.user = org_admin - kwargs = { - 'kwargs': {'service': logic_module.name}, - 'request': request - } + kwargs = {'kwargs': {'service': logic_module.name}, 'request': request} permission_obj = AllowLogicModuleGroup() view = APIGatewayView(**kwargs) result = permission_obj.has_permission(kwargs['request'], view) assert result - def test_has_permission_normal_user_org_level_diff_org(self, auth_api_client, org_admin, org, logic_module, - core_group): + def test_has_permission_normal_user_org_level_diff_org( + self, auth_api_client, org_admin, org, logic_module, core_group + ): """ Organization level permissions of a service aren't applied to users from an org different to the one from the permissions @@ -112,12 +126,11 @@ def test_has_permission_normal_user_org_level_diff_org(self, auth_api_client, or core_group.save() logic_module.core_groups.add(core_group) - request = WSGIRequest(auth_api_client._base_environ(**auth_api_client._credentials)) + request = WSGIRequest( + auth_api_client._base_environ(**auth_api_client._credentials) + ) request.user = org_admin - kwargs = { - 'kwargs': {'service': logic_module.name}, - 'request': request - } + kwargs = {'kwargs': {'service': logic_module.name}, 'request': request} permission_obj = AllowLogicModuleGroup() view = APIGatewayView(**kwargs) diff --git a/gateway/tests/test_swagger_aggregator.py b/gateway/tests/test_swagger_aggregator.py index fa599ec1..d0a1a3a1 100644 --- a/gateway/tests/test_swagger_aggregator.py +++ b/gateway/tests/test_swagger_aggregator.py @@ -5,10 +5,12 @@ from gateway import utils from gateway.tests.fixtures import aggregator, logic_module + @pytest.mark.django_db() class TestSwaggerAggregator: - - def test_get_aggregate_swagger_without_api_specification(self, aggregator, logic_module, monkeypatch): + def test_get_aggregate_swagger_without_api_specification( + self, aggregator, logic_module, monkeypatch + ): test_swagger = {'name': 'test'} mocked_swagger = Mock() mocked_swagger.return_value.json.return_value = test_swagger @@ -22,7 +24,9 @@ def test_get_aggregate_swagger_without_api_specification(self, aggregator, logic mocked_swagger.assert_called_once() - def test_get_aggregate_swagger_with_api_specification(self, aggregator, logic_module, monkeypatch): + def test_get_aggregate_swagger_with_api_specification( + self, aggregator, logic_module, monkeypatch + ): test_swagger = {'name': 'test'} logic_module.api_specification = test_swagger logic_module.save() @@ -38,7 +42,9 @@ def test_get_aggregate_swagger_with_api_specification(self, aggregator, logic_mo mocked_swagger.assert_not_called() - def test_get_aggregate_swagger_connection_error(self, aggregator, logic_module, monkeypatch): + def test_get_aggregate_swagger_connection_error( + self, aggregator, logic_module, monkeypatch + ): mocked_swagger = Mock(side_effect=ConnectionError) monkeypatch.setattr(utils, 'get_swagger_from_url', mocked_swagger) @@ -46,7 +52,9 @@ def test_get_aggregate_swagger_connection_error(self, aggregator, logic_module, assert result == {} mocked_swagger.assert_called_once() - def test_get_aggregate_swagger_timeout_error(self, aggregator, logic_module, monkeypatch): + def test_get_aggregate_swagger_timeout_error( + self, aggregator, logic_module, monkeypatch + ): mocked_swagger = Mock(side_effect=TimeoutError) monkeypatch.setattr(utils, 'get_swagger_from_url', mocked_swagger) @@ -54,7 +62,9 @@ def test_get_aggregate_swagger_timeout_error(self, aggregator, logic_module, mon assert result == {} mocked_swagger.assert_called_once() - def test_get_aggregate_swagger_value_error(self, aggregator, logic_module, monkeypatch): + def test_get_aggregate_swagger_value_error( + self, aggregator, logic_module, monkeypatch + ): mocked_swagger = Mock(side_effect=ValueError) monkeypatch.setattr(utils, 'get_swagger_from_url', mocked_swagger) diff --git a/gateway/tests/test_urls.py b/gateway/tests/test_urls.py index d61b7d15..13dc788e 100644 --- a/gateway/tests/test_urls.py +++ b/gateway/tests/test_urls.py @@ -4,24 +4,34 @@ class URLPatternsTest(TestCase): def test_api_gateway_urls_with_fragment(self): - for url in ('/crm/appointment/#some-section', - '/crm/appointment#some-section'): + for url in ('/crm/appointment/#some-section', '/crm/appointment#some-section'): match = resolve(url) self.assertEqual(match.url_name, 'api-gateway') self.assertDictContainsSubset( - {'service': 'crm', 'model': 'appointment', 'pk': None, - 'fragment': 'some-section'}, - match.kwargs, f'Failing URL: {url}') + { + 'service': 'crm', + 'model': 'appointment', + 'pk': None, + 'fragment': 'some-section', + }, + match.kwargs, + f'Failing URL: {url}', + ) def test_api_gateway_urls_with_queryparams(self): - for url in ('/crm/appointment/?k1=v1&k2=v2', - '/crm/appointment?k1=v1&k2=v2'): + for url in ('/crm/appointment/?k1=v1&k2=v2', '/crm/appointment?k1=v1&k2=v2'): match = resolve(url) self.assertEqual(match.url_name, 'api-gateway') self.assertDictContainsSubset( - {'service': 'crm', 'model': 'appointment', 'pk': None, - 'query': 'k1=v1&k2=v2'}, - match.kwargs, f'Failing URL: {url}') + { + 'service': 'crm', + 'model': 'appointment', + 'pk': None, + 'query': 'k1=v1&k2=v2', + }, + match.kwargs, + f'Failing URL: {url}', + ) def test_api_gateway_urls_with_int_pk(self): for url in ('/crm/appointment/123456/', '/crm/appointment/123456'): @@ -29,29 +39,34 @@ def test_api_gateway_urls_with_int_pk(self): self.assertEqual(match.url_name, 'api-gateway') self.assertDictContainsSubset( {'service': 'crm', 'model': 'appointment', 'pk': '123456'}, - match.kwargs, f'Failing URL: {url}') + match.kwargs, + f'Failing URL: {url}', + ) def test_api_gateway_urls_with_uuid_pk(self): - match = resolve( - '/crm/appointment/39da9369-838e-4750-91a5-f7805cd82839/') + match = resolve('/crm/appointment/39da9369-838e-4750-91a5-f7805cd82839/') self.assertEqual(match.url_name, 'api-gateway') self.assertDictContainsSubset( - {'service': 'crm', 'model': 'appointment', - 'pk': '39da9369-838e-4750-91a5-f7805cd82839'}, - match.kwargs) + { + 'service': 'crm', + 'model': 'appointment', + 'pk': '39da9369-838e-4750-91a5-f7805cd82839', + }, + match.kwargs, + ) def test_api_gateway_urls_without_pk(self): match = resolve('/crm/appointment/') self.assertEqual(match.url_name, 'api-gateway') self.assertDictContainsSubset( - {'service': 'crm', 'model': 'appointment', 'pk': None}, - match.kwargs) + {'service': 'crm', 'model': 'appointment', 'pk': None}, match.kwargs + ) match = resolve('/crm/appointment') self.assertEqual(match.url_name, 'api-gateway') self.assertDictContainsSubset( - {'service': 'crm', 'model': 'appointment', 'pk': None}, - match.kwargs) + {'service': 'crm', 'model': 'appointment', 'pk': None}, match.kwargs + ) def test_admin_url(self): match = resolve('/admin/') diff --git a/gateway/tests/test_utils.py b/gateway/tests/test_utils.py index b619f7d8..b7d084a5 100644 --- a/gateway/tests/test_utils.py +++ b/gateway/tests/test_utils.py @@ -14,7 +14,13 @@ import factories from gateway.exceptions import GatewayError -from gateway.utils import GatewayJSONEncoder, validate_object_access, get_swagger_url_by_logic_module, get_swagger_urls, get_swagger_from_url +from gateway.utils import ( + GatewayJSONEncoder, + validate_object_access, + get_swagger_url_by_logic_module, + get_swagger_urls, + get_swagger_from_url, +) from gateway.views import APIGatewayView @@ -37,8 +43,7 @@ def test_validate_buildly_wfl1_access_superuser(self): self.core_user.save() request = self.get_mock_request('/', APIGatewayView, self.core_user) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) validate_object_access(request, wflvl1) def test_validate_buildly_wfl1_no_permission(self): @@ -65,7 +70,9 @@ def test_validate_buildly_logic_module_no_viewset(self): validate_object_access(request, lm) def test_validate_core_user_access(self): - request = self.get_mock_request('/a-jedis-path/', APIGatewayView, self.core_user) + request = self.get_mock_request( + '/a-jedis-path/', APIGatewayView, self.core_user + ) request.resolver_match = Mock(url_name='obi-wan-kenobi') core_user = factories.CoreUser() ret = validate_object_access(request, core_user) @@ -76,33 +83,32 @@ def test_json_dump(): obj = { "string": "test1234", "integer": 123, - "array": ['1', 2, ], + "array": ['1', 2], "uuid": uuid.UUID('50096bc6-848a-456f-ad36-3ac04607ff67'), "datetime": datetime.datetime(2019, 2, 5, 12, 36, 0, 147972), } response = json.dumps(obj, cls=GatewayJSONEncoder) - expected_response = '{"string": "test1234",' \ - ' "integer": 123,' \ - ' "array": ["1", 2],' \ - ' "uuid": "50096bc6-848a-456f-ad36-3ac04607ff67",' \ - ' "datetime": "2019-02-05T12:36:00.147972"}' + expected_response = ( + '{"string": "test1234",' + ' "integer": 123,' + ' "array": ["1", 2],' + ' "uuid": "50096bc6-848a-456f-ad36-3ac04607ff67",' + ' "datetime": "2019-02-05T12:36:00.147972"}' + ) assert response == expected_response @pytest.mark.django_db() def test_json_dump_w_core_user(): core_user = factories.CoreUser(pk=5) - obj = { - "model_instance": core_user, - } + obj = {"model_instance": core_user} result = json.dumps(obj, cls=GatewayJSONEncoder) expected_result = '{"model_instance": 5}' assert result == expected_result def test_json_dump_exception(): - class TestObj(object): pass @@ -117,12 +123,11 @@ class TestObj(object): @pytest.mark.django_db() class TestGettingSwaggerURLs: - def test_get_swagger_url_by_logic_module(self): module = factories.LogicModule.create() url = get_swagger_url_by_logic_module(module) assert url.startswith(module.endpoint) - + def test_get_swagger_url_by_logic_module_specified_docs(self): module = factories.LogicModule.create(docs_endpoint="api-docs") url = get_swagger_url_by_logic_module(module) @@ -135,7 +140,7 @@ def test_get_swagger_urls(self): for module in modules: assert module.endpoint_name in urls assert urls[module.endpoint_name] == get_swagger_url_by_logic_module(module) - + @patch('requests.get') def test_unavailable_logic_module_timeout_exception(self, mock_request_get): mock_request_get.side_effect = TimeoutError diff --git a/gateway/tests/test_views.py b/gateway/tests/test_views.py index 218a4dfc..ac695ac4 100644 --- a/gateway/tests/test_views.py +++ b/gateway/tests/test_views.py @@ -11,11 +11,18 @@ CURRENT_PATH = os.path.dirname(os.path.abspath(__file__)) -@pytest.mark.parametrize("content,content_type", [('{"details": "IT IS A TEST"}', 'application/json'), - ('IT IS A TEST', 'text/html; charset=utf-8')]) +@pytest.mark.parametrize( + "content,content_type", + [ + ('{"details": "IT IS A TEST"}', 'application/json'), + ('IT IS A TEST', 'text/html; charset=utf-8'), + ], +) @pytest.mark.django_db() @httpretty.activate -def test_make_service_request_data_and_raw(auth_api_client, logic_module, content, content_type): +def test_make_service_request_data_and_raw( + auth_api_client, logic_module, content, content_type +): url = f'/{logic_module.endpoint_name}/thumbnail/1/' # mock requests @@ -25,13 +32,13 @@ def test_make_service_request_data_and_raw(auth_api_client, logic_module, conten httpretty.GET, f'{logic_module.endpoint}/docs/swagger.json', body=swagger_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{logic_module.endpoint}/thumbnail/1/', body=content, - adding_headers={'Content-Type': content_type} + adding_headers={'Content-Type': content_type}, ) # make api request @@ -45,7 +52,9 @@ def test_make_service_request_data_and_raw(auth_api_client, logic_module, conten @pytest.mark.django_db() @httpretty.activate -def test_make_service_request_to_unexisting_list_endpoint(auth_api_client, logic_module): +def test_make_service_request_to_unexisting_list_endpoint( + auth_api_client, logic_module +): url = f'/{logic_module.endpoint_name}/nowhere/' @@ -56,7 +65,7 @@ def test_make_service_request_to_unexisting_list_endpoint(auth_api_client, logic httpretty.GET, f'{logic_module.endpoint}/docs/swagger.json', body=swagger_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) # make api request @@ -71,7 +80,9 @@ def test_make_service_request_to_unexisting_list_endpoint(auth_api_client, logic @pytest.mark.django_db() @httpretty.activate -def test_make_service_request_to_unexisting_detail_endpoint(auth_api_client, logic_module): +def test_make_service_request_to_unexisting_detail_endpoint( + auth_api_client, logic_module +): url = f'/{logic_module.endpoint_name}/nowhere/123/' @@ -82,7 +93,7 @@ def test_make_service_request_to_unexisting_detail_endpoint(auth_api_client, log httpretty.GET, f'{logic_module.endpoint}/docs/swagger.json', body=swagger_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) # make api request @@ -92,16 +103,23 @@ def test_make_service_request_to_unexisting_detail_endpoint(auth_api_client, log assert response.status_code == 404 assert response.has_header('Content-Type') assert response.get('Content-Type') == 'application/json' - assert json.loads(response.content)['detail'] == "Endpoint not found: GET /nowhere/{id}/" + assert ( + json.loads(response.content)['detail'] + == "Endpoint not found: GET /nowhere/{id}/" + ) @pytest.mark.django_db() @httpretty.activate def test_make_service_request_with_datamesh_detailed(auth_api_client, datamesh): lm1, lm2, relationship = datamesh - factories.JoinRecord(relationship=relationship, - record_id=None, record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', - related_record_id=1, related_record_uuid=None) + factories.JoinRecord( + relationship=relationship, + record_id=None, + record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', + related_record_id=1, + related_record_uuid=None, + ) url = f'/{lm1.endpoint_name}/siteprofiles/19a7f600-74a0-4123-9be5-dfa69aa172cc/' @@ -118,25 +136,25 @@ def test_make_service_request_with_datamesh_detailed(auth_api_client, datamesh): httpretty.GET, f'{lm1.endpoint}/docs/swagger.json', body=swagger_location_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm2.endpoint}/docs/swagger.json', body=swagger_documents_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm1.endpoint}/siteprofiles/19a7f600-74a0-4123-9be5-dfa69aa172cc/', body=data_location_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm2.endpoint}/documents/1/', body=data_documents_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) # make api request @@ -155,9 +173,13 @@ def test_make_service_request_with_datamesh_detailed(auth_api_client, datamesh): @httpretty.activate def test_make_service_request_with_reverse_datamesh_detailed(auth_api_client, datamesh): lm1, lm2, relationship = datamesh - factories.JoinRecord(relationship=relationship, - record_id=None, record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', - related_record_id=1, related_record_uuid=None) + factories.JoinRecord( + relationship=relationship, + record_id=None, + record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', + related_record_id=1, + related_record_uuid=None, + ) url = f'/{lm2.endpoint_name}/documents/1/' @@ -174,25 +196,25 @@ def test_make_service_request_with_reverse_datamesh_detailed(auth_api_client, da httpretty.GET, f'{lm1.endpoint}/docs/swagger.json', body=swagger_location_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm2.endpoint}/docs/swagger.json', body=swagger_documents_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm1.endpoint}/siteprofiles/19a7f600-74a0-4123-9be5-dfa69aa172cc/', body=data_location_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm2.endpoint}/documents/1/', body=data_documents_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) # make api request @@ -211,9 +233,13 @@ def test_make_service_request_with_reverse_datamesh_detailed(auth_api_client, da @httpretty.activate def test_make_service_request_with_datamesh_list(auth_api_client, datamesh): lm1, lm2, relationship = datamesh - factories.JoinRecord(relationship=relationship, - record_id=None, record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', - related_record_id=1, related_record_uuid=None) + factories.JoinRecord( + relationship=relationship, + record_id=None, + record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', + related_record_id=1, + related_record_uuid=None, + ) url = f'/{lm1.endpoint_name}/siteprofiles/' @@ -230,25 +256,25 @@ def test_make_service_request_with_datamesh_list(auth_api_client, datamesh): httpretty.GET, f'{lm1.endpoint}/docs/swagger.json', body=swagger_location_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm2.endpoint}/docs/swagger.json', body=swagger_documents_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm1.endpoint}/siteprofiles/', body=data_location_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) httpretty.register_uri( httpretty.GET, f'{lm2.endpoint}/documents/1/', body=data_documents_body, - adding_headers={'Content-Type': 'application/json'} + adding_headers={'Content-Type': 'application/json'}, ) # make api request diff --git a/gateway/tests/test_views_async.py b/gateway/tests/test_views_async.py index a49a317c..44b5fbe8 100644 --- a/gateway/tests/test_views_async.py +++ b/gateway/tests/test_views_async.py @@ -13,15 +13,24 @@ CURRENT_PATH = os.path.dirname(os.path.abspath(__file__)) -@pytest.mark.parametrize("content,content_type", [ - (b'{"details": "IT IS A TEST"}', 'application/json'), - (b'IT IS A TEST', 'text/html; charset=utf-8'), - (None, 'application/octet-stream'), ] +@pytest.mark.parametrize( + "content,content_type", + [ + (b'{"details": "IT IS A TEST"}', 'application/json'), + (b'IT IS A TEST', 'text/html; charset=utf-8'), + (None, 'application/octet-stream'), + ], ) @pytest.mark.django_db() @patch('gateway.request.aiohttp.ClientSession') -def test_make_service_request_data_and_raw(client_session_mock, auth_api_client, logic_module, content, content_type, - event_loop): +def test_make_service_request_data_and_raw( + client_session_mock, + auth_api_client, + logic_module, + content, + content_type, + event_loop, +): url = f'/async/{logic_module.endpoint_name}/thumbnail/1/' # mock aiohttp responses @@ -29,12 +38,24 @@ def test_make_service_request_data_and_raw(client_session_mock, auth_api_client, swagger_body = r.read() responses = [ - AiohttpResponseMock(method='GET', url=f'{logic_module.endpoint}/docs/swagger.json', status=200, - body=swagger_body, headers={'Content-Type': 'application/json'}), - AiohttpResponseMock(method='GET', url=f'{logic_module.endpoint}/thumbnail/1/', status=200, body=content, - headers={'Content-Type': content_type}), + AiohttpResponseMock( + method='GET', + url=f'{logic_module.endpoint}/docs/swagger.json', + status=200, + body=swagger_body, + headers={'Content-Type': 'application/json'}, + ), + AiohttpResponseMock( + method='GET', + url=f'{logic_module.endpoint}/thumbnail/1/', + status=200, + body=content, + headers={'Content-Type': content_type}, + ), ] - client_session_mock.return_value = create_aiohttp_session_mock(responses, loop=event_loop) + client_session_mock.return_value = create_aiohttp_session_mock( + responses, loop=event_loop + ) # make api request response = auth_api_client.get(url) @@ -50,8 +71,9 @@ def test_make_service_request_data_and_raw(client_session_mock, auth_api_client, @pytest.mark.django_db() @patch('gateway.request.aiohttp.ClientSession') -def test_make_service_request_to_unexisting_list_endpoint(client_session_mock, auth_api_client, logic_module, - event_loop): +def test_make_service_request_to_unexisting_list_endpoint( + client_session_mock, auth_api_client, logic_module, event_loop +): url = f'/async/{logic_module.endpoint_name}/nowhere/' @@ -59,10 +81,17 @@ def test_make_service_request_to_unexisting_list_endpoint(client_session_mock, a with open(os.path.join(CURRENT_PATH, 'fixtures/swagger_documents.json'), 'rb') as r: swagger_body = r.read() responses = [ - AiohttpResponseMock(method='GET', url=f'{logic_module.endpoint}/docs/swagger.json', status=200, - body=swagger_body, headers={'Content-Type': 'application/json'}), + AiohttpResponseMock( + method='GET', + url=f'{logic_module.endpoint}/docs/swagger.json', + status=200, + body=swagger_body, + headers={'Content-Type': 'application/json'}, + ) ] - client_session_mock.return_value = create_aiohttp_session_mock(responses, loop=event_loop) + client_session_mock.return_value = create_aiohttp_session_mock( + responses, loop=event_loop + ) # make api request response = auth_api_client.get(url) @@ -76,8 +105,9 @@ def test_make_service_request_to_unexisting_list_endpoint(client_session_mock, a @pytest.mark.django_db() @patch('gateway.request.aiohttp.ClientSession') -def test_make_service_request_to_unexisting_detail_endpoint(client_session_mock, auth_api_client, logic_module, - event_loop): +def test_make_service_request_to_unexisting_detail_endpoint( + client_session_mock, auth_api_client, logic_module, event_loop +): url = f'/async/{logic_module.endpoint_name}/nowhere/123/' @@ -85,10 +115,17 @@ def test_make_service_request_to_unexisting_detail_endpoint(client_session_mock, with open(os.path.join(CURRENT_PATH, 'fixtures/swagger_documents.json'), 'rb') as r: swagger_body = r.read() responses = [ - AiohttpResponseMock(method='GET', url=f'{logic_module.endpoint}/docs/swagger.json', status=200, - body=swagger_body, headers={'Content-Type': 'application/json'}), + AiohttpResponseMock( + method='GET', + url=f'{logic_module.endpoint}/docs/swagger.json', + status=200, + body=swagger_body, + headers={'Content-Type': 'application/json'}, + ) ] - client_session_mock.return_value = create_aiohttp_session_mock(responses, loop=event_loop) + client_session_mock.return_value = create_aiohttp_session_mock( + responses, loop=event_loop + ) # make api request response = auth_api_client.get(url) @@ -97,40 +134,76 @@ def test_make_service_request_to_unexisting_detail_endpoint(client_session_mock, assert response.status_code == 404 assert response.has_header('Content-Type') assert response.get('Content-Type') == 'application/json' - assert json.loads(response.content)['detail'] == "Endpoint not found: GET /nowhere/{id}/" + assert ( + json.loads(response.content)['detail'] + == "Endpoint not found: GET /nowhere/{id}/" + ) @pytest.mark.django_db() @patch('gateway.request.aiohttp.ClientSession') -def test_make_service_request_with_datamesh_detailed(client_session_mock, auth_api_client, datamesh, event_loop): +def test_make_service_request_with_datamesh_detailed( + client_session_mock, auth_api_client, datamesh, event_loop +): lm1, lm2, relationship = datamesh - factories.JoinRecord(relationship=relationship, - record_id=None, record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', - related_record_id=1, related_record_uuid=None) - - url = f'/async/{lm1.endpoint_name}/siteprofiles/19a7f600-74a0-4123-9be5-dfa69aa172cc/' + factories.JoinRecord( + relationship=relationship, + record_id=None, + record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', + related_record_id=1, + related_record_uuid=None, + ) + + url = ( + f'/async/{lm1.endpoint_name}/siteprofiles/19a7f600-74a0-4123-9be5-dfa69aa172cc/' + ) # mock aiohttp responses with open(os.path.join(CURRENT_PATH, 'fixtures/swagger_location.json'), 'rb') as r: swagger_location_body = r.read() with open(os.path.join(CURRENT_PATH, 'fixtures/swagger_documents.json'), 'rb') as r: swagger_documents_body = r.read() - with open(os.path.join(CURRENT_PATH, 'fixtures/data_detail_siteprofile.json'), 'rb') as r: + with open( + os.path.join(CURRENT_PATH, 'fixtures/data_detail_siteprofile.json'), 'rb' + ) as r: data_location_body = r.read() - with open(os.path.join(CURRENT_PATH, 'fixtures/data_detail_document.json'), 'rb') as r: + with open( + os.path.join(CURRENT_PATH, 'fixtures/data_detail_document.json'), 'rb' + ) as r: data_documents_body = r.read() responses = [ - AiohttpResponseMock(method='GET', url=f'{lm1.endpoint}/docs/swagger.json', status=200, - body=swagger_location_body, headers={'Content-Type': 'application/json'}), - AiohttpResponseMock(method='GET', url=f'{lm2.endpoint}/docs/swagger.json', status=200, - body=swagger_documents_body, headers={'Content-Type': 'application/json'}), - AiohttpResponseMock(method='GET', url=f'{lm1.endpoint}/siteprofiles/19a7f600-74a0-4123-9be5-dfa69aa172cc/', - status=200, - body=data_location_body, headers={'Content-Type': 'application/json'}), - AiohttpResponseMock(method='GET', url=f'{lm2.endpoint}/documents/1/', status=200, - body=data_documents_body, headers={'Content-Type': 'application/json'}), + AiohttpResponseMock( + method='GET', + url=f'{lm1.endpoint}/docs/swagger.json', + status=200, + body=swagger_location_body, + headers={'Content-Type': 'application/json'}, + ), + AiohttpResponseMock( + method='GET', + url=f'{lm2.endpoint}/docs/swagger.json', + status=200, + body=swagger_documents_body, + headers={'Content-Type': 'application/json'}, + ), + AiohttpResponseMock( + method='GET', + url=f'{lm1.endpoint}/siteprofiles/19a7f600-74a0-4123-9be5-dfa69aa172cc/', + status=200, + body=data_location_body, + headers={'Content-Type': 'application/json'}, + ), + AiohttpResponseMock( + method='GET', + url=f'{lm2.endpoint}/documents/1/', + status=200, + body=data_documents_body, + headers={'Content-Type': 'application/json'}, + ), ] - client_session_mock.return_value = create_aiohttp_session_mock(responses, loop=event_loop) + client_session_mock.return_value = create_aiohttp_session_mock( + responses, loop=event_loop + ) # make api request response = auth_api_client.get(url, {'join': 'true'}) @@ -146,11 +219,17 @@ def test_make_service_request_with_datamesh_detailed(client_session_mock, auth_a @pytest.mark.django_db() @patch('gateway.request.aiohttp.ClientSession') -def test_make_service_request_with_datamesh_list(client_session_mock, auth_api_client, datamesh, event_loop): +def test_make_service_request_with_datamesh_list( + client_session_mock, auth_api_client, datamesh, event_loop +): lm1, lm2, relationship = datamesh - factories.JoinRecord(relationship=relationship, - record_id=None, record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', - related_record_id=1, related_record_uuid=None) + factories.JoinRecord( + relationship=relationship, + record_id=None, + record_uuid='19a7f600-74a0-4123-9be5-dfa69aa172cc', + related_record_id=1, + related_record_uuid=None, + ) url = f'/async/{lm1.endpoint_name}/siteprofiles/' @@ -159,21 +238,47 @@ def test_make_service_request_with_datamesh_list(client_session_mock, auth_api_c swagger_location_body = r.read() with open(os.path.join(CURRENT_PATH, 'fixtures/swagger_documents.json'), 'rb') as r: swagger_documents_body = r.read() - with open(os.path.join(CURRENT_PATH, 'fixtures/data_list_siteprofile.json'), 'rb') as r: + with open( + os.path.join(CURRENT_PATH, 'fixtures/data_list_siteprofile.json'), 'rb' + ) as r: data_location_body = r.read() - with open(os.path.join(CURRENT_PATH, 'fixtures/data_detail_document.json'), 'rb') as r: + with open( + os.path.join(CURRENT_PATH, 'fixtures/data_detail_document.json'), 'rb' + ) as r: data_documents_body = r.read() responses = [ - AiohttpResponseMock(method='GET', url=f'{lm1.endpoint}/docs/swagger.json', status=200, - body=swagger_location_body, headers={'Content-Type': 'application/json'}), - AiohttpResponseMock(method='GET', url=f'{lm2.endpoint}/docs/swagger.json', status=200, - body=swagger_documents_body, headers={'Content-Type': 'application/json'}), - AiohttpResponseMock(method='GET', url=f'{lm1.endpoint}/siteprofiles/', status=200, - body=data_location_body, headers={'Content-Type': 'application/json'}), - AiohttpResponseMock(method='GET', url=f'{lm2.endpoint}/documents/1/', status=200, - body=data_documents_body, headers={'Content-Type': 'application/json'}), + AiohttpResponseMock( + method='GET', + url=f'{lm1.endpoint}/docs/swagger.json', + status=200, + body=swagger_location_body, + headers={'Content-Type': 'application/json'}, + ), + AiohttpResponseMock( + method='GET', + url=f'{lm2.endpoint}/docs/swagger.json', + status=200, + body=swagger_documents_body, + headers={'Content-Type': 'application/json'}, + ), + AiohttpResponseMock( + method='GET', + url=f'{lm1.endpoint}/siteprofiles/', + status=200, + body=data_location_body, + headers={'Content-Type': 'application/json'}, + ), + AiohttpResponseMock( + method='GET', + url=f'{lm2.endpoint}/documents/1/', + status=200, + body=data_documents_body, + headers={'Content-Type': 'application/json'}, + ), ] - client_session_mock.return_value = create_aiohttp_session_mock(responses, loop=event_loop) + client_session_mock.return_value = create_aiohttp_session_mock( + responses, loop=event_loop + ) # make api request response = auth_api_client.get(url, {'join': 'true'}) diff --git a/gateway/tests/utils.py b/gateway/tests/utils.py index 09b1f4e2..ab6f19ad 100644 --- a/gateway/tests/utils.py +++ b/gateway/tests/utils.py @@ -7,7 +7,6 @@ class AiohttpResponseMock: - def __init__(self, method, url, status, body, headers=None): self.method = method self.url = url @@ -44,7 +43,10 @@ def text(self, encoding='utf-8'): @asyncio.coroutine def json(self, encoding='utf-8'): if not getattr(self.body, "decode", False): - raise ContentTypeError(request_info=RequestInfo(self.url, self.method, self.headers), history=[self]) + raise ContentTypeError( + request_info=RequestInfo(self.url, self.method, self.headers), + history=[self], + ) return json.loads(self.body.decode(encoding)) @asyncio.coroutine @@ -52,9 +54,10 @@ def release(self): pass -def create_aiohttp_session_mock(response_mocks: typing.Iterable[AiohttpResponseMock], - loop: asyncio.AbstractEventLoop = None) -> ClientSession: - +def create_aiohttp_session_mock( + response_mocks: typing.Iterable[AiohttpResponseMock], + loop: asyncio.AbstractEventLoop = None, +) -> ClientSession: async def _request(method, url, *args, **kwargs): for response in response_mocks: if response.match_request(method, url): diff --git a/gateway/urls.py b/gateway/urls.py index ef8d7e6f..6918d472 100644 --- a/gateway/urls.py +++ b/gateway/urls.py @@ -18,7 +18,7 @@ swagger_info, public=True, permission_classes=(permissions.AllowAny,), - generator_class=generator.OpenAPISchemaGenerator + generator_class=generator.OpenAPISchemaGenerator, ) urlpatterns = [ @@ -30,7 +30,9 @@ r"(?:(?P[^?#/]+)/?)?" # pk (numeric or UUID) r"(?:\?(?P[^#]*))?" # queryparams (?key1=value1&key2=value2) r"(?:#(?P.*))?", # fragment (#some-anchor) - views.APIAsyncGatewayView.as_view(), name='api-gateway-async'), + views.APIAsyncGatewayView.as_view(), + name='api-gateway-async', + ), re_path( rf"^(?!{'|'.join(API_GATEWAY_RESERVED_NAMES)})" # Reject any of these r"(?P[^/?#]+)/" # service (timetracking) @@ -38,10 +40,17 @@ r"(?:(?P[^?#/]+)/?)?" # pk (numeric or UUID) r"(?:\?(?P[^#]*))?" # queryparams (?key1=value1&key2=value2) r"(?:#(?P.*))?", # fragment (#some-anchor) - views.APIGatewayView.as_view(), name='api-gateway'), - re_path(r'^docs/swagger(?P\.json|\.yaml)$', - schema_view.without_ui(cache_timeout=0), - name='schema-swagger-json'), - path('docs/', schema_view.with_ui('swagger', cache_timeout=0), - name='schema-swagger-ui'), + views.APIGatewayView.as_view(), + name='api-gateway', + ), + re_path( + r'^docs/swagger(?P\.json|\.yaml)$', + schema_view.without_ui(cache_timeout=0), + name='schema-swagger-json', + ), + path( + 'docs/', + schema_view.with_ui('swagger', cache_timeout=0), + name='schema-swagger-ui', + ), ] diff --git a/gateway/utils.py b/gateway/utils.py index eb8c6666..66cb991d 100644 --- a/gateway/utils.py +++ b/gateway/utils.py @@ -14,8 +14,19 @@ from workflow import models as wfm from . import exceptions -from core.models import CoreUser, LogicModule, Organization, OrganizationType, Consortium -from core.views import CoreUserViewSet, OrganizationViewSet, OrganizationTypeViewSet, ConsortiumViewSet +from core.models import ( + CoreUser, + LogicModule, + Organization, + OrganizationType, + Consortium, +) +from core.views import ( + CoreUserViewSet, + OrganizationViewSet, + OrganizationTypeViewSet, + ConsortiumViewSet, +) SWAGGER_LOOKUP_FIELD = 'swagger' @@ -40,8 +51,12 @@ def get_swagger_url_by_logic_module(module: LogicModule) -> str: :param LogicModule module: the logic module (service) :return: OpenAPI schema URL for the logic module """ - swagger_lookup = module.docs_endpoint if module.docs_endpoint else SWAGGER_LOOKUP_PATH - return '{}/{}/{}.{}'.format(module.endpoint, swagger_lookup, SWAGGER_LOOKUP_FIELD, SWAGGER_LOOKUP_FORMAT) + swagger_lookup = ( + module.docs_endpoint if module.docs_endpoint else SWAGGER_LOOKUP_PATH + ) + return '{}/{}/{}.{}'.format( + module.endpoint, swagger_lookup, SWAGGER_LOOKUP_FIELD, SWAGGER_LOOKUP_FORMAT + ) def get_swagger_urls() -> Dict[str, str]: @@ -72,10 +87,10 @@ def get_swagger_from_url(api_url: str): return requests.get(api_url) except requests.exceptions.ConnectTimeout as error: raise TimeoutError( - f'Connection timed out. Please, check that {api_url} is accessible.') from error + f'Connection timed out. Please, check that {api_url} is accessible.' + ) from error except requests.exceptions.ConnectionError as error: - raise ConnectionError( - f'Please, check that {api_url} is accessible.') from error + raise ConnectionError(f'Please, check that {api_url} is accessible.') from error def validate_object_access(request: Request, obj): @@ -93,7 +108,8 @@ def validate_object_access(request: Request, obj): except KeyError: logging.critical(f'{model} needs to be added to MODEL_VIEWSETS_DICT') raise exceptions.GatewayError( - msg=f'{model} not defined for object access lookup.') + msg=f'{model} not defined for object access lookup.' + ) else: viewset.request = request viewset.check_object_permissions(request, obj) @@ -103,6 +119,7 @@ class ObjectAccessValidator: """ Create an instance of this class to validate access to an object """ + def __init__(self, request: Request): self._request = request @@ -114,6 +131,7 @@ class GatewayJSONEncoder(json.JSONEncoder): """ JSON encoder for API Gateway """ + def default(self, obj): """ JSON doesn't have a default datetime and UUID type, so this is why @@ -133,7 +151,9 @@ def default(self, obj): def valid_uuid4(uuid_string): - uuid4hex = re.compile('^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z', # noqa - re.I) + uuid4hex = re.compile( + '^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z', # noqa + re.I, + ) match = uuid4hex.match(uuid_string) return bool(match) diff --git a/gateway/views.py b/gateway/views.py index 01a25473..cf0b686a 100644 --- a/gateway/views.py +++ b/gateway/views.py @@ -56,20 +56,27 @@ def make_service_request(self, request, *args, **kwargs): try: self._validate_incoming_request(request, **kwargs) except exceptions.RequestValidationError as e: - return HttpResponse(content=e.content, status=e.status, content_type=e.content_type) + return HttpResponse( + content=e.content, status=e.status, content_type=e.content_type + ) gw_request = self.gateway_request_class(request, **kwargs) gw_response = gw_request.perform() - return HttpResponse(content=gw_response.content, - status=gw_response.status_code, - content_type=gw_response.headers.get('Content-Type')) + return HttpResponse( + content=gw_response.content, + status=gw_response.status_code, + content_type=gw_response.headers.get('Content-Type'), + ) def _validate_incoming_request(self, request: Request, **kwargs: dict) -> None: """ Do certain validations to the request before starting to create a new request to services """ - if request.META['REQUEST_METHOD'] in ['PUT', 'PATCH', 'DELETE'] and kwargs['pk'] is None: + if ( + request.META['REQUEST_METHOD'] in ['PUT', 'PATCH', 'DELETE'] + and kwargs['pk'] is None + ): raise exceptions.RequestValidationError('The object ID is missing.', 400) diff --git a/manage.py b/manage.py index 0937b3c7..51445a03 100755 --- a/manage.py +++ b/manage.py @@ -3,8 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", - "buildly.settings.base") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "buildly.settings.base") try: from django.core.management import execute_from_command_line except ImportError: diff --git a/workflow/admin.py b/workflow/admin.py index 35c776e7..b288c945 100644 --- a/workflow/admin.py +++ b/workflow/admin.py @@ -1,13 +1,22 @@ from django.contrib import admin -from .models import WorkflowLevel1, WorkflowLevel2, WorkflowLevel2Sort, WorkflowTeam, WorkflowLevelStatus +from .models import ( + WorkflowLevel1, + WorkflowLevel2, + WorkflowLevel2Sort, + WorkflowTeam, + WorkflowLevelStatus, +) class WorkflowTeamAdmin(admin.ModelAdmin): list_display = ('workflow_user', 'workflowlevel1') display = 'Workflow Team' - search_fields = ('workflow_user__username', 'workflowlevel1__name', - 'workflow_user__last_name') + search_fields = ( + 'workflow_user__username', + 'workflowlevel1__name', + 'workflow_user__last_name', + ) list_filter = ('create_date',) diff --git a/workflow/migrations/0001_initial.py b/workflow/migrations/0001_initial.py index 423aaba5..9ab80a9c 100644 --- a/workflow/migrations/0001_initial.py +++ b/workflow/migrations/0001_initial.py @@ -12,39 +12,127 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( name='Internationalization', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('language', models.CharField(blank=True, max_length=100, null=True, verbose_name='Language')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'language', + models.CharField( + blank=True, max_length=100, null=True, verbose_name='Language' + ), + ), ('language_file', django.contrib.postgres.fields.jsonb.JSONField()), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), ], - options={ - 'ordering': ('language',), - }, + options={'ordering': ('language',)}, ), migrations.CreateModel( name='WorkflowLevel1', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('level1_uuid', models.CharField(default=uuid.uuid4, editable=False, max_length=255, unique=True, verbose_name='WorkflowLevel1 UUID')), - ('unique_id', models.CharField(blank=True, help_text='User facing unique ID field if needed', max_length=255, null=True, verbose_name='ID')), - ('name', models.CharField(blank=True, help_text="Top level workflow can have child workflowleves, name it according to it's grouping of children", max_length=255, verbose_name='Name')), - ('description', models.TextField(blank=True, help_text='Describe how this collection of related workflows are used', max_length=765, null=True, verbose_name='Description')), - ('start_date', models.DateTimeField(blank=True, help_text='If required a time span can be associated with workflow level', null=True)), - ('end_date', models.DateTimeField(blank=True, help_text='If required a time span can be associated with workflow level', null=True)), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'level1_uuid', + models.CharField( + default=uuid.uuid4, + editable=False, + max_length=255, + unique=True, + verbose_name='WorkflowLevel1 UUID', + ), + ), + ( + 'unique_id', + models.CharField( + blank=True, + help_text='User facing unique ID field if needed', + max_length=255, + null=True, + verbose_name='ID', + ), + ), + ( + 'name', + models.CharField( + blank=True, + help_text="Top level workflow can have child workflowleves, name it according to it's grouping of children", + max_length=255, + verbose_name='Name', + ), + ), + ( + 'description', + models.TextField( + blank=True, + help_text='Describe how this collection of related workflows are used', + max_length=765, + null=True, + verbose_name='Description', + ), + ), + ( + 'start_date', + models.DateTimeField( + blank=True, + help_text='If required a time span can be associated with workflow level', + null=True, + ), + ), + ( + 'end_date', + models.DateTimeField( + blank=True, + help_text='If required a time span can be associated with workflow level', + null=True, + ), + ), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), ('sort', models.IntegerField(default=0)), - ('core_groups', models.ManyToManyField(blank=True, related_name='workflowlevel1s', related_query_name='workflowlevel1s', to='core.CoreGroup', verbose_name='Core groups')), - ('organization', models.ForeignKey(blank=True, help_text='Related Org to associate with', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Organization')), - ('user_access', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ( + 'core_groups', + models.ManyToManyField( + blank=True, + related_name='workflowlevel1s', + related_query_name='workflowlevel1s', + to='core.CoreGroup', + verbose_name='Core groups', + ), + ), + ( + 'organization', + models.ForeignKey( + blank=True, + help_text='Related Org to associate with', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='core.Organization', + ), + ), + ( + 'user_access', + models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), + ), ], options={ 'verbose_name': 'Workflow Level 1', @@ -55,18 +143,97 @@ class Migration(migrations.Migration): migrations.CreateModel( name='WorkflowLevel2', fields=[ - ('level2_uuid', models.UUIDField(default=uuid.uuid4, help_text='Unique ID', primary_key=True, serialize=False, verbose_name='WorkflowLevel2 UUID')), - ('description', models.TextField(blank=True, help_text='Description of the workflow level use', null=True, verbose_name='Description')), - ('name', models.CharField(help_text='Name of workflow level as it relates to workflow level 1', max_length=255, verbose_name='Name')), + ( + 'level2_uuid', + models.UUIDField( + default=uuid.uuid4, + help_text='Unique ID', + primary_key=True, + serialize=False, + verbose_name='WorkflowLevel2 UUID', + ), + ), + ( + 'description', + models.TextField( + blank=True, + help_text='Description of the workflow level use', + null=True, + verbose_name='Description', + ), + ), + ( + 'name', + models.CharField( + help_text='Name of workflow level as it relates to workflow level 1', + max_length=255, + verbose_name='Name', + ), + ), ('notes', models.TextField(blank=True, null=True)), - ('parent_workflowlevel2', models.IntegerField(blank=True, default=0, help_text='Workflow level 2 can relate to another workflow level 2 creating multiple levels of relationships', verbose_name='Parent')), - ('short_name', models.CharField(blank=True, help_text='Shortened name autogenerated', max_length=20, null=True, verbose_name='Code')), - ('create_date', models.DateTimeField(blank=True, null=True, verbose_name='Date Created')), - ('edit_date', models.DateTimeField(blank=True, null=True, verbose_name='Last Edit Date')), - ('start_date', models.DateTimeField(blank=True, null=True, verbose_name='Start Date')), - ('end_date', models.DateTimeField(blank=True, null=True, verbose_name='End Date')), - ('core_groups', models.ManyToManyField(blank=True, related_name='workflowlevel2s', related_query_name='workflowlevel2s', to='core.CoreGroup', verbose_name='Core groups')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowlevel2', to=settings.AUTH_USER_MODEL)), + ( + 'parent_workflowlevel2', + models.IntegerField( + blank=True, + default=0, + help_text='Workflow level 2 can relate to another workflow level 2 creating multiple levels of relationships', + verbose_name='Parent', + ), + ), + ( + 'short_name', + models.CharField( + blank=True, + help_text='Shortened name autogenerated', + max_length=20, + null=True, + verbose_name='Code', + ), + ), + ( + 'create_date', + models.DateTimeField( + blank=True, null=True, verbose_name='Date Created' + ), + ), + ( + 'edit_date', + models.DateTimeField( + blank=True, null=True, verbose_name='Last Edit Date' + ), + ), + ( + 'start_date', + models.DateTimeField( + blank=True, null=True, verbose_name='Start Date' + ), + ), + ( + 'end_date', + models.DateTimeField( + blank=True, null=True, verbose_name='End Date' + ), + ), + ( + 'core_groups', + models.ManyToManyField( + blank=True, + related_name='workflowlevel2s', + related_query_name='workflowlevel2s', + to='core.CoreGroup', + verbose_name='Core groups', + ), + ), + ( + 'created_by', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='workflowlevel2', + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ 'verbose_name': 'Workflow Level 2', @@ -77,11 +244,26 @@ class Migration(migrations.Migration): migrations.CreateModel( name='WorkflowLevelStatus', fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ( + 'uuid', + models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), ('order', models.PositiveSmallIntegerField(default=0)), - ('name', models.CharField(help_text='Name of WorkflowLevelStatus', max_length=255, verbose_name='Name')), + ( + 'name', + models.CharField( + help_text='Name of WorkflowLevelStatus', + max_length=255, + verbose_name='Name', + ), + ), ('short_name', models.SlugField(max_length=63, unique=True)), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ( + 'create_date', + models.DateTimeField(default=django.utils.timezone.now), + ), ('edit_date', models.DateTimeField(auto_now=True)), ], options={ @@ -93,28 +275,108 @@ class Migration(migrations.Migration): migrations.CreateModel( name='WorkflowLevelType', fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(help_text='Name of workflow2 type', max_length=255, verbose_name='Name')), - ('create_date', models.DateTimeField(default=django.utils.timezone.now)), + ( + 'uuid', + models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + ( + 'name', + models.CharField( + help_text='Name of workflow2 type', + max_length=255, + verbose_name='Name', + ), + ), + ( + 'create_date', + models.DateTimeField(default=django.utils.timezone.now), + ), ('edit_date', models.DateTimeField(auto_now=True)), ], - options={ - 'ordering': ('create_date',), - }, + options={'ordering': ('create_date',)}, ), migrations.CreateModel( name='WorkflowTeam', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('team_uuid', models.CharField(default=uuid.uuid4, editable=False, max_length=255, unique=True, verbose_name='WorkflowLevel1 UUID')), - ('start_date', models.DateTimeField(blank=True, help_text='If required a time span can be associated with workflow level access', null=True)), - ('end_date', models.DateTimeField(blank=True, help_text='If required a time span can be associated with workflow level access expiration', null=True)), - ('status', models.CharField(blank=True, help_text='Active status of access', max_length=255, null=True)), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'team_uuid', + models.CharField( + default=uuid.uuid4, + editable=False, + max_length=255, + unique=True, + verbose_name='WorkflowLevel1 UUID', + ), + ), + ( + 'start_date', + models.DateTimeField( + blank=True, + help_text='If required a time span can be associated with workflow level access', + null=True, + ), + ), + ( + 'end_date', + models.DateTimeField( + blank=True, + help_text='If required a time span can be associated with workflow level access expiration', + null=True, + ), + ), + ( + 'status', + models.CharField( + blank=True, + help_text='Active status of access', + max_length=255, + null=True, + ), + ), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), - ('role', models.ForeignKey(blank=True, help_text='Type of access via related group', null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), - ('workflow_user', models.ForeignKey(blank=True, help_text='User with access/permissions to related workflowlevels', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='auth_approving', to=settings.AUTH_USER_MODEL)), - ('workflowlevel1', models.ForeignKey(blank=True, help_text='Related workflowlevel 1', null=True, on_delete=django.db.models.deletion.CASCADE, to='workflow.WorkflowLevel1')), + ( + 'role', + models.ForeignKey( + blank=True, + help_text='Type of access via related group', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='auth.Group', + ), + ), + ( + 'workflow_user', + models.ForeignKey( + blank=True, + help_text='User with access/permissions to related workflowlevels', + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='auth_approving', + to=settings.AUTH_USER_MODEL, + ), + ), + ( + 'workflowlevel1', + models.ForeignKey( + blank=True, + help_text='Related workflowlevel 1', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='workflow.WorkflowLevel1', + ), + ), ], options={ 'verbose_name': 'Workflow Team', @@ -125,13 +387,50 @@ class Migration(migrations.Migration): migrations.CreateModel( name='WorkflowLevel2Sort', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('workflowlevel2_pk', models.UUIDField(default='00000000-0000-4000-8000-000000000000', verbose_name='UUID to be Sorted')), - ('sort_array', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Sorted JSON array of workflow levels', null=True)), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'workflowlevel2_pk', + models.UUIDField( + default='00000000-0000-4000-8000-000000000000', + verbose_name='UUID to be Sorted', + ), + ), + ( + 'sort_array', + django.contrib.postgres.fields.jsonb.JSONField( + blank=True, + help_text='Sorted JSON array of workflow levels', + null=True, + ), + ), ('create_date', models.DateTimeField(blank=True, null=True)), ('edit_date', models.DateTimeField(blank=True, null=True)), - ('workflowlevel1', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='workflow.WorkflowLevel1')), - ('workflowlevel2_parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='workflow.WorkflowLevel2')), + ( + 'workflowlevel1', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='workflow.WorkflowLevel1', + ), + ), + ( + 'workflowlevel2_parent', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='workflow.WorkflowLevel2', + ), + ), ], options={ 'verbose_name': 'Workflow Level Sort', @@ -142,16 +441,34 @@ class Migration(migrations.Migration): migrations.AddField( model_name='workflowlevel2', name='status', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowlevel2s', to='workflow.WorkflowLevelStatus'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='workflowlevel2s', + to='workflow.WorkflowLevelStatus', + ), ), migrations.AddField( model_name='workflowlevel2', name='type', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowlevel2s', to='workflow.WorkflowLevelType'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='workflowlevel2s', + to='workflow.WorkflowLevelType', + ), ), migrations.AddField( model_name='workflowlevel2', name='workflowlevel1', - field=models.ForeignKey(help_text='Primary or parent Workflow', on_delete=django.db.models.deletion.CASCADE, related_name='workflowlevel2', to='workflow.WorkflowLevel1', verbose_name='Workflow Level 1'), + field=models.ForeignKey( + help_text='Primary or parent Workflow', + on_delete=django.db.models.deletion.CASCADE, + related_name='workflowlevel2', + to='workflow.WorkflowLevel1', + verbose_name='Workflow Level 1', + ), ), ] diff --git a/workflow/models.py b/workflow/models.py index d368c942..09ba093e 100644 --- a/workflow/models.py +++ b/workflow/models.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import Group from django.contrib.postgres.fields import JSONField from django.core.exceptions import ValidationError + try: from django.utils import timezone except ImportError: @@ -41,13 +42,15 @@ class WorkflowLevelType(models.Model): edit_date = models.DateTimeField(auto_now=True) class Meta: - ordering = ('create_date', ) + ordering = ('create_date',) class WorkflowLevelStatus(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4) order = models.PositiveSmallIntegerField(default=0) - name = models.CharField("Name", max_length=255, help_text="Name of WorkflowLevelStatus") + name = models.CharField( + "Name", max_length=255, help_text="Name of WorkflowLevelStatus" + ) short_name = models.SlugField(max_length=63, unique=True) create_date = models.DateTimeField(default=timezone.now) edit_date = models.DateTimeField(auto_now=True) @@ -56,24 +59,67 @@ def __str__(self): return self.name class Meta: - ordering = ('order', ) + ordering = ('order',) verbose_name = "Workflow Level Status" verbose_name_plural = "Workflow Level Statuses" class WorkflowLevel1(models.Model): - level1_uuid = models.CharField(max_length=255, editable=False, verbose_name='WorkflowLevel1 UUID', default=uuid.uuid4, unique=True) - unique_id = models.CharField("ID", max_length=255, blank=True, null=True, help_text="User facing unique ID field if needed") - name = models.CharField("Name", max_length=255, blank=True, help_text="Top level workflow can have child workflowleves, name it according to it's grouping of children") - organization = models.ForeignKey(Organization, blank=True, on_delete=models.CASCADE, null=True, help_text='Related Org to associate with') - description = models.TextField("Description", max_length=765, null=True, blank=True, help_text='Describe how this collection of related workflows are used') + level1_uuid = models.CharField( + max_length=255, + editable=False, + verbose_name='WorkflowLevel1 UUID', + default=uuid.uuid4, + unique=True, + ) + unique_id = models.CharField( + "ID", + max_length=255, + blank=True, + null=True, + help_text="User facing unique ID field if needed", + ) + name = models.CharField( + "Name", + max_length=255, + blank=True, + help_text="Top level workflow can have child workflowleves, name it according to it's grouping of children", + ) + organization = models.ForeignKey( + Organization, + blank=True, + on_delete=models.CASCADE, + null=True, + help_text='Related Org to associate with', + ) + description = models.TextField( + "Description", + max_length=765, + null=True, + blank=True, + help_text='Describe how this collection of related workflows are used', + ) user_access = models.ManyToManyField(CoreUser, blank=True) - start_date = models.DateTimeField(null=True, blank=True, help_text='If required a time span can be associated with workflow level') - end_date = models.DateTimeField(null=True, blank=True, help_text='If required a time span can be associated with workflow level') + start_date = models.DateTimeField( + null=True, + blank=True, + help_text='If required a time span can be associated with workflow level', + ) + end_date = models.DateTimeField( + null=True, + blank=True, + help_text='If required a time span can be associated with workflow level', + ) create_date = models.DateTimeField(null=True, blank=True) edit_date = models.DateTimeField(null=True, blank=True) sort = models.IntegerField(default=0) # sort array - core_groups = models.ManyToManyField(CoreGroup, verbose_name='Core groups', blank=True, related_name='workflowlevel1s', related_query_name='workflowlevel1s') + core_groups = models.ManyToManyField( + CoreGroup, + verbose_name='Core groups', + blank=True, + related_name='workflowlevel1s', + related_query_name='workflowlevel1s', + ) class Meta: ordering = ('name',) @@ -100,21 +146,76 @@ def __str__(self): class WorkflowLevel2(models.Model): - level2_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name='WorkflowLevel2 UUID', help_text="Unique ID") - description = models.TextField("Description", blank=True, null=True, help_text="Description of the workflow level use") - name = models.CharField("Name", max_length=255, help_text="Name of workflow level as it relates to workflow level 1") + level2_uuid = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + verbose_name='WorkflowLevel2 UUID', + help_text="Unique ID", + ) + description = models.TextField( + "Description", + blank=True, + null=True, + help_text="Description of the workflow level use", + ) + name = models.CharField( + "Name", + max_length=255, + help_text="Name of workflow level as it relates to workflow level 1", + ) notes = models.TextField(blank=True, null=True) - parent_workflowlevel2 = models.IntegerField("Parent", default=0, blank=True, help_text="Workflow level 2 can relate to another workflow level 2 creating multiple levels of relationships") - short_name = models.CharField("Code", max_length=20, blank=True, null=True, help_text="Shortened name autogenerated") - workflowlevel1 = models.ForeignKey(WorkflowLevel1, verbose_name="Workflow Level 1", on_delete=models.CASCADE, related_name="workflowlevel2", help_text="Primary or parent Workflow") + parent_workflowlevel2 = models.IntegerField( + "Parent", + default=0, + blank=True, + help_text="Workflow level 2 can relate to another workflow level 2 creating multiple levels of relationships", + ) + short_name = models.CharField( + "Code", + max_length=20, + blank=True, + null=True, + help_text="Shortened name autogenerated", + ) + workflowlevel1 = models.ForeignKey( + WorkflowLevel1, + verbose_name="Workflow Level 1", + on_delete=models.CASCADE, + related_name="workflowlevel2", + help_text="Primary or parent Workflow", + ) create_date = models.DateTimeField("Date Created", null=True, blank=True) - created_by = models.ForeignKey(CoreUser, related_name='workflowlevel2', null=True, blank=True, on_delete=models.SET_NULL) + created_by = models.ForeignKey( + CoreUser, + related_name='workflowlevel2', + null=True, + blank=True, + on_delete=models.SET_NULL, + ) edit_date = models.DateTimeField("Last Edit Date", null=True, blank=True) - core_groups = models.ManyToManyField(CoreGroup, verbose_name='Core groups', blank=True, related_name='workflowlevel2s', related_query_name='workflowlevel2s') + core_groups = models.ManyToManyField( + CoreGroup, + verbose_name='Core groups', + blank=True, + related_name='workflowlevel2s', + related_query_name='workflowlevel2s', + ) start_date = models.DateTimeField("Start Date", null=True, blank=True) end_date = models.DateTimeField("End Date", null=True, blank=True) - type = models.ForeignKey(WorkflowLevelType, null=True, blank=True, on_delete=models.SET_NULL, related_name='workflowlevel2s') - status = models.ForeignKey(WorkflowLevelStatus, null=True, blank=True, on_delete=models.SET_NULL, related_name='workflowlevel2s') + type = models.ForeignKey( + WorkflowLevelType, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='workflowlevel2s', + ) + status = models.ForeignKey( + WorkflowLevelStatus, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='workflowlevel2s', + ) class Meta: ordering = ('name',) @@ -141,13 +242,49 @@ class WorkflowTeam(models.Model): WorkflowTeam defines m2m relations between CoreUser and Workflowlevel1. It also defines a role for this relationship (as a fk to Group instance). """ - team_uuid = models.CharField(max_length=255, editable=False, verbose_name='WorkflowLevel1 UUID', default=uuid.uuid4, unique=True) - workflow_user = models.ForeignKey(CoreUser, blank=True, null=True, on_delete=models.CASCADE, related_name="auth_approving", help_text='User with access/permissions to related workflowlevels') - workflowlevel1 = models.ForeignKey(WorkflowLevel1, null=True, on_delete=models.CASCADE, blank=True, help_text='Related workflowlevel 1') - start_date = models.DateTimeField(null=True, blank=True, help_text='If required a time span can be associated with workflow level access') - end_date = models.DateTimeField(null=True, blank=True, help_text='If required a time span can be associated with workflow level access expiration') - status = models.CharField(max_length=255, null=True, blank=True, help_text='Active status of access') - role = models.ForeignKey(Group, null=True, blank=True, on_delete=models.CASCADE, help_text='Type of access via related group') + + team_uuid = models.CharField( + max_length=255, + editable=False, + verbose_name='WorkflowLevel1 UUID', + default=uuid.uuid4, + unique=True, + ) + workflow_user = models.ForeignKey( + CoreUser, + blank=True, + null=True, + on_delete=models.CASCADE, + related_name="auth_approving", + help_text='User with access/permissions to related workflowlevels', + ) + workflowlevel1 = models.ForeignKey( + WorkflowLevel1, + null=True, + on_delete=models.CASCADE, + blank=True, + help_text='Related workflowlevel 1', + ) + start_date = models.DateTimeField( + null=True, + blank=True, + help_text='If required a time span can be associated with workflow level access', + ) + end_date = models.DateTimeField( + null=True, + blank=True, + help_text='If required a time span can be associated with workflow level access expiration', + ) + status = models.CharField( + max_length=255, null=True, blank=True, help_text='Active status of access' + ) + role = models.ForeignKey( + Group, + null=True, + blank=True, + on_delete=models.CASCADE, + help_text='Type of access via related group', + ) create_date = models.DateTimeField(null=True, blank=True) edit_date = models.DateTimeField(null=True, blank=True) @@ -177,10 +314,18 @@ def organization(self) -> Union[Organization, None]: class WorkflowLevel2Sort(models.Model): - workflowlevel1 = models.ForeignKey(WorkflowLevel1, null=True, on_delete=models.CASCADE, blank=True) - workflowlevel2_parent = models.ForeignKey(WorkflowLevel2, on_delete=models.CASCADE, null=True, blank=True) - workflowlevel2_pk = models.UUIDField("UUID to be Sorted", default='00000000-0000-4000-8000-000000000000') - sort_array = JSONField(null=True, blank=True, help_text="Sorted JSON array of workflow levels") + workflowlevel1 = models.ForeignKey( + WorkflowLevel1, null=True, on_delete=models.CASCADE, blank=True + ) + workflowlevel2_parent = models.ForeignKey( + WorkflowLevel2, on_delete=models.CASCADE, null=True, blank=True + ) + workflowlevel2_pk = models.UUIDField( + "UUID to be Sorted", default='00000000-0000-4000-8000-000000000000' + ) + sort_array = JSONField( + null=True, blank=True, help_text="Sorted JSON array of workflow levels" + ) create_date = models.DateTimeField(null=True, blank=True) edit_date = models.DateTimeField(null=True, blank=True) diff --git a/workflow/pagination.py b/workflow/pagination.py index 929d49b5..9b31cd5f 100644 --- a/workflow/pagination.py +++ b/workflow/pagination.py @@ -1,4 +1,8 @@ -from rest_framework.pagination import CursorPagination, PageNumberPagination, LimitOffsetPagination +from rest_framework.pagination import ( + CursorPagination, + PageNumberPagination, + LimitOffsetPagination, +) class StandardResultsSetPagination(PageNumberPagination): diff --git a/workflow/permissions.py b/workflow/permissions.py index 9a5eaf5a..dd7d8824 100644 --- a/workflow/permissions.py +++ b/workflow/permissions.py @@ -20,20 +20,21 @@ class IsSuperUserOrReadOnly(permissions.BasePermission): def has_permission(self, request, view): return ( - request.method in permissions.SAFE_METHODS or - request.user and - request.user.is_authenticated and - request.user.is_superuser + request.method in permissions.SAFE_METHODS + or request.user + and request.user.is_authenticated + and request.user.is_superuser ) class CoreGroupsPermissions(permissions.BasePermission): - def _get_workflowlevel(self, view, request_data, field_name): wflvl_serializer = view.serializer_class().get_fields()[field_name] # Check if the field is Many-To-Many or not - if wflvl_serializer.__class__ == ManyRelatedField and isinstance(request_data, QueryDict): + if wflvl_serializer.__class__ == ManyRelatedField and isinstance( + request_data, QueryDict + ): primitive_value = request_data.getlist(field_name) else: primitive_value = request_data.get(field_name) @@ -57,23 +58,36 @@ def has_permission(self, request, view): return True # TODO: check if we can optimize following query using 'through' M2M Models - user_groups = request.user.core_groups.prefetch_related('workflowlevel1s', 'workflowlevel2s') + user_groups = request.user.core_groups.prefetch_related( + 'workflowlevel1s', 'workflowlevel2s' + ) # sort up permissions into more convenient way (default is read-only '0100') viewonly_display_permissions = '{0:04b}'.format(PERMISSIONS_VIEW_ONLY) - global_permissions, org_permissions = viewonly_display_permissions, viewonly_display_permissions + global_permissions, org_permissions = ( + viewonly_display_permissions, + viewonly_display_permissions, + ) wl1_permissions = defaultdict(lambda: viewonly_display_permissions) wl2_permissions = defaultdict(lambda: viewonly_display_permissions) for group in user_groups: if group.is_global: - global_permissions = merge_permissions(global_permissions, group.display_permissions) + global_permissions = merge_permissions( + global_permissions, group.display_permissions + ) elif group.is_org_level: - org_permissions = merge_permissions(org_permissions, group.display_permissions) + org_permissions = merge_permissions( + org_permissions, group.display_permissions + ) else: for wl1 in group.workflowlevel1s.all(): - wl1_permissions[wl1.pk] = merge_permissions(wl1_permissions[wl1.pk], group.display_permissions) + wl1_permissions[wl1.pk] = merge_permissions( + wl1_permissions[wl1.pk], group.display_permissions + ) for wl2 in group.workflowlevel2s.all(): - wl2_permissions[wl2.pk] = merge_permissions(wl2_permissions[wl2.pk], group.display_permissions) + wl2_permissions[wl2.pk] = merge_permissions( + wl2_permissions[wl2.pk], group.display_permissions + ) action = view.action if has_permission(global_permissions, action): @@ -90,7 +104,9 @@ def has_permission(self, request, view): if data.get('workflowlevel1'): wflvl1 = self._get_workflowlevel(view, data, 'workflowlevel1') else: - wflvl1 = self._get_workflowlevel1_from_level2(data['workflowlevel2']) + wflvl1 = self._get_workflowlevel1_from_level2( + data['workflowlevel2'] + ) if not wflvl1: return False @@ -114,16 +130,19 @@ def _queryset(self, view): :param view: :return: QuerySet """ - assert hasattr(view, 'get_queryset') or getattr(view, 'queryset', None) is not None, ( + assert ( + hasattr(view, 'get_queryset') or getattr(view, 'queryset', None) is not None + ), ( 'Cannot apply {} on a view that does not set ' '`.queryset` or have a `.get_queryset()` method.' - ).format(self.__class__.__name__) + ).format( + self.__class__.__name__ + ) if hasattr(view, 'get_queryset'): queryset = view.get_queryset() - assert queryset is not None, ( - '{}.get_queryset() returned None'.format( - view.__class__.__name__) + assert queryset is not None, '{}.get_queryset() returned None'.format( + view.__class__.__name__ ) return queryset @@ -147,7 +166,9 @@ def has_object_permission(self, request, view, obj): # Permissions on WorkflowLevel1 itself are defined by Org-level permissions groups = request.user.core_groups.filter(is_org_level=True) elif hasattr(obj, 'workflowlevel1'): - groups = request.user.core_groups.all().intersection(obj.workflowlevel1.core_groups.all()) + groups = request.user.core_groups.all().intersection( + obj.workflowlevel1.core_groups.all() + ) else: return True diff --git a/workflow/serializers.py b/workflow/serializers.py index 9b88273d..6e82efc2 100755 --- a/workflow/serializers.py +++ b/workflow/serializers.py @@ -3,7 +3,6 @@ class WorkflowLevel1Serializer(serializers.ModelSerializer): - class Meta: model = wfm.WorkflowLevel1 fields = '__all__' @@ -34,30 +33,26 @@ class Meta: class InternationalizationSerializer(serializers.ModelSerializer): - class Meta: model = wfm.Internationalization fields = '__all__' class WorkflowLevel2NameSerializer(serializers.ModelSerializer): - class Meta: model = wfm.WorkflowLevel2 fields = ('level2_uuid', 'name') - read_only_fields = ('level2_uuid', ) + read_only_fields = ('level2_uuid',) class WorkflowLevel2SortSerializer(serializers.ModelSerializer): - class Meta: model = wfm.WorkflowLevel2Sort fields = '__all__' - read_only_fields = ('level2_uuid', ) + read_only_fields = ('level2_uuid',) class WorkflowTeamSerializer(serializers.ModelSerializer): - class Meta: model = wfm.WorkflowTeam fields = '__all__' diff --git a/workflow/tests/test_internationalizationview.py b/workflow/tests/test_internationalizationview.py index 3cc0667f..a1433a72 100644 --- a/workflow/tests/test_internationalizationview.py +++ b/workflow/tests/test_internationalizationview.py @@ -53,7 +53,7 @@ def test_create_internationalization_superuser(self): data = { 'language': 'pt-BR', - 'language_file': '{"name": "Nome", "gender": "Gênero"}' + 'language_file': '{"name": "Nome", "gender": "Gênero"}', } request = self.factory.post('/internationalization/', data) request.user = self.core_user @@ -69,7 +69,7 @@ def test_create_internationalization_normaluser(self): """ data = { 'language': 'pt-BR', - 'language_file': '{"name": "Nome", "gender": "Gênero"}' + 'language_file': '{"name": "Nome", "gender": "Gênero"}', } request = self.factory.post('/internationalization/', data) request.user = self.core_user @@ -100,8 +100,7 @@ def test_retrieve_internationalization_superuser(self): self.core_user.save() inter = factories.Internationalization() - request = self.factory.get('/internationalization/{}'.format( - inter.id)) + request = self.factory.get('/internationalization/{}'.format(inter.id)) request.user = self.core_user view = InternationalizationViewSet.as_view({'get': 'retrieve'}) response = view(request, pk=inter.pk) @@ -114,8 +113,7 @@ def test_retrieve_internationalization_normaluser(self): """ inter = factories.Internationalization() - request = self.factory.get('/internationalization/{}'.format( - inter.id)) + request = self.factory.get('/internationalization/{}'.format(inter.id)) request.user = self.core_user view = InternationalizationViewSet.as_view({'get': 'retrieve'}) response = view(request, pk=inter.pk) @@ -133,9 +131,7 @@ def test_update_unexisting_internationalization(self): self.core_user.is_superuser = True self.core_user.save() - data = { - 'language': 'pt-BR', - } + data = {'language': 'pt-BR'} request = self.factory.post('/internationalization/', data) request.user = self.core_user view = InternationalizationViewSet.as_view({'post': 'update'}) @@ -154,7 +150,7 @@ def test_update_internationalization_superuser(self): data = { 'language': 'pt-BR', - 'language_file': '{"name": "Nome", "gender": "Gênero"}' + 'language_file': '{"name": "Nome", "gender": "Gênero"}', } request = self.factory.post('/internationalization/', data) request.user = self.core_user @@ -170,9 +166,7 @@ def test_update_internationalization_normaluser(self): """ inter = factories.Internationalization() - data = { - 'language': 'pt-BR', - } + data = {'language': 'pt-BR'} request = self.factory.post('/internationalization/', data) request.user = self.core_user view = InternationalizationViewSet.as_view({'post': 'update'}) @@ -215,7 +209,9 @@ def test_delete_internationalization_superuser(self): self.assertEqual(response.status_code, 204) self.assertRaises( Internationalization.DoesNotExist, - Internationalization.objects.get, pk=inter.pk) + Internationalization.objects.get, + pk=inter.pk, + ) def test_delete_internationalization_normaluser(self): """ diff --git a/workflow/tests/test_serializers.py b/workflow/tests/test_serializers.py index aa48087d..7b08dcba 100644 --- a/workflow/tests/test_serializers.py +++ b/workflow/tests/test_serializers.py @@ -3,28 +3,30 @@ from workflow.serializers import WorkflowLevel2Serializer, WorkflowLevelTypeSerializer from .fixtures import wfl2, wfl_type + @pytest.mark.django_db() def test_workflow_level2_serializer(request_factory, wfl2): request = request_factory.get('') serializer = WorkflowLevel2Serializer(wfl2, context={'request': request}) data = serializer.data - keys = ["id", - "level2_uuid", - "description", - "name", - "notes", - "parent_workflowlevel2", - "short_name", - "create_date", - "edit_date", - "start_date", - "end_date", - "workflowlevel1", - "created_by", - "type", - "core_groups", - "status", - ] + keys = [ + "id", + "level2_uuid", + "description", + "name", + "notes", + "parent_workflowlevel2", + "short_name", + "create_date", + "edit_date", + "start_date", + "end_date", + "workflowlevel1", + "created_by", + "type", + "core_groups", + "status", + ] assert set(data.keys()) == set(keys) assert data["id"] == data["level2_uuid"] @@ -34,11 +36,6 @@ def test_workflow_level_type_serializer(request_factory, wfl_type): request = request_factory.get('') serializer = WorkflowLevelTypeSerializer(wfl_type, context={'request': request}) data = serializer.data - keys = ["id", - "uuid", - "name", - "create_date", - "edit_date", - ] + keys = ["id", "uuid", "name", "create_date", "edit_date"] assert set(data.keys()) == set(keys) assert data["id"] == data["uuid"] diff --git a/workflow/tests/test_workflowlevel1view.py b/workflow/tests/test_workflowlevel1view.py index 4f98b898..ac0bfd33 100644 --- a/workflow/tests/test_workflowlevel1view.py +++ b/workflow/tests/test_workflowlevel1view.py @@ -6,8 +6,12 @@ import factories from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory -from core.models import PERMISSIONS_ORG_ADMIN, PERMISSIONS_WORKFLOW_ADMIN, PERMISSIONS_WORKFLOW_TEAM, \ - PERMISSIONS_VIEW_ONLY +from core.models import ( + PERMISSIONS_ORG_ADMIN, + PERMISSIONS_WORKFLOW_ADMIN, + PERMISSIONS_WORKFLOW_TEAM, + PERMISSIONS_VIEW_ONLY, +) from workflow.models import WorkflowLevel1 from ..views import WorkflowLevel1ViewSet @@ -37,9 +41,12 @@ def test_list_workflowlevel1_superuser(self): def test_list_workflowlevel1_superuser_and_org_admin(self): wflvl1 = factories.WorkflowLevel1() wflvl2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) self.core_user.is_staff = True @@ -55,12 +62,14 @@ def test_list_workflowlevel1_superuser_and_org_admin(self): self.assertEqual(response.data[0]['name'], wflvl1.name) def test_list_workflowlevel1_org_admin(self): - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) wflvl2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) request = self.factory.get(reverse('workflowlevel1-list')) @@ -72,11 +81,12 @@ def test_list_workflowlevel1_org_admin(self): self.assertEqual(response.data[0]['name'], wflvl1.name) def test_list_workflowlevel1_program_admin(self): - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) wflvl2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wfl1_admin = factories.CoreGroup(name='Workflow Admin', permissions=PERMISSIONS_WORKFLOW_ADMIN) + group_wfl1_admin = factories.CoreGroup( + name='Workflow Admin', permissions=PERMISSIONS_WORKFLOW_ADMIN + ) wflvl1.core_groups.add(group_wfl1_admin) self.core_user.core_groups.add(group_wfl1_admin) @@ -89,18 +99,19 @@ def test_list_workflowlevel1_program_admin(self): self.assertEqual(response.data[0]['name'], wflvl1.name) def test_list_filter_workflowlevel1_program_admin(self): - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) wflvl1_2 = factories.WorkflowLevel1( name='Population Health Initiative', - organization=self.core_user.organization) + organization=self.core_user.organization, + ) - group_wfl1_admin = factories.CoreGroup(name='Workflow Admin', permissions=PERMISSIONS_WORKFLOW_ADMIN) + group_wfl1_admin = factories.CoreGroup( + name='Workflow Admin', permissions=PERMISSIONS_WORKFLOW_ADMIN + ) wflvl1.core_groups.add(group_wfl1_admin) wflvl1_2.core_groups.add(group_wfl1_admin) self.core_user.core_groups.add(group_wfl1_admin) - request = self.factory.get( '{}?name={}'.format(reverse('workflowlevel1-list'), wflvl1.name) ) @@ -113,11 +124,12 @@ def test_list_filter_workflowlevel1_program_admin(self): self.assertNotEqual(response.data[0]['name'], wflvl1_2.name) def test_list_workflowlevel1_program_team(self): - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) @@ -131,9 +143,11 @@ def test_list_workflowlevel1_program_team(self): def test_list_workflowlevel1_normal_user_same_org(self): wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF View Only', - permissions=PERMISSIONS_VIEW_ONLY, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF View Only', + permissions=PERMISSIONS_VIEW_ONLY, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) @@ -144,19 +158,27 @@ def test_list_workflowlevel1_normal_user_same_org(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) - @patch('workflow.pagination.DefaultCursorPagination.page_size', new_callable=PropertyMock) + @patch( + 'workflow.pagination.DefaultCursorPagination.page_size', + new_callable=PropertyMock, + ) def test_list_workflowlevel1_pagination(self, page_size_mock): """ For page_size 1 and pagination true, list wfl1 endpoint should return 1 wfl1 for each page""" # set page_size =1 page_size_mock.return_value = 1 wfl1_1 = factories.WorkflowLevel1( - name='1. wfl', organization=self.core_user.organization) + name='1. wfl', organization=self.core_user.organization + ) wfl1_2 = factories.WorkflowLevel1( - name='2. wfl', organization=self.core_user.organization) - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + name='2. wfl', organization=self.core_user.organization + ) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) request = self.factory.get('?paginate=true') @@ -172,8 +194,7 @@ def test_list_workflowlevel1_pagination(self, page_size_mock): m = re.search('=(.*)&', response.data['next']) cursor = m.group(1) - request = self.factory.get('?cursor={}&paginate=true'.format( - cursor)) + request = self.factory.get('?cursor={}&paginate=true'.format(cursor)) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'get': 'list'}) response = view(request) @@ -195,7 +216,7 @@ def test_create_workflowlevel1_superuser(self): data = { 'name': 'Save the Children', - 'organization': self.core_user.organization.pk + 'organization': self.core_user.organization.pk, } request = self.factory.post(reverse('workflowlevel1-list'), data) request.user = self.core_user @@ -210,9 +231,12 @@ def test_create_workflowlevel1_superuser(self): self.assertEqual(wflvl1.user_access.first(), self.core_user) def test_create_workflowlevel1_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) data = {'name': 'Save the Children'} @@ -238,20 +262,25 @@ def test_create_workflowlevel1_normal_user(self): self.assertEqual(response.status_code, 403) def test_create_workflowlevel1_uuid_is_self_generated(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) data = { 'name': 'Save the Children', - 'level1_uuid': '75e4c912-4149-11e8-842f-0ed5f89f718b' + 'level1_uuid': '75e4c912-4149-11e8-842f-0ed5f89f718b', } request = self.factory.post(reverse('workflowlevel1-list'), data) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'post': 'create'}) response = view(request) self.assertEqual(response.status_code, 201) - self.assertNotEqual(response.data['level1_uuid'], '75e4c912-4149-11e8-842f-0ed5f89f718b') + self.assertNotEqual( + response.data['level1_uuid'], '75e4c912-4149-11e8-842f-0ed5f89f718b' + ) class WorkflowLevel1UpdateViewsTest(TestCase): @@ -260,15 +289,16 @@ def setUp(self): self.core_user = factories.CoreUser() def test_update_unexisting_workflowlevel1(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) data = {'salary': '10'} - request = self.factory.put( - reverse('workflowlevel1-detail', args=(288,)), data - ) + request = self.factory.put(reverse('workflowlevel1-detail', args=(288,)), data) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'put': 'update'}) response = view(request, pk=288) @@ -293,9 +323,12 @@ def test_update_workflowlevel1_superuser(self): self.assertEqual(wflvl1.name, data['name']) def test_update_workflowlevel1_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) @@ -312,13 +345,17 @@ def test_update_workflowlevel1_org_admin(self): self.assertEqual(wflvl1.name, data['name']) def test_update_workflowlevel1_different_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) wflvl1 = factories.WorkflowLevel1( - organization=factories.Organization(name='Other Org')) + organization=factories.Organization(name='Other Org') + ) data = {'name': 'Save the Lennons'} request = self.factory.put( reverse('workflowlevel1-detail', args=(wflvl1.pk,)), data @@ -329,15 +366,22 @@ def test_update_workflowlevel1_different_org_admin(self): self.assertEqual(response.status_code, 403) def test_update_workflowlevel1_program_admin(self): - wfl1 = factories.WorkflowLevel1.create(name='Save the Children', organization=self.core_user.organization) + wfl1 = factories.WorkflowLevel1.create( + name='Save the Children', organization=self.core_user.organization + ) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wfl1.core_groups.add(group_wf_admin) - request = self.factory.put(reverse('workflowlevel1-detail', args=(wfl1.pk,)), {'name': 'Save the Lennons'}) + request = self.factory.put( + reverse('workflowlevel1-detail', args=(wfl1.pk,)), + {'name': 'Save the Lennons'}, + ) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'put': 'update'}) response = view(request, pk=wfl1.pk) @@ -345,18 +389,22 @@ def test_update_workflowlevel1_program_admin(self): self.assertEqual(response.status_code, 403) def test_update_workflowlevel1_program_admin_json(self): - wfl1 = factories.WorkflowLevel1.create(name='Save the Children', organization=self.core_user.organization) + wfl1 = factories.WorkflowLevel1.create( + name='Save the Children', organization=self.core_user.organization + ) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wfl1.core_groups.add(group_wf_admin) request = self.factory.put( reverse('workflowlevel1-detail', args=(wfl1.pk,)), json.dumps({'name': 'Save the Lennons'}), - content_type='application/json' + content_type='application/json', ) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'put': 'update'}) @@ -366,13 +414,18 @@ def test_update_workflowlevel1_program_admin_json(self): def test_update_workflowlevel1_program_team(self): wflvl1 = factories.WorkflowLevel1() - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) - request = self.factory.put(reverse('workflowlevel1-detail', args=(wflvl1.pk,)), {'name': 'Save the Lennons'}) + request = self.factory.put( + reverse('workflowlevel1-detail', args=(wflvl1.pk,)), + {'name': 'Save the Lennons'}, + ) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'put': 'update'}) response = view(request, pk=wflvl1.pk) @@ -381,14 +434,15 @@ def test_update_workflowlevel1_program_team(self): def test_update_workflowlevel1_same_org_different_program_team(self): wflvl1_other = factories.WorkflowLevel1() - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1_other.core_groups.add(group_wf_team) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) data = {'name': 'Save the Lennons'} request = self.factory.put( @@ -417,30 +471,35 @@ def test_delete_workflowlevel1_superuser(self): response = view(request, pk=wflvl1.pk) self.assertEqual(response.status_code, 204) self.assertRaises( - WorkflowLevel1.DoesNotExist, - WorkflowLevel1.objects.get, pk=wflvl1.pk) + WorkflowLevel1.DoesNotExist, WorkflowLevel1.objects.get, pk=wflvl1.pk + ) def test_delete_workflowlevel1_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) request = self.factory.delete(reverse('workflowlevel1-list')) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'delete': 'destroy'}) response = view(request, pk=wflvl1.pk) self.assertEqual(response.status_code, 204) self.assertRaises( - WorkflowLevel1.DoesNotExist, - WorkflowLevel1.objects.get, pk=wflvl1.pk) + WorkflowLevel1.DoesNotExist, WorkflowLevel1.objects.get, pk=wflvl1.pk + ) def test_delete_workflowlevel1_different_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) org_other = factories.Organization(name='Other Org') @@ -455,9 +514,11 @@ def test_delete_workflowlevel1_different_org_admin(self): def test_delete_workflowlevel1_program_admin(self): wfl1 = factories.WorkflowLevel1(name='Save the Children') - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wfl1.core_groups.add(group_wf_admin) @@ -491,9 +552,12 @@ def test_delete_workflowlevel1_normal_user(self): WorkflowLevel1.objects.get(pk=wflvl1.pk) def test_delete_workflowlevel1_program_admin_just_one(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) # Create a program @@ -520,7 +584,9 @@ def test_delete_workflowlevel1_program_admin_just_one(self): self.assertEqual(response.status_code, 204) self.assertRaises( WorkflowLevel1.DoesNotExist, - WorkflowLevel1.objects.get, pk=second_program_id) + WorkflowLevel1.objects.get, + pk=second_program_id, + ) WorkflowLevel1.objects.get(pk=first_program_id) @@ -549,13 +615,21 @@ def test_filter_workflowlevel1_superuser(self): def test_filter_workflowlevel1_org_admin(self): wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - factories.WorkflowLevel1(name='Population Health Initiative', organization=self.core_user.organization) - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + factories.WorkflowLevel1( + name='Population Health Initiative', + organization=self.core_user.organization, + ) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) - request = self.factory.get('{}?name={}'.format(reverse('workflowlevel1-list'), wflvl1.name)) + request = self.factory.get( + '{}?name={}'.format(reverse('workflowlevel1-list'), wflvl1.name) + ) request.user = self.core_user view = WorkflowLevel1ViewSet.as_view({'get': 'list'}) response = view(request) diff --git a/workflow/tests/test_workflowlevel2serializers.py b/workflow/tests/test_workflowlevel2serializers.py index 5b6d98f0..eb633269 100644 --- a/workflow/tests/test_workflowlevel2serializers.py +++ b/workflow/tests/test_workflowlevel2serializers.py @@ -16,9 +16,6 @@ def test_contains_expected_fields(self): data = serializer.data - keys = [ - 'level2_uuid', - 'name' - ] + keys = ['level2_uuid', 'name'] self.assertEqual(set(data.keys()), set(keys)) diff --git a/workflow/tests/test_workflowlevel2sortview.py b/workflow/tests/test_workflowlevel2sortview.py index f05e4346..ebf5a9f2 100644 --- a/workflow/tests/test_workflowlevel2sortview.py +++ b/workflow/tests/test_workflowlevel2sortview.py @@ -3,8 +3,12 @@ from django.test import TestCase import factories from rest_framework.test import APIRequestFactory -from core.models import PERMISSIONS_ORG_ADMIN, PERMISSIONS_VIEW_ONLY, PERMISSIONS_WORKFLOW_ADMIN, \ - PERMISSIONS_WORKFLOW_TEAM +from core.models import ( + PERMISSIONS_ORG_ADMIN, + PERMISSIONS_VIEW_ONLY, + PERMISSIONS_WORKFLOW_ADMIN, + PERMISSIONS_WORKFLOW_TEAM, +) from workflow.models import WorkflowLevel2Sort from ..views import WorkflowLevel2SortViewSet @@ -13,8 +17,12 @@ class WorkflowLevel2SortListViewsTest(TestCase): def setUp(self): self.not_default_org = factories.Organization.create(name='Some Org') - wfl1_not_default_org = factories.WorkflowLevel1.create(organization=self.not_default_org) - factories.WorkflowLevel2Sort.create_batch(2, workflowlevel1=wfl1_not_default_org) + wfl1_not_default_org = factories.WorkflowLevel1.create( + organization=self.not_default_org + ) + factories.WorkflowLevel2Sort.create_batch( + 2, workflowlevel1=wfl1_not_default_org + ) self.factory = APIRequestFactory() self.core_user = factories.CoreUser() @@ -23,8 +31,7 @@ def test_list_workflowlevel2sort_superuser(self): list view should return all objs to super users """ request = self.factory.get('/workflowlevel2sort/') - request.user = factories.CoreUser.build(is_superuser=True, - is_staff=True) + request.user = factories.CoreUser.build(is_superuser=True, is_staff=True) view = WorkflowLevel2SortViewSet.as_view({'get': 'list'}) response = view(request) self.assertEqual(response.status_code, 200) @@ -35,9 +42,12 @@ def test_list_workflowlevel2sort_org_admin(self): list view should return only objs of an org to org admins """ request = self.factory.get('/workflowlevel2sort/') - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) wflvl1 = factories.WorkflowLevel1(organization=self.not_default_org) @@ -55,9 +65,11 @@ def test_list_workflowlevel2sort_program_admin(self): """ request = self.factory.get('/workflowlevel2sort/') wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) request.user = self.core_user @@ -73,9 +85,11 @@ def test_list_workflowlevel2sort_program_team(self): """ request = self.factory.get('/workflowlevel2sort/') wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) request.user = self.core_user @@ -91,9 +105,11 @@ def test_list_workflowlevel2sort_view_only(self): """ request = self.factory.get('/workflowlevel2sort/') wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF View Only', - permissions=PERMISSIONS_VIEW_ONLY, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF View Only', + permissions=PERMISSIONS_VIEW_ONLY, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) request.user = self.core_user @@ -117,8 +133,7 @@ def test_create_workflowlevel2_superuser(self): request = self.factory.post('/workflowlevel2sort/') wflvl1 = factories.WorkflowLevel1() - data = {'workflowlevel2_pk': uuid.uuid4(), - 'workflowlevel1': wflvl1.pk} + data = {'workflowlevel2_pk': uuid.uuid4(), 'workflowlevel1': wflvl1.pk} request = self.factory.post('/workflowlevel2/', data) request.user = self.core_user @@ -126,15 +141,15 @@ def test_create_workflowlevel2_superuser(self): response = view(request) self.assertEqual(response.status_code, 201) - self.assertEqual(response.data['workflowlevel2_pk'], str(data['workflowlevel2_pk'])) + self.assertEqual( + response.data['workflowlevel2_pk'], str(data['workflowlevel2_pk']) + ) def test_create_workflowlevel2sort_normal_user(self): request = self.factory.post('/workflowlevel2sort/') - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - data = {'workflowlevel2_pk': uuid.uuid4(), - 'workflowlevel1': wflvl1.pk} + data = {'workflowlevel2_pk': uuid.uuid4(), 'workflowlevel1': wflvl1.pk} request = self.factory.post('/workflowlevel2/', data) request.user = self.core_user @@ -142,7 +157,9 @@ def test_create_workflowlevel2sort_normal_user(self): response = view(request) self.assertEqual(response.status_code, 201) - self.assertEqual(response.data['workflowlevel2_pk'], str(data['workflowlevel2_pk'])) + self.assertEqual( + response.data['workflowlevel2_pk'], str(data['workflowlevel2_pk']) + ) class WorkflowLevel2SortUpdateViewsTest(TestCase): @@ -152,9 +169,12 @@ def setUp(self): factories.Group() def test_update_unexisting_workflowlevel2sort(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) data = {'workflowlevel2_pk': 1} @@ -172,11 +192,9 @@ def test_update_workflowlevel2sort_superuser(self): request = self.factory.post('/workflowlevel2sort/') wflvl1 = factories.WorkflowLevel1() - workflowlevel2sort = \ - factories.WorkflowLevel2Sort(workflowlevel1=wflvl1) + workflowlevel2sort = factories.WorkflowLevel2Sort(workflowlevel1=wflvl1) - data = {'workflowlevel2_pk': uuid.uuid4(), - 'workflowlevel1': wflvl1.pk} + data = {'workflowlevel2_pk': uuid.uuid4(), 'workflowlevel1': wflvl1.pk} request = self.factory.post('/workflowlevel2sort/', data) request.user = self.core_user @@ -185,19 +203,17 @@ def test_update_workflowlevel2sort_superuser(self): response = view(request, pk=workflowlevel2sort.pk) self.assertEqual(response.status_code, 200) - workflowlevel2sort = WorkflowLevel2Sort.objects.get( - pk=response.data['id']) - self.assertEqual(str(workflowlevel2sort.workflowlevel2_pk), str(data['workflowlevel2_pk'])) + workflowlevel2sort = WorkflowLevel2Sort.objects.get(pk=response.data['id']) + self.assertEqual( + str(workflowlevel2sort.workflowlevel2_pk), str(data['workflowlevel2_pk']) + ) def test_update_workflowlevel2sort_normal_user(self): request = self.factory.post('/workflowlevel2sort/') - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) - workflowlevel2sort = factories.WorkflowLevel2Sort( - workflowlevel1=wflvl1) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) + workflowlevel2sort = factories.WorkflowLevel2Sort(workflowlevel1=wflvl1) - data = {'workflowlevel2_pk': uuid.uuid4(), - 'workflowlevel1': wflvl1.pk} + data = {'workflowlevel2_pk': uuid.uuid4(), 'workflowlevel1': wflvl1.pk} request = self.factory.post('/workflowlevel2sort/', data) request.user = self.core_user @@ -206,20 +222,18 @@ def test_update_workflowlevel2sort_normal_user(self): self.assertEqual(response.status_code, 200) - workflowlevel2sort = WorkflowLevel2Sort.objects.get( - pk=response.data['id']) - self.assertEqual(workflowlevel2sort.workflowlevel2_pk, - data['workflowlevel2_pk']) + workflowlevel2sort = WorkflowLevel2Sort.objects.get(pk=response.data['id']) + self.assertEqual( + workflowlevel2sort.workflowlevel2_pk, data['workflowlevel2_pk'] + ) def test_update_workflowlevel2sort_diff_org_normal_user(self): request = self.factory.post('/workflowlevel2sort/') another_org = factories.Organization(name='Another Org') wflvl1 = factories.WorkflowLevel1(organization=another_org) - workflowlevel2sort = factories.WorkflowLevel2Sort( - workflowlevel1=wflvl1) + workflowlevel2sort = factories.WorkflowLevel2Sort(workflowlevel1=wflvl1) - data = {'workflowlevel2_pk': uuid.uuid4(), - 'workflowlevel1': wflvl1.pk} + data = {'workflowlevel2_pk': uuid.uuid4(), 'workflowlevel1': wflvl1.pk} request = self.factory.post('/workflowlevel2sort/', data) request.user = self.core_user @@ -247,13 +261,13 @@ def test_delete_workflowlevel2sort_superuser(self): self.assertEqual(response.status_code, 204) self.assertRaises( WorkflowLevel2Sort.DoesNotExist, - WorkflowLevel2Sort.objects.get, pk=workflowlevel2sort.pk) + WorkflowLevel2Sort.objects.get, + pk=workflowlevel2sort.pk, + ) def test_delete_workflowlevel2sort_normal_user(self): - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) - workflowlevel2sort = factories.WorkflowLevel2Sort( - workflowlevel1=wflvl1) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) + workflowlevel2sort = factories.WorkflowLevel2Sort(workflowlevel1=wflvl1) request = self.factory.delete('/workflowlevel2sort/') request.user = self.core_user @@ -262,18 +276,22 @@ def test_delete_workflowlevel2sort_normal_user(self): self.assertEqual(response.status_code, 204) self.assertRaises( WorkflowLevel2Sort.DoesNotExist, - WorkflowLevel2Sort.objects.get, pk=workflowlevel2sort.pk) + WorkflowLevel2Sort.objects.get, + pk=workflowlevel2sort.pk, + ) def test_delete_workflowlevel2sort_diff_org_normal_user(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) another_org = factories.Organization(name='Another Org') wflvl1 = factories.WorkflowLevel1(organization=another_org) - workflowlevel2sort = factories.WorkflowLevel2Sort( - workflowlevel1=wflvl1) + workflowlevel2sort = factories.WorkflowLevel2Sort(workflowlevel1=wflvl1) request = self.factory.delete('/workflowlevel2sort/') request.user = self.core_user diff --git a/workflow/tests/test_workflowlevel2view.py b/workflow/tests/test_workflowlevel2view.py index 1f4136ee..85adbb90 100644 --- a/workflow/tests/test_workflowlevel2view.py +++ b/workflow/tests/test_workflowlevel2view.py @@ -6,8 +6,12 @@ import factories from rest_framework.test import APIRequestFactory from rest_framework.reverse import reverse -from core.models import PERMISSIONS_WORKFLOW_ADMIN, PERMISSIONS_ORG_ADMIN, PERMISSIONS_VIEW_ONLY, \ - PERMISSIONS_WORKFLOW_TEAM +from core.models import ( + PERMISSIONS_WORKFLOW_ADMIN, + PERMISSIONS_ORG_ADMIN, + PERMISSIONS_VIEW_ONLY, + PERMISSIONS_WORKFLOW_TEAM, +) from workflow.models import WorkflowLevel2 from ..views import WorkflowLevel2ViewSet @@ -16,7 +20,9 @@ class WorkflowLevel2ListViewsTest(TestCase): def setUp(self): self.not_default_org = factories.Organization.create(name='Some Org') - wfl1_not_default_org = factories.WorkflowLevel1.create(organization=self.not_default_org) + wfl1_not_default_org = factories.WorkflowLevel1.create( + organization=self.not_default_org + ) factories.WorkflowLevel2.create_batch(2, workflowlevel1=wfl1_not_default_org) self.factory = APIRequestFactory() self.core_user = factories.CoreUser() @@ -31,9 +37,12 @@ def test_list_workflowlevel2_superuser(self): def test_list_workflowlevel2_org_admin(self): request = self.factory.get(reverse('workflowlevel2-list')) - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) wflvl1 = factories.WorkflowLevel1(organization=self.not_default_org) @@ -55,9 +64,11 @@ def test_list_workflowlevel2_program_admin(self): request = self.factory.get(reverse('workflowlevel2-list')) wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) @@ -76,9 +87,11 @@ def test_list_workflowlevel2_program_team(self): request = self.factory.get(reverse('workflowlevel2-list')) wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) @@ -97,9 +110,11 @@ def test_list_workflowlevel2_view_only(self): request = self.factory.get('/api/workflowlevel2/') wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF View Only', - permissions=PERMISSIONS_VIEW_ONLY, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF View Only', + permissions=PERMISSIONS_VIEW_ONLY, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) @@ -114,7 +129,10 @@ def test_list_workflowlevel2_view_only(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data['results']), 1) - @patch('workflow.pagination.DefaultLimitOffsetPagination.default_limit', new_callable=PropertyMock) + @patch( + 'workflow.pagination.DefaultLimitOffsetPagination.default_limit', + new_callable=PropertyMock, + ) def test_list_workflowlevel2_pagination(self, default_limit_mock): """ For default_limit 1 and pagination by default, list wfl2 endpoint should return 1 wfl2 for each page""" @@ -124,9 +142,12 @@ def test_list_workflowlevel2_pagination(self, default_limit_mock): wfl2_1 = factories.WorkflowLevel2(name='1. wfl2', workflowlevel1=wfl1_1) wfl2_2 = factories.WorkflowLevel2(name='2. wfl2', workflowlevel1=wfl1_1) - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) request = self.factory.get('') @@ -161,8 +182,7 @@ def test_create_workflowlevel2_superuser(self): request = self.factory.post(reverse('workflowlevel2-list')) wflvl1 = factories.WorkflowLevel1() - data = {'name': 'Help Syrians', - 'workflowlevel1': wflvl1.pk} + data = {'name': 'Help Syrians', 'workflowlevel1': wflvl1.pk} request = self.factory.post(reverse('workflowlevel2-list'), data) request.user = self.core_user @@ -173,18 +193,22 @@ def test_create_workflowlevel2_superuser(self): self.assertEqual(response.data['name'], u'Help Syrians') def test_create_workflowlevel2_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) request = self.factory.post(reverse('workflowlevel2-list')) wfltype = factories.WorkflowLevelType() - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) - data = {'name': 'Help Syrians', - 'workflowlevel1': wflvl1.pk, - 'type': wfltype.uuid, } + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) + data = { + 'name': 'Help Syrians', + 'workflowlevel1': wflvl1.pk, + 'type': wfltype.uuid, + } request = self.factory.post(reverse('workflowlevel2-list'), data) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'post': 'create'}) @@ -198,15 +222,15 @@ def test_create_workflowlevel2_program_admin(self): request = self.factory.post(reverse('workflowlevel2-list')) wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) - data = {'name': 'Help Syrians', - 'workflowlevel1': wflvl1.pk - } + data = {'name': 'Help Syrians', 'workflowlevel1': wflvl1.pk} request = self.factory.post(reverse('workflowlevel2-list'), data) request.user = self.core_user @@ -220,18 +244,21 @@ def test_create_workflowlevel2_program_admin_json(self): request = self.factory.post(reverse('workflowlevel2-list')) wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) - data = {'name': 'Help Syrians', - 'workflowlevel1': wflvl1.pk} + data = {'name': 'Help Syrians', 'workflowlevel1': wflvl1.pk} - request = self.factory.post(reverse('workflowlevel2-list'), - json.dumps(data), - content_type='application/json') + request = self.factory.post( + reverse('workflowlevel2-list'), + json.dumps(data), + content_type='application/json', + ) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'post': 'create'}) response = view(request) @@ -241,16 +268,16 @@ def test_create_workflowlevel2_program_admin_json(self): def test_create_workflowlevel2_program_team(self): request = self.factory.post(reverse('workflowlevel2-list')) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) - data = {'name': 'Help Syrians', - 'workflowlevel1': wflvl1.pk} + data = {'name': 'Help Syrians', 'workflowlevel1': wflvl1.pk} request = self.factory.post(reverse('workflowlevel2-list'), data) request.user = self.core_user @@ -262,17 +289,17 @@ def test_create_workflowlevel2_program_team(self): def test_create_workflowlevel2_view_only(self): request = self.factory.post(reverse('workflowlevel2-list')) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF View Only', - permissions=PERMISSIONS_VIEW_ONLY, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF View Only', + permissions=PERMISSIONS_VIEW_ONLY, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) - data = {'name': 'Help Syrians', - 'workflowlevel1': wflvl1.pk} + data = {'name': 'Help Syrians', 'workflowlevel1': wflvl1.pk} request = self.factory.post(reverse('workflowlevel2-list'), data) request.user = self.core_user @@ -283,15 +310,15 @@ def test_create_workflowlevel2_view_only(self): def test_create_workflowlevel2_uuid_is_self_generated(self): wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) - data = { - 'name': 'Save the Children', - 'workflowlevel1': wflvl1.pk} + data = {'name': 'Save the Children', 'workflowlevel1': wflvl1.pk} request = self.factory.post(reverse('workflowlevel2-list'), data) request.user = self.core_user @@ -309,16 +336,17 @@ def setUp(self): factories.Group() def test_update_unexisting_workflowlevel2(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) data = {'name': 'Community awareness program conducted to plant trees'} - request = self.factory.put( - reverse('workflowlevel2-detail', args=(228,)), data - ) + request = self.factory.put(reverse('workflowlevel2-detail', args=(228,)), data) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'put': 'update'}) response = view(request, pk=288) @@ -333,8 +361,10 @@ def test_update_workflowlevel2_superuser(self): wflvl1 = factories.WorkflowLevel1() workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + } request = self.factory.put( reverse('workflowlevel2-detail', args=(str(workflowlevel2.pk),)), data @@ -348,20 +378,24 @@ def test_update_workflowlevel2_superuser(self): self.assertEqual(workflowlevel2.name, data['name']) def test_update_workflowlevel2_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) request = self.factory.post(reverse('workflowlevel2-list')) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) wfltype = factories.WorkflowLevelType() - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk, - 'type': wfltype.uuid} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + 'type': wfltype.uuid, + } request = self.factory.put( reverse('workflowlevel2-detail', args=(str(workflowlevel2.pk),)), data @@ -376,9 +410,12 @@ def test_update_workflowlevel2_org_admin(self): self.assertEqual(workflowlevel2.type, wfltype) def test_update_workflowlevel2_diff_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) request = self.factory.post(reverse('workflowlevel2-list')) @@ -386,8 +423,10 @@ def test_update_workflowlevel2_diff_org_admin(self): wflvl1 = factories.WorkflowLevel1(organization=another_org) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + } request = self.factory.put( reverse('workflowlevel2-detail', args=(str(workflowlevel2.pk),)), data @@ -399,18 +438,21 @@ def test_update_workflowlevel2_diff_org_admin(self): def test_update_workflowlevel2_program_admin(self): request = self.factory.post(reverse('workflowlevel2-list')) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + } request = self.factory.put( reverse('workflowlevel2-detail', args=(str(workflowlevel2.pk),)), data @@ -425,23 +467,26 @@ def test_update_workflowlevel2_program_admin(self): def test_update_workflowlevel2_program_admin_json(self): request = self.factory.post(reverse('workflowlevel2-list')) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + } request = self.factory.put( reverse('workflowlevel2-detail', args=(str(workflowlevel2.pk),)), json.dumps(data), - content_type='application/json' + content_type='application/json', ) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'put': 'update'}) @@ -453,18 +498,21 @@ def test_update_workflowlevel2_program_admin_json(self): def test_update_workflowlevel2_program_team(self): request = self.factory.post(reverse('workflowlevel2-list')) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + } request = self.factory.put( reverse('workflowlevel2-detail', args=(str(workflowlevel2.pk),)), data @@ -479,18 +527,21 @@ def test_update_workflowlevel2_program_team(self): def test_update_workflowlevel2_view_only(self): request = self.factory.post(reverse('workflowlevel2-list')) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_team = factories.CoreGroup(name='WF View Only', - permissions=PERMISSIONS_VIEW_ONLY, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF View Only', + permissions=PERMISSIONS_VIEW_ONLY, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + } request = self.factory.put( reverse('workflowlevel2-detail', args=(str(workflowlevel2.pk),)), data @@ -504,28 +555,31 @@ def test_update_workflowlevel2_view_only(self): def test_update_workflowlevel2_uuid_is_self_generated(self): wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk} + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + } request = self.factory.post(reverse('workflowlevel2-list'), data) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'post': 'create'}) response = view(request) self.assertEqual(response.status_code, 201) first_level2_uuid = response.data['level2_uuid'] - data = {'name': 'Community awareness program conducted to plant trees', - 'workflowlevel1': wflvl1.pk, - 'level2_uuid': '84a9888-4149-11e8-842f-0ed5f89f718b' - } + data = { + 'name': 'Community awareness program conducted to plant trees', + 'workflowlevel1': wflvl1.pk, + 'level2_uuid': '84a9888-4149-11e8-842f-0ed5f89f718b', + } pk = first_level2_uuid - request = self.factory.put( - reverse('workflowlevel2-detail', args=(pk,)), data - ) + request = self.factory.put(reverse('workflowlevel2-detail', args=(pk,)), data) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'put': 'update'}) response = view(request, pk=pk) @@ -551,16 +605,20 @@ def test_delete_workflowlevel2_superuser(self): self.assertEqual(response.status_code, 204) self.assertRaises( WorkflowLevel2.DoesNotExist, - WorkflowLevel2.objects.get, pk=str(workflowlevel2.pk)) + WorkflowLevel2.objects.get, + pk=str(workflowlevel2.pk), + ) def test_delete_workflowlevel2_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) request = self.factory.delete(reverse('workflowlevel2-list')) @@ -570,12 +628,17 @@ def test_delete_workflowlevel2_org_admin(self): self.assertEqual(response.status_code, 204) self.assertRaises( WorkflowLevel2.DoesNotExist, - WorkflowLevel2.objects.get, pk=str(workflowlevel2.pk)) + WorkflowLevel2.objects.get, + pk=str(workflowlevel2.pk), + ) def test_delete_workflowlevel2_diff_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) another_org = factories.Organization(name='Another Org') @@ -592,9 +655,11 @@ def test_delete_workflowlevel2_diff_org_admin(self): def test_delete_workflowlevel2_program_admin(self): wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) @@ -607,15 +672,19 @@ def test_delete_workflowlevel2_program_admin(self): self.assertEqual(response.status_code, 204) self.assertRaises( WorkflowLevel2.DoesNotExist, - WorkflowLevel2.objects.get, pk=str(workflowlevel2.pk)) + WorkflowLevel2.objects.get, + pk=str(workflowlevel2.pk), + ) def test_delete_workflowlevel2_diff_org(self): another_org = factories.Organization(name='Another Org') wflvl1 = factories.WorkflowLevel1(organization=another_org) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_admin = factories.CoreGroup(name='WF Admin', - permissions=PERMISSIONS_WORKFLOW_ADMIN, - organization=self.core_user.organization) + group_wf_admin = factories.CoreGroup( + name='WF Admin', + permissions=PERMISSIONS_WORKFLOW_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_admin) wflvl1.core_groups.add(group_wf_admin) @@ -627,13 +696,14 @@ def test_delete_workflowlevel2_diff_org(self): WorkflowLevel2.objects.get(pk=str(workflowlevel2.pk)) def test_delete_workflowlevel2_program_team(self): - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_team = factories.CoreGroup(name='WF Team', - permissions=PERMISSIONS_WORKFLOW_TEAM, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF Team', + permissions=PERMISSIONS_WORKFLOW_TEAM, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) @@ -645,13 +715,14 @@ def test_delete_workflowlevel2_program_team(self): WorkflowLevel2.objects.get(pk=str(workflowlevel2.pk)) def test_delete_workflowlevel2_view_only(self): - wflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) workflowlevel2 = factories.WorkflowLevel2(workflowlevel1=wflvl1) - group_wf_team = factories.CoreGroup(name='WF View Only', - permissions=PERMISSIONS_VIEW_ONLY, - organization=self.core_user.organization) + group_wf_team = factories.CoreGroup( + name='WF View Only', + permissions=PERMISSIONS_VIEW_ONLY, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_wf_team) wflvl1.core_groups.add(group_wf_team) @@ -679,22 +750,25 @@ def setUp(self): self.core_user = factories.CoreUser() def test_filter_workflowlevel2_wkflvl1_name_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) wkflvl1_1 = factories.WorkflowLevel1(organization=self.core_user.organization) wkflvl1_2 = factories.WorkflowLevel1( - name='Construction Project', - organization=self.core_user.organization) + name='Construction Project', organization=self.core_user.organization + ) wkflvl2 = factories.WorkflowLevel2(workflowlevel1=wkflvl1_1) - factories.WorkflowLevel2( - name='Develop brief survey', workflowlevel1=wkflvl1_2) + factories.WorkflowLevel2(name='Develop brief survey', workflowlevel1=wkflvl1_2) request = self.factory.get( - '{}?workflowlevel1__name={}'.format(reverse('workflowlevel2-list'), - wkflvl1_1.name) + '{}?workflowlevel1__name={}'.format( + reverse('workflowlevel2-list'), wkflvl1_1.name + ) ) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'get': 'list'}) @@ -704,22 +778,23 @@ def test_filter_workflowlevel2_wkflvl1_name_org_admin(self): self.assertEqual(response.data['results'][0]['name'], wkflvl2.name) def test_filter_workflowlevel2_wkflvl1_id_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) - wkflvl1_1 = factories.WorkflowLevel1( - organization=self.core_user.organization) - wkflvl1_2 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wkflvl1_1 = factories.WorkflowLevel1(organization=self.core_user.organization) + wkflvl1_2 = factories.WorkflowLevel1(organization=self.core_user.organization) wkflvl2 = factories.WorkflowLevel2(workflowlevel1=wkflvl1_1) - factories.WorkflowLevel2( - name='Develop brief survey', workflowlevel1=wkflvl1_2) + factories.WorkflowLevel2(name='Develop brief survey', workflowlevel1=wkflvl1_2) request = self.factory.get( - '{}?workflowlevel1__id={}'.format(reverse('workflowlevel2-list'), - wkflvl1_1.pk) + '{}?workflowlevel1__id={}'.format( + reverse('workflowlevel2-list'), wkflvl1_1.pk + ) ) request.user = self.core_user view = WorkflowLevel2ViewSet.as_view({'get': 'list'}) @@ -729,25 +804,26 @@ def test_filter_workflowlevel2_wkflvl1_id_org_admin(self): self.assertEqual(response.data['results'][0]['name'], wkflvl2.name) def test_filter_workflowlevel2_create_date_range_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) - wkflvl1 = factories.WorkflowLevel1( - organization=self.core_user.organization) + wkflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) date1 = '2019-05-01' date2 = '2019-05-02' date3 = '2019-05-03' - factories.WorkflowLevel2( - workflowlevel1=wkflvl1, create_date=date1) + factories.WorkflowLevel2(workflowlevel1=wkflvl1, create_date=date1) level22_uuid = uuid.uuid4() wkflvl22 = WorkflowLevel2.objects.create( - workflowlevel1=wkflvl1, create_date=date2, level2_uuid=level22_uuid) - factories.WorkflowLevel2( - workflowlevel1=wkflvl1, create_date=date3) + workflowlevel1=wkflvl1, create_date=date2, level2_uuid=level22_uuid + ) + factories.WorkflowLevel2(workflowlevel1=wkflvl1, create_date=date3) request = self.factory.get( f'{reverse("workflowlevel2-list")}?create_date_gte={date2}&create_date_lte={date2}' @@ -761,20 +837,27 @@ def test_filter_workflowlevel2_create_date_range_org_admin(self): self.assertEqual(response.data['results'][0]['level2_uuid'], str(level22_uuid)) def test_filter_workflowlevel2_status_org_admin(self): - group_org_admin = factories.CoreGroup(name='Org Admin', is_org_level=True, - permissions=PERMISSIONS_ORG_ADMIN, - organization=self.core_user.organization) + group_org_admin = factories.CoreGroup( + name='Org Admin', + is_org_level=True, + permissions=PERMISSIONS_ORG_ADMIN, + organization=self.core_user.organization, + ) self.core_user.core_groups.add(group_org_admin) wkflvl1 = factories.WorkflowLevel1(organization=self.core_user.organization) - wfl_status1 = factories.WorkflowLevelStatus(name="Started Test Status", short_name="started") - wfl_status2 = factories.WorkflowLevelStatus(name="Finished Test Status", short_name="finished") - wkflvl2_1 = factories.WorkflowLevel2(name='Started brief survey', - workflowlevel1=wkflvl1, - status=wfl_status1) - wkflvl2_2 = factories.WorkflowLevel2(name='Finished brief survey', - workflowlevel1=wkflvl1, - status=wfl_status2) + wfl_status1 = factories.WorkflowLevelStatus( + name="Started Test Status", short_name="started" + ) + wfl_status2 = factories.WorkflowLevelStatus( + name="Finished Test Status", short_name="finished" + ) + wkflvl2_1 = factories.WorkflowLevel2( + name='Started brief survey', workflowlevel1=wkflvl1, status=wfl_status1 + ) + wkflvl2_2 = factories.WorkflowLevel2( + name='Finished brief survey', workflowlevel1=wkflvl1, status=wfl_status2 + ) # filter by status.uuid request = self.factory.get( diff --git a/workflow/tests/test_workflowlevelstatus.py b/workflow/tests/test_workflowlevelstatus.py index 789b6ae7..94ea5f7a 100644 --- a/workflow/tests/test_workflowlevelstatus.py +++ b/workflow/tests/test_workflowlevelstatus.py @@ -7,6 +7,7 @@ from ..views import WorkflowLevelStatusViewSet from core.tests.fixtures import org_member, org + @pytest.mark.django_db() def test_list_workflowlevelstatus(request_factory, org_member): request = request_factory.get(reverse('workflowlevelstatus-list')) @@ -29,8 +30,7 @@ def test_create_workflowlevelstatus(request_factory, org_member): @pytest.mark.django_db() def test_update_workflowlevelstatus(request_factory, org_member): wflstatus = factories.WorkflowLevelStatus(name='change this') - data = {"name": "Changed WFL Status Name", - "short_name": "changed"} + data = {"name": "Changed WFL Status Name", "short_name": "changed"} request = request_factory.put('', data) request.user = org_member view = WorkflowLevelStatusViewSet.as_view({'put': 'update'}) diff --git a/workflow/tests/test_workflowleveltypeview.py b/workflow/tests/test_workflowleveltypeview.py index ccf0f901..ffa64d3e 100644 --- a/workflow/tests/test_workflowleveltypeview.py +++ b/workflow/tests/test_workflowleveltypeview.py @@ -7,6 +7,7 @@ from ..views import WorkflowLevelTypeViewSet from core.tests.fixtures import org_member, org + @pytest.mark.django_db() def test_list_workflowleveltype(request_factory, org_member): request = request_factory.get(reverse('workflowleveltype-list')) diff --git a/workflow/views/workflowlevel1.py b/workflow/views/workflowlevel1.py index bc4966f1..e3f6291d 100644 --- a/workflow/views/workflowlevel1.py +++ b/workflow/views/workflowlevel1.py @@ -34,6 +34,7 @@ class WorkflowLevel1ViewSet(viewsets.ModelViewSet): create: Create a new workflow instance. """ + # Remove CSRF request verification for posts to this API @method_decorator(csrf_exempt) def dispatch(self, *args, **kwargs): @@ -60,8 +61,9 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) self.perform_create(serializer) # inherited from CreateModelMixin headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, - headers=headers) + return Response( + serializer.data, status=status.HTTP_201_CREATED, headers=headers + ) def perform_create(self, serializer): organization = self.request.user.organization @@ -76,7 +78,10 @@ def destroy(self, request, *args, **kwargs): ordering_fields = ('name',) ordering = ('name',) filterset_fields = ('name', 'level1_uuid') - filter_backends = (django_filters.rest_framework.DjangoFilterBackend, filters.OrderingFilter) + filter_backends = ( + django_filters.rest_framework.DjangoFilterBackend, + filters.OrderingFilter, + ) queryset = WorkflowLevel1.objects.all() serializer_class = WorkflowLevel1Serializer diff --git a/workflow/views/workflowlevel2.py b/workflow/views/workflowlevel2.py index f95dc22a..8c75de20 100644 --- a/workflow/views/workflowlevel2.py +++ b/workflow/views/workflowlevel2.py @@ -6,7 +6,12 @@ from core.permissions import IsOrgMember from workflow.filters import WorkflowLevel2Filter -from workflow.models import WorkflowLevel2, WorkflowLevel2Sort, WorkflowTeam, ROLE_ORGANIZATION_ADMIN +from workflow.models import ( + WorkflowLevel2, + WorkflowLevel2Sort, + WorkflowTeam, + ROLE_ORGANIZATION_ADMIN, +) from workflow.serializers import WorkflowLevel2Serializer, WorkflowLevel2SortSerializer from workflow.permissions import CoreGroupsPermissions from workflow.pagination import DefaultLimitOffsetPagination @@ -31,6 +36,7 @@ class WorkflowLevel2ViewSet(viewsets.ModelViewSet): create: Create a new workflow level 2 instance. """ + # Remove CSRF request verification for posts to this API @method_decorator(csrf_exempt) def dispatch(self, *args, **kwargs): @@ -65,7 +71,7 @@ def perform_create(self, serializer): ordering = ('name',) filter_backends = ( django_filters.rest_framework.DjangoFilterBackend, - filters.OrderingFilter + filters.OrderingFilter, ) filter_class = WorkflowLevel2Filter queryset = WorkflowLevel2.objects.all() @@ -102,11 +108,12 @@ def list(self, request, *args, **kwargs): if ROLE_ORGANIZATION_ADMIN in user_groups: organization_id = request.user.organization_id queryset = queryset.filter( - workflowlevel1__organization_id=organization_id) + workflowlevel1__organization_id=organization_id + ) else: wflvl1_ids = WorkflowTeam.objects.filter( - workflow_user=request.user).values_list( - 'workflowlevel1__id', flat=True) + workflow_user=request.user + ).values_list('workflowlevel1__id', flat=True) queryset = queryset.filter(workflowlevel1__in=wflvl1_ids) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) diff --git a/workflow/views/workflowlevelstatus.py b/workflow/views/workflowlevelstatus.py index c120397b..8df72bd2 100644 --- a/workflow/views/workflowlevelstatus.py +++ b/workflow/views/workflowlevelstatus.py @@ -43,8 +43,9 @@ class WorkflowLevelStatusViewSet(viewsets.ModelViewSet): Delete the WorkflowlevelStatus instance. """ + queryset = WorkflowLevelStatus.objects.all() - ordering = ('order', ) - filter_backends = (filters.OrderingFilter, ) + ordering = ('order',) + filter_backends = (filters.OrderingFilter,) serializer_class = WorkflowLevelStatusSerializer pagination_class = DefaultLimitOffsetPagination diff --git a/workflow/views/workflowleveltype.py b/workflow/views/workflowleveltype.py index 14c03fe0..1a023674 100644 --- a/workflow/views/workflowleveltype.py +++ b/workflow/views/workflowleveltype.py @@ -43,8 +43,9 @@ class WorkflowLevelTypeViewSet(viewsets.ModelViewSet): Delete the workflowleveltype instance. """ + queryset = WorkflowLevelType.objects.all() - ordering = ('create_date', ) - filter_backends = (filters.OrderingFilter, ) + ordering = ('create_date',) + filter_backends = (filters.OrderingFilter,) serializer_class = WorkflowLevelTypeSerializer pagination_class = DefaultCursorPagination diff --git a/workflow/views/workflowteam.py b/workflow/views/workflowteam.py index 7a9b8568..c5bc773d 100644 --- a/workflow/views/workflowteam.py +++ b/workflow/views/workflowteam.py @@ -28,18 +28,22 @@ class WorkflowTeamViewSet(viewsets.ModelViewSet): create: Create a new workflow team instance. """ + def list(self, request, *args, **kwargs): # Use this queryset or the django-filters lib will not work queryset = self.filter_queryset(self.get_queryset()) if not request.user.is_superuser: if ROLE_ORGANIZATION_ADMIN in request.user.groups.values_list( - 'name', flat=True): + 'name', flat=True + ): organization_id = request.user.organization_id queryset = queryset.filter( - workflow_user__organization_id=organization_id) + workflow_user__organization_id=organization_id + ) else: wflvl1_ids = WorkflowTeam.objects.filter( - workflow_user=request.user).values_list('workflowlevel1__id', flat=True) + workflow_user=request.user + ).values_list('workflowlevel1__id', flat=True) queryset = queryset.filter(workflowlevel1__in=wflvl1_ids) nested = request.GET.get('nested_models') From 0961ca69a81a3c79b96aa31ab81d3c37eb476693 Mon Sep 17 00:00:00 2001 From: Radhika Patel Date: Mon, 13 Feb 2023 14:42:17 +0530 Subject: [PATCH 105/109] Remove dev deployment setup --- .github/workflows/dev-build.yml | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 3bf2cabe..fab57205 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -1,28 +1,28 @@ -name: Build and Push to Development +# name: Build and Push to Development -on: - push: - branches: - - dev +# on: +# push: +# branches: +# - dev -jobs: - build: - name: Build and Push to GCR - runs-on: ubuntu-latest - env: - IMAGE_NAME: gcr.io/dev-buildly/transparent-path/buildly-core - steps: - - uses: actions/checkout@v2 +# jobs: +# build: +# name: Build and Push to GCR +# runs-on: ubuntu-latest +# env: +# IMAGE_NAME: gcr.io/dev-buildly/transparent-path/buildly-core +# steps: +# - uses: actions/checkout@v2 - - name: Docker login - uses: docker/login-action@v1 - with: - registry: gcr.io - username: _json_key - password: ${{ secrets.DEV_GCR_JSON_KEY }} +# - name: Docker login +# uses: docker/login-action@v1 +# with: +# registry: gcr.io +# username: _json_key +# password: ${{ secrets.DEV_GCR_JSON_KEY }} - - name: Build docker image - run: docker build -t $IMAGE_NAME:latest . +# - name: Build docker image +# run: docker build -t $IMAGE_NAME:latest . - - name: Push to Google Container Registry - run: docker push $IMAGE_NAME:latest +# - name: Push to Google Container Registry +# run: docker push $IMAGE_NAME:latest From 352d71faf54cb52017e382b0d6f93b4619f2c0b0 Mon Sep 17 00:00:00 2001 From: Radhika Patel Date: Mon, 13 Feb 2023 15:29:58 +0530 Subject: [PATCH 106/109] Setup github actions --- .github/workflows/demo-build.yml | 29 ++------------------- .github/workflows/prod-build.yml | 43 +++++++++++++------------------- Dockerfile | 5 +++- 3 files changed, 24 insertions(+), 53 deletions(-) diff --git a/.github/workflows/demo-build.yml b/.github/workflows/demo-build.yml index ecd8ade6..4ca48d36 100644 --- a/.github/workflows/demo-build.yml +++ b/.github/workflows/demo-build.yml @@ -4,49 +4,24 @@ on: push: branches: - demo - jobs: build: name: Build and Push to GCR runs-on: ubuntu-latest env: - IMAGE_NAME: gcr.io/spry-bricolage-298920/transparent-path/buildly-core + IMAGE_NAME: gcr.io/spry-bricolage-298920/transparent-path/demo/buildly-core steps: - uses: actions/checkout@v2 - # Login to docker - name: Docker login uses: docker/login-action@v1 with: registry: gcr.io username: _json_key - password: ${{ secrets.DEMO_GCR_JSON_KEY }} + password: ${{ secrets.GCR_JSON_KEY }} - # Build docker image - name: Build docker image run: docker build -t $IMAGE_NAME:latest . - # Push docker image to GCR - name: Push to Google Container Registry run: docker push $IMAGE_NAME:latest - - # Send message on Slack - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_MESSAGE: 'Demo Docker Image of Transparent Path buildly-core pushed to Google Container Registry Successfully' - MSG_MINIMAL: true - - # Send email alert - - name: Email Alert - uses: dawidd6/action-send-mail@v3 - with: - server_address: smtp.gmail.com - server_port: 465 - username: ${{ secrets.MAIL_USERNAME }} - password: ${{ secrets.MAIL_PASSWORD }} - subject: Github Actions Build and Push job alert - to: ${{ secrets.RECIPIENT_EMAIL }} - from: ${{ secrets.SENDER_EMAIL }} - body: Demo Docker Image of Transparent Path buildly-core pushed to Google Container Registry Successfully \ No newline at end of file diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index c06f913e..f0b82ffe 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -1,4 +1,4 @@ -name: Build and Push to Production +name: Build and Push Docker Image to Prod on: push: @@ -7,8 +7,10 @@ on: jobs: build: - name: Build and Push to AWS + name: Build and Push image to GCR runs-on: ubuntu-latest + env: + IMAGE_NAME: gcr.io/spry-bricolage-298920/transparent-path/prod/buildly-core steps: - uses: actions/checkout@v2 with: @@ -42,35 +44,28 @@ jobs: draft: false prerelease: false - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 + # Login to docker + - name: Docker login + uses: docker/login-action@v1 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-2 + registry: gcr.io + username: _json_key + password: ${{ secrets.GCR_JSON_KEY }} - # Login to Amazon ECR - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 + # Build docker image + - name: Build docker image + run: docker build -t $IMAGE_NAME:latest . - # Build, tag and push docker image - - name: Build, tag, and push image to Amazon ECR - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: transparent-path/buildly_core - - IMAGE_TAG: latest - run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + # Push docker image to GCR + - name: Push to Google Container Registry + run: docker push $IMAGE_NAME:latest # Send message on Slack - name: Slack Notification uses: rtCamp/action-slack-notify@v2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_MESSAGE: 'Production Release for Transparent Path buildly-core created and Docker Image pushed to AWS ECR Successfully' + SLACK_MESSAGE: 'Production Docker Image of buildly core pushed to Google Container Registry Successfully' MSG_MINIMAL: true # Send email alert @@ -84,6 +79,4 @@ jobs: subject: Github Actions Build and Push job alert to: ${{ secrets.RECIPIENT_EMAIL }} from: ${{ secrets.SENDER_EMAIL }} - body: Production Release for Transparent Path buildly-core created and Docker Image pushed to AWS ECR Successfully - -# Reference : https://towardsaws.com/build-push-docker-image-to-aws-ecr-using-github-actions-8396888a8f9e \ No newline at end of file + body: Production Docker Image of buildly core pushed to Google Container Registry Successfully \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index eaacea4d..2b85fd62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7-alpine3.10 +FROM --platform=linux/amd64 python:3.7-alpine3.10 # Do not buffer log messages in memory; some messages can be lost otherwise ENV PYTHONUNBUFFERED 1 @@ -21,5 +21,8 @@ RUN ./scripts/collectstatic.sh RUN apk del .build-deps +# Specify tag name to be created on github +LABEL version="1.0.10" + EXPOSE 8080 ENTRYPOINT ["bash", "/code/scripts/docker-entrypoint.sh"] From 8c0bd363c60e1c42c2181a7c88f348130bfdea64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:57:58 +0000 Subject: [PATCH 107/109] Bump the pip group group in /requirements with 6 updates Bumps the pip group group in /requirements with 6 updates: | Package | From | To | | --- | --- | --- | | [django](https://github.com/django/django) | `2.2.10` | `3.2.24` | | [django-filter](https://github.com/carltongibson/django-filter) | `2.2.0` | `2.4.0` | | [djangorestframework](https://github.com/encode/django-rest-framework) | `3.9.4` | `3.11.2` | | [requests](https://github.com/psf/requests) | `2.25.0` | `2.31.0` | | [aiohttp](https://github.com/aio-libs/aiohttp) | `3.5.4` | `3.9.2` | | [ipython](https://github.com/ipython/ipython) | `7.2.0` | `8.10.0` | Updates `django` from 2.2.10 to 3.2.24 - [Commits](https://github.com/django/django/compare/2.2.10...3.2.24) Updates `django-filter` from 2.2.0 to 2.4.0 - [Release notes](https://github.com/carltongibson/django-filter/releases) - [Changelog](https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst) - [Commits](https://github.com/carltongibson/django-filter/compare/2.2.0...2.4.0) Updates `djangorestframework` from 3.9.4 to 3.11.2 - [Release notes](https://github.com/encode/django-rest-framework/releases) - [Commits](https://github.com/encode/django-rest-framework/compare/3.9.4...3.11.2) Updates `requests` from 2.25.0 to 2.31.0 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.25.0...v2.31.0) Updates `aiohttp` from 3.5.4 to 3.9.2 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.5.4...v3.9.2) Updates `ipython` from 7.2.0 to 8.10.0 - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/7.2.0...8.10.0) --- updated-dependencies: - dependency-name: django dependency-type: direct:production dependency-group: pip-security-group - dependency-name: django-filter dependency-type: direct:production dependency-group: pip-security-group - dependency-name: djangorestframework dependency-type: direct:production dependency-group: pip-security-group - dependency-name: requests dependency-type: direct:production dependency-group: pip-security-group - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip-security-group - dependency-name: ipython dependency-type: direct:production dependency-group: pip-security-group ... Signed-off-by: dependabot[bot] --- requirements/base.txt | 10 +++++----- requirements/test.txt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 7de01526..938bb2eb 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,9 +1,9 @@ -Django==2.2.10 -django-filter==2.2.0 +Django==3.2.24 +django-filter==2.4.0 jsonschema==3.2.0 django-health-check==3.6.1 git+https://github.com/buildlyio/django-oauth-toolkit-jwt@v0.5.2#egg=django-oauth-toolkit-jwt -djangorestframework==3.9.4 +djangorestframework==3.11.2 psycopg2-binary==2.8.6 social-auth-app-django==3.1.0 django-oauth-toolkit==1.3.0 @@ -12,6 +12,6 @@ django-cors-headers==2.5.3 pyswagger==0.8.39 bravado-core==5.13.1 drf-yasg==1.10.2 -requests==2.25.0 -aiohttp==3.5.4 +requests==2.31.0 +aiohttp==3.9.2 django-auth-ldap==2.1.0 \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt index 157ab6bd..e983fa34 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,5 @@ -r ci.txt factory_boy==2.12.0 -ipython==7.2.0 +ipython==8.10.0 ipdb==0.12.2 From 4f0337d4399e410dfc59d09117fcb6659aebb5e2 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Tue, 26 Mar 2024 07:45:28 -0700 Subject: [PATCH 108/109] Update dev-build.yml The entire file was commented out, checking in without comments. --- .github/workflows/dev-build.yml | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index fab57205..3bf2cabe 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -1,28 +1,28 @@ -# name: Build and Push to Development +name: Build and Push to Development -# on: -# push: -# branches: -# - dev +on: + push: + branches: + - dev -# jobs: -# build: -# name: Build and Push to GCR -# runs-on: ubuntu-latest -# env: -# IMAGE_NAME: gcr.io/dev-buildly/transparent-path/buildly-core -# steps: -# - uses: actions/checkout@v2 +jobs: + build: + name: Build and Push to GCR + runs-on: ubuntu-latest + env: + IMAGE_NAME: gcr.io/dev-buildly/transparent-path/buildly-core + steps: + - uses: actions/checkout@v2 -# - name: Docker login -# uses: docker/login-action@v1 -# with: -# registry: gcr.io -# username: _json_key -# password: ${{ secrets.DEV_GCR_JSON_KEY }} + - name: Docker login + uses: docker/login-action@v1 + with: + registry: gcr.io + username: _json_key + password: ${{ secrets.DEV_GCR_JSON_KEY }} -# - name: Build docker image -# run: docker build -t $IMAGE_NAME:latest . + - name: Build docker image + run: docker build -t $IMAGE_NAME:latest . -# - name: Push to Google Container Registry -# run: docker push $IMAGE_NAME:latest + - name: Push to Google Container Registry + run: docker push $IMAGE_NAME:latest From 61e26518a54e763bfed55d1fba3e5dbf527efbf5 Mon Sep 17 00:00:00 2001 From: Greg Lind Date: Tue, 26 Mar 2024 07:52:58 -0700 Subject: [PATCH 109/109] Update base.txt Fix 3.9,.2 aiohttp not found error from dependabot --- requirements/base.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 938bb2eb..bc815500 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -13,5 +13,5 @@ pyswagger==0.8.39 bravado-core==5.13.1 drf-yasg==1.10.2 requests==2.31.0 -aiohttp==3.9.2 -django-auth-ldap==2.1.0 \ No newline at end of file +aiohttp==3.8.6 +django-auth-ldap==2.1.0