From 3a027d9f8328ee7d925bcce34aece1b8d1c720b0 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Thu, 21 May 2020 19:44:30 -0400 Subject: [PATCH 01/19] =?UTF-8?q?=E2=9C=A8=20Add=20create=20refferal=20tok?= =?UTF-8?q?en=20event=20to=20event=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/events/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/creator/events/models.py b/creator/events/models.py index 8c573689b..b0e98ae52 100644 --- a/creator/events/models.py +++ b/creator/events/models.py @@ -61,6 +61,8 @@ class Meta: ("IN_UPD", "Ingestion Status Updated"), ("PH_UPD", "Phenotype Status Updated"), ("ST_UPD", "Sequencing Status Updated"), + ("RT_CRE", "Referral Token Created"), + ("RT_CLA", "Referral Token Claimed"), ("OTH", "Other"), ), default="OTH", From e777ce76366b99f376a05af58ae60656bcc04d1c Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Thu, 21 May 2020 19:44:39 -0400 Subject: [PATCH 02/19] =?UTF-8?q?=F0=9F=9A=9A=20Add=20migration=20for=20ev?= =?UTF-8?q?ent=20type=20adding=20referral=20type=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0011_add_referral_tokens_status.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 creator/events/migrations/0011_add_referral_tokens_status.py diff --git a/creator/events/migrations/0011_add_referral_tokens_status.py b/creator/events/migrations/0011_add_referral_tokens_status.py new file mode 100644 index 000000000..2c397624b --- /dev/null +++ b/creator/events/migrations/0011_add_referral_tokens_status.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.11 on 2020-05-26 20:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0010_add_phenotype_status'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='event_type', + field=models.CharField(choices=[('SF_CRE', 'Study File Created'), ('SF_UPD', 'Study File Updated'), ('SF_DEL', 'Study File Deleted'), ('FV_CRE', 'File Version Created'), ('FV_UPD', 'File Version Updated'), ('SD_CRE', 'Study Created'), ('SD_UPD', 'Study Updated'), ('PR_CRE', 'Project Created'), ('PR_UPD', 'Project Updated'), ('PR_DEL', 'Project Deleted'), ('PR_LIN', 'Project Linked'), ('PR_UNL', 'Project Unlinked'), ('PR_STR', 'Project Creation Start'), ('PR_ERR', 'Project Creation Error'), ('PR_SUC', 'Project Creation Success'), ('BK_STR', 'Bucket Creation Start'), ('BK_ERR', 'Bucket Creation Error'), ('BK_SUC', 'Bucket Creation Success'), ('BK_LIN', 'Bucket Linked'), ('BK_UNL', 'Bucket Unlinked'), ('IM_STR', 'File Import Start'), ('IM_ERR', 'File Import Error'), ('IM_SUC', 'File Import Success'), ('CB_ADD', 'Collaborator Added'), ('CB_REM', 'Collaborator Removed'), ('IN_UPD', 'Ingestion Status Updated'), ('PH_UPD', 'Phenotype Status Updated'), ('ST_UPD', 'Sequencing Status Updated'), ('RT_CRE', 'Referral Token Created'), ('RT_CLA', 'Referral Token Claimed'), ('OTH', 'Other')], default='OTH', max_length=6), + ), + ] From 32958a69e0e65b9d29bee7e584f352c8e6a77fd6 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Thu, 21 May 2020 19:44:50 -0400 Subject: [PATCH 03/19] =?UTF-8?q?=E2=9C=A8=20Add=20view=20and=20add=20refe?= =?UTF-8?q?rral=20tokens=20permission=20to=20Admin=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/groups.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/creator/groups.py b/creator/groups.py index cb494078f..1301a5b46 100644 --- a/creator/groups.py +++ b/creator/groups.py @@ -45,6 +45,9 @@ "unlink_project", "import_volume", "sync_project", + "view_referraltoken", + "list_all_referraltoken", + "add_referraltoken", ], "Developers": [ "view_study", From c7d9425c0724813aa9ce3c6fc83a1b3cecb8d566 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Thu, 21 May 2020 19:46:39 -0400 Subject: [PATCH 04/19] =?UTF-8?q?=F0=9F=93=9D=20Document=20view=20and=20ad?= =?UTF-8?q?d=20referral=20tokens=20permission=20in=20Admin=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/authorization.rst | 87 ++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index a00a67b0b..76b5726b4 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -29,48 +29,51 @@ Users may be assigned this group by another administrator or they will automatically be promoted when logging in with an `ADMIN` role in their Auth0 token. -==================== ====================================== -Permission Description -==================== ====================================== -view_group Can view group -view_permission Can view permission -link_bucket Can link a bucket to a study -list_all_bucket Can list all buckets -unlink_bucket Can unlink a bucket to a study -view_bucket Can view bucket -view_job Can view job -view_queue Can view queues -view_settings Can view settings -change_user Can change user -list_all_user Can list all users -view_event Can view event -list_all_version Can list all versions -add_downloadtoken Can add download token -delete_downloadtoken Can delete download token -view_downloadtoken Can view download token -add_file Can add file -change_file Can change file -delete_file Can delete file -list_all_file Can list all files -view_file Can view file -add_version Can add version -change_version Can change version -delete_version Can delete version -view_version Can view version -add_project Can add project -change_project Can change project -import_volume Can import a volume to a project -link_project Can link a project to a study -list_all_project Can list all projects -sync_project Can sync projects with Cavatica -unlink_project Can unlink a project from a study -view_project Can view project -add_collaborator Can add a collaborator to the study -add_study Can add study -remove_collaborator Can remove a collaborator to the study -view_study Can view study -change_study Can change study -==================== ====================================== +====================== ====================================== +Permission Description +====================== ====================================== +view_group Can view group +view_permission Can view permission +link_bucket Can link a bucket to a study +list_all_bucket Can list all buckets +unlink_bucket Can unlink a bucket to a study +view_bucket Can view bucket +view_job Can view job +view_queue Can view queues +view_settings Can view settings +change_user Can change user +list_all_user Can list all users +view_event Can view event +list_all_version Can list all versions +add_downloadtoken Can add download token +delete_downloadtoken Can delete download token +view_downloadtoken Can view download token +add_file Can add file +change_file Can change file +delete_file Can delete file +list_all_file Can list all files +view_file Can view file +add_version Can add version +change_version Can change version +delete_version Can delete version +view_version Can view version +add_project Can add project +change_project Can change project +import_volume Can import a volume to a project +link_project Can link a project to a study +list_all_project Can list all projects +sync_project Can sync projects with Cavatica +unlink_project Can unlink a project from a study +view_project Can view project +add_collaborator Can add a collaborator to the study +add_study Can add study +remove_collaborator Can remove a collaborator to the study +view_study Can view study +change_study Can change study +view_referraltoken Can view referral token +list_all_referraltoken Can view all referral token +add_referraltoken Can add referral token +====================== ====================================== Developers ++++++++++ From 5d2c855efdd1261977e5465d5dc928c8f11402b6 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Thu, 21 May 2020 19:46:52 -0400 Subject: [PATCH 05/19] =?UTF-8?q?=E2=9C=A8=20Add=20referral=5Ftokens=20to?= =?UTF-8?q?=20INSTALLED=5FAPPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/settings/development.py | 1 + creator/settings/production.py | 1 + creator/settings/testing.py | 1 + 3 files changed, 3 insertions(+) diff --git a/creator/settings/development.py b/creator/settings/development.py index 1c4864c62..ad2366094 100644 --- a/creator/settings/development.py +++ b/creator/settings/development.py @@ -55,6 +55,7 @@ 'creator.projects', 'creator.buckets', 'creator.email', + 'creator.referral_tokens', 'creator.events.apps.EventsConfig', 'creator', 'corsheaders' diff --git a/creator/settings/production.py b/creator/settings/production.py index 498bf5078..222a40a37 100644 --- a/creator/settings/production.py +++ b/creator/settings/production.py @@ -56,6 +56,7 @@ 'creator.projects', 'creator.buckets', 'creator.email', + 'creator.referral_tokens', 'creator.events.apps.EventsConfig', 'creator', 'corsheaders' diff --git a/creator/settings/testing.py b/creator/settings/testing.py index 8beacba27..e8f9049bc 100644 --- a/creator/settings/testing.py +++ b/creator/settings/testing.py @@ -56,6 +56,7 @@ 'creator.projects', 'creator.buckets', 'creator.email', + 'creator.referral_tokens', 'creator.events.apps.EventsConfig', 'creator', 'corsheaders' From e9c31fac22ca6a0a1aff3ddf4d887fb21747134d Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Thu, 21 May 2020 19:47:06 -0400 Subject: [PATCH 06/19] =?UTF-8?q?=E2=9C=A8=20Add=20referral=5Ftokens=20que?= =?UTF-8?q?ries=20to=20general=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/schema.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/creator/schema.py b/creator/schema.py index a035bbab4..7c3aae41b 100644 --- a/creator/schema.py +++ b/creator/schema.py @@ -16,6 +16,7 @@ import creator.events.schema from creator.projects import schema as project_mutations from creator.buckets import schema as bucket_mutations +from creator.referral_tokens import schema as referral_tokens_mutations def get_version_info(): @@ -239,6 +240,7 @@ class Query( creator.events.schema.Query, project_mutations.Query, bucket_mutations.Query, + referral_tokens_mutations.Query, graphene.ObjectType, ): status = graphene.Field(Status) @@ -319,6 +321,16 @@ class Mutation(StudyMutation, graphene.ObjectType): unlink_bucket = bucket_mutations.UnlinkBucketMutation.Field( description="Unlink a bucket from a Study" ) + create_referral_token = ( + referral_tokens_mutations.CreateReferralTokenMutation.Field( + description="Create a referral token" + ) + ) + exchange_referral_token = ( + referral_tokens_mutations.ExchangeReferralTokenMutation.Field( + description="Exchange a referral token" + ) + ) schema = graphene.Schema(query=Query, mutation=Mutation) From 8daf0a82ecda40046851341af00d11f6c487c4e8 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Thu, 21 May 2020 20:18:28 -0400 Subject: [PATCH 07/19] =?UTF-8?q?=E2=9C=A8=20Add=20referral=5Ftokens=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/referral_tokens/__init__.py | 0 creator/referral_tokens/apps.py | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 creator/referral_tokens/__init__.py create mode 100644 creator/referral_tokens/apps.py diff --git a/creator/referral_tokens/__init__.py b/creator/referral_tokens/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/creator/referral_tokens/apps.py b/creator/referral_tokens/apps.py new file mode 100644 index 000000000..1e50dca4c --- /dev/null +++ b/creator/referral_tokens/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ReferralTokensConfig(AppConfig): + name = "referral_tokens" From 7d5c5b2e6a95f628e393d7d6749d761b60d29f42 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Fri, 22 May 2020 02:37:09 -0400 Subject: [PATCH 08/19] =?UTF-8?q?=E2=9C=A8=20Add=20referral=5Ftokens=20sch?= =?UTF-8?q?ema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/referral_tokens/schema.py | 277 ++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 creator/referral_tokens/schema.py diff --git a/creator/referral_tokens/schema.py b/creator/referral_tokens/schema.py new file mode 100644 index 000000000..a87c2e1c6 --- /dev/null +++ b/creator/referral_tokens/schema.py @@ -0,0 +1,277 @@ +import graphene +import django_filters +from graphene import relay, ObjectType, Field +from graphene_django import DjangoObjectType +from graphene_django.filter import DjangoFilterConnectionField +from django_filters import OrderingFilter +from graphql_relay import from_global_id +from botocore.exceptions import ClientError +from django.conf import settings +from graphql import GraphQLError +from datetime import datetime, timedelta +from django.core.mail import send_mail +from django.template.loader import render_to_string +from .models import ReferralToken +from creator.studies.models import Study +from django.contrib.auth.models import Group +from creator.events.models import Event +from creator.users.schema import UserNode + + +class ReferralTokenNode(DjangoObjectType): + is_valid = graphene.Boolean(source="is_valid") + + class Meta: + model = ReferralToken + interfaces = (relay.Node,) + filter_fields = () + + @classmethod + def get_node(cls, info, uuid): + """ + Only return if the user is allowed to view referral token + """ + user = info.context.user + + if not (user.has_perm("referral_tokens.view_referraltoken")): + raise GraphQLError("Not allowed") + + if user.has_perm("referral_tokens.view_referraltoken"): + try: + return cls._meta.model.objects.get(uuid=uuid) + except cls._meta.model.DoesNotExist: + raise GraphQLError("Referral token not found") + + return ReferralToken.objects.none() + + +class ReferralTokenFilter(django_filters.FilterSet): + created_before = django_filters.DateTimeFilter( + field_name="created_at", lookup_expr="lt" + ) + created_after = django_filters.DateTimeFilter( + field_name="created_at", lookup_expr="gt" + ) + email_contains = django_filters.CharFilter( + field_name="email", lookup_expr="contains" + ) + + class Meta: + model = ReferralToken + fields = { + "email": ["exact"], + "claimed": ["exact"], + "studies": ["exact"], + "groups": ["exact"], + } + + order_by = OrderingFilter(fields=("created_at",)) + + +def get_studies(input): + """ + Convert a list of id of studies into a list of Study objects + """ + studies_input = input.get("studies", []) + study_ids = [from_global_id(s)[1] for s in studies_input] + studies = Study.objects.filter(kf_id__in=study_ids).all() + if len(studies) < len(studies_input): + raise GraphQLError("Study does not exist.") + + return studies + + +def get_groups(input): + """ + Convert a list of id of groups into a list of Group objects + """ + groups_input = input.get("groups", []) + group_ids = [from_global_id(g)[1] for g in groups_input] + groups = Group.objects.filter(id__in=group_ids).all() + if len(groups) < len(groups_input): + raise GraphQLError("Group does not exist.") + + return groups + + +class ReferralTokenInput(graphene.InputObjectType): + email = graphene.String( + required=True, description="The email address to send the referral to", + ) + + studies = graphene.List( + graphene.ID, + description="A list of studies that the user will be added to upon" + " exchange of the token", + required=False, + ) + + groups = graphene.List( + graphene.ID, + required=False, + description="The permission group the user will be given after" + " exchanging the token", + ) + + +class CreateReferralTokenMutation(graphene.Mutation): + class Arguments: + input = ReferralTokenInput( + required=True, description="Attributes for the new referral token" + ) + + referral_token = Field(ReferralTokenNode) + + def mutate(self, info, input): + """ + Create a new referral token + - Checks for existing tokens who has same email and is valid to avoid + duplicated invitations being sent + - Create a new referral token if there is no existing ones and return + the referral token object + """ + + user = info.context.user + + if not user.has_perm("referral_tokens.add_referraltoken"): + raise GraphQLError("Not allowed") + + studies = get_studies(input) + groups = get_groups(input) + + existing_token = ( + ReferralToken.objects.filter(email=input["email"]) + .filter( + created_at__lte=datetime.now() + + timedelta(days=settings.REFERRAL_TOKEN_EXPIRATION_DAYS) + ) + .count() + ) + + if existing_token > 0: + raise GraphQLError("Invite already sent, awaiting user response") + + referral_token = ReferralToken(email=input["email"], created_by=user) + referral_token.full_clean() + referral_token.save() + referral_token.studies.set(studies) + referral_token.groups.set(groups) + referral_token.save() + + # Send email + if referral_token.created_by.username: + subject = ( + f"{referral_token.created_by.display_name} invited you to " + "join the Kids First Data Tracker" + ) + else: + subject = "Invitation to join the Kids First Data Tracker" + ctx = {"studies": studies, "url": referral_token.invite_url} + send_mail( + subject=subject, + recipient_list=[referral_token.email], + from_email=None, + fail_silently=False, + message=render_to_string(template_name="invite.txt", context=ctx), + html_message=render_to_string( + template_name="invite.html", context=ctx + ), + ) + + # Log an event + if user: + message = ( + f"{user.username} invited {referral_token.email} to" + " {len(input['studies'])} studies" + ) + else: + message = ( + f"{referral_token.email} was invited to" + " {len(input['studies'])} studies" + ) + + event = Event(description=message, event_type="RT_CRE") + if not user._state.adding: + event.user = user + event.save() + + return CreateReferralTokenMutation(referral_token=referral_token) + + +class ExchangeReferralTokenMutation(graphene.Mutation): + class Arguments: + token = graphene.ID( + required=True, description="The id of the referral token" + ) + + user = graphene.Field(UserNode) + referral_token = Field(ReferralTokenNode) + + def mutate(self, info, token): + """ + Exchange a referral token + - Check if the token exists and is still valid + - If valid, add studies and groups to user object and return the user + object with success message + """ + + user = info.context.user + + if not user.is_authenticated: + raise GraphQLError("Not allowed") + + try: + _, uuid = from_global_id(token) + referral_token = ReferralToken.objects.get(uuid=uuid) + except ReferralToken.DoesNotExist: + raise GraphQLError("Referral token does not exist.") + + if not referral_token.is_valid: + raise GraphQLError("Referral token is not valid.") + + user.studies.set(referral_token.studies.all()) + user.groups.set(referral_token.groups.all()) + referral_token.claimed = True + referral_token.claimed_by = user + referral_token.save() + + # Log an event + message = ( + f"{user.display_name} claimed token for " + "{len(referral_token.studies)} studies." + ) + + event = Event(description=message, event_type="RT_CLA") + if not user._state.adding: + event.user = user + event.save() + + return ExchangeReferralTokenMutation( + referral_token=referral_token, user=user + ) + + +class Query(object): + referral_token = relay.Node.Field( + ReferralTokenNode, description="Get a single referral token" + ) + all_referral_tokens = DjangoFilterConnectionField( + ReferralTokenNode, + filterset_class=ReferralTokenFilter, + description="Get all referral tokens", + ) + + def resolve_all_referral_tokens(self, info, **kwargs): + """ + Return all referral tokens if user has list_all_referraltoken + Return not allowed otherwise + """ + user = info.context.user + + if not (user.has_perm("referral_tokens.list_all_referraltoken")): + raise GraphQLError("Not allowed") + + if user.has_perm("referral_tokens.list_all_referraltoken"): + return ReferralToken.objects.all() + + return ReferralToken.objects.none() From 0e709b60b30f194c28087e2b3cdbca1d851c765c Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Fri, 22 May 2020 02:37:17 -0400 Subject: [PATCH 09/19] =?UTF-8?q?=E2=9C=A8=20Add=20referral=5Ftokens=20mod?= =?UTF-8?q?el?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/referral_tokens/models.py | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 creator/referral_tokens/models.py diff --git a/creator/referral_tokens/models.py b/creator/referral_tokens/models.py new file mode 100644 index 000000000..a162df53b --- /dev/null +++ b/creator/referral_tokens/models.py @@ -0,0 +1,94 @@ +import uuid +import pytz +from datetime import datetime +from django.db import models +from django.utils import timezone +from django.contrib.auth import get_user_model +from django.conf import settings +from creator.studies.models import Study +from django.contrib.auth.models import Group + + +User = get_user_model() + + +class ReferralToken(models.Model): + """ + An ReferralToken will be exchanged by a new or existing user in order to + be added to a studies with a given role. + """ + + class Meta: + permissions = [ + ("list_all_referraltoken", "Can view all referral tokens"), + ] + + uuid = models.UUIDField( + default=uuid.uuid4, unique=True, editable=False, primary_key=True + ) + + email = models.EmailField( + max_length=254, + help_text="The email that the token link will be sent to", + ) + + claimed = models.BooleanField( + default=False, help_text="If the token has been used" + ) + + studies = models.ManyToManyField( + Study, + related_name="referral_tokens", + help_text="List of studies that the user will be added to", + ) + + groups = models.ManyToManyField( + Group, + blank=True, + help_text="The role the user will assume in these studies", + related_name="referral_tokens", + related_query_name="referral_tokens", + ) + + created_at = models.DateTimeField( + default=timezone.now, + null=False, + help_text="Time the referral token was created", + ) + + claimed_by = models.ForeignKey( + User, + related_name="referral_tokens_claimed_by", + help_text="The user who claims the token", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + created_by = models.ForeignKey( + User, + related_name="referral_tokens_created_by", + help_text="The user who created the token", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + @property + def is_valid(self): + """ + If the token has already been claimed or if it has expired + """ + + exp_length = settings.REFERRAL_TOKEN_EXPIRATION_DAYS + expired = ( + datetime.now().replace(tzinfo=pytz.UTC) - self.created_at + ).days > exp_length + return not self.claimed and not expired + + @property + def invite_url(self): + """ + User invitation url + """ + return f"{settings.DATA_TRACKER_URL}/join?token={self.uuid}" From f811333a4d1187e23eeb0f56358da4c19000c3dc Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Fri, 22 May 2020 02:37:25 -0400 Subject: [PATCH 10/19] =?UTF-8?q?=E2=9C=A8=20Add=20referral=5Ftokens=20fac?= =?UTF-8?q?tories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/referral_tokens/factories.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 creator/referral_tokens/factories.py diff --git a/creator/referral_tokens/factories.py b/creator/referral_tokens/factories.py new file mode 100644 index 000000000..6365a1ca8 --- /dev/null +++ b/creator/referral_tokens/factories.py @@ -0,0 +1,15 @@ +import pytz +import factory +from .models import ReferralToken + + +class ReferralTokenFactory(factory.DjangoModelFactory): + class Meta: + model = ReferralToken + django_get_or_create = ("uuid",) + + uuid = factory.Faker("uuid4") + email = factory.Faker("email") + created_at = factory.Faker( + "date_time_between", start_date="-2d", end_date="now", tzinfo=pytz.UTC + ) From b6b198fadbad56c50b3dbedb9a4cdf815e4da6c1 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Fri, 22 May 2020 16:07:19 -0400 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=9A=9A=20Add=20referral=5Ftokens=20?= =?UTF-8?q?initial=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0001_initial.py | 37 +++++++++++++++++++ .../referral_tokens/migrations/__init__.py | 0 2 files changed, 37 insertions(+) create mode 100644 creator/referral_tokens/migrations/0001_initial.py create mode 100644 creator/referral_tokens/migrations/__init__.py diff --git a/creator/referral_tokens/migrations/0001_initial.py b/creator/referral_tokens/migrations/0001_initial.py new file mode 100644 index 000000000..04a2b8327 --- /dev/null +++ b/creator/referral_tokens/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 2.1.11 on 2020-05-22 20:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0009_alter_user_last_name_max_length'), + ('studies', '0016_add_phenotype_status'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ReferralToken', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('email', models.EmailField(help_text='The email that the token link will be sent to', max_length=254)), + ('claimed', models.BooleanField(default=False, help_text='If the token has been used')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, help_text='Time the referral token was created')), + ('claimed_by', models.ForeignKey(blank=True, help_text='The user who claims the token', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='referral_tokens_claimed_by', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(blank=True, help_text='The user who created the token', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='referral_tokens_created_by', to=settings.AUTH_USER_MODEL)), + ('groups', models.ManyToManyField(blank=True, help_text='The role the user will assume in these studies', related_name='referral_tokens', related_query_name='referral_tokens', to='auth.Group')), + ('studies', models.ManyToManyField(help_text='List of studies that the user will be added to', related_name='referral_tokens', to='studies.Study')), + ], + options={ + 'permissions': [('list_all_referraltoken', 'Can view all referral tokens')], + }, + ), + ] diff --git a/creator/referral_tokens/migrations/__init__.py b/creator/referral_tokens/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb From 6b430653ac234f0c304da03e9af86a9e860f34c2 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Tue, 26 May 2020 16:26:37 -0400 Subject: [PATCH 12/19] =?UTF-8?q?=E2=9C=A8=20Add=20user=20display=5Fname?= =?UTF-8?q?=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/models.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/creator/models.py b/creator/models.py index 32524ab0e..2abc6e5d7 100644 --- a/creator/models.py +++ b/creator/models.py @@ -89,6 +89,22 @@ class Meta: def is_admin(self): return 'ADMIN' in self.ego_roles + @property + def display_name(self): + """ + Display user first name last name if exist, and username if not + """ + user_name = self.username if self.username else "Unknown user" + user_full_name = ( + ( + (self.first_name if self.first_name + " " else "") + + (self.last_name if self.last_name else "") + ) + if (self.first_name or self.last_name) + else user_name + ) + return user_full_name + class Job(models.Model): """ From 8ff42a9399fe5f4054b872e356f4dbd3ae6088bf Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Tue, 26 May 2020 16:26:55 -0400 Subject: [PATCH 13/19] =?UTF-8?q?=E2=9C=A8=20Add=20referral=20token=20to?= =?UTF-8?q?=20invite=20email=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/email/templates/invite.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/creator/email/templates/invite.html b/creator/email/templates/invite.html index 1795c42e5..335924988 100644 --- a/creator/email/templates/invite.html +++ b/creator/email/templates/invite.html @@ -19,8 +19,8 @@

Take me to my studies

From 3d76f617c29ef45546d8ad8972b15d7af17de9b2 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Fri, 22 May 2020 19:40:03 -0400 Subject: [PATCH 14/19] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20referral?= =?UTF-8?q?=20token=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/referral_tokens/test_query.py | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/referral_tokens/test_query.py diff --git a/tests/referral_tokens/test_query.py b/tests/referral_tokens/test_query.py new file mode 100644 index 000000000..c87c5c2fa --- /dev/null +++ b/tests/referral_tokens/test_query.py @@ -0,0 +1,83 @@ +import pytest +from graphql_relay import to_global_id +from django.contrib.auth import get_user_model +from creator.referral_tokens.factories import ReferralTokenFactory + +User = get_user_model() + +REFERRALTOKEN = """ +query ($id: ID!) { + referralToken(id: $id) { + id + email + } +} +""" + +ALL_REFERRALTOKENS = """ +query { + allReferralTokens { + edges { node { id } } + } +} +""" + + +@pytest.mark.parametrize( + "user_group,allowed", + [ + ("Administrators", True), + ("Services", False), + ("Developers", False), + ("Investigators", False), + ("Bioinformatics", False), + (None, False), + ], +) +def test_query_referral_token(db, clients, user_group, allowed): + client = clients.get(user_group) + + referral_token = ReferralTokenFactory() + variables = {"id": to_global_id("ReferralTokenNode", referral_token.uuid)} + resp = client.post( + "/graphql", + data={"query": REFERRALTOKEN, "variables": variables}, + content_type="application/json", + ) + + if allowed: + assert ( + resp.json()["data"]["referralToken"]["email"] + == referral_token.email + ) + else: + assert resp.json()["errors"][0]["message"] == "Not allowed" + + +@pytest.mark.parametrize( + "user_group,allowed,number", + [ + ("Administrators", True, 2), + ("Services", False, 0), + ("Developers", False, 0), + ("Investigators", False, 0), + ("Bioinformatics", False, 0), + (None, False, 0), + ], +) +def test_query_all(db, clients, user_group, allowed, number): + client = clients.get(user_group) + if user_group: + user = User.objects.get(groups__name=user_group) + referral_tokens = ReferralTokenFactory.create_batch(2) + + resp = client.post( + "/graphql", + data={"query": ALL_REFERRALTOKENS}, + content_type="application/json", + ) + + if allowed: + assert len(resp.json()["data"]["allReferralTokens"]["edges"]) == number + else: + assert resp.json()["errors"][0]["message"] == "Not allowed" From e4d2f0508037d8edd282780bb0ff0e1b482d7cee Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Fri, 22 May 2020 19:40:14 -0400 Subject: [PATCH 15/19] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20new=20refe?= =?UTF-8?q?rral=20token=20mutation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_new_referral_token.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/referral_tokens/test_new_referral_token.py diff --git a/tests/referral_tokens/test_new_referral_token.py b/tests/referral_tokens/test_new_referral_token.py new file mode 100644 index 000000000..9fc6f645d --- /dev/null +++ b/tests/referral_tokens/test_new_referral_token.py @@ -0,0 +1,120 @@ +import pytest +from graphql_relay import to_global_id +from creator.studies.models import Study +from django.contrib.auth.models import Group +from creator.referral_tokens.models import ReferralToken + +CREATE_REFERRALTOKEN = """ +mutation newReferralToken($input: ReferralTokenInput!) { + createReferralToken(input: $input) { + referralToken { + uuid + email + claimed + isValid + groups { + edges { + node { + id + } + } + } + studies { + edges { + node { + id + } + } + } + } + } +} +""" + + +@pytest.mark.parametrize( + "user_group,allowed", + [ + ("Administrators", True), + ("Services", False), + ("Developers", False), + ("Investigators", False), + ("Bioinformatics", False), + (None, False), + ], +) +def test_create_referral_token_mutation(db, clients, user_group, allowed): + """ + Test that correct users may create referral token + """ + client = clients.get(user_group) + + study = Study(kf_id="SD_00000000") + study.save() + study_id = to_global_id("StudyNode", "SD_00000000") + + group = Group.objects.first() + group_id = to_global_id("GroupNode", group.id) + + email = "test@email.com" + variables = { + "input": {"email": email, "studies": [study_id], "groups": [group_id]} + } + + resp = client.post( + "/graphql", + content_type="application/json", + data={"query": CREATE_REFERRALTOKEN, "variables": variables}, + ) + + if allowed: + resp_body = resp.json()["data"]["createReferralToken"]["referralToken"] + assert resp_body["email"] == email + assert resp_body["isValid"] is True + assert resp_body["groups"]["edges"][0]["node"]["id"] == group_id + assert resp_body["studies"]["edges"][0]["node"]["id"] == study_id + assert ReferralToken.objects.count() == 1 + assert ReferralToken.objects.first().claimed is False + else: + assert ReferralToken.objects.count() == 0 + assert resp.json()["errors"][0]["message"] == "Not allowed" + + +def test_create_referral_token_mutation_existing(db, clients): + """ + Test that Admin cannot create a new referral token when existing a valid + referral token + """ + client = clients.get("Administrators") + + study = Study(kf_id="SD_00000000") + study.save() + study_id = to_global_id("StudyNode", "SD_00000000") + + group = Group.objects.first() + group_id = to_global_id("GroupNode", group.id) + + email = "test@email.com" + variables = { + "input": {"email": email, "studies": [study_id], "groups": [group_id]} + } + + resp_create = client.post( + "/graphql", + content_type="application/json", + data={"query": CREATE_REFERRALTOKEN, "variables": variables}, + ) + + assert ReferralToken.objects.count() == 1 + + resp_existing = client.post( + "/graphql", + content_type="application/json", + data={"query": CREATE_REFERRALTOKEN, "variables": variables}, + ) + + assert ( + resp_existing.json()["errors"][0]["message"] + == "Invite already sent, awaiting user response" + ) + assert ReferralToken.objects.count() == 1 From 31ec0e841c20ed48f51645c1da3cc995a1397a01 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Fri, 22 May 2020 20:51:16 -0400 Subject: [PATCH 16/19] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20exchange?= =?UTF-8?q?=20referral=20token=20mutation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_exchange_referral_token.py | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 tests/referral_tokens/test_exchange_referral_token.py diff --git a/tests/referral_tokens/test_exchange_referral_token.py b/tests/referral_tokens/test_exchange_referral_token.py new file mode 100644 index 000000000..397c94158 --- /dev/null +++ b/tests/referral_tokens/test_exchange_referral_token.py @@ -0,0 +1,231 @@ +import pytest +from graphql_relay import to_global_id +from creator.studies.models import Study +from django.contrib.auth.models import Group +from creator.referral_tokens.models import ReferralToken +from creator.studies.factories import StudyFactory +from creator.referral_tokens.factories import ReferralTokenFactory + +EXCHANGE_REFERRALTOKEN = """ +mutation ($token: ID!) { + exchangeReferralToken(token: $token) { + referralToken { + uuid + claimed + isValid + } + user { + email + studies { + edges { + node { + id + } + } + } + groups { + edges { + node { + id + } + } + } + } + } +} +""" + +CREATE_REFERRALTOKEN = """ +mutation newReferralToken($input: ReferralTokenInput!) { + createReferralToken(input: $input) { + referralToken { + uuid + claimed + isValid + } + } +} +""" + + +@pytest.mark.parametrize( + "user_group,allowed", + [ + ("Administrators", True), + ("Services", True), + ("Developers", True), + ("Investigators", True), + ("Bioinformatics", True), + (None, False), + ], +) +def test_exchange_referral_token(db, clients, user_group, allowed): + """ + All login user can exchange referral token + """ + client = clients.get(user_group) + + referral_token = ReferralTokenFactory() + assert ReferralToken.objects.first().claimed is False + + resp = client.post( + "/graphql", + data={ + "query": EXCHANGE_REFERRALTOKEN, + "variables": { + "token": to_global_id("ReferralTokenNode", referral_token.uuid) + }, + }, + content_type="application/json", + ) + + if allowed: + resp_body = resp.json()["data"]["exchangeReferralToken"] + assert resp_body["referralToken"]["isValid"] is False + assert ReferralToken.objects.first().claimed is True + else: + assert resp.json()["errors"][0]["message"] == "Not allowed" + + +def test_exchange_referral_token_no_multiple_claim(db, clients): + """ + User cannot exchange referral token when it is not valid + """ + client = clients.get("Administrators") + + referral_token = ReferralTokenFactory() + resp_claim = client.post( + "/graphql", + data={ + "query": EXCHANGE_REFERRALTOKEN, + "variables": { + "token": to_global_id("ReferralTokenNode", referral_token.uuid) + }, + }, + content_type="application/json", + ) + resp_invalid = client.post( + "/graphql", + data={ + "query": EXCHANGE_REFERRALTOKEN, + "variables": { + "token": to_global_id("ReferralTokenNode", referral_token.uuid) + }, + }, + content_type="application/json", + ) + assert ( + resp_invalid.json()["errors"][0]["message"] + == "Referral token is not valid." + ) + + +def test_exchange_referral_token_not_exist(db, clients): + """ + User cannot exchange referral token when it does not exist + """ + client = clients.get("Administrators") + random_token = ( + "UmVmZXJyYWxUb2tlbk5vZGU6ODBjZWY2ZjItNjA5OC00MjYyLTg3MDY" + "tMTAzNjVlM2ZhOTRh" + ) + resp = client.post( + "/graphql", + data={ + "query": EXCHANGE_REFERRALTOKEN, + "variables": {"token": random_token}, + }, + content_type="application/json", + ) + assert ( + resp.json()["errors"][0]["message"] == "Referral token does not exist." + ) + + +def test_exchange_referral_token_expired(db, clients, settings): + """ + User cannot exchange referral token when it is expired + """ + client = clients.get("Administrators") + # Reset the referral token expiring days to have an expired token + settings.REFERRAL_TOKEN_EXPIRATION_DAYS = -1 + + study = Study(kf_id="SD_00000000") + study.save() + study_id = to_global_id("StudyNode", "SD_00000000") + + group = Group.objects.first() + group_id = to_global_id("GroupNode", group.id) + + email = "test@email.com" + variables = { + "input": {"email": email, "studies": [study_id], "groups": [group_id]} + } + + resp_create = client.post( + "/graphql", + content_type="application/json", + data={"query": CREATE_REFERRALTOKEN, "variables": variables}, + ) + resp_create_body = resp_create.json()["data"]["createReferralToken"] + assert resp_create_body["referralToken"]["isValid"] is False + assert ReferralToken.objects.count() == 1 + referral_token = ReferralToken.objects.first() + resp_exchange = client.post( + "/graphql", + data={ + "query": EXCHANGE_REFERRALTOKEN, + "variables": { + "token": to_global_id("ReferralTokenNode", referral_token.uuid) + }, + }, + content_type="application/json", + ) + assert ( + resp_exchange.json()["errors"][0]["message"] + == "Referral token is not valid." + ) + + +def test_exchange_referral_token_study_group(db, clients): + """ + Studies and groups are currectly added to user on exchanging referral token + """ + client = clients.get("Administrators") + + study = Study(kf_id="SD_00000000") + study.save() + study_id = to_global_id("StudyNode", "SD_00000000") + + group = Group.objects.first() + group_id = to_global_id("GroupNode", group.id) + + email = "test@email.com" + variables = { + "input": {"email": email, "studies": [study_id], "groups": [group_id]} + } + + resp_create = client.post( + "/graphql", + content_type="application/json", + data={"query": CREATE_REFERRALTOKEN, "variables": variables}, + ) + + assert ReferralToken.objects.count() == 1 + assert Study.objects.first().collaborators.count() == 0 + + referral_token = ReferralToken.objects.first() + resp_exchange = client.post( + "/graphql", + data={ + "query": EXCHANGE_REFERRALTOKEN, + "variables": { + "token": to_global_id("ReferralTokenNode", referral_token.uuid) + }, + }, + content_type="application/json", + ) + resp_body = resp_exchange.json()["data"]["exchangeReferralToken"]["user"] + assert resp_body["groups"]["edges"][0]["node"]["id"] == group_id + assert resp_body["studies"]["edges"][0]["node"]["id"] == study_id + assert Study.objects.first().collaborators.count() == 1 From 3a95ab49ab5957d17ba551a9d4c9a624add789a3 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Tue, 26 May 2020 12:12:13 -0400 Subject: [PATCH 17/19] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Add=20setting=20conf?= =?UTF-8?q?ig=20for=20refferal=20token=20valid=20date=20length?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/settings/development.py | 5 +++++ creator/settings/production.py | 5 +++++ creator/settings/testing.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/creator/settings/development.py b/creator/settings/development.py index ad2366094..c1a5efa7b 100644 --- a/creator/settings/development.py +++ b/creator/settings/development.py @@ -377,3 +377,8 @@ FEAT_STUDY_BUCKETS_CREATE_BUCKETS = os.environ.get( "FEAT_STUDY_BUCKETS_CREATE_BUCKETS", False ) + +# How many days to expire a referral token +REFERRAL_TOKEN_EXPIRATION_DAYS = os.environ.get( + 'REFERRAL_TOKEN_EXPIRATION_DAYS', 3 +) diff --git a/creator/settings/production.py b/creator/settings/production.py index 222a40a37..9ec65da2c 100644 --- a/creator/settings/production.py +++ b/creator/settings/production.py @@ -378,3 +378,8 @@ FEAT_STUDY_BUCKETS_CREATE_BUCKETS = os.environ.get( "FEAT_STUDY_BUCKETS_CREATE_BUCKETS", False ) + +# How many days to expire a referral token +REFERRAL_TOKEN_EXPIRATION_DAYS = os.environ.get( + 'REFERRAL_TOKEN_EXPIRATION_DAYS', 3 +) diff --git a/creator/settings/testing.py b/creator/settings/testing.py index e8f9049bc..f0a385471 100644 --- a/creator/settings/testing.py +++ b/creator/settings/testing.py @@ -376,3 +376,8 @@ FEAT_STUDY_BUCKETS_CREATE_BUCKETS = os.environ.get( "FEAT_STUDY_BUCKETS_CREATE_BUCKETS", False ) + +# How many days to expire a referral token +REFERRAL_TOKEN_EXPIRATION_DAYS = os.environ.get( + 'REFERRAL_TOKEN_EXPIRATION_DAYS', 3 +) From 9dbe68ebbfa1b4d02c40f0ca25545cd4d4b2724f Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Tue, 26 May 2020 12:12:31 -0400 Subject: [PATCH 18/19] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Add=20settings=20for?= =?UTF-8?q?=20default=20from=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/settings/development.py | 5 +++++ creator/settings/production.py | 5 +++++ creator/settings/testing.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/creator/settings/development.py b/creator/settings/development.py index c1a5efa7b..47fcc6f14 100644 --- a/creator/settings/development.py +++ b/creator/settings/development.py @@ -382,3 +382,8 @@ REFERRAL_TOKEN_EXPIRATION_DAYS = os.environ.get( 'REFERRAL_TOKEN_EXPIRATION_DAYS', 3 ) + +# Set default from email +DEFAULT_FROM_EMAIL = os.environ.get( + 'DEFAULT_FROM_EMAIL', 'data-tracker@kidsfirstdrc.org' +) diff --git a/creator/settings/production.py b/creator/settings/production.py index 9ec65da2c..60b4e1385 100644 --- a/creator/settings/production.py +++ b/creator/settings/production.py @@ -383,3 +383,8 @@ REFERRAL_TOKEN_EXPIRATION_DAYS = os.environ.get( 'REFERRAL_TOKEN_EXPIRATION_DAYS', 3 ) + +# Set default from email +DEFAULT_FROM_EMAIL = os.environ.get( + 'DEFAULT_FROM_EMAIL', 'data-tracker@kidsfirstdrc.org' +) diff --git a/creator/settings/testing.py b/creator/settings/testing.py index f0a385471..251902c04 100644 --- a/creator/settings/testing.py +++ b/creator/settings/testing.py @@ -381,3 +381,8 @@ REFERRAL_TOKEN_EXPIRATION_DAYS = os.environ.get( 'REFERRAL_TOKEN_EXPIRATION_DAYS', 3 ) + +# Set default from email +DEFAULT_FROM_EMAIL = os.environ.get( + 'DEFAULT_FROM_EMAIL', 'data-tracker@kidsfirstdrc.org' +) From 27bab536a928aab1581af07f092ca99684764999 Mon Sep 17 00:00:00 2001 From: XuTheBunny Date: Tue, 26 May 2020 16:25:28 -0400 Subject: [PATCH 19/19] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Add=20settings=20for?= =?UTF-8?q?=20default=20data=20tracker=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- creator/settings/development.py | 5 +++++ creator/settings/production.py | 5 +++++ creator/settings/testing.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/creator/settings/development.py b/creator/settings/development.py index 47fcc6f14..2eaa71de1 100644 --- a/creator/settings/development.py +++ b/creator/settings/development.py @@ -387,3 +387,8 @@ DEFAULT_FROM_EMAIL = os.environ.get( 'DEFAULT_FROM_EMAIL', 'data-tracker@kidsfirstdrc.org' ) + +# Default data tracker url +DATA_TRACKER_URL = os.environ.get( + 'DATA_TRACKER_URL', 'https://kf-ui-data-tracker.kidsfirstdrc.org' +) diff --git a/creator/settings/production.py b/creator/settings/production.py index 60b4e1385..47e286540 100644 --- a/creator/settings/production.py +++ b/creator/settings/production.py @@ -388,3 +388,8 @@ DEFAULT_FROM_EMAIL = os.environ.get( 'DEFAULT_FROM_EMAIL', 'data-tracker@kidsfirstdrc.org' ) + +# Default data tracker url +DATA_TRACKER_URL = os.environ.get( + 'DATA_TRACKER_URL', 'https://kf-ui-data-tracker.kidsfirstdrc.org' +) diff --git a/creator/settings/testing.py b/creator/settings/testing.py index 251902c04..1ced61f4f 100644 --- a/creator/settings/testing.py +++ b/creator/settings/testing.py @@ -386,3 +386,8 @@ DEFAULT_FROM_EMAIL = os.environ.get( 'DEFAULT_FROM_EMAIL', 'data-tracker@kidsfirstdrc.org' ) + +# Default data tracker url +DATA_TRACKER_URL = os.environ.get( + 'DATA_TRACKER_URL', 'https://kf-ui-data-tracker.kidsfirstdrc.org' +)