# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2020 Bitergia
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Authors:
# Santiago Dueñas <sduenas@bitergia.com>
# Miguel Ángel Fernández <mafesan@bitergia.com>
#
import json
import re
import graphene
import graphql_jwt
from django.conf import settings
from django.core.paginator import Paginator
from django.db.models import Q, Subquery
from django.db.models import JSONField
from django_rq import enqueue
from graphene.types.generic import GenericScalar
from graphene.utils.str_converters import to_snake_case
from graphene_django.converter import convert_django_field
from graphene_django.types import DjangoObjectType
from grimoirelab_toolkit.datetime import (str_to_datetime,
InvalidDateError)
from .api import (add_identity,
delete_identity,
update_profile,
move_identity,
lock,
unlock,
merge,
unmerge_identities,
add_organization,
add_domain,
delete_organization,
delete_domain,
enroll,
withdraw,
update_enrollment)
from .context import SortingHatContext
from .decorators import check_auth
from .errors import InvalidFilterError
from .jobs import (affiliate,
unify,
find_job,
get_jobs,
recommend_affiliations,
recommend_matches,
recommend_gender,
genderize)
from .models import (Organization,
Domain,
Country,
Individual,
Identity,
Profile,
Enrollment,
Transaction,
Operation)
[docs]@convert_django_field.register(JSONField)
def convert_json_field_to_generic_scalar(field, registry=None):
"""Convert the content of a `JSONField` loading it as an object"""
return OperationArgsType(description=field.help_text, required=not field.null)
[docs]def parse_date_filter(filter_value):
"""Extract the filter terms from a date filter
The accepted formats are controlled by regular expressions
matching two patterns: a comparison operator (>, >=, <, <=) and a date
OR a range operator (..) between two dates.
The accepted date format is ISO 8601, YYYY-MM-DDTHH:MM:SSZ, also
accepting microseconds and time zone offset (YYYY-MM-DDTHH:MM:SS.ms+HH:HH).
:param filter_value: String containing the filter value
:returns: A dictionary including an operator and the datetime values
"""
# Accepted date format is ISO 8601, YYYY-MM-DDTHH:MM:SSZ (no `Z` is accepted too)
filter_data = {
"operator": None,
"date1": None,
"date2": None
}
iso_date_group = r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|\+\d{2}:\d{2})?)"
# Filter with a comparison operator (>, >=, <, <=) and a date (e.g. `>=YYYY-MM-DDTHH:MM:SS`)
oper_comparison = r"^(<=?|>=?)%s$" % iso_date_group
# Filter with a range operator (..) between two dates
# (e.g. YYYY-MM-DDTHH:MM:SSZ..YYYY-MM-DDTHH:MM:SSZ)
range_comparison = r"^%s\.{2}%s$" % (iso_date_group, iso_date_group)
oper_result = re.match(oper_comparison, filter_value)
range_result = re.match(range_comparison, filter_value)
if not oper_result and not range_result:
raise ValueError('Filter format is not valid')
if oper_result:
filter_data['operator'] = oper_result.group(1)
filter_data['date1'] = str_to_datetime(oper_result.group(2))
if range_result:
filter_data['operator'] = '..'
filter_data['date1'] = str_to_datetime(range_result.group(1))
filter_data['date2'] = str_to_datetime(range_result.group(4))
if filter_data['date1'] > filter_data['date2']:
range_msg = 'Date range is invalid. Upper bound must be greater than the lower bound'
raise ValueError(range_msg)
return filter_data
[docs]class OperationArgsType(GenericScalar):
[docs] @classmethod
def serialize(cls, value):
value = super().serialize(value)
value = json.loads(value)
return value
[docs]class OperationType(DjangoObjectType):
class Meta:
model = Operation
[docs]class TransactionType(DjangoObjectType):
class Meta:
model = Transaction
[docs]class OrganizationType(DjangoObjectType):
class Meta:
model = Organization
[docs]class DomainType(DjangoObjectType):
class Meta:
model = Domain
[docs]class CountryType(DjangoObjectType):
class Meta:
model = Country
[docs]class IndividualType(DjangoObjectType):
class Meta:
model = Individual
[docs]class IdentityType(DjangoObjectType):
class Meta:
model = Identity
[docs]class ProfileType(DjangoObjectType):
class Meta:
model = Profile
[docs]class EnrollmentType(DjangoObjectType):
class Meta:
model = Enrollment
[docs]class AffiliationRecommendationType(graphene.ObjectType):
uuid = graphene.String(description='The unique identifier of an individual.')
organizations = graphene.List(graphene.String, description='List of recommended organizations.')
[docs]class MatchesRecommendationType(graphene.ObjectType):
uuid = graphene.String(description='The unique identifier of an individual.')
matches = graphene.List(graphene.String, description='List of recommended matches.')
[docs]class GenderRecommendationType(graphene.ObjectType):
uuid = graphene.String(description='The unique identifier of an individual.')
gender = graphene.String(description='The suggested gender of an individual')
accuracy = graphene.Int(description='The probability of the gender to be accurate')
[docs]class AffiliationResultType(graphene.ObjectType):
uuid = graphene.String(description='The unique identifier of an individual.')
organizations = graphene.List(
graphene.String,
description='List of organizations an individual was affilated to using matching recommendations.'
)
[docs]class UnifyResultType(graphene.ObjectType):
merged = graphene.List(
graphene.String,
description='List of individuals that were merged using matching recommendations.'
)
[docs]class GenderizeResultType(graphene.ObjectType):
uuid = graphene.String(description='The unique identifier of an individual.')
gender = graphene.String(description='The suggested gender of an individual')
accuracy = graphene.Int(description='The probability of the gender to be accurate')
[docs]class JobResultType(graphene.Union):
class Meta:
types = (AffiliationResultType,
AffiliationRecommendationType,
MatchesRecommendationType,
UnifyResultType,
GenderRecommendationType,
GenderizeResultType)
[docs]class JobType(graphene.ObjectType):
job_id = graphene.String(description='Job identifier.')
job_type = graphene.String(description='Type of job.')
status = graphene.String(description='Job status (`started`, `deferred`, `finished`, `failed` or `scheduled`).')
result = graphene.List(JobResultType, description='List of job results.')
errors = graphene.List(graphene.String, description='List of errors.')
enqueued_at = graphene.DateTime(description='Time the job was enqueued at.')
[docs]class CountryFilterType(graphene.InputObjectType):
code = graphene.String(
required=False,
description='Filter countries with an ISO Alpha 2 country code. Examples: `DK` for Denmark, `IT` for Italy.'
)
term = graphene.String(
required=False,
description='Filter countries whose name contains the term.'
)
[docs]class OrganizationFilterType(graphene.InputObjectType):
name = graphene.String(
required=False,
description='Filter organizations with an exact name match.'
)
term = graphene.String(
required=False,
description='Filter organizations whose name or domains include the term.'
)
[docs]class IdentityFilterType(graphene.InputObjectType):
uuid = graphene.String(
required=False,
description='Find an identity by its unique identifier.'
)
term = graphene.String(
required=False,
description='Filter individuals whose name, email or username contain the term.'
)
is_locked = graphene.Boolean(
required=False,
description='Filters individuals by whether their profiles are locked and cannot be edited.'
)
is_bot = graphene.Boolean(
required=False,
description='Filters individuals by whether they have been marked as bots.'
)
gender = graphene.String(
required=False,
description='Filters individuals by their gender.'
)
country = graphene.String(
required=False,
description='Filters individuals using an ISO Alpha 3 or Alpha 2 country code, or with a country name.\
Examples:\n * `GB`\n * `GBR`\n * `United Kingdom`'
)
source = graphene.String(
required=False,
description='Filters individuals by the data source of their identities.'
)
enrollment = graphene.String(
required=False,
description='Filters individuals affiliated to an organization.'
)
enrollment_date = graphene.String(
required=False,
description='Filter with a comparison operator (>, >=, <, <=) and a date OR with a range operator (..) between\
two dates, following ISO-8601 format. Examples:\n* `>=2020-10-12T09:35:06.13045+01:00` \
\n * `2020-10-12T00:00:00..2020-11-22T00:00:00`.'
)
is_enrolled = graphene.Boolean(
required=False,
description='Filter individuals by whether they are affiliated to any organization.'
)
last_updated = graphene.String(
required=False,
description='Filter with a comparison operator (>, >=, <, <=) and a date OR with a range operator (..) between\
two dates, following ISO-8601 format. Examples:\n* `>=2020-10-12T09:35:06.13045+01:00` \
\n * `2020-10-12T00:00:00..2020-11-22T00:00:00`.'
)
[docs]class TransactionFilterType(graphene.InputObjectType):
tuid = graphene.String(
required=False,
description='Find a transaction using its unique id.'
)
name = graphene.String(
required=False,
description='Find a transaction using its name.'
)
is_closed = graphene.Boolean(
required=False,
description='Filter transactions by whether they are closed.'
)
from_date = graphene.DateTime(
required=False,
description='Find transactions created after a date, following ISO-8601 format. For example, `2020-04-22T00:00:00Z`.'
)
to_date = graphene.DateTime(
required=False,
description='Find transactions created before a date, following ISO-8601 format. For example, `2020-04-22T00:00:00Z`.'
)
authored_by = graphene.String(
required=False,
description='Filter transactions using the username of their author.'
)
[docs]class OperationFilterType(graphene.InputObjectType):
ouid = graphene.String(
required=False,
description='Find an operation using its unique id.'
)
op_type = graphene.String(
required=False,
description='Filter operations by their type: `ADD`, `DELETE` or `UPDATE`.'
)
entity_type = graphene.String(
required=False,
description='Filter by the type of entity involved in the operations, eg. `individual`, `profile`, `enrollment`.'
)
target = graphene.String(
required=False,
description='Filter by the argument which the operation is directed to.'
)
from_date = graphene.DateTime(
required=False,
description='Find operations created after a date, following ISO-8601 format. For example, `2020-04-22T00:00:00Z`.'
)
to_date = graphene.DateTime(
required=False,
description='Find operations created before a date, following ISO-8601 format. For example, `2020-04-22T00:00:00Z`.'
)
[docs]class AbstractPaginatedType(graphene.ObjectType):
[docs] @classmethod
def create_paginated_result(cls, query, page=1,
page_size=settings.DEFAULT_GRAPHQL_PAGE_SIZE):
paginator = Paginator(query, page_size)
result = paginator.page(page)
entities = result.object_list
page_info = PaginationType(
page=result.number,
page_size=page_size,
num_pages=paginator.num_pages,
has_next=result.has_next(),
has_prev=result.has_previous(),
start_index=result.start_index(),
end_index=result.end_index(),
total_results=len(query)
)
return cls(entities=entities, page_info=page_info)
[docs]class CountryPaginatedType(AbstractPaginatedType):
entities = graphene.List(CountryType, description='A list of countries.')
page_info = graphene.Field(PaginationType, description='Information to aid in pagination.')
[docs]class OrganizationPaginatedType(AbstractPaginatedType):
entities = graphene.List(OrganizationType, description='A list of organizations.')
page_info = graphene.Field(PaginationType, description='Information to aid in pagination.')
[docs]class IdentityPaginatedType(AbstractPaginatedType):
entities = graphene.List(IndividualType, description='A list of identities.')
page_info = graphene.Field(PaginationType, description='Information to aid in pagination.')
[docs]class TransactionPaginatedType(AbstractPaginatedType):
entities = graphene.List(TransactionType, description='A list of transactions.')
page_info = graphene.Field(PaginationType, description='Information to aid in pagination.')
[docs]class OperationPaginatedType(AbstractPaginatedType):
entities = graphene.List(OperationType, description='A list of operations.')
page_info = graphene.Field(PaginationType, description='Information to aid in pagination.')
[docs]class JobPaginatedType(AbstractPaginatedType):
entities = graphene.List(JobType, description='A list of jobs.')
page_info = graphene.Field(PaginationType, description='Information to aid in pagination.')
[docs]class AddOrganization(graphene.Mutation):
[docs] class Arguments:
name = graphene.String()
organization = graphene.Field(lambda: OrganizationType)
[docs] @check_auth
def mutate(self, info, name):
user = info.context.user
ctx = SortingHatContext(user)
org = add_organization(ctx, name)
return AddOrganization(
organization=org
)
[docs]class DeleteOrganization(graphene.Mutation):
[docs] class Arguments:
name = graphene.String()
organization = graphene.Field(lambda: OrganizationType)
[docs] @check_auth
def mutate(self, info, name):
user = info.context.user
ctx = SortingHatContext(user)
org = delete_organization(ctx, name)
return DeleteOrganization(
organization=org
)
[docs]class AddDomain(graphene.Mutation):
[docs] class Arguments:
organization = graphene.String()
domain = graphene.String()
is_top_domain = graphene.Boolean()
domain = graphene.Field(lambda: DomainType)
[docs] @check_auth
def mutate(self, info, organization, domain, is_top_domain=False):
user = info.context.user
ctx = SortingHatContext(user)
dom = add_domain(ctx,
organization,
domain,
is_top_domain=is_top_domain)
return AddDomain(
domain=dom
)
[docs]class DeleteDomain(graphene.Mutation):
[docs] class Arguments:
domain = graphene.String()
domain = graphene.Field(lambda: DomainType)
[docs] @check_auth
def mutate(self, info, domain):
user = info.context.user
ctx = SortingHatContext(user)
dom = delete_domain(ctx, domain)
return DeleteDomain(
domain=dom
)
[docs]class AddIdentity(graphene.Mutation):
[docs] class Arguments:
source = graphene.String()
name = graphene.String()
email = graphene.String()
username = graphene.String()
uuid = graphene.String()
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, source,
name=None, email=None, username=None,
uuid=None):
user = info.context.user
ctx = SortingHatContext(user)
identity = add_identity(ctx,
source,
name=name,
email=email,
username=username,
uuid=uuid)
individual = identity.individual
return AddIdentity(
uuid=identity.uuid,
individual=individual
)
[docs]class DeleteIdentity(graphene.Mutation):
[docs] class Arguments:
uuid = graphene.String()
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, uuid):
user = info.context.user
ctx = SortingHatContext(user)
individual = delete_identity(ctx, uuid)
return DeleteIdentity(
uuid=uuid,
individual=individual
)
[docs]class Lock(graphene.Mutation):
[docs] class Arguments:
uuid = graphene.String()
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, uuid):
user = info.context.user
ctx = SortingHatContext(user)
individual = lock(ctx, uuid)
return Lock(
uuid=uuid,
individual=individual
)
[docs]class Unlock(graphene.Mutation):
[docs] class Arguments:
uuid = graphene.String()
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, uuid):
user = info.context.user
ctx = SortingHatContext(user)
individual = unlock(ctx, uuid)
return Unlock(
uuid=uuid,
individual=individual
)
[docs]class UpdateProfile(graphene.Mutation):
[docs] class Arguments:
uuid = graphene.String()
data = ProfileInputType()
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, uuid, data):
user = info.context.user
ctx = SortingHatContext(user)
individual = update_profile(ctx, uuid, **data)
return UpdateProfile(
uuid=individual.mk,
individual=individual
)
[docs]class MoveIdentity(graphene.Mutation):
[docs] class Arguments:
from_uuid = graphene.String()
to_uuid = graphene.String()
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, from_uuid, to_uuid):
user = info.context.user
ctx = SortingHatContext(user)
individual = move_identity(ctx, from_uuid, to_uuid)
return MoveIdentity(
uuid=individual.mk,
individual=individual
)
[docs]class Merge(graphene.Mutation):
[docs] class Arguments:
from_uuids = graphene.List(graphene.String)
to_uuid = graphene.String()
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, from_uuids, to_uuid):
user = info.context.user
ctx = SortingHatContext(user)
individual = merge(ctx, from_uuids, to_uuid)
return Merge(
uuid=individual.mk,
individual=individual
)
[docs]class UnmergeIdentities(graphene.Mutation):
[docs] class Arguments:
uuids = graphene.List(graphene.String)
uuids = graphene.Field(lambda: graphene.List(graphene.String))
individuals = graphene.Field(lambda: graphene.List(IndividualType))
[docs] @check_auth
def mutate(self, info, uuids):
user = info.context.user
ctx = SortingHatContext(user)
individuals = unmerge_identities(ctx, uuids)
uuids = [individual.mk for individual in individuals]
return UnmergeIdentities(
uuids=uuids,
individuals=individuals
)
[docs]class Enroll(graphene.Mutation):
[docs] class Arguments:
uuid = graphene.String()
organization = graphene.String()
from_date = graphene.DateTime(required=False)
to_date = graphene.DateTime(required=False)
force = graphene.Boolean(required=False)
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, uuid, organization,
from_date=None, to_date=None,
force=False):
user = info.context.user
ctx = SortingHatContext(user)
individual = enroll(ctx, uuid, organization,
from_date=from_date, to_date=to_date,
force=force)
return Enroll(
uuid=individual.mk,
individual=individual
)
[docs]class Withdraw(graphene.Mutation):
[docs] class Arguments:
uuid = graphene.String()
organization = graphene.String()
from_date = graphene.DateTime(required=False)
to_date = graphene.DateTime(required=False)
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, uuid, organization, from_date=None, to_date=None):
user = info.context.user
ctx = SortingHatContext(user)
individual = withdraw(ctx, uuid, organization,
from_date=from_date, to_date=to_date)
return Withdraw(
uuid=individual.mk,
individual=individual
)
[docs]class UpdateEnrollment(graphene.Mutation):
[docs] class Arguments:
uuid = graphene.String()
organization = graphene.String()
from_date = graphene.DateTime()
to_date = graphene.DateTime()
new_from_date = graphene.DateTime(required=False)
new_to_date = graphene.DateTime(required=False)
force = graphene.Boolean(required=False)
uuid = graphene.Field(lambda: graphene.String)
individual = graphene.Field(lambda: IndividualType)
[docs] @check_auth
def mutate(self, info, uuid, organization,
from_date, to_date,
new_from_date=None, new_to_date=None,
force=True):
user = info.context.user
ctx = SortingHatContext(user)
individual = update_enrollment(ctx, uuid, organization,
from_date=from_date,
to_date=to_date,
new_from_date=new_from_date,
new_to_date=new_to_date,
force=force)
return UpdateEnrollment(
uuid=individual.mk,
individual=individual
)
[docs]class RecommendAffiliations(graphene.Mutation):
[docs] class Arguments:
uuids = graphene.List(graphene.String,
required=False)
job_id = graphene.Field(lambda: graphene.String)
[docs] @check_auth
def mutate(self, info, uuids=None):
user = info.context.user
ctx = SortingHatContext(user)
job = enqueue(recommend_affiliations, ctx, uuids)
return RecommendAffiliations(
job_id=job.id
)
[docs]class RecommendMatches(graphene.Mutation):
[docs] class Arguments:
source_uuids = graphene.List(graphene.String)
target_uuids = graphene.List(graphene.String,
required=False)
criteria = graphene.List(graphene.String)
verbose = graphene.Boolean(required=False)
job_id = graphene.Field(lambda: graphene.String)
[docs] @check_auth
def mutate(self, info, source_uuids, criteria, target_uuids=None, verbose=False):
user = info.context.user
ctx = SortingHatContext(user)
job = enqueue(recommend_matches, ctx, source_uuids, target_uuids, criteria, verbose)
return RecommendMatches(
job_id=job.id
)
[docs]class RecommendGender(graphene.Mutation):
[docs] class Arguments:
uuids = graphene.List(graphene.String)
job_id = graphene.Field(lambda: graphene.String)
[docs] @check_auth
def mutate(self, info, uuids=None):
user = info.context.user
ctx = SortingHatContext(user)
job = enqueue(recommend_gender, ctx, uuids)
return RecommendGender(
job_id=job.id
)
[docs]class Affiliate(graphene.Mutation):
[docs] class Arguments:
uuids = graphene.List(graphene.String,
required=False)
job_id = graphene.Field(lambda: graphene.String)
[docs] @check_auth
def mutate(self, info, uuids=None):
user = info.context.user
ctx = SortingHatContext(user)
job = enqueue(affiliate, ctx, uuids)
return Affiliate(
job_id=job.id
)
[docs]class Unify(graphene.Mutation):
[docs] class Arguments:
source_uuids = graphene.List(graphene.String)
target_uuids = graphene.List(graphene.String,
required=False)
criteria = graphene.List(graphene.String)
job_id = graphene.Field(lambda: graphene.String)
[docs] @check_auth
def mutate(self, info, source_uuids, criteria, target_uuids=None):
user = info.context.user
ctx = SortingHatContext(user)
job = enqueue(unify, ctx, source_uuids, target_uuids, criteria)
return Unify(
job_id=job.id
)
[docs]class Genderize(graphene.Mutation):
[docs] class Arguments:
uuids = graphene.List(graphene.String)
job_id = graphene.Field(lambda: graphene.String)
[docs] @check_auth
def mutate(self, info, uuids=None):
user = info.context.user
ctx = SortingHatContext(user)
job = enqueue(genderize, ctx, uuids)
return Genderize(
job_id=job.id
)
[docs]class SortingHatQuery:
countries = graphene.Field(
CountryPaginatedType,
page_size=graphene.Int(),
page=graphene.Int(),
filters=CountryFilterType(required=False),
description='Find countries.'
)
organizations = graphene.Field(
OrganizationPaginatedType,
page_size=graphene.Int(),
page=graphene.Int(),
filters=OrganizationFilterType(required=False),
description='Find organizations.'
)
individuals = graphene.Field(
IdentityPaginatedType,
page_size=graphene.Int(),
page=graphene.Int(),
filters=IdentityFilterType(required=False),
order_by=graphene.String(required=False),
description='Find individuals.'
)
transactions = graphene.Field(
TransactionPaginatedType,
page_size=graphene.Int(),
page=graphene.Int(),
filters=TransactionFilterType(required=False),
description='Find transactions.'
)
operations = graphene.Field(
OperationPaginatedType,
page_size=graphene.Int(),
page=graphene.Int(),
filters=OperationFilterType(required=False),
description='Find operations.'
)
job = graphene.Field(
JobType,
job_id=graphene.String(),
description='Find a single job by its id.'
)
jobs = graphene.Field(
JobPaginatedType,
page_size=graphene.Int(),
page=graphene.Int(),
description='Get all jobs.'
)
[docs] @check_auth
def resolve_countries(self, info, filters=None,
page=1,
page_size=settings.DEFAULT_GRAPHQL_PAGE_SIZE):
query = Country.objects.order_by('code')
if filters and 'code' in filters:
query = query.filter(code=filters['code'])
if filters and 'term' in filters:
query = query.filter(name__icontains=filters['term'])
return CountryPaginatedType.create_paginated_result(query,
page,
page_size=page_size)
[docs] @check_auth
def resolve_organizations(self, info, filters=None,
page=1,
page_size=settings.DEFAULT_GRAPHQL_PAGE_SIZE,
**kwargs):
query = Organization.objects.order_by('name')
if filters and 'name' in filters:
query = query.filter(name=filters['name'])
if filters and 'term' in filters:
search_term = filters['term']
query = query.filter(Q(name__icontains=search_term) |
Q(name__in=Subquery(Domain.objects
.filter(domain__icontains=search_term)
.values_list('organization__name'))))
return OrganizationPaginatedType.create_paginated_result(query,
page,
page_size=page_size)
[docs] @check_auth
def resolve_individuals(self, info, filters=None,
page=1,
page_size=settings.DEFAULT_GRAPHQL_PAGE_SIZE,
order_by='mk',
**kwargs):
query = Individual.objects.order_by(to_snake_case(order_by))
if filters and 'uuid' in filters:
indv_uuid = filters['uuid']
# Search among all the individuals and their identities
query = query.filter(mk__in=Subquery(Identity.objects
.filter(Q(uuid=indv_uuid) |
Q(individual__mk=indv_uuid))
.values_list('individual__mk')))
if filters and 'term' in filters:
search_term = filters['term']
# Filter matching individuals by their mk and their identities
query = query.filter(mk__in=Subquery(Identity.objects
.filter(Q(name__icontains=search_term) |
Q(email__icontains=search_term) |
Q(username__icontains=search_term) |
Q(individual__profile__name__icontains=search_term) |
Q(individual__profile__email__icontains=search_term))
.values_list('individual__mk')))
if filters and 'is_locked' in filters:
query = query.filter(is_locked=filters['is_locked'])
if filters and 'is_bot' in filters:
query = query.filter(mk__in=Subquery(Profile.objects
.filter(is_bot=filters['is_bot'])
.values_list('individual__mk')))
if filters and 'gender' in filters:
query = query.filter(profile__gender=filters['gender'])
if filters and 'country' in filters:
country = filters['country']
query = query.filter(mk__in=Subquery(Profile.objects
.filter(Q(country__name__icontains=country) |
Q(country__code=country) |
Q(country__alpha3=country))
.values_list('individual__mk')))
if filters and 'source' in filters:
query = query.filter(identities__source=filters['source'])
if filters and 'enrollment' in filters:
query = query.filter(enrollments__organization__name__icontains=filters['enrollment'])
if filters and 'enrollment_date' in filters:
# Accepted date format is ISO 8601, YYYY-MM-DDTHH:MM:SS
try:
filter_data = parse_date_filter(filters['enrollment_date'])
except ValueError as e:
raise InvalidFilterError(filter_name='enrollment_date', msg=e)
except InvalidDateError as e:
raise InvalidFilterError(filter_name='enrollment_date', msg=e)
date1 = filter_data['date1']
date2 = filter_data['date2']
if filter_data['operator']:
operator = filter_data['operator']
if operator == '<':
query = query.filter(mk__in=Subquery(Enrollment.objects
.filter(start__lt=date1)
.values_list('individual__mk')))
elif operator == '<=':
query = query.filter(mk__in=Subquery(Enrollment.objects
.filter(start__lte=date1)
.values_list('individual__mk')))
elif operator == '>':
query = query.filter(mk__in=Subquery(Enrollment.objects
.filter(end__gt=date1)
.values_list('individual__mk')))
elif operator == '>=':
query = query.filter(mk__in=Subquery(Enrollment.objects
.filter(end__gte=date1)
.values_list('individual__mk')))
elif operator == '..':
query = query.filter(mk__in=Subquery(Enrollment.objects
.filter(start__lte=date2,
end__gte=date1)
.values_list('individual__mk')))
if filters and 'is_enrolled' in filters:
query = query.filter(enrollments__isnull=not filters['is_enrolled'])
if filters and 'last_updated' in filters:
# Accepted date format is ISO 8601, YYYY-MM-DDTHH:MM:SS
try:
filter_data = parse_date_filter(filters['last_updated'])
except ValueError as e:
raise InvalidFilterError(filter_name='last_updated', msg=e)
except InvalidDateError as e:
raise InvalidFilterError(filter_name='last_updated', msg=e)
date1 = filter_data['date1']
date2 = filter_data['date2']
if filter_data['operator']:
operator = filter_data['operator']
if operator == '<':
query = query.filter(last_modified__lt=date1)
elif operator == '<=':
query = query.filter(last_modified__lte=date1)
elif operator == '>':
query = query.filter(last_modified__gt=date1)
elif operator == '>=':
query = query.filter(last_modified__gte=date1)
elif operator == '..':
query = query.filter(last_modified__range=(date1, date2))
return IdentityPaginatedType.create_paginated_result(query,
page,
page_size=page_size)
[docs] @check_auth
def resolve_job(self, info, job_id):
job = find_job(job_id)
status = job.get_status()
job_type = job.func_name.split('.')[-1]
enqueued_at = job.enqueued_at
result = None
errors = None
if (job.result) and (job_type == 'affiliate'):
errors = job.result['errors']
result = [
AffiliationResultType(uuid=uuid, organizations=orgs)
for uuid, orgs in job.result['results'].items()
]
elif (job.result) and (job_type == 'recommend_affiliations'):
result = [
AffiliationRecommendationType(uuid=uuid, organizations=orgs)
for uuid, orgs in job.result['results'].items()
]
elif (job.result) and (job_type == 'recommend_matches'):
result = [
MatchesRecommendationType(uuid=uuid, matches=matches)
for uuid, matches in job.result['results'].items()
]
elif (job.result) and (job_type == 'recommend_gender'):
result = [
GenderRecommendationType(uuid=uuid,
gender=rec['gender'],
accuracy=rec['accuracy'])
for uuid, rec in job.result['results'].items()
]
elif (job.result) and (job_type == 'unify'):
errors = job.result['errors']
result = [
UnifyResultType(merged=job.result['results'])
]
elif (job.result) and (job_type == 'genderize'):
errors = job.result['errors']
result = [
GenderizeResultType(uuid=uuid, gender=rec[0], accuracy=rec[1])
for uuid, rec in job.result['results'].items()
]
elif status == 'failed':
errors = [job.exc_info]
return JobType(job_id=job_id,
job_type=job_type,
status=status,
result=result,
errors=errors,
enqueued_at=enqueued_at)
[docs] @check_auth
def resolve_jobs(self, info, page=1, page_size=settings.DEFAULT_GRAPHQL_PAGE_SIZE):
jobs = get_jobs()
result = []
for job in jobs:
job_id = job.get_id()
status = job.get_status()
job_type = job.func_name.split('.')[-1]
enqueued_at = job.enqueued_at
result.append(JobType(job_id=job_id,
job_type=job_type,
status=status,
result=[],
errors=[],
enqueued_at=enqueued_at))
return JobPaginatedType.create_paginated_result(result,
page,
page_size=page_size)
[docs] @check_auth
def resolve_transactions(self, info, filters=None,
page=1,
page_size=settings.DEFAULT_GRAPHQL_PAGE_SIZE,
**kwargs):
query = Transaction.objects.order_by('created_at')
if filters and 'tuid' in filters:
query = query.filter(tuid=filters['tuid'])
if filters and 'name' in filters:
query = query.filter(name=filters['name'])
if filters and 'is_closed' in filters:
query = query.filter(is_closed=filters['isClosed'])
if filters and 'from_date' in filters:
query = query.filter(created_at__gte=filters['from_date'])
if filters and 'to_date' in filters:
query = query.filter(created_at__lte=filters['to_date'])
if filters and 'authored_by' in filters:
query = query.filter(authored_by=filters['authored_by'])
return TransactionPaginatedType.create_paginated_result(query,
page,
page_size=page_size)
[docs] @check_auth
def resolve_operations(self, info, filters=None,
page=1,
page_size=settings.DEFAULT_GRAPHQL_PAGE_SIZE,
**kwargs):
query = Operation.objects.order_by('timestamp')
if filters and 'ouid' in filters:
query = query.filter(ouid=filters['ouid'])
if filters and 'op_type' in filters:
query = query.filter(op_type=filters['op_type'])
if filters and 'entity_type' in filters:
query = query.filter(entity_type=filters['entity_type'])
if filters and 'target' in filters:
query = query.filter(target=filters['target'])
if filters and 'from_date' in filters:
query = query.filter(timestamp__gte=filters['from_date'])
if filters and 'to_date' in filters:
query = query.filter(timestamp__lte=filters['to_date'])
return OperationPaginatedType.create_paginated_result(query,
page,
page_size=page_size)
[docs]class SortingHatMutation(graphene.ObjectType):
add_organization = AddOrganization.Field(
description='Add an organization to the registry.'
)
delete_organization = DeleteOrganization.Field(
description='Remove an organization from the registry. Related information\
such as domains or enrollments is also removed.'
)
add_domain = AddDomain.Field(
description='Add a new domain to an organization. The new domain is set\
as a top domain by default. A domain can only be assigned to one organization.'
)
delete_domain = DeleteDomain.Field(
description='Remove a domain from the registry.'
)
add_identity = AddIdentity.Field(
description='Add a new identity to the registry. A new individual will be\
also added and associated to the new identity unless an `uuid` is provided.\
When `uuid` is set, it creates a new identity associated to the individual\
defined by this identifier.'
)
delete_identity = DeleteIdentity.Field(
description='Remove an identity from the registry. If the `uuid` also\
belongs to an individual, this entry and those identities linked to it\
will be removed too.'
)
update_profile = UpdateProfile.Field(
description='Update an individual profile.'
)
move_identity = MoveIdentity.Field(
description='Shift the identity identified by `from_uuid` to the individual\
identified by `to_uuid`.'
)
lock = Lock.Field(
description='Lock an individual so it cannot be modified.'
)
unlock = Unlock.Field(
description='Unlock an individual so it can be modified.'
)
merge = Merge.Field(
description='Join a list of individuals, defined in `from_uuid` by any of\
their valid identities ids, into `to_uuid` individual. Identities and enrollments\
related to each `from_uuid` will be assigned to `to_uuid`. In addition, each\
`from_uuid` will be removed from the registry.'
)
unmerge_identities = UnmergeIdentities.Field(
description='Separate a list of `uuid` identities, creating an individual for each one.'
)
enroll = Enroll.Field(
description='Enroll an individual in an organization. Existing enrollments\
for the same individual and organization which overlap with the new period\
will be merged into a single enrollment.'
)
withdraw = Withdraw.Field(
description='Withdraw an individual identified by `uuid` from the given\
`organization` during the given period of time.'
)
update_enrollment = UpdateEnrollment.Field(
description='Update one or more enrollments from an individual given a new\
date range. By default, `force` is set to `true`. In case any of the new\
dates are missing, the former value for that date will be preserved.'
)
recommend_affiliations = RecommendAffiliations.Field(
description='Recommend organizations for a list of individuals based on their emails.'
)
recommend_matches = RecommendMatches.Field(
description='Recommend identity matches for a list of individuals based\
on a list of criteria composed by `email`, `name` and/or `username`.'
)
recommend_gender = RecommendGender.Field(
description='Recommend genders for a list of individuals based on their names\
using the genderize.io API.'
)
affiliate = Affiliate.Field(
description='Affiliate a set of individuals using recommendations.'
)
unify = Unify.Field(
description='Unify a set of individuals by merging them using matching recommendations.'
)
genderize = Genderize.Field(
description='Autocomplete the gender information of a set of individuals\
using genderize.io recommendations.'
)
# JWT authentication
token_auth = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field(description='Verify a JSON Web Token.')
refresh_token = graphql_jwt.Refresh.Field(description='Refresh a JSON Web Token.')