diff --git a/Dockerfile b/Dockerfile index 20694463a..7d374aadd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ -FROM alpine:latest +FROM python:3.7-alpine LABEL authors="Markus Krogh " -RUN apk add --no-cache ca-certificates python3 libpq +RUN apk add --no-cache ca-certificates +RUN apk --update add python3-dev libpq libxml2-dev libxslt-dev libffi-dev gcc musl-dev libgcc openssl-dev curl +RUN apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev py-pillow RUN pip3 install --upgrade pip RUN mkdir /app WORKDIR /app diff --git a/requirements/common.txt b/requirements/common.txt index ffc2fe650..94e602725 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -13,3 +13,9 @@ django-dotenv<1.5 django-dynamic-preferences<2.0 django-attachments<2.0 configparser +graphene-django>=2.0 +django-cors-headers +vakt +django-vakt +django-graphql-jwt +Pillow diff --git a/requirements/dev.txt b/requirements/dev.txt index b2bebc97f..ba39872e2 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,2 +1,6 @@ -r common.txt ipython +django-nose<1.5 +django-extensions +ipdb +Faker diff --git a/src/niweb/apps/noclook/admin.py b/src/niweb/apps/noclook/admin.py index a3b041145..7128fbfa2 100644 --- a/src/niweb/apps/noclook/admin.py +++ b/src/niweb/apps/noclook/admin.py @@ -4,7 +4,9 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User -from .models import NodeHandle, NodeType, UniqueIdGenerator, NordunetUniqueId, OpticalNodeType, ServiceType, ServiceClass, Dropdown, Choice, SwitchType +from .models import NodeHandle, NodeType, Role, RoleGroup, UniqueIdGenerator,\ + NordunetUniqueId, OpticalNodeType, ServiceType, ServiceClass, Dropdown, \ + Choice, GroupContextAuthzAction, NodeHandleContext, SwitchType class UserModelAdmin(UserAdmin): inlines = [ApiKeyInline] @@ -13,7 +15,7 @@ class NodeHandleAdmin(admin.ModelAdmin): list_filter = ('node_type', 'creator') search_fields = ['node_name'] actions = ['delete_object'] - + # Remove the bulk delete option from the admin interface as it does not # run the NodeHandle delete-function. def get_actions(self, request): @@ -21,7 +23,7 @@ def get_actions(self, request): if 'delete_selected' in actions: del actions['delete_selected'] return actions - + def delete_object(self, request, queryset): deleted = 0 for obj in queryset: @@ -33,12 +35,12 @@ def delete_object(self, request, queryset): message_bit = "%s NodeHandles were" % deleted self.message_user(request, "%s successfully deleted." % message_bit) delete_object.short_description = "Delete the selected NodeHandle(s)" - + class NodeTypeAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('type',)} actions = ['delete_object'] - + # Remove the bulk delete option from the admin interface as it does not # run the NodeHandle delete-function. def get_actions(self, request): @@ -46,7 +48,7 @@ def get_actions(self, request): if 'delete_selected' in actions: del actions['delete_selected'] return actions - + def delete_object(self, request, queryset): deleted = 0 for obj in queryset: @@ -102,4 +104,8 @@ class SwitchTypeAdmin(admin.ModelAdmin): admin.site.register(ServiceClass) admin.site.register(Dropdown, DropdownAdmin) admin.site.register(Choice, ChoiceAdmin) +admin.site.register(RoleGroup) +admin.site.register(Role) +admin.site.register(GroupContextAuthzAction) +admin.site.register(NodeHandleContext) admin.site.register(SwitchType, SwitchTypeAdmin) diff --git a/src/niweb/apps/noclook/dynamic_preferences_registry.py b/src/niweb/apps/noclook/dynamic_preferences_registry.py index 47abcb72f..310f64526 100644 --- a/src/niweb/apps/noclook/dynamic_preferences_registry.py +++ b/src/niweb/apps/noclook/dynamic_preferences_registry.py @@ -26,6 +26,14 @@ class MoreInfoLink(StringPreference): help_text = 'Base url for more information links on detail pages' +@global_preferences_registry.register +class NOCLookMenuMode(StringPreference): + section = general + name = 'menu_mode' + default = 'ni' + help_text = 'sri|ni' + + @global_preferences_registry.register class PageFlashMessage(StringPreference): section = announcements diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 7c1451f8b..2c7d33e5e 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -1,11 +1,19 @@ from datetime import datetime from django import forms +from django.utils.translation import gettext_lazy as _ from django.forms.utils import ErrorDict, ErrorList, ValidationError -from django.forms.widgets import HiddenInput +from django.forms.widgets import HiddenInput, Textarea from django.db import IntegrityError import json import csv -from apps.noclook.models import NodeHandle, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown, SwitchType +from apps.noclook import helpers +from apps.noclook.models import NodeType, NodeHandle, RoleGroup, Role,\ + UniqueIdGenerator, ServiceType,\ + NordunetUniqueId, Dropdown, DEFAULT_ROLES,\ + DEFAULT_ROLE_KEY, DEFAULT_ROLEGROUP_NAME,\ + SwitchType + +from .validators import * from .. import unique_ids import norduniclient as nc from dynamic_preferences.registries import global_preferences_registry @@ -20,6 +28,14 @@ def country_codes(): return zip(codes, codes) +def email_choices(): + return Dropdown.get('email_type').as_choices() + + +def phone_choices(): + return Dropdown.get('phone_type').as_choices() + + def countries(): choices = Dropdown.get('countries').as_choices() codes, countries = zip(*choices) @@ -48,6 +64,27 @@ def get_node_type_tuples(node_type): choices.extend([tuple([item['handle_id'], item['name']]) for item in l]) return choices + +def get_contacts_for_organization(organization_id): + """ + Returns a list of tuple of node.handle_id and node['name'] of contacts that + works for a certain organization + """ + organization = NodeHandle.objects.get(handle_id=organization_id) + contacts = organization.get_node().get_contacts() + + return _get_tuples_for_iterator(contacts) + + +def _get_tuples_for_iterator(iterator): + """ + Returns a list of tuple of handle_id and name of iterator. + """ + choices = [('', '')] + choices.extend([tuple([item['handle_id'], item['name']]) for item in iterator]) + return choices + + class IPAddrField(forms.CharField): def __init__(self, *args, **kwargs): if 'widget' not in kwargs: @@ -124,14 +161,14 @@ def description_field(name): help_text=u'Short description of the {}.'.format(name)) -def relationship_field(name, select=False): +def relationship_field(name, select=False, validators=[]): labels = { } label = labels.get(name, name.title()) if select: - return forms.ChoiceField(required=False, label=label, widget=forms.widgets.Select) + return forms.ChoiceField(required=False, label=label, widget=forms.widgets.Select, validators=validators) else: - return forms.IntegerField(required=False, label=label, widget=forms.widgets.HiddenInput) + return forms.IntegerField(required=False, label=label, widget=forms.widgets.HiddenInput, validators=validators) class ReserveIdForm(forms.Form): @@ -171,13 +208,13 @@ def __init__(self, *args, **kwargs): address = forms.CharField(required=False) postarea = forms.CharField(required=False) postcode = forms.CharField(required=False) - + def clean(self): cleaned_data = super(NewSiteForm, self).clean() cleaned_data['country'] = country_map(cleaned_data['country_code']) return cleaned_data - - + + class EditSiteForm(forms.Form): def __init__(self, *args, **kwargs): @@ -210,8 +247,8 @@ def clean(self): cleaned_data['name'] = cleaned_data['name'] cleaned_data['country_code'] = country_code_map(cleaned_data['country']) return cleaned_data - - + + class NewSiteOwnerForm(forms.Form): name = forms.CharField() description = description_field('site owner') @@ -897,7 +934,7 @@ def __init__(self, csv_headers, *args, **kwargs): csv_data = forms.CharField(required=False, widget=forms.Textarea( - attrs={'rows': '5', + attrs={'rows': '5', 'class': 'input-xxlarge'})) reviewed = forms.BooleanField(required=False) @@ -930,6 +967,290 @@ def form_to_csv(form, headers): return u",".join([cleaned.get(h) or raw.get(h, '') for h in headers]) +class NewOrganizationForm(forms.Form): + organization_number = forms.CharField(required=False) + name = forms.CharField() + description = description_field('organization') + website = forms.CharField(required=False) + organization_id = forms.CharField(required=False) + type = forms.ChoiceField(widget=forms.widgets.Select, required=False) + incident_management_info = forms.CharField(widget=forms.widgets.Textarea, required=False, label="Additional info for incident Mgmt") + affiliation_customer = forms.BooleanField(required=False) + affiliation_end_customer = forms.BooleanField(required=False) + affiliation_provider = forms.BooleanField(required=False) + affiliation_partner = forms.BooleanField(required=False) + affiliation_host_user = forms.BooleanField(required=False) + affiliation_site_owner = forms.BooleanField(required=False) + + def __init__(self, *args, **kwargs): + super(NewOrganizationForm, self).__init__(*args, **kwargs) + self.fields['type'].choices = Dropdown.get('organization_types').as_choices() + + + def clean_organization_id(self): + organization_id = self.cleaned_data['organization_id'] + + # if it's not empty + if organization_id: + exists = nc.models.OrganizationModel.check_existent_organization_id(organization_id) + if exists: + raise ValidationError( + _('The Organization ID %(organization_id)s already exist on the system'), + params={'organization_id': organization_id}, + ) + + return organization_id + + +class EditOrganizationForm(NewOrganizationForm): + def __init__(self, *args, **kwargs): + # set initial for contact combos + initial = {} if 'initial' not in kwargs else kwargs['initial'] + + if 'handle_id' in args[0]: + handle_id = args[0]['handle_id'] + self.cached_handle_id = handle_id + + for field, roledict in DEFAULT_ROLES.items(): + role = Role.objects.get(slug=field) + possible_contact = helpers.get_contact_for_orgrole(handle_id, role) + + if possible_contact: + args[0][field] = possible_contact.handle_id + + self.strict_validation = False + + super(EditOrganizationForm, self).__init__(*args, **kwargs) + self.fields['relationship_parent_of'].choices = get_node_type_tuples('Organization') + self.fields['relationship_uses_a'].choices = get_node_type_tuples('Procedure') + + # contact choices + contact_type = NodeType.objects.get(slug='contact') + contact_choices = [('', '')] + list(NodeHandle.objects.filter(node_type=contact_type).values_list('handle_id', 'node_name')) + + self.fields['abuse_contact'].choices = contact_choices + self.fields['primary_contact'].choices = contact_choices + self.fields['secondary_contact'].choices = contact_choices + self.fields['it_technical_contact'].choices = contact_choices + self.fields['it_security_contact'].choices = contact_choices + self.fields['it_manager_contact'].choices = contact_choices + + relationship_parent_of = relationship_field('organization', True, [validate_organization]) + relationship_uses_a = relationship_field('procedure', True, [validate_procedure]) + + abuse_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Abuse", validators=[validate_contact]) + primary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Primary contact at incidents", validators=[validate_contact]) # Primary contact at incidents + secondary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Secondary contact at incidents", validators=[validate_contact]) # Secondary contact at incidents + it_technical_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="NOC Technical", validators=[validate_contact]) # NOC Technical + it_security_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="NOC Security", validators=[validate_contact]) # NOC Security + it_manager_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="NOC Manager", validators=[validate_contact]) # NOC Manager + + def clean(self): + """ + Sets the default roles + """ + cleaned_data = super(EditOrganizationForm, self).clean() + for field, roledict in DEFAULT_ROLES.items(): + if field in self.data: + value = self.data[field] + if value: + try: + contact_handle_id = int(value) + cleaned_data[field] = contact_handle_id + except ValueError: + cleaned_data[field] = value + + if not self.strict_validation and field in self._errors: + del self._errors[field] + + return cleaned_data + + def clean_organization_id(self): + organization_id = self.cleaned_data['organization_id'] + handle_id = getattr(self, 'cached_handle_id', None) + + # if it's not empty + if organization_id and self.strict_validation: + exists = nc.models.OrganizationModel.check_existent_organization_id(organization_id, handle_id) + if exists: + raise ValidationError( + _('The Organization ID %(organization_id)s already exist on the system'), + params={'organization_id': organization_id}, + ) + + return organization_id + + +class NewContactForm(forms.Form): + def __init__(self, *args, **kwargs): + super(NewContactForm, self).__init__(*args, **kwargs) + self.fields['contact_type'].choices = Dropdown.get('contact_type').as_choices() + + first_name = forms.CharField() + last_name = forms.CharField() + contact_type = forms.ChoiceField(widget=forms.widgets.Select) + name = forms.CharField(required=False, widget=forms.widgets.HiddenInput) + title = forms.CharField(required=False) + pgp_fingerprint = forms.CharField(required=False, label='PGP fingerprint') + notes = forms.CharField(widget=forms.widgets.Textarea, required=False, label="Notes") + + def clean(self): + """ + Sets name from first and second name + """ + cleaned_data = super(NewContactForm, self).clean() + # Set name to a generated id if the service is not a manually named service. + first_name = cleaned_data.get("first_name") + last_name = cleaned_data.get("last_name") + + if six.PY2: + first_name = first_name.encode('utf-8') + last_name = last_name.encode('utf-8') + + full_name = '{} {}'.format(first_name, last_name) + cleaned_data['name'] = full_name + + return cleaned_data + + +class EditContactForm(NewContactForm): + def __init__(self, *args, **kwargs): + super(EditContactForm, self).__init__(*args, **kwargs) + + # init combos + self.fields['relationship_works_for'].choices = get_node_type_tuples('Organization') + self.fields['relationship_member_of'].choices = get_node_type_tuples('Group') + self.fields['role'].choices = [('', '')] + list(Role.objects.all().values_list('handle_id', 'name')) + + relationship_works_for = relationship_field('organization', True, [validate_organization]) + relationship_member_of = relationship_field('group', True, [validate_group]) + role = forms.ChoiceField(required=False, widget=forms.widgets.Select) + + def clean(self): + """ + Check empty role, set to employee + """ + cleaned_data = super(EditContactForm, self).clean() + role_id = cleaned_data.get("role") + + if not role_id: + default_role = Role.objects.get(slug=DEFAULT_ROLE_KEY) + cleaned_data['role'] = default_role.handle_id + + # clear organization and role selects + if 'relationship_works_for' in self.data: + self.data = self.data.copy() + del self.data['relationship_works_for'] + + if 'role' in self.data: + self.data = self.data.copy() + del self.data['role'] + + +class MailPhoneContactForm(EditContactForm): + # this form adds extra data for the underlying nodes/relations + # extra email form data + email_id = forms.CharField(widget=forms.widgets.HiddenInput, required=False) + email = forms.EmailField(required=False) + email_type = forms.ChoiceField(widget=forms.widgets.Select, required=False) + + phone_id = forms.CharField(widget=forms.widgets.HiddenInput, required=False) + phone = forms.CharField(required=False) + phone_type = forms.ChoiceField(widget=forms.widgets.Select, required=False) + + role_id = forms.CharField(required=False) + + def __init__(self, *args, **kwargs): + super(MailPhoneContactForm, self).__init__(*args, **kwargs) + self.fields['email_type'].choices = email_choices() + self.fields['phone_type'].choices = phone_choices() + + +class NewProcedureForm(forms.Form): + name = forms.CharField() + description = description_field('procedure') + + +class EditProcedureForm(NewProcedureForm): + pass + + +class NewGroupForm(forms.Form): + name = forms.CharField() + description = description_field('group') + + +class EditGroupForm(NewGroupForm): + def __init__(self, *args, **kwargs): + super(EditGroupForm, self).__init__(*args, **kwargs) + self.fields['relationship_member_of'].choices = get_node_type_tuples('Contact') + + relationship_member_of = relationship_field('contact', True, [validate_contact]) + + def clean(self): + if 'relationship_member_of' in self.data: + self.data = self.data.copy() + del self.data['relationship_member_of'] + + +class NewRoleForm(forms.ModelForm): + class Meta: + model = Role + fields = ['name', 'description'] + + +class EditRoleForm(forms.ModelForm): + def save(self, commit=True): + initial_name = self.initial['name'] + role = super(EditRoleForm, self).save(False) + + if self.has_changed(): + if 'name' in self.changed_data: + nc.models.RoleRelationship.update_roles_withname(initial_name, role.name) + role.save() + + return role + + class Meta: + model = Role + fields = ['name', 'description'] + + +class PhoneForm(forms.Form): + contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Contact", validators=[validate_contact]) + name = forms.CharField() + type = forms.ChoiceField(widget=forms.widgets.Select) + + def __init__(self, *args, **kwargs): + super(PhoneForm, self).__init__(*args, **kwargs) + self.fields['contact'].choices = get_node_type_tuples('Contact') + self.fields['type'].choices = phone_choices() + + +class EmailForm(forms.Form): + contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Contact", validators=[validate_contact]) + name = forms.CharField() + type = forms.ChoiceField(widget=forms.widgets.Select) + + def __init__(self, *args, **kwargs): + super(EmailForm, self).__init__(*args, **kwargs) + self.fields['contact'].choices = get_node_type_tuples('Contact') + self.fields['type'].choices = email_choices() + + +class AddressForm(forms.Form): + organization = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Organizations", validators=[validate_organization]) + name = forms.CharField() + phone = forms.CharField(required=False) + street = forms.CharField(required=False) + postal_code = forms.CharField(required=False) + postal_area = forms.CharField(required=False) + + def __init__(self, *args, **kwargs): + super(AddressForm, self).__init__(*args, **kwargs) + self.fields['organization'].choices = get_node_type_tuples('Organization') + + class TrunkCableForm(forms.Form): trunk_base_name = forms.CharField( required=False, diff --git a/src/niweb/apps/noclook/forms/validators.py b/src/niweb/apps/noclook/forms/validators.py new file mode 100644 index 000000000..9956a4f23 --- /dev/null +++ b/src/niweb/apps/noclook/forms/validators.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from apps.noclook.models import NodeType, NodeHandle + +def validate_nodetype(value, type): + nh = NodeHandle.objects.get(handle_id=value) + + if nh.node_type != type: + raise ValidationError( + _('This field requires a %(type) but a %(badtype) was provided'), + params={'type': str(type), 'badtype': str(nh.node_type)}, + ) + +def validate_organization(value): + type_str = 'Organization' + type = NodeType.objects.get_or_create( + type=type_str, slug=type_str.lower())[0] + + return validate_nodetype(value, type) + +def validate_contact(value): + type_str = 'Contact' + type = NodeType.objects.get_or_create( + type=type_str, slug=type_str.lower())[0] + + return validate_nodetype(value, type) + +def validate_group(value): + type_str = 'Group' + type = NodeType.objects.get_or_create( + type=type_str, slug=type_str.lower())[0] + + return validate_nodetype(value, type) + +def validate_procedure(value): + type_str = 'Procedure' + type = NodeType.objects.get_or_create( + type=type_str, slug=type_str.lower())[0] + + return validate_nodetype(value, type) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 9f5639c6f..b59de698e 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -14,13 +14,15 @@ from datetime import datetime, timedelta from actstream.models import action_object_stream, target_stream from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.utils import six import csv import xlwt import re import os from neo4j.v1.types import Node -from .models import NodeHandle, NodeType +from .models import NodeHandle, NodeType, RoleGroup, Role,\ + DEFAULT_ROLEGROUP_NAME, DEFAULT_ROLES from . import activitylog import norduniclient as nc from norduniclient.exceptions import UniqueNodeError, NodeNotFound @@ -116,7 +118,7 @@ def form_update_node(user, handle_id, form, property_keys=None): meta_fields = ['relationship_location', 'relationship_end_a', 'relationship_end_b', 'relationship_parent', 'relationship_provider', 'relationship_end_user', 'relationship_customer', 'relationship_depends_on', 'relationship_user', 'relationship_owner', 'relationship_located_in', 'relationship_ports', - 'services_checked', 'relationship_responsible_for', 'relationship_connected_to'] + 'services_checked', 'relationship_responsible_for', 'relationship_connected_to', 'role_name'] nh, node = get_nh_node(handle_id) if not property_keys: property_keys = [] @@ -220,7 +222,7 @@ def create_unique_node_handle(user, node_name, slug, node_meta_type): def set_noclook_auto_manage(item, auto_manage): """ - Sets the node or relationship noclook_auto_manage flag to True or False. + Sets the node or relationship noclook_auto_manage flag to True or False. Also sets the noclook_last_seen flag to now. :param item: norduclient model @@ -239,11 +241,11 @@ def set_noclook_auto_manage(item, auto_manage): relationship = nc.get_relationship_model(nc.graphdb.manager, item.id) relationship.data.update(auto_manage_data) nc.set_relationship_properties(nc.graphdb.manager, relationship.id, relationship.data) - + def update_noclook_auto_manage(item): """ - Updates the noclook_auto_manage and noclook_last_seen properties. If + Updates the noclook_auto_manage and noclook_last_seen properties. If noclook_auto_manage is not set, it is set to True. :param item: norduclient model @@ -882,6 +884,239 @@ def attachment_content(attachment): content = f.read() return content +def set_parent_of(user, node, parent_org_id): + """ + :param user: Django user + :param node: norduniclient model + :param child_org_id: unique id + :return: norduniclient model, boolean + """ + result = node.set_parent(parent_org_id) + relationship_id = result.get('Parent_of')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Parent_of')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) + return relationship, created + +def set_uses_a(user, node, procedure_id): + """ + :param user: Django user + :param node: norduniclient model + :param procedure_id: unique id + :return: norduniclient model, boolean + """ + result = node.add_procedure(procedure_id) + relationship_id = result.get('Uses_a')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Uses_a')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) + return relationship, created + +def set_works_for(user, node, organization_id, role_name): + """ + :param user: Django user + :param node: norduniclient model + :param organization_id: unique id + :param role_name: string for role name + :return: norduniclient model, boolean + """ + contact_id = node.handle_id + relationship = nc.models.RoleRelationship.link_contact_organization(contact_id, organization_id, role_name) + + if not relationship: + relationship = nc.models.RoleRelationship(nc.graphdb.manager) + relationship.load_from_nodes(contact_id, organization_id) + + node = node.reload() + created = node.outgoing.get('Works_for')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) + return relationship, created + +def set_member_of(user, node, group_id): + """ + :param user: Django user + :param node: norduniclient model + :param group_id: unique id + :return: norduniclient model, boolean + """ + result = node.add_group(group_id) + relationship_id = result.get('Member_of')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Member_of')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) + return relationship, created + + +def set_of_member(user, node, contact_id): + """ + :param user: Django user + :param node: norduniclient model + :param contact_id: unique id + :return: norduniclient model, boolean + """ + result = node.add_member(contact_id) + relationship_id = result.get('Member_of')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Member_of')[0].get('created') + + if created: + activitylog.create_relationship(user, relationship) + + return relationship, created + + +def link_contact_role_for_organization(user, node, contact_handle_id, role, \ + relationship_id=None): + """ + :param user: Django user + :param node: norduniclient organization model + :param contact_handle_id: contact's handle_id + :param role: the selected role + :return: contact + """ + relationship = None + + if not relationship_id: + relationship = nc.models.RoleRelationship.link_contact_organization( + contact_handle_id, + node.handle_id, + role.name + ) + else: + relationship = nc.models.RoleRelationship.update_contact_organization( + contact_handle_id, + node.handle_id, + role.name, + relationship_id + ) + + if not relationship: + relationship = nc.models.RoleRelationship(nc.graphdb.manager) + relationship.load_from_nodes(contact_handle_id, node.handle_id) + + node = node.reload() + created = False + works_for = node.incoming.get('Works_for') + if works_for: + created = works_for[0].get('created') + + if created: + activitylog.create_relationship(user, relationship) + + contact = NodeHandle.objects.get(handle_id=contact_handle_id) + + return contact, relationship + + +def unlink_contact_with_role_from_org(user, organization, role): + """ + :param user: Django user + :param organization: norduniclient organization model + :param role: role model + """ + relationship = nc.models.RoleRelationship.get_role_relation_from_organization( + organization.handle_id, + role.name, + ) + + if relationship: + activitylog.delete_relationship(user, relationship) + relationship.delete() + +def unlink_contact_and_role_from_org(user, organization, contact_id, role): + relationship = nc.models.RoleRelationship.get_role_relation_from_contact_organization( + organization.handle_id, + role.name, + contact_id + ) + + if relationship: + activitylog.delete_relationship(user, relationship) + nc.models.RoleRelationship.unlink_contact_with_role_organization( + contact_id, + organization.handle_id, + role.name, + ) + + +def get_contact_for_orgrole(organization_id, role): + """ + :param organization_id: Organization's handle_id + :param role_name: Role object + """ + contact_handle_id = nc.models.RoleRelationship.get_contact_with_role_in_organization( + organization_id, + role.name, + ) + + if contact_handle_id: + contact = NodeHandle.objects.get(handle_id=contact_handle_id) + + return contact + + +def add_phone_contact(user, phone, contact_id): + """ + :param user: Django user + :param phone: norduniclient model (phone) + :param contact_id: contact id to associate to the phone instance + :return: norduniclient model, boolean + """ + contact = NodeHandle.objects.get(handle_id=contact_id) + result = contact.get_node().add_phone(phone.handle_id) + + relationship_id = result.get('Has_phone')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Has_phone')[0].get('created') + + if created: + activitylog.create_relationship(user, relationship) + + return relationship, created + + + +def add_email_contact(user, email, contact_id): + """ + :param user: Django user + :param email: norduniclient model (email) + :param contact_id: contact id to associate to the email instance + :return: norduniclient model, boolean + """ + contact = NodeHandle.objects.get(handle_id=contact_id) + result = contact.get_node().add_email(email.handle_id) + + relationship_id = result.get('Has_email')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Has_email')[0].get('created') + + if created: + activitylog.create_relationship(user, relationship) + + return relationship, created + +def add_address_organization(user, address, organization_id): + """ + :param user: Django user + :param address: norduniclient model (address) + :param organization_id: organization id to associate to the address instance + :return: norduniclient model, boolean + """ + organization = NodeHandle.objects.get(handle_id=organization_id) + result = organization.get_node().add_address(address.handle_id) + + relationship_id = result.get('Has_address')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Has_address')[0].get('created') + + if created: + activitylog.create_relationship(user, relationship) + + return relationship, created def relationship_to_str(relationship): """ diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py new file mode 100644 index 000000000..5852274d7 --- /dev/null +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -0,0 +1,502 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook import helpers +from apps.noclook.models import User, NodeType, NodeHandle, Role, Dropdown, NODE_META_TYPE_CHOICES, DEFAULT_ROLE_KEY +from apps.nerds.lib.consumer_util import get_user +from django.core.management.base import BaseCommand, CommandError +from pprint import pprint +from time import sleep + +import argparse +import norduniclient as nc +import logging +import traceback +import sys + +logger = logging.getLogger('noclook.management.csvimport') + +class Command(BaseCommand): + help = 'Imports csv files from Salesforce' + new_types = ['Organization', 'Procedure', 'Contact', 'Group', 'Role'] + + def add_arguments(self, parser): + parser.add_argument("-o", "--organizations", help="organizations CSV file", + type=argparse.FileType('r')) + parser.add_argument("-c", "--contacts", help="contacts CSV file", + type=argparse.FileType('r')) + parser.add_argument("-s", "--secroles", help="security roles CSV file", + type=argparse.FileType('r')) + parser.add_argument("-f", "--fixroles", + action='store_true', help="regenerate roles in intermediate setup") + parser.add_argument("-m", "--emailphones", + action='store_true', help="regenerate emails and phones to separate models") + parser.add_argument("-a", "--addressfix", + action='store_true', help="regenerate organizations' address to the new SRI") + parser.add_argument("-w", "--movewebsite", + action='store_true', help="move organizations' website back from address") + parser.add_argument("-r", "--reorgprops", + action='store_true', help="rename organization properties") + parser.add_argument('-d', "--delimiter", nargs='?', default=';', + help='Delimiter to use use. Default ";".') + + def handle(self, *args, **options): + # check if the fixroles option has been called, do it and exit + if options['fixroles']: + self.fix_roles() + return + + # check if the emailphones option has been called, do it and exit + if options['emailphones']: + self.fix_emails_phones() + return + + # check if the addressfix option has been called, do it and exit + if options['addressfix']: + self.fix_organizations_address() + return + + # check if the addressfix option has been called, do it and exit + if options['movewebsite']: + self.fix_website_field() + return + + # check if the addressfix option has been called, do it and exit + if options['reorgprops']: + self.fix_organizations_fields() + return + + relation_meta_type = 'Relation' + logical_meta_type = 'Logical' + + self.delimiter = ';' + if options['delimiter']: + self.delimiter = options['delimiter'] + + ## (We'll use handle_id on to get the node on cql code) + # check if new types exists + if options['verbosity'] > 0: + self.stdout.write('Checking if the types are already in the db') + + for type in self.new_types: + dbtype = NodeType.objects.filter(type=type) + + if not dbtype: + dbtype = NodeType( + type=type, + slug=type.lower(), + ) + dbtype.save() + else: + dbtype = dbtype.first() + + total_lines = 0 + + csv_organizations = None + csv_contacts = None + csv_secroles = None + self.user = get_user() + + # IMPORT ORGANIZATIONS + if options['organizations']: + # py: count lines + csv_organizations = options['organizations'] + org_lines = self.count_lines(csv_organizations) + + if options['verbosity'] > 0: + self.stdout.write('Importing {} Organizations from file "{}"'\ + .format(org_lines, csv_organizations.name)) + + total_lines = total_lines + org_lines + + # IMPORT CONTACTS AND ROLES + if options['contacts']: + # py: count lines + csv_contacts = options['contacts'] + con_lines = self.count_lines(csv_contacts) + + if options['verbosity'] > 0: + self.stdout.write('Importing {} Contacts from file "{}"'\ + .format(con_lines, csv_contacts.name)) + + total_lines = total_lines + con_lines + + # IMPORT SECURITY ROLES + if options['secroles']: + # py: count lines + csv_secroles = options['secroles'] + srl_lines = self.count_lines(csv_secroles) + + if options['verbosity'] > 0: + self.stdout.write('Importing {} Security Roles from file "{}"'\ + .format(srl_lines, csv_secroles.name)) + + total_lines = total_lines + srl_lines + + imported_lines = 0 + # print progress bar + if options['verbosity'] > 0: + self.printProgressBar(imported_lines, total_lines) + + # process organizations + if options['organizations']: + # contact + node_type = NodeType.objects.filter(type=self.new_types[0]).first() + csv_organizations = options['organizations'] + node_list = self.read_csv(csv_organizations, delim=self.delimiter) + + for node in node_list: + account_name = node['account_name'] + + # dj: organization exist?: create or get (using just the name) + new_organization = NodeHandle.objects.get_or_create( + node_name = account_name, + node_type = node_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, + )[0] + + # n4: add attributes + graph_node = new_organization.get_node() + + for key in node.keys(): + if key not in ['account_name', 'parent_account'] and node[key]: + graph_node.add_property(key, node[key]) + + # dj: if parent organization: create or get (using just the name) + if key == 'parent_account' and node['parent_account']: + parent_org_name = node['parent_account'] + + parent_organization = NodeHandle.objects.get_or_create( + node_name = parent_org_name, + node_type = node_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, + )[0] + + parent_node = parent_organization.get_node() + + # n4: add relation between org and parent_org + graph_node.set_parent(parent_organization.handle_id) + + # Print iterations progress + if options['verbosity'] > 0: + imported_lines = imported_lines + 1 + self.printProgressBar(imported_lines, total_lines) + + csv_organizations.close() + + # process contacts + if options['contacts']: + node_type = NodeType.objects.filter(type=self.new_types[2]).first() # contact + node_list = self.read_csv(csv_contacts, delim=self.delimiter) + + for node in node_list: + full_name = '{} {}'.format( + node['first_name'], + node['last_name'] + ) + + # dj: contact exists?: create or get + new_contact = NodeHandle.objects.get_or_create( + node_name = full_name, + node_type = node_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, + )[0] + + # n4: add attributes + graph_node = new_contact.get_node() + + for key in node.keys(): + if key not in ['node_type', 'contact_role', 'name', 'account_name', 'salutation'] and node[key]: + graph_node.add_property(key, node[key]) + + # dj: organization exist?: create or get + organization_name = node.get('account_name', None) + + if organization_name: + org_type = NodeType.objects.filter(type=self.new_types[0]).first() # organization + + new_org = NodeHandle.objects.get_or_create( + node_name = organization_name, + node_type = org_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, + )[0] + + # add role relatioship, use employee role if empty + role_name = node['contact_role'] + + if role_name: + role = Role.objects.get_or_create(name = role_name)[0] + else: + role = Role.objects.get(slug=DEFAULT_ROLE_KEY) + + nc.models.RoleRelationship.link_contact_organization( + new_contact.handle_id, + new_org.handle_id, + role.name + ) + + + # Print iterations progress + if options['verbosity'] > 0: + imported_lines = imported_lines + 1 + self.printProgressBar(imported_lines, total_lines) + + csv_contacts.close() + + # process security roles + if options['secroles']: + orga_type = NodeType.objects.filter(type=self.new_types[0]).first() # organization + cont_type = NodeType.objects.filter(type=self.new_types[2]).first() # contact + role_type = NodeType.objects.filter(type=self.new_types[4]).first() # role + node_list = self.read_csv(csv_secroles, delim=self.delimiter) + + for node in node_list: + # create or get nodes + organization = NodeHandle.objects.get_or_create( + node_name = node['organisation'], + node_type = orga_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, + )[0] + + contact = NodeHandle.objects.get_or_create( + node_name = node['contact'], + node_type = cont_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, + )[0] + + role_name = node['role'] + role = Role.objects.get_or_create(name = role_name)[0] + + nc.models.RoleRelationship.link_contact_organization( + contact.handle_id, + organization.handle_id, + role.name + ) + + csv_secroles.close() + + def fix_roles(self): + ''' + This method is provided to update an existing setup into the new + role database representation in both databases. It runs over the + neo4j db and creates the existent roles into the relational db + ''' + # get all unique role string in all Works_for relation in neo4j db + role_names = nc.models.RoleRelationship.get_all_role_names() + + # create a role for each of them + for role_name in role_names: + if Role.objects.filter(name=role_name): + role = Role.objects.filter(name=role_name).first() + elif role_name != '': + role = Role(name=role_name) + role.save() + + def fix_emails_phones(self): + self.user = get_user() + + work_type_str = 'work' + personal_type_str = 'personal' + logical_meta_type = 'Logical' + + old_email_fields = { 'email': work_type_str, 'other_email': personal_type_str } + old_phone_fields = { 'phone': work_type_str, 'mobile': personal_type_str } + + # check that the options are available + phone_type_val = Dropdown.objects.get(name="phone_type").as_values(False) + email_type_val = Dropdown.objects.get(name="email_type").as_values(False) + + if not ((work_type_str in phone_type_val) and\ + (personal_type_str in phone_type_val) and\ + (personal_type_str in email_type_val) and\ + (personal_type_str in email_type_val)): + raise Exception('Work/Personal values are not available for the \ + Email/phone dropdown types') + + contact_type = NodeType.objects.get_or_create(type='Contact', slug='contact')[0] # contact + email_type = NodeType.objects.get_or_create(type='Email', slug='email', hidden=True)[0] # contact + phone_type = NodeType.objects.get_or_create(type='Phone', slug='phone', hidden=True)[0] # contact + all_contacts = NodeHandle.objects.filter(node_type=contact_type) + + for contact in all_contacts: + contact_node = contact.get_node() + + for old_phone_field, assigned_type in old_phone_fields.items(): + # phones + if old_phone_field in contact_node.data: + old_phone_value = contact_node.data.get(old_phone_field) + new_phone = NodeHandle.objects.get_or_create( + node_name=old_phone_value, + node_type=phone_type, + node_meta_type=logical_meta_type, + creator=self.user, + modifier=self.user, + )[0] + contact_node.add_phone(new_phone.handle_id) + contact_node.remove_property(old_phone_field) + new_phone.get_node().add_property('type', assigned_type) + + for old_email_field, assigned_type in old_email_fields.items(): + # emails + if old_email_field in contact_node.data: + old_email_value = contact_node.data.get(old_email_field) + new_email = NodeHandle.objects.get_or_create( + node_name=old_email_value, + node_type=email_type, + node_meta_type=logical_meta_type, + creator=self.user, + modifier=self.user, + )[0] + contact_node.add_email(new_email.handle_id) + contact_node.remove_property(old_email_field) + new_email.get_node().add_property('type', assigned_type) + + def fix_organizations_address(self): + self.user = get_user() + address_type = NodeType.objects.get_or_create(type='Address', slug='address', hidden=True)[0] # address + organization_type = NodeType.objects.get_or_create(type='Organization', slug='organization')[0] # organization + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + logical_meta_type = 'Logical' + + phone_field = 'phone' + + for organization in all_organizations: + organization_node = organization.get_node() + address_name = 'Address: {}'.format(organization.node_name) + + old_phone = organization_node.data.get(phone_field, None) + + if old_phone: + # create an Address and asociate it to the Organization + new_address = NodeHandle.objects.get_or_create( + node_name=address_name, + node_type=address_type, + node_meta_type=logical_meta_type, + creator=self.user, + modifier=self.user, + )[0] + + new_address.get_node().add_property(phone_field, old_phone) + organization_node.remove_property(phone_field) + + organization_node.add_address(new_address.handle_id) + + def fix_website_field(self): + self.user = get_user() + address_type = NodeType.objects.get_or_create(type='Address', slug='address', hidden=True)[0] # address + organization_type = NodeType.objects.get_or_create(type='Organization', slug='organization')[0] # organization + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + + website_field = 'website' + + for organization in all_organizations: + orgnode = organization.get_node() + relations = orgnode.get_outgoing_relations() + address_relations = relations.get('Has_address', None) + if address_relations: + for rel in address_relations: + address_end = rel['relationship'].end_node + + if website_field in address_end._properties: + website_str = address_end._properties[website_field] + handle_id = address_end._properties['handle_id'] + address_node = NodeHandle.objects.get(handle_id=handle_id).get_node() + + # remove if it already exists + orgnode.remove_property(website_field) + orgnode.add_property(website_field, website_str) + + # remove value in address_node + address_node.remove_property(website_field) + + def fix_organizations_fields(self): + self.user = get_user() + organization_type = NodeType.objects.get_or_create(type='Organization', slug='organization')[0] # organization + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + + old_field1 = 'customer_id' + new_field1 = 'organization_id' + + for organization in all_organizations: + orgnode = organization.get_node() + org_id_val = orgnode.data.get(old_field1, None) + if org_id_val: + orgnode.remove_property(old_field1) + orgnode.add_property(new_field1, org_id_val) + + def count_lines(self, file): + ''' + Counts lines in a file + ''' + num_lines = 0 + try: + num_lines = sum(1 for line in file) + logger.warn(num_lines) + num_lines = num_lines - 1 # remove header + + file.seek(0) # reset to start line + except IOError as e: + self.stderr.write("I/O error({0}): {1}".format(e.errno, e.strerror)) + except: #handle other exceptions such as attribute errors + self.stderr.write("Unexpected error:\n" + traceback.format_exc()) + + return num_lines + + def normalize_whitespace(self, text): + ''' + Remove redundant whitespace from a string. + ''' + text = text.replace('"', '').replace("'", '') + return ' '.join(text.split()) + + def read_csv(self, f, delim=';', empty_keys=True): + ''' + Read csv method (from csv_producer) + ''' + node_list = [] + key_list = self.normalize_whitespace(f.readline()).split(delim) + line = self.normalize_whitespace(f.readline()) + while line: + value_list = line.split(delim) + tmp = {} + for i in range(0, len(key_list)): + key = self.normalize_whitespace(key_list[i].replace(' ','_').lower()) + value = self.normalize_whitespace(value_list[i]) + if value or empty_keys: + tmp[key] = value + node_list.append(tmp) + line = self.normalize_whitespace(f.readline()) + return node_list + + def printProgressBar (self, iteration, total, prefix = 'Progress', suffix = 'Complete', decimals = 1, length = 100, fill = '█'): + """ + Call in a loop to create terminal progress bar + (from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console) + @params: + iteration - Required : current iteration (Int) + total - Required : total iterations (Int) + prefix - Optional : prefix string (Str) + suffix - Optional : suffix string (Str) + decimals - Optional : positive number of decimals in percent complete (Int) + length - Optional : character length of bar (Int) + fill - Optional : bar fill character (Str) + """ + percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + filledLength = int(length * iteration // total) + bar = fill * filledLength + '-' * (length - filledLength) + self.stdout.write('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), ending = '\r') + # Print New Line on Complete + if iteration == total: + self.stdout.write('') diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py new file mode 100644 index 000000000..72f27b7ea --- /dev/null +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook.models import * +from apps.noclook.tests.stressload.data_generator import NetworkFakeDataGenerator +from django.core.management.base import BaseCommand, CommandError +from django.conf import settings + +logger = logging.getLogger('noclook.management.datafaker') + +class Command(BaseCommand): + help = 'Create fake data for the Network module' + generated_types = [ + 'Customer', 'End User', 'Site Owner', 'Provider', 'Peering Group', 'Peering Partner', + 'Cable', 'Port', 'Host', 'Router', 'Switch'] + + option_organizations = 'organizations' + option_equipment = 'equipmentcables' + option_deleteall = 'deleteall' + cmd_name = 'datafaker' + + def add_arguments(self, parser): + parser.add_argument("--{}".format(self.option_organizations), + help="Create organization nodes", type=int, default=0) + parser.add_argument("--{}".format(self.option_equipment), + help="Create equipment and cables nodes", type=int, default=0) + parser.add_argument("-d", "--{}".format(self.option_deleteall), action='store_true', + help="BEWARE: This command deletes information in the database") + + def handle(self, *args, **options): + if options[self.option_deleteall]: + self.delete_network_nodes() + return + + if options[self.option_organizations]: + numnodes = options[self.option_organizations] + if numnodes > 0: + self.stdout\ + .write('Forging fake organizations: {} for each subtype:'\ + .format(numnodes)) + self.create_organizations(numnodes) + + if options[self.option_equipment]: + numnodes = options[self.option_equipment] + if numnodes > 0: + self.stdout\ + .write('Forging fake equipement & cables: {} for each subtype:'\ + .format(numnodes)) + self.create_equipment_cables(numnodes) + + return + + def create_entities(self, numnodes, create_funcs): + total_nodes = numnodes * len(create_funcs) + created_nodes = 0 + self.printProgressBar(0, total_nodes) + + for create_func in create_funcs: + for i in range(numnodes): + # dirty hack to get rid of accidental unscaped strings + loop_lock = True + safe_tries = 5 + + while loop_lock and safe_tries > 0: + try: + node = create_func() + loop_lock = False + except: + safe_tries = safe_tries - 1 + + created_nodes = created_nodes + 1 + self.printProgressBar(created_nodes, total_nodes) + + NetworkFakeDataGenerator.clean_rogue_nodetype() + + + def create_organizations(self, numnodes): + generator = NetworkFakeDataGenerator() + + create_funcs = [ + generator.create_customer, + generator.create_end_user, + generator.create_peering_partner, + generator.create_peering_group, + generator.create_site_owner, + ] + + self.create_entities(numnodes, create_funcs) + + def create_equipment_cables(self, numnodes): + generator = NetworkFakeDataGenerator() + + create_funcs = [ + generator.create_cable, + generator.create_host, + generator.create_router, + generator.create_switch, + ] + + self.create_entities(numnodes, create_funcs) + + def delete_network_nodes(self): + if settings.DEBUG: # guard against accidental deletion on the wrong environment + delete_types = self.generated_types + + total_nodes = 0 + + for delete_type in delete_types: + total_nodes = total_nodes + self.get_node_num(delete_type) + + if total_nodes > 0: + self.stdout.write('Delete {} nodes:'.format(total_nodes)) + deleted_nodes = 0 + + self.printProgressBar(deleted_nodes, total_nodes) + + for delete_type in delete_types: + deleted_nodes = self.delete_type(delete_type, deleted_nodes, total_nodes) + + # delete node types + for delete_type in delete_types: + NodeType.objects.filter(type=delete_type).delete() + + def get_nodetype(self, type_name): + return NetworkFakeDataGenerator.get_nodetype(type_name) + + def get_node_num(self, type_name): + node_type = self.get_nodetype(type_name) + node_num = NodeHandle.objects.filter(node_type=node_type).count() + + return node_num + + def delete_type(self, type_name, deleted_nodes, total_nodes): + node_type = self.get_nodetype(type_name) + node_num = self.get_node_num(type_name) + + [x.delete() for x in NodeHandle.objects.filter(node_type=node_type)] + deleted_nodes = deleted_nodes + node_num + + if node_num > 0: + self.printProgressBar(deleted_nodes, total_nodes) + + return deleted_nodes + + def printProgressBar (self, iteration, total, prefix = 'Progress', suffix = 'Complete', decimals = 1, length = 100, fill = '█'): + """ + Call in a loop to create terminal progress bar + (from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console) + @params: + iteration - Required : current iteration (Int) + total - Required : total iterations (Int) + prefix - Optional : prefix string (Str) + suffix - Optional : suffix string (Str) + decimals - Optional : positive number of decimals in percent complete (Int) + length - Optional : character length of bar (Int) + fill - Optional : bar fill character (Str) + """ + percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + filledLength = int(length * iteration // total) + bar = fill * filledLength + '-' * (length - filledLength) + self.stdout.write('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), ending = '\r') + # Print New Line on Complete + if iteration == total: + self.stdout.write('') diff --git a/src/niweb/apps/noclook/management/commands/datafixer.py b/src/niweb/apps/noclook/management/commands/datafixer.py new file mode 100644 index 000000000..aec4ea2d3 --- /dev/null +++ b/src/niweb/apps/noclook/management/commands/datafixer.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook import helpers +from apps.noclook.models import User, NodeType, NodeHandle, Dropdown, Choice +from apps.nerds.lib.consumer_util import get_user +from django.core.management.base import BaseCommand, CommandError +from pprint import pprint +from time import sleep + +import argparse +import norduniclient as nc +import logging +import traceback +import sys + +logger = logging.getLogger('noclook.management.datafixer') + +class Command(BaseCommand): + help = 'Fix local data' + + def add_arguments(self, parser): + parser.add_argument("-x", "--fixtestdata", + action='store_true', + help="fix the test data to avoid conflicts with the front") + + def handle(self, *args, **options): + if options['fixtestdata']: + self.fix_test_data() + return + + def fix_test_data(self): + self.user = get_user() + + organization_type = NodeType.objects.get_or_create( + type='Organization', slug='organization')[0] # organization + + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + + + total_lines = all_organizations.count() + current_line = 0 + + org_types_drop = Dropdown.objects.get(name='organization_types') + org_types = Choice.objects.filter(dropdown=org_types_drop) + + first_field = 'type' + second_field = 'organization_id' + organization_ids = {} + + # get all the possible organizations id + for organization in all_organizations: + orgnode = organization.get_node() + + organization_id = orgnode.data.get(second_field, None) + + if organization_id not in organization_ids: + organization_ids[organization_id] = [organization.handle_id] + else: + organization_ids[organization_id].append(organization.handle_id) + + total_lines = total_lines + len(organization_ids) + + # fix organization type + for organization in all_organizations: + self.printProgressBar(current_line, total_lines) + + orgnode = organization.get_node() + org_type = orgnode.data.get(first_field, None) + + if org_type: + correct_type = org_types.filter(value=org_type).exists() + + if not correct_type: + # get the first value as default + selected_type = org_types.first() + + # check if exist choice with that name + if org_types.filter(name__icontains=org_type).exists(): + selected_type = org_types.filter(name__icontains=org_type).first() + elif org_types.filter(value__icontains=org_type).exists(): + # if not, check if exists with that value + selected_type = org_types.filter(value__icontains=org_type).first() + + orgnode.remove_property(first_field) + orgnode.add_property(first_field, selected_type.value) + + current_line = current_line + 1 + + for organization_id, org_handle_ids in organization_ids.items(): + self.printProgressBar(current_line, total_lines) + + # check if the array lenght is more than one + if len(org_handle_ids) > 1: + for org_handle_id in org_handle_ids: + # if it is so, iterate and check if the uppercased name of the + # organization is already set as organization_id + organization = NodeHandle.objects.get(handle_id=org_handle_id) + orgnode = organization.get_node() + possible_org_id = organization.node_name.upper() + count = self.checkOrgInUse(possible_org_id) + + # if it's alredy in use, append handle_id + if count > 0: + possible_org_id = '{}_{}'.format( + possible_org_id, organization.handle_id + ) + + orgnode.remove_property(second_field) + orgnode.add_property(second_field, possible_org_id) + + current_line = current_line + 1 + + self.printProgressBar(current_line, total_lines) + + def checkOrgInUse(self, organization_id): + q = """ + MATCH (o:Organization) WHERE o.organization_id = '{organization_id}' + RETURN COUNT(DISTINCT o.organization_id) AS orgs; + """.format(organization_id=organization_id) + res = nc.query_to_dict(nc.graphdb.manager, q) + + return res['orgs'] + + def printProgressBar (self, iteration, total, prefix = 'Progress', suffix = 'Complete', decimals = 1, length = 100, fill = '█'): + """ + Call in a loop to create terminal progress bar + (from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console) + @params: + iteration - Required : current iteration (Int) + total - Required : total iterations (Int) + prefix - Optional : prefix string (Str) + suffix - Optional : suffix string (Str) + decimals - Optional : positive number of decimals in percent complete (Int) + length - Optional : character length of bar (Int) + fill - Optional : bar fill character (Str) + """ + percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + filledLength = int(length * iteration // total) + bar = fill * filledLength + '-' * (length - filledLength) + self.stdout.write('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), ending = '\r') + # Print New Line on Complete + if iteration == total: + self.stdout.write('') diff --git a/src/niweb/apps/noclook/middleware.py b/src/niweb/apps/noclook/middleware.py new file mode 100644 index 000000000..82b0b4340 --- /dev/null +++ b/src/niweb/apps/noclook/middleware.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from datetime import datetime +from django.conf import settings +from django.contrib.auth.middleware import get_user +from django.contrib.auth.models import User +from django.contrib.sessions.models import Session +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import redirect +from django.utils.cache import patch_vary_headers +from django.utils.functional import SimpleLazyObject +from django.utils.http import cookie_date +from graphql_jwt import signals +from graphql_jwt.settings import jwt_settings +from graphql_jwt.shortcuts import get_token, get_user_by_token +from graphql_jwt.refresh_token.shortcuts import refresh_token_lazy +from graphql_jwt.refresh_token.signals import refresh_token_rotated +from graphql_jwt.utils import get_credentials, get_payload +from graphql_jwt.exceptions import JSONWebTokenError, JSONWebTokenExpired +from importlib import import_module + +import time +import logging + +logger = logging.getLogger(__name__) + +def token_is_expired(token): + ret = False + + try: + get_payload(token) + except JSONWebTokenError: + ret = True + except JSONWebTokenExpired: + ret = True + + return ret + + +def get_user_from_session_key(session_key): + session = Session.objects.get(session_key=session_key) + session_data = session.get_decoded() + uid = session_data.get('_auth_user_id') + user = User.objects.get(id=uid) + + return user + + +class SRIJWTAuthMiddleware(object): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + session_created = False + has_token = False + + # add user + request.user = SimpleLazyObject(lambda: get_user(request)) + token = get_credentials(request) + + if token is not None and token != '' and token != 'None' and \ + not token_is_expired(token): + user = get_user_by_token(token, request) + request.user = user + has_token = True + + # add session + if not hasattr(request, 'session'): + session_engine = import_module(settings.SESSION_ENGINE) + session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME) + request.session = session_engine.SessionStore(session_key) + request.session.save() + session_created = True + + + max_age = request.session.get_expiry_age() + expires_time = time.time() + max_age + anti_expires_time = cookie_date(time.time() - max_age) + cookie_expires = cookie_date(expires_time) + + if request.session.get_expire_at_browser_close(): + max_age = None + cookie_expires = None + + if token and token_is_expired(token): + cookie_token = request.COOKIES.get(jwt_settings.JWT_COOKIE_NAME) + session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME) + + if cookie_token and cookie_token != '""': + try: + user = get_user_from_session_key(session_key) + request.user = user + refresh_token_lazy(request.user) + token = get_token(request.user) + refresh_token_rotated.send( + sender=SRIJWTAuthMiddleware, + request=request, + refresh_token=self, + ) + signals.token_issued.send( + sender=SRIJWTAuthMiddleware, request=request, user=request.user) + except ObjectDoesNotExist: + ## fallback solution + response = redirect(request.get_full_path()) + response.set_cookie( + jwt_settings.JWT_COOKIE_NAME, + '', + domain=settings.COOKIE_DOMAIN, + expires=anti_expires_time, + httponly=False, + secure=jwt_settings.JWT_COOKIE_SECURE, + ) + response.delete_cookie(jwt_settings.JWT_COOKIE_NAME) + patch_vary_headers(response, ('Cookie',)) + + return response + + # process response with inner middleware + response = self.get_response(request) + + if request.user.is_authenticated and not has_token: + token = get_token(request.user) + signals.token_issued.send( + sender=SRIJWTAuthMiddleware, request=request, user=request.user) + + # if token is expired, refresh it + if token_is_expired(token): + refresh_token_lazy(request.user) + token = get_token(request.user) + refresh_token_rotated.send( + sender=SRIJWTAuthMiddleware, + request=request, + refresh_token=self, + ) + signals.token_issued.send( + sender=SRIJWTAuthMiddleware, request=request, user=request.user) + + #expires = datetime.utcnow() + jwt_settings.JWT_EXPIRATION_DELTA + response.set_cookie( + jwt_settings.JWT_COOKIE_NAME, + token, + domain=settings.COOKIE_DOMAIN, + max_age=max_age, + expires=cookie_expires, + httponly=False, + secure=jwt_settings.JWT_COOKIE_SECURE, + ) + patch_vary_headers(response, ('Cookie',)) + + accessed = request.session.accessed + modified = request.session.modified + empty = request.session.is_empty() + + # we'll force the session cookie creation if: + # * we have a valid token but we didn't have a session for the user + # * the session was not created because the user is logged in + create_session_cookie = token and session_created \ + or token and not request.user.is_authenticated + + if settings.SESSION_COOKIE_NAME in request.COOKIES and empty: + response.delete_cookie( + settings.SESSION_COOKIE_NAME, + path=settings.SESSION_COOKIE_PATH, + domain=settings.SESSION_COOKIE_DOMAIN, + ) + response.delete_cookie(jwt_settings.JWT_COOKIE_NAME) + patch_vary_headers(response, ('Cookie',)) + else: + if accessed: + patch_vary_headers(response, ('Cookie',)) + + try: + SESSION_SAVE_EVERY_REQUEST = settings.SESSION_SAVE_EVERY_REQUEST + except AttributeError: + SESSION_SAVE_EVERY_REQUEST = None + + if (modified or SESSION_SAVE_EVERY_REQUEST) and not empty or create_session_cookie: + # Save the session data and refresh the client cookie. + # Skip session save for 500 responses, refs #3881. + if response.status_code != 500: + try: + request.session.save() + except UpdateError: + raise SuspiciousOperation( + "The request's session was deleted before the " + "request completed. The user may have logged " + "out in a concurrent request, for example." + ) + response.set_cookie( + settings.SESSION_COOKIE_NAME, + request.session.session_key, max_age=max_age, + expires=cookie_expires, domain=settings.SESSION_COOKIE_DOMAIN, + path=settings.SESSION_COOKIE_PATH, + secure=settings.SESSION_COOKIE_SECURE or None, + httponly=settings.SESSION_COOKIE_HTTPONLY or None, + ) + + return response diff --git a/src/niweb/apps/noclook/migrations/0007_auto_20190410_1341.py b/src/niweb/apps/noclook/migrations/0007_auto_20190410_1341.py new file mode 100644 index 000000000..9b4513d34 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0007_auto_20190410_1341.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-10 13:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0006_default_dropdowns'), + ] + + operations = [ + migrations.AlterField( + model_name='nodehandle', + name='node_meta_type', + field=models.CharField(choices=[('Physical', 'Physical'), ('Logical', 'Logical'), ('Relation', 'Relation'), ('Location', 'Location')], max_length=255), + ), + migrations.AlterField( + model_name='nodetype', + name='hidden', + field=models.BooleanField(default=False, help_text='Hide from menus'), + ), + migrations.AlterField( + model_name='nodetype', + name='slug', + field=models.SlugField(help_text='Suggested value #automatically generated from type. Must be unique.', unique=True), + ), + migrations.AlterField( + model_name='uniqueidgenerator', + name='base_id_length', + field=models.IntegerField(default=0, help_text='Base id will be filled with leading zeros to this length if zfill is checked.'), + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0008_role.py b/src/niweb/apps/noclook/migrations/0008_role.py new file mode 100644 index 000000000..3e21a927c --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0008_role.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-07-17 11:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0007_auto_20190410_1341'), + ] + + operations = [ + migrations.CreateModel( + name='Role', + fields=[ + ('handle_id', models.AutoField(primary_key=True, serialize=False)), + ('node_name', models.CharField(max_length=200)), + ('description', models.TextField()), + ], + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0008_role_squashed_0013_auto_20190725_1153.py b/src/niweb/apps/noclook/migrations/0008_role_squashed_0013_auto_20190725_1153.py new file mode 100644 index 000000000..94494c4c5 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0008_role_squashed_0013_auto_20190725_1153.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-07-25 11:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + +from apps.noclook.models import DEFAULT_ROLEGROUP_NAME, DEFAULT_ROLE_KEY, DEFAULT_ROLES + +def init_default_roles_rolegroup(Role, RoleGroup): + default_rolegroup = RoleGroup.objects.filter(name=DEFAULT_ROLEGROUP_NAME) + + # create the group first + default_rolegroup = RoleGroup(name=DEFAULT_ROLEGROUP_NAME, hidden=True) + default_rolegroup.save() + + # and then get or create the default roles and link them + for role_slug, roledict in DEFAULT_ROLES.items(): + role = Role.objects.get_or_create(slug=role_slug)[0] + role.role_group = default_rolegroup + + # add a default description and name to the roles + if not role.description and roledict['description']: + role.description = roledict['description'] + role.save() + + if not role.name and roledict['name']: + role.name = roledict['name'] + role.save() + + role.save() + + +def delete_roles_rolegroup(Role, RoleGroup): + # delete all roles and rolegroups + Role.objects.all().delete() + RoleGroup.objects.all().delete() + + +def forwards_func(apps, schema_editor): + Role = apps.get_model("noclook", "Role") + RoleGroup = apps.get_model("noclook", "RoleGroup") + init_default_roles_rolegroup(Role, RoleGroup) + + +def reverse_func(apps, schema_editor): + Role = apps.get_model("noclook", "Role") + RoleGroup = apps.get_model("noclook", "RoleGroup") + delete_roles_rolegroup(Role, RoleGroup) + + +class Migration(migrations.Migration): + + replaces = [('noclook', '0008_role'), ('noclook', '0009_auto_20190717_1240'), ('noclook', '0010_auto_20190719_1005'), ('noclook', '0011_auto_20190719_1157'), ('noclook', '0012_role_slug'), ('noclook', '0013_auto_20190725_1153')] + + dependencies = [ + ('noclook', '0007_auto_20190410_1341'), + ] + + operations = [ + migrations.CreateModel( + name='Role', + fields=[ + ('handle_id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='RoleGroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('hidden', models.BooleanField(default=False)), + ], + ), + migrations.AddField( + model_name='role', + name='role_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='noclook.RoleGroup'), + ), + migrations.AddField( + model_name='role', + name='slug', + field=models.CharField(max_length=200, unique=True), + ), + migrations.AlterField( + model_name='role', + name='name', + field=models.CharField(max_length=200, unique=True), + ), + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0009_auto_20190717_1240.py b/src/niweb/apps/noclook/migrations/0009_auto_20190717_1240.py new file mode 100644 index 000000000..e296753c9 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0009_auto_20190717_1240.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-07-17 12:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0008_role'), + ] + + operations = [ + migrations.RenameField( + model_name='role', + old_name='node_name', + new_name='name', + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0009_auto_20190902_0759.py b/src/niweb/apps/noclook/migrations/0009_auto_20190902_0759.py new file mode 100644 index 000000000..fcefbdd76 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0009_auto_20190902_0759.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-09-02 07:59 +from __future__ import unicode_literals + +from django.db import migrations +import django.db.models.deletion + +from apps.noclook.models import DEFAULT_ROLEGROUP_NAME, DEFAULT_ROLE_KEY, DEFAULT_ROLES + +def init_default_roles(Role): + # and then get or create the default roles and link them + for role_slug, roledict in DEFAULT_ROLES.items(): + role = Role.objects.get(slug=role_slug) + + if role: + # add a default description and name to the roles + if not role.description and roledict['description']: + role.description = roledict['description'] + role.save() + + if roledict['name']: + role.name = roledict['name'] + role.save() + + role.save() + + +def forwards_func(apps, schema_editor): + Role = apps.get_model("noclook", "Role") + init_default_roles(Role) + + +def reverse_func(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0008_role_squashed_0013_auto_20190725_1153'), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0009_merge_20190729_1517.py b/src/niweb/apps/noclook/migrations/0009_merge_20190729_1517.py new file mode 100644 index 000000000..2f2ba553e --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0009_merge_20190729_1517.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-29 13:17 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0007_auto_20190320_1254'), + ('noclook', '0008_role_squashed_0013_auto_20190725_1153'), + ] + + operations = [ + ] diff --git a/src/niweb/apps/noclook/migrations/0010_auto_20190719_1005.py b/src/niweb/apps/noclook/migrations/0010_auto_20190719_1005.py new file mode 100644 index 000000000..e789e974b --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0010_auto_20190719_1005.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-07-19 10:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0009_auto_20190717_1240'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='description', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0010_merge_20190903_1136.py b/src/niweb/apps/noclook/migrations/0010_merge_20190903_1136.py new file mode 100644 index 000000000..ff3747363 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0010_merge_20190903_1136.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-09-03 11:36 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0007_auto_20190320_1254'), + ('noclook', '0009_auto_20190902_0759'), + ] + + operations = [ + ] diff --git a/src/niweb/apps/noclook/migrations/0011_auto_20190719_1157.py b/src/niweb/apps/noclook/migrations/0011_auto_20190719_1157.py new file mode 100644 index 000000000..ffaf7e9e0 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0011_auto_20190719_1157.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-07-19 11:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0010_auto_20190719_1005'), + ] + + operations = [ + migrations.CreateModel( + name='RoleGroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('hidden', models.BooleanField(default=False)), + ], + ), + migrations.AddField( + model_name='role', + name='role_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='noclook.RoleGroup'), + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0011_auto_20190918_0615.py b/src/niweb/apps/noclook/migrations/0011_auto_20190918_0615.py new file mode 100644 index 000000000..85c60af41 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0011_auto_20190918_0615.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-09-18 06:15 +from __future__ import unicode_literals + +from django.db import migrations +from os.path import dirname, abspath, join +import csv + +BASE_DIR = dirname(abspath(__file__)) + + +new_types = ['Email', 'Phone'] +new_dropdowns = ['phone_type', 'email_type'] + +def add_default_dropdowns(apps, schema_editor): + Dropdown = apps.get_model('noclook', 'Dropdown') + Choice = apps.get_model('noclook', 'Choice') + + with open(join(BASE_DIR, 'common_dropdowns.csv')) as f: + for line in csv.DictReader(f): + dropdown_name = line['dropdown'] + + if dropdown_name in new_dropdowns: + dropdown, created = Dropdown.objects.get_or_create(name=dropdown_name) + value = line['value'] + name = line['name'] or value + if value: + Choice.objects.get_or_create(dropdown=dropdown, + value=value, + name=name) + +def remove_default_dropdowns(apps, schema_editor): + Dropdown = apps.get_model('noclook', 'Dropdown') + with open(join(BASE_DIR, 'common_dropdowns.csv')) as f: + unique_dropdowns = set([l['dropdown'] for l in csv.DictReader(f)]) + for dropdown in unique_dropdowns: + if dropdown in new_dropdowns: + Dropdown.objects.filter(name=dropdown).delete() + +def add_new_nodetype(NodeType, new_type): + new_nodetype = NodeType( + type=new_type, + slug=new_type.lower(), + hidden=True, + ) + + new_nodetype.save() + return new_nodetype + +def remove_node_type(NodeType, type_name): + qs = NodeType.objects.filter(type=type_name, slug=type_name.lower()) + if qs: + for nodetype in qs: + nodetype.delete() + +def forwards_func(apps, schema_editor): + NodeType = apps.get_model("noclook", "NodeType") + + for new_type in new_types: + add_new_nodetype(NodeType, new_type) + + add_default_dropdowns(apps, schema_editor) + +def reverse_func(apps, schema_editor): + NodeType = apps.get_model("noclook", "NodeType") + + for new_type in new_types: + remove_node_type(NodeType, new_type) + + remove_default_dropdowns(apps, schema_editor) + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0010_merge_20190903_1136'), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0012_authzaction_context_groupcontextauthzaction_nodehandlecontext.py b/src/niweb/apps/noclook/migrations/0012_authzaction_context_groupcontextauthzaction_nodehandlecontext.py new file mode 100644 index 000000000..4af668260 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0012_authzaction_context_groupcontextauthzaction_nodehandlecontext.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-09-27 18:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +from os.path import dirname, abspath, join +import csv + +BASE_DIR = dirname(abspath(__file__)) + +def add_defaults_from_csv(model, csv_file): + with open(join(BASE_DIR, csv_file)) as f: + for line in csv.DictReader(f): + obj, created = model.objects.get_or_create(name=line['name']) + + +def forwards_func(apps, schema_editor): + AuthzAction = apps.get_model('noclook', 'AuthzAction') + Context = apps.get_model('noclook', 'Context') + + add_defaults_from_csv(AuthzAction, 'default_authzactions.csv') + add_defaults_from_csv(Context, 'default_contexts.csv') + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ('noclook', '0011_auto_20190918_0615'), + ] + + operations = [ + migrations.CreateModel( + name='AuthzAction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ], + ), + migrations.CreateModel( + name='Context', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ], + ), + migrations.CreateModel( + name='GroupContextAuthzAction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('authzprofile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='noclook.AuthzAction')), + ('context', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='noclook.Context')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), + ], + ), + migrations.CreateModel( + name='NodeHandleContext', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('context', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='noclook.Context')), + ('nodehandle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='noclook.NodeHandle')), + ], + ), + migrations.RunPython(forwards_func, migrations.RunPython.noop), + ] diff --git a/src/niweb/apps/noclook/migrations/0012_role_slug.py b/src/niweb/apps/noclook/migrations/0012_role_slug.py new file mode 100644 index 000000000..6da13e058 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0012_role_slug.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-07-23 07:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0011_auto_20190719_1157'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='slug', + field=models.CharField(max_length=20, null=True, unique=True), + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0013_auto_20190725_1153.py b/src/niweb/apps/noclook/migrations/0013_auto_20190725_1153.py new file mode 100644 index 000000000..2dc58d6f6 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0013_auto_20190725_1153.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-07-25 11:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0012_role_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='name', + field=models.CharField(max_length=200, unique=True), + ), + migrations.AlterField( + model_name='role', + name='slug', + field=models.CharField(max_length=200, unique=True), + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0013_default_policies_20190927_1937.py b/src/niweb/apps/noclook/migrations/0013_default_policies_20190927_1937.py new file mode 100644 index 000000000..eedf88f25 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0013_default_policies_20190927_1937.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-09-27 19:37 +from __future__ import unicode_literals + +from django.db import migrations +from vakt import Policy, ALLOW_ACCESS, DENY_ACCESS + +import apps.noclook.vakt.rules as srirules +import apps.noclook.vakt.utils as sriutils +import uuid +import vakt.rules as vakt_rules + + +def forwards_func(apps, schema_editor): + # get storage and guard + storage, guard = sriutils.get_vakt_storage_and_guard() + + # create policies using storage instead + Context = apps.get_model('noclook', 'Context') + AuthzAction = apps.get_model('noclook', 'AuthzAction') + + # iterate over all existent contexts and authzactions + # and create policies for each of them + all_contexts = Context.objects.all() + rw_authzactions = AuthzAction.objects.filter(name__in=( + sriutils.READ_AA_NAME, + sriutils.WRITE_AA_NAME, + )) + + # add read and write policies + for context in all_contexts: + for authzaction in rw_authzactions: + policy = Policy( + uuid.uuid4(), + actions=[vakt_rules.Eq(authzaction.name)], + resources=[srirules.BelongsContext(context)], + subjects=[srirules.HasAuthAction(authzaction, context)], + context={ 'module': srirules.ContainsElement(context.name) }, + effect=ALLOW_ACCESS, + description='Automatically created policy' + ) + storage.add(policy) + + # add admin policies + admin_aa = AuthzAction.objects.get(name=sriutils.ADMIN_AA_NAME) + for context in all_contexts: + policy = Policy( + uuid.uuid4(), + actions=[vakt_rules.Eq(admin_aa.name)], + resources=[vakt_rules.Any()], + subjects=[srirules.HasAuthAction(admin_aa, context)], + context={ 'module': srirules.ContainsElement(context.name) }, + effect=ALLOW_ACCESS, + description='Automatically created policy' + ) + storage.add(policy) + +def backwards_func(apps, schema_editor): + # delete all stored policies + DjPolicy = apps.get_model('djangovakt', 'Policy') + DjPolicy.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangovakt', '0001_initial'), + ('noclook', '0012_authzaction_context_groupcontextauthzaction_nodehandlecontext'), + ] + + operations = [ + migrations.RunPython(forwards_func, backwards_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0014_default_context_20191002_1159.py b/src/niweb/apps/noclook/migrations/0014_default_context_20191002_1159.py new file mode 100644 index 000000000..5bea95325 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0014_default_context_20191002_1159.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-10-02 11:59 +from __future__ import unicode_literals + +from django.db import migrations + +import apps.noclook.vakt.utils as sriutils + + +# add default community context to all nodes with these types +for_types = [ + 'organization', + 'procedure', + 'contact', + 'group', + 'email', + 'phone', +] + +def forwards_func(apps, schema_editor): + Context = apps.get_model('noclook', 'Context') + NodeType = apps.get_model('noclook', 'NodeType') + NodeHandle = apps.get_model('noclook', 'NodeHandle') + NodeHandleContext = apps.get_model('noclook', 'NodeHandleContext') + + default_context = sriutils.get_default_context(Context) + types_qs = NodeType.objects.filter(slug__in=for_types) + nodes = NodeHandle.objects.filter(node_type__in=types_qs) + + for node in nodes: + NodeHandleContext( + nodehandle=node, + context=default_context + ).save() + + +def backwards_func(apps, schema_editor): + NodeHandleContext = apps.get_model('noclook', 'NodeHandleContext') + NodeHandleContext.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0013_default_policies_20190927_1937'), + ] + + operations = [ + migrations.RunPython(forwards_func, backwards_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0015_nodehandle_contexts.py b/src/niweb/apps/noclook/migrations/0015_nodehandle_contexts.py new file mode 100644 index 000000000..3f8738416 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0015_nodehandle_contexts.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.25 on 2019-10-07 17:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0014_default_context_20191002_1159'), + ] + + operations = [ + migrations.AddField( + model_name='nodehandle', + name='contexts', + field=models.ManyToManyField(through='noclook.NodeHandleContext', to='noclook.Context'), + ), + ] diff --git a/src/niweb/apps/noclook/migrations/0016_list_policy_20191023_0558.py b/src/niweb/apps/noclook/migrations/0016_list_policy_20191023_0558.py new file mode 100644 index 000000000..a9bd9d869 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0016_list_policy_20191023_0558.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-09-27 19:37 +from __future__ import unicode_literals + +from django.db import migrations +from vakt import Policy, Inquiry, ALLOW_ACCESS, DENY_ACCESS + +import apps.noclook.vakt.rules as srirules +import apps.noclook.vakt.utils as sriutils +import uuid +import vakt.rules as vakt_rules + + +def forwards_func(apps, schema_editor): + # add AuthzAction + AuthzAction = apps.get_model('noclook', 'AuthzAction') + list_aa, created = AuthzAction.objects.get_or_create(name="list") + + # get storage and guard + storage, guard = sriutils.get_vakt_storage_and_guard() + Context = apps.get_model('noclook', 'Context') + DjPolicy = apps.get_model('djangovakt', 'Policy') + + # iterate over all the existent contexts to add + # a list policy for each of them + all_contexts = Context.objects.all() + + for context in all_contexts: + # check if the policy exist first + qs = DjPolicy.objects.filter(doc__actions__0__val=list_aa.name) + qs = qs.filter(doc__context__module__elem__in=(context.name,)) + + if not qs.exists(): + policy = Policy( + uuid.uuid4(), + actions=[vakt_rules.Eq(list_aa.name)], + resources=[vakt_rules.Any()], + subjects=[srirules.HasAuthAction(list_aa, context)], + context={ 'module': srirules.ContainsElement(context.name) }, + effect=ALLOW_ACCESS, + description='Automatically created policy' + ) + storage.add(policy) + + +def backwards_func(apps, schema_editor): + # get storage and guard + storage, guard = sriutils.get_vakt_storage_and_guard() + + # get authaction + AuthzAction = apps.get_model('noclook', 'AuthzAction') + list_aa = sriutils.get_list_authaction() + + # get the list policy for every context + Context = apps.get_model('noclook', 'Context') + DjPolicy = apps.get_model('djangovakt', 'Policy') + all_contexts = Context.objects.all() + + for context in all_contexts: + qs = DjPolicy.objects.filter(doc__actions__0__val=list_aa.name) + qs = qs.filter(doc__context__module__elem__in=(context.name,)) + qs.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0015_nodehandle_contexts'), + ] + + operations = [ + migrations.RunPython(forwards_func, backwards_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0017_auth_default_groups_20191023_0823.py b/src/niweb/apps/noclook/migrations/0017_auth_default_groups_20191023_0823.py new file mode 100644 index 000000000..6a0d7df6d --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0017_auth_default_groups_20191023_0823.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.25 on 2019-10-23 08:23 +from __future__ import unicode_literals + +from django.db import migrations +import apps.noclook.vakt.utils as sriutils + +def forwards_func(apps, schema_editor): + # get models + Group = apps.get_model('auth', 'Group') + Context = apps.get_model('noclook', 'Context') + AuthzAction = apps.get_model('noclook', 'AuthzAction') + GroupContextAuthzAction = apps.get_model('noclook', 'GroupContextAuthzAction') + NodeHandleContext = apps.get_model('noclook', 'NodeHandleContext') + + contexts = Context.objects.all() + + for context in contexts: + gr_name = 'read_{}'.format(context.name.lower()) + gw_name = 'write_{}'.format(context.name.lower()) + gl_name = 'list_{}'.format(context.name.lower()) + ga_name = 'admin_{}'.format(context.name.lower()) + + # create or get groups + groupr, created = Group.objects.get_or_create(name=gr_name) + groupw, created = Group.objects.get_or_create(name=gw_name) + groupl, created = Group.objects.get_or_create(name=gl_name) + groupa, created = Group.objects.get_or_create(name=ga_name) + + aa_read = sriutils.get_read_authaction(AuthzAction) + aa_write = sriutils.get_write_authaction(AuthzAction) + aa_list = sriutils.get_list_authaction(AuthzAction) + aa_admin = sriutils.get_admin_authaction(AuthzAction) + + GroupContextAuthzAction.objects.get_or_create( group = groupr, authzprofile = aa_read, context = context ) + GroupContextAuthzAction.objects.get_or_create( group = groupw, authzprofile = aa_write, context = context ) + GroupContextAuthzAction.objects.get_or_create( group = groupl, authzprofile = aa_list, context = context ) + GroupContextAuthzAction.objects.get_or_create( group = groupa, authzprofile = aa_admin, context = context ) + + +def backwards_func(apps, schema_editor): + # create or get groups + Group = apps.get_model('auth', 'Group') + grouprs = Group.objects.filter(name__startswith="read_") + groupws = Group.objects.filter(name__startswith="write_") + groupls = Group.objects.filter(name__startswith="list_") + groupas = Group.objects.filter(name__startswith="admin_") + + # delete groups + grouprs.delete() + groupws.delete() + groupls.delete() + groupas.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0016_list_policy_20191023_0558'), + ] + + operations = [ + migrations.RunPython(forwards_func, backwards_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0018_orgtypes_20191030_1256.py b/src/niweb/apps/noclook/migrations/0018_orgtypes_20191030_1256.py new file mode 100644 index 000000000..7a9c16138 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0018_orgtypes_20191030_1256.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.25 on 2019-10-30 12:56 +from __future__ import unicode_literals + +from django.db import migrations +from os.path import dirname, abspath, join +import csv + +BASE_DIR = dirname(abspath(__file__)) + + +def forwards_func(apps, schema_editor): + ''' + Reloads the organization type combo + ''' + Dropdown = apps.get_model('noclook', 'Dropdown') + Choice = apps.get_model('noclook', 'Choice') + + orgtype_dropname = 'organization_types' + + # delete the already present options + orgdropdown, created = \ + Dropdown.objects.get_or_create(name=orgtype_dropname) + Choice.objects.filter(dropdown=orgdropdown).delete() + + # add them again from the csv file + with open(join(BASE_DIR, 'common_dropdowns.csv')) as f: + for line in csv.DictReader(f): + dropdown_name = line['dropdown'] + + if dropdown_name == orgtype_dropname: + dropdown, created = \ + Dropdown.objects.get_or_create(name=dropdown_name) + value = line['value'] + name = line['name'] or value + if value: + Choice.objects.get_or_create(dropdown=dropdown, + value=value, + name=name) + +def reverse_func(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0017_auth_default_groups_20191023_0823'), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0019_org_affiliation_fields_20191107_1015.py b/src/niweb/apps/noclook/migrations/0019_org_affiliation_fields_20191107_1015.py new file mode 100644 index 000000000..164e88c68 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0019_org_affiliation_fields_20191107_1015.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.25 on 2019-11-07 10:15 +from __future__ import unicode_literals + +from django.db import migrations +import norduniclient as nc + + +def forwards_func(apps, schema_editor): + NodeType = apps.get_model('noclook', 'NodeType') + NodeHandle = apps.get_model('noclook', 'NodeHandle') + + organization_type = NodeType.objects.get_or_create(type='Organization', slug='organization')[0] # organization + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + check_fields = [ + 'affiliation_customer', + 'affiliation_end_customer', + 'affiliation_provider', + 'affiliation_partner', + 'affiliation_host_user', + 'affiliation_site_owner', + ] + + for organization in all_organizations: + orgnode = nc.get_node_model(nc.graphdb.manager, organization.handle_id) + #orgnode = organization.get_node() + for field in check_fields: + org_val = orgnode.data.get(field, None) + if not org_val: + orgnode.add_property(field, False) + + +def reverse_func(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0018_orgtypes_20191030_1256'), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/src/niweb/apps/noclook/migrations/0020_merge_20200331_0709.py b/src/niweb/apps/noclook/migrations/0020_merge_20200331_0709.py new file mode 100644 index 000000000..77d7770cd --- /dev/null +++ b/src/niweb/apps/noclook/migrations/0020_merge_20200331_0709.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-03-31 07:09 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('noclook', '0009_merge_20190729_1517'), + ('noclook', '0010_add_actstream_actor_index'), + ('noclook', '0019_org_affiliation_fields_20191107_1015'), + ] + + operations = [ + ] diff --git a/src/niweb/apps/noclook/migrations/common_dropdowns.csv b/src/niweb/apps/noclook/migrations/common_dropdowns.csv index a161bb0e7..8c77c51f3 100644 --- a/src/niweb/apps/noclook/migrations/common_dropdowns.csv +++ b/src/niweb/apps/noclook/migrations/common_dropdowns.csv @@ -44,3 +44,15 @@ countries,NO,Norway countries,SE,Sweden countries,UK,United Kingdom countries,US,USA +organization_types,university_college,"University, College" +organization_types,university_coldep,"University, College dep" +organization_types,museum,"Museum, Institution" +organization_types,research,Research facility +organization_types,student_net,Student network +organization_types,partner,Commercial +contact_type,person,Person +contact_type,group,Group +phone_type,work,Work +phone_type,personal,Personal +email_type,work,Work +email_type,personal,Personal diff --git a/src/niweb/apps/noclook/migrations/default_authzactions.csv b/src/niweb/apps/noclook/migrations/default_authzactions.csv new file mode 100644 index 000000000..5e1554a0e --- /dev/null +++ b/src/niweb/apps/noclook/migrations/default_authzactions.csv @@ -0,0 +1,5 @@ +name +read +write +admin +list diff --git a/src/niweb/apps/noclook/migrations/default_contexts.csv b/src/niweb/apps/noclook/migrations/default_contexts.csv new file mode 100644 index 000000000..f314b8f59 --- /dev/null +++ b/src/niweb/apps/noclook/migrations/default_contexts.csv @@ -0,0 +1,4 @@ +name +Network +Community +Contracts diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index 6fa762d63..04b7a13f5 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from django.db import models -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django_comments.signals import comment_was_posted, comment_was_flagged from django.dispatch import receiver from django_comments.models import Comment @@ -73,6 +73,11 @@ class NodeHandle(models.Model): created = models.DateTimeField(auto_now_add=True) modifier = models.ForeignKey(User, related_name='modifier', null=True, on_delete=models.SET_NULL) modified = models.DateTimeField(auto_now=True) + contexts = models.ManyToManyField( + 'Context', + through='NodeHandleContext', + through_fields=('nodehandle', 'context'), + ) def __str__(self): return '%s %s' % (self.node_type, self.node_name) @@ -122,6 +127,105 @@ def delete(self, **kwargs): delete.alters_data = True +DEFAULT_ROLEGROUP_NAME = 'default' +DEFAULT_ROLE_KEY = 'employee' +DEFAULT_ROLES = { + 'abuse_contact': { 'name': 'Abuse', 'description': '' }, + 'primary_contact': { 'name': 'Primary contact at incidents', 'description': '' }, + 'secondary_contact': { 'name': 'Secondary contact at incidents', 'description': '' }, + 'it_technical_contact': { 'name': 'NOC Technical', 'description': '' }, + 'it_security_contact': { 'name': 'NOC Security', 'description': '' }, + 'it_manager_contact': { 'name': 'NOC Manager', 'description': '' }, + DEFAULT_ROLE_KEY: { 'name': nc.models.RoleRelationship.DEFAULT_ROLE_NAME, 'description': '' }, +} + + +@python_2_unicode_compatible +class RoleGroup(models.Model): + name = models.CharField(max_length=100, unique=True) + hidden = models.BooleanField(default=False, blank=True) + + def __str__(self): + return 'RoleGroup %s' % (self.name) + + +@python_2_unicode_compatible +class Role(models.Model): + # Data shared with the relationship + handle_id = models.AutoField(primary_key=True) # Handle <-> Node data + name = models.CharField(max_length=200, unique=True) + slug = models.CharField(max_length=200, unique=True) + # Data only present in the relational database + description = models.TextField(blank=True, null=True) + role_group = models.ForeignKey(RoleGroup, models.SET_NULL, blank=True, null=True) + + def __str__(self): + return 'Role %s' % (self.name) + + def get_absolute_url(self): + return self.url() + + def url(self): + return '/role/{}'.format(self.handle_id) + + def save(self, **kwargs): + # set slug value if empty + if not self.slug: + self.slug = self.name.replace(' ', '_').lower() + + super(Role, self).save() + return self + + def delete(self, **kwargs): + """ + Propagate the changes over the graph db + """ + default_rolegroup = RoleGroup.objects.get(name=DEFAULT_ROLEGROUP_NAME) + + if self.role_group != default_rolegroup: + nc.models.RoleRelationship.delete_roles_withname(self.name) + super(Role, self).delete() + + +@python_2_unicode_compatible +class AuthzAction(models.Model): + name = models.CharField(max_length=100, unique=True) + + def __str__(self): + return 'AuthzAction %s' % (self.name) + + +@python_2_unicode_compatible +class Context(models.Model): + name = models.CharField(max_length=100, unique=True) + + def __str__(self): + return 'Context %s' % (self.name) + + +@python_2_unicode_compatible +class GroupContextAuthzAction(models.Model): + group = models.ForeignKey(Group, models.CASCADE) + authzprofile = models.ForeignKey(AuthzAction, models.CASCADE) + context = models.ForeignKey(Context, models.CASCADE) + + def __str__(self): + return '{} / {} / {}'.format( + str(self.group), + self.authzprofile.name, + self.context.name + ) + + +@python_2_unicode_compatible +class NodeHandleContext(models.Model): + nodehandle = models.ForeignKey(NodeHandle, models.CASCADE) + context = models.ForeignKey(Context, models.CASCADE) + + def __str__(self): + return '{} / {}'.format(self.nodehandle.node_name, self.context.name) + + @python_2_unicode_compatible class UniqueIdGenerator(models.Model): """ diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py new file mode 100644 index 000000000..a226a67c0 --- /dev/null +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from django.contrib.auth.decorators import login_required +from django.utils.decorators import method_decorator +from graphene_django.views import GraphQLView + +from .core import * +from .types import * +from .query import * +from .mutations import * + +NOCSCHEMA_TYPES = [ + User, + Dropdown, + Choice, + Neo4jChoice, + NodeHandler, + Group, + Contact, + Procedure, + Host, + Address, + Phone, + Email, +] + +NOCSCHEMA_QUERIES = [ + NOCRootQuery, +] + +NOCSCHEMA_MUTATIONS = [ + NOCRootMutation, +] + +@method_decorator(login_required, name='dispatch') +class AuthGraphQLView(GraphQLView): + pass diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py new file mode 100644 index 000000000..76fcc5de6 --- /dev/null +++ b/src/niweb/apps/noclook/schema/core.py @@ -0,0 +1,2133 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import datetime +import graphene +import logging +import norduniclient as nc +import re + +import apps.noclook.vakt.utils as sriutils + +from apps.noclook import activitylog, helpers +from apps.noclook.models import NodeType, NodeHandle, NodeHandleContext +from collections import OrderedDict, Iterable +from django import forms +from django.contrib.auth.models import User as DjangoUser +from django.forms.utils import ValidationError +from django.test import RequestFactory +from django_comments.models import Comment +from graphene import relay +from graphene.types import Scalar, DateTime +from graphene_django import DjangoObjectType +from graphene_django.types import DjangoObjectTypeOptions, ErrorType +from graphql import GraphQLError +from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible + +from .scalars import * +from .fields import * +from .querybuilders import * +from ..models import NodeType, NodeHandle + +logger = logging.getLogger(__name__) + +########## RELATION AND NODE TYPES + +NIMETA_LOGICAL = 'logical' +NIMETA_RELATION = 'relation' +NIMETA_PHYSICAL = 'physical' +NIMETA_LOCATION = 'location' + +class User(DjangoObjectType): + ''' + The django user type + ''' + class Meta: + model = DjangoUser + only_fields = ['id', 'username', 'first_name', 'last_name', 'email'] + +class UserInputType(graphene.InputObjectType): + ''' + This object represents an input for an user used in connections + ''' + username = graphene.String(required=True) + +class NINodeHandlerType(DjangoObjectType): + ''' + Generic NodeHandler graphene type + ''' + class Meta: + model = NodeHandle + interfaces = (relay.Node, ) + +class NIRelationType(graphene.ObjectType): + ''' + This class represents a relationship and its properties + ''' + @classmethod + def __init_subclass_with_meta__( + cls, + **options, + ): + super(NIRelationType, cls).__init_subclass_with_meta__( + **options + ) + + relation_id = graphene.Int(required=True) + type = graphene.String(required=True) # this may be set to an Enum + start = graphene.Field(NINodeHandlerType, required=True) + end = graphene.Field(NINodeHandlerType, required=True) + nidata = graphene.List(DictEntryType) + + def resolve_relation_id(self, info, **kwargs): + self.relation_id = self.id + + return self.relation_id + + def resolve_nidata(self, info, **kwargs): + ''' + Is just the same than old resolve_nidata, but it doesn't resolve the node + ''' + ret = [] + + alldata = self.data + for key, value in alldata.items(): + if key and value: + ret.append(DictEntryType(name=key, value=value)) + + return ret + + def resolve_start(self, info, **kwargs): + return NodeHandle.objects.get(handle_id=self.start['handle_id']) + + def resolve_end(self, info, **kwargs): + return NodeHandle.objects.get(handle_id=self.end['handle_id']) + + @classmethod + def get_filter_input_fields(cls): + ''' + Method used by build_filter_and_order for a Relation type + ''' + input_fields = {} + classes = NIRelationType, cls + + ni_metatype = getattr(cls, 'NIMetaType') + filter_include = getattr(ni_metatype, 'filter_include', None) + filter_exclude = getattr(ni_metatype, 'filter_exclude', None) + + if filter_include and filter_exclude: + raise Exception("Only filter_include or filter_include metafields can be defined on {}".format(cls)) + + # add type NIRelationType and subclass + for clz in classes: + for name, field in clz.__dict__.items(): + if field: + add_field = False + + if isinstance(field, graphene.types.scalars.String) or\ + isinstance(field, graphene.types.scalars.Int): + add_field = True + + if filter_include: + if name not in filter_include: + add_field = False + elif filter_exclude: + if name in filter_exclude: + add_field = False + + if add_field: + input_field = type(field) + input_fields[name] = input_field + + return input_fields + + @classproperty + def match_additional_clause(cls): + nimetatype = getattr(cls, 'NIMetaType', None) + relation_name = '' + if nimetatype: + relation_name = nimetatype.nimodel.RELATION_NAME + + if relation_name: + relation_name = ':{}'.format(relation_name) + + return "({})-[{}{}{}]-({})".format('{}', cls.neo4j_var_name, '{}', relation_name, '{}') + + neo4j_var_name = "r" + + class Meta: + interfaces = (relay.Node, ) + +class DictRelationType(graphene.ObjectType): + ''' + This type represents an key value pair for a relationship dictionary, + the key is the name of the relationship and the value the NIRelationType itself + ''' + name = graphene.String(required=True) + relation = graphene.Field(NIRelationType, required=True) + +class CommentType(DjangoObjectType): + ''' + This type represents a comment in the API, it uses the comments model just + like the rest of noclook + ''' + object_id = graphene.ID(required=True) + + def resolve_object_id(self, info, **kwargs): + node = NodeHandle.objects.get(handle_id = self.object_pk) + object_id = relay.Node.to_global_id(str(node.node_type), + str(self.object_pk)) + + return object_id + + class Meta: + model = Comment + interfaces = (relay.Node, ) + +input_fields_clsnames = {} + +class NIObjectType(DjangoObjectType): + ''' + This class expands graphene_django object type adding the defined fields in + the types subclasses and extracts the data from the norduniclient nodes and + adds a resolver for each field, a nidata field is also added to hold the + values of the node data dict. + ''' + + filter_names = None + + _connection_input = None + _connection_order = None + _order_field_match = None + _asc_suffix = 'ASC' + _desc_suffix = 'DESC' + + @classmethod + def __init_subclass_with_meta__( + cls, + **options, + ): + allfields = cls.__dict__ + graphfields = OrderedDict() + + # getting all not magic attributes, also filter non NI fields + for name, field in allfields.items(): + pattern = re.compile("^__.*__$") + is_nibasicfield = issubclass(field.__class__, NIBasicField) + if pattern.match(name) or callable(field) or not is_nibasicfield: + continue + graphfields[name] = field + + # run over the fields defined and adding graphene fields and resolvers + for name, field in graphfields.items(): + field_fields = field.__dict__ + + field_type = field_fields.get('field_type') + manual_resolver = field_fields.get('manual_resolver') + type_kwargs = field_fields.get('type_kwargs') + type_args = field_fields.get('type_args') + rel_name = field_fields.get('rel_name') + rel_method = field_fields.get('rel_method') + not_null_list = field_fields.get('not_null_list') + + # adding the field + field_value = None + if type_kwargs: + field_value = field_type(**type_kwargs) + elif type_args: + field_value = field_type(*type_args) + if not_null_list: + field_value = graphene.NonNull(field_type(*type_args)) + else: + field_value = field_type(**{}) + + setattr(cls, name, field_value) + + # adding the resolver + if not manual_resolver: + setattr(cls, 'resolve_{}'.format(name), \ + field.get_resolver( + field_name=name, + rel_name=rel_name, + rel_method=rel_method, + ) + ) + + elif callable(manual_resolver): + setattr(cls, 'resolve_{}'.format(name), manual_resolver) + else: + raise Exception( + 'NIObjectField manual_resolver must be a callable in field {}'\ + .format(name) + ) + + options['model'] = NIObjectType._meta.model + options['interfaces'] = NIObjectType._meta.interfaces + + super(NIObjectType, cls).__init_subclass_with_meta__( + **options + ) + + nidata = graphene.List(DictEntryType, resolver=resolve_nidata) + + incoming = graphene.List(DictRelationType) + outgoing = graphene.List(DictRelationType) + comments = graphene.List(CommentType) + + def resolve_incoming(self, info, **kwargs): + ''' + Resolver for incoming relationships for the node + ''' + incoming_rels = self.get_node().incoming + ret = [] + for rel_name, rel_list in incoming_rels.items(): + for rel in rel_list: + relation_id = rel['relationship_id'] + relation_model = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) + ret.append(DictRelationType(name=rel_name, relation=relation_model)) + + return ret + + def resolve_outgoing(self, info, **kwargs): + ''' + Resolver for outgoing relationships for the node + ''' + outgoing_rels = self.get_node().outgoing + ret = [] + for rel_name, rel_list in outgoing_rels.items(): + for rel in rel_list: + relation_id = rel['relationship_id'] + rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) + ret.append(DictRelationType(name=rel_name, relation=rel)) + + return ret + + def resolve_comments(self, info, **kwargs): + handle_id = self.handle_id + return Comment.objects.filter(object_pk=handle_id) + + @classmethod + def get_from_nimetatype(cls, attr): + ni_metatype = getattr(cls, 'NIMetaType', None) + return getattr(ni_metatype, attr) + + @classmethod + def get_type_name(cls): + ni_type = cls.get_from_nimetatype('ni_type') + node_type = NodeType.objects.filter(type=ni_type).first() + return node_type.type + + @classmethod + def get_type_context(cls): + context_resolver = cls.get_from_nimetatype('context_method') + + if not context_resolver: + context_resolver = sriutils.get_default_context + + return context_resolver() + + @classmethod + def get_filter_input_fields(cls): + ''' + Method used by build_filter_and_order for a Node type + ''' + input_fields = {} + + ni_metatype = getattr(cls, 'NIMetaType') + filter_include = getattr(ni_metatype, 'filter_include', None) + filter_exclude = getattr(ni_metatype, 'filter_exclude', None) + + for name, field in cls.__dict__.items(): + # string or string like fields + if field: + if isinstance(field, graphene.types.scalars.String) or\ + isinstance(field, graphene.types.scalars.Int) or\ + isinstance(field, graphene.types.scalars.Boolean) or\ + isinstance(field, ChoiceScalar): + input_field = type(field) + input_fields[name] = input_field + elif isinstance(field, graphene.types.structures.List): + # create arguments for input_field + field_of_type = field._of_type + + # recase to lower camelCase + name_fot = field_of_type.__name__ + components = name_fot.split('_') + name_fot = components[0] + ''.join(x.title() for x in components[1:]) + + # get object attributes by their filter input fields + # to build the filter field for the nested object + filter_attrib = {} + instance_inputfield = False + + if hasattr(field_of_type, 'get_filter_input_fields'): + instance_inputfield = True + for a, b in field_of_type.get_filter_input_fields().items(): + if callable(b): + filter_attrib[a] = b() + else: + filter_attrib[a] = b[0]() + + filter_attrib['_of_type'] = field._of_type + + if instance_inputfield: + ifield_clsname = '{}InputField'.format(name_fot) + + if not ifield_clsname in input_fields_clsnames: + binput_field = type(ifield_clsname, (graphene.InputObjectType, ), filter_attrib) + input_fields_clsnames[ifield_clsname] = binput_field + else: + binput_field = input_fields_clsnames[ifield_clsname] + + input_fields[name] = binput_field, field._of_type + + input_fields['id'] = graphene.ID + + # add 'created' and 'modified' datetime fields + for date_ffield in DateQueryBuilder.fields: + input_fields[date_ffield] = DateTime + + # add 'creator' and 'modifier' user fields + for user_ffield in UserQueryBuilder.fields: + input_fields[user_ffield] = UserInputType + + return input_fields + + @classmethod + def build_filter_and_order(cls): + ''' + This method generates a Filter and Order object from the class itself + to be used in filtering connections + ''' + if cls._connection_input and cls._connection_order: + return cls._connection_input, cls._connection_order + + ni_type = cls.get_from_nimetatype('ni_type') + + # build filter input class and order enum + filter_attrib = {} + cls.filter_names = {} + cls._order_field_match = {} + enum_options = [] + input_fields = cls.get_filter_input_fields() + + for field_name, input_field in input_fields.items(): + # creating field instance + field_instance = None + the_field = None + + # is a plain scalar field? + if not isinstance(input_field, Iterable): + field_instance = input_field() + the_field = input_field + of_type = input_field + + else: # it must be a list other_node + field_instance = input_field[0]() + the_field = input_field[0] + of_type = input_field[1] + + # adding order attributes and store in field property + if of_type == graphene.Int or \ + of_type == graphene.String or \ + of_type == ChoiceScalar or \ + issubclass(of_type, DateTime) or \ + issubclass(of_type, NIObjectType) or \ + issubclass(of_type, NIRelationType): + asc_field_name = '{}_{}'.format(field_name, cls._asc_suffix) + desc_field_name = '{}_{}'.format(field_name, cls._desc_suffix) + enum_options.append([asc_field_name, asc_field_name]) + enum_options.append([desc_field_name, desc_field_name]) + + cls._order_field_match[asc_field_name] = { + 'field': field_name, + 'is_desc': False, + 'input_field': of_type, + } + cls._order_field_match[desc_field_name] = { + 'field': field_name, + 'is_desc': True, + 'input_field': of_type, + } + + # adding filter attributes + for suffix, suffix_attr in AbstractQueryBuilder.filter_array.items(): + # filter field naming + if not suffix == '': + suffix = '_{}'.format(suffix) + + fmt_filter_field = '{}{}'.format(field_name, suffix) + + if not suffix_attr['only_strings'] \ + or isinstance(field_instance, graphene.String) \ + or isinstance(field_instance, ChoiceScalar) \ + or isinstance(field_instance, graphene.InputObjectType): + if 'wrapper_field' not in suffix_attr or not suffix_attr['wrapper_field']: + filter_attrib[fmt_filter_field] = field_instance + cls.filter_names[fmt_filter_field] = { + 'field' : field_name, + 'suffix': suffix, + 'field_type': field_instance, + } + else: + wrapped_field = the_field + for wrapper_field in suffix_attr['wrapper_field']: + wrapped_field = wrapper_field(wrapped_field) + + filter_attrib[fmt_filter_field] = wrapped_field + cls.filter_names[fmt_filter_field] = { + 'field' : field_name, + 'suffix': suffix, + 'field_type': field_instance, + } + + simple_filter_input = type('{}NestedFilter'.format(ni_type), (graphene.InputObjectType, ), filter_attrib) + + filter_attrib = {} + filter_attrib['AND'] = graphene.List(graphene.NonNull(simple_filter_input)) + filter_attrib['OR'] = graphene.List(graphene.NonNull(simple_filter_input)) + + filter_input = type('{}Filter'.format(ni_type), (graphene.InputObjectType, ), filter_attrib) + + # add the handle id field manually + handle_id_field = 'handle_id' + asc_field_name = '{}_{}'.format(handle_id_field, cls._asc_suffix) + desc_field_name = '{}_{}'.format(handle_id_field, cls._desc_suffix) + enum_options.append([asc_field_name, asc_field_name]) + enum_options.append([desc_field_name, desc_field_name]) + + orderBy = graphene.Enum('{}OrderBy'.format(ni_type), enum_options) + + # store the created objects + cls._connection_input = filter_input + cls._connection_order = orderBy + + return filter_input, orderBy + + @classmethod + def get_byid_resolver(cls): + ''' + This method returns a generic by id resolver for every nodetype in NOCAutoQuery + ''' + type_name = cls.get_type_name() + + def generic_byid_resolver(self, info, **args): + id = args.get('id') + handle_id = None + ret = None + + try: + _type, handle_id = relay.Node.from_global_id(id) + except: + pass # nothing is done, we'll return None + + node_type = NodeType.objects.get(type=type_name) + ret = None + + if info.context and info.context.user.is_authenticated: + if handle_id: + authorized = sriutils.authorice_read_resource( + info.context.user, handle_id + ) + + if authorized: + try: + int_id = str(handle_id) + ret = NodeHandle.objects.filter(node_type=node_type).get(handle_id=int_id) + except ValueError: + ret = NodeHandle.objects.filter(node_type=node_type).get(handle_id=handle_id) + else: + raise GraphQLError('A handle_id must be provided') + + if not ret: + raise GraphQLError("There isn't any {} with handle_id {}".format(type_name, handle_id)) + + return ret + else: + raise GraphQLAuthException() + + return generic_byid_resolver + + @classmethod + def get_list_resolver(cls): + ''' + This method returns a simple list resolver for every nodetype in NOCAutoQuery + ''' + type_name = cls.get_type_name() + + def generic_list_resolver(self, info, **args): + qs = NodeHandle.objects.none() + + if info.context and info.context.user.is_authenticated: + context = cls.get_type_context() + authorized = sriutils.authorize_list_module( + info.context.user, context + ) + + if authorized: + node_type = NodeType.objects.get(type=type_name) + qs = NodeHandle.objects.filter(node_type=node_type).order_by('node_name') + + # the node list is trimmed to the nodes that the user can read + qs = sriutils.trim_readable_queryset(qs, info.context.user) + else: + raise GraphQLAuthException() + + return qs + + return generic_list_resolver + + @classmethod + def get_count_resolver(cls): + ''' + This method returns a simple list resolver for every nodetype in NOCAutoQuery + ''' + type_name = cls.get_type_name() + + def generic_count_resolver(self, info, **args): + qs = NodeHandle.objects.none() + + if info.context and info.context.user.is_authenticated: + context = cls.get_type_context() + authorized = sriutils.authorize_list_module( + info.context.user, context + ) + + if authorized: + node_type = NodeType.objects.get(type=type_name) + qs = NodeHandle.objects.filter(node_type=node_type).order_by('node_name') + + # the node list is trimmed to the nodes that the user can read + qs = sriutils.trim_readable_queryset(qs, info.context.user) + else: + raise GraphQLAuthException() + + return qs.count() + + return generic_count_resolver + + @classmethod + def filter_is_empty(cls, filter): + empty = False + + if not filter: + empty = True + + if not empty and filter: + and_portion = None + if 'AND' in filter: + and_portion = filter['AND'] + + or_portion = None + if 'OR' in filter: + or_portion = filter['OR'] + + if not and_portion and not or_portion: + empty = True + elif not (and_portion and and_portion[0])\ + and not (or_portion and or_portion[0]): + empty = True + + return empty + + @classmethod + def order_is_empty(cls, orderBy): + empty = False + + if not orderBy: + empty = True + + return empty + + @classmethod + def get_connection_resolver(cls): + ''' + This method returns a generic connection resolver for every nodetype in NOCAutoQuery + ''' + type_name = cls.get_type_name() + + def generic_list_resolver(self, info, **args): + ''' + The idea for the connection resolver is to filter the whole NodeHandle + queryset using the date and users in the filter input, but also + the neo4j attributes exposed in the api. + + Likewise, the ordering of it is based in neo4j attributes, so in + order to return an ordered node collection we have to query each + node by its handle_id and append to a list. + ''' + ret = NodeHandle.objects.none() + filter = args.get('filter', None) + orderBy = args.get('orderBy', None) + + apply_handle_id_order = False + revert_default_order = False + use_neo4j_query = False + + context = cls.get_type_context() + + if info.context and info.context.user.is_authenticated and \ + sriutils.authorize_list_module(info.context.user, context): + # filtering will take a different approach + nodes = None + node_type = NodeType.objects.get(type=type_name) + qs = NodeHandle.objects.filter(node_type=node_type) + + # instead of vakt here, we reduce the original qs + # to only the ones the user has right to read + qs = sriutils.trim_readable_queryset(qs, info.context.user) + + # remove default ordering prop if there's no filter + if not cls.order_is_empty(orderBy): + if orderBy == 'handle_id_DESC': + orderBy = None + apply_handle_id_order = True + revert_default_order = False + elif orderBy == 'handle_id_ASC': + orderBy = None + apply_handle_id_order = True + revert_default_order = True + + qs_order_prop = None + qs_order_order = None + + if not cls.order_is_empty(orderBy): + m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) + prop = m[1] + order = m[2] + + if prop in DateQueryBuilder.fields: + # set model attribute ordering + qs_order_prop = prop + qs_order_order = order + + if not cls.filter_is_empty(filter) or not cls.order_is_empty(orderBy): + # filter queryset with dates and users + qs = DateQueryBuilder.filter_queryset(filter, qs) + qs = UserQueryBuilder.filter_queryset(filter, qs) + + # remove order if is a date order + if qs_order_prop and qs_order_order: + orderBy = None + + # create query + q = cls.build_filter_query(filter, orderBy, type_name, + apply_handle_id_order, revert_default_order) + nodes = nc.query_to_list(nc.graphdb.manager, q) + nodes = [ node['n'] for node in nodes] + + use_neo4j_query = True + else: + use_neo4j_query = False + + if use_neo4j_query: + ret = [] + + handle_ids = [] + for node in nodes: + if node['handle_id'] not in handle_ids: + handle_ids.append(node['handle_id']) + + for handle_id in handle_ids: + nodeqs = qs.filter(handle_id=handle_id) + try: + the_node = nodeqs.first() + if the_node: + ret.append(the_node) + except: + pass # nothing to do if the qs doesn't have elements + + # apply date order if it applies + if qs_order_prop and qs_order_order: + reverse = True if qs_order_order == 'DESC' else False + ret.sort(key=lambda x: getattr(x, qs_order_prop, ''), reverse=reverse) + else: + # do nodehandler attributes ordering now that we have + # the nodes set, if this order is requested + if qs_order_prop and qs_order_order: + reverse = True if qs_order_order == 'DESC' else False + + if reverse: + qs = qs.order_by('{}'.format(qs_order_prop)) + else: + qs = qs.order_by('-{}'.format(qs_order_prop)) + + if apply_handle_id_order: + logger.debug('Apply handle_id order') + + if not revert_default_order: + logger.debug('Apply descendent handle_id') + qs = qs.order_by('-handle_id') + else: + logger.debug('Apply ascending handle_id') + qs = qs.order_by('handle_id') + + ret = list(qs) + + if not ret: + ret = [] + + return ret + + return generic_list_resolver + + @classmethod + def build_filter_query(cls, filter, orderBy, nodetype, handle_id_order=False, revert_order=False): + build_query = '' + order_query = '' + optional_matches = '' + + operations = { + 'AND': { + 'filters': [], + 'predicates': [], + }, + 'OR': { + 'filters': [], + 'predicates': [], + }, + } + + # build AND block + and_filters = [] + and_predicates = [] + + if filter and 'AND' in filter: + and_filters = filter.get('AND', []) + operations['AND']['filters'] = and_filters + + # build OR block + or_filters = [] + or_predicates = [] + + if filter and 'OR' in filter: + or_filters = filter.get('OR', []) + operations['OR']['filters'] = or_filters + + # additional clauses + match_additional_nodes = [] + match_additional_rels = [] + + and_node_predicates = [] + and_rels_predicates = [] + + raw_additional_clause = {} + + # neo4j vars dict + neo4j_vars = {} + + # embed entity index + idxdict = { + 'rel_idx': 1, + 'node_idx': 1, + 'subnode_idx': 1, + 'subrel_idx': 1, + } + + filtered_fields = [] + + if filter: + for operation in operations.keys(): + filters = operations[operation]['filters'] + predicates = operations[operation]['predicates'] + + # iterate through the nested filters + for a_filter in filters: + # iterate though values of a nested filter + for filter_key, filter_value in a_filter.items(): + # choose filter array for query building + filter_array, queryBuilder = None, None + is_nested_query = False + neo4j_var = '' + + # transform relay id into handle_id + old_filter_key = filter_key + + try: + if filter_key.index('id') == 0: + # change value + try: # list value + nfilter_value = [] + for fval in filter_value: + handle_id_fval = relay.Node.from_global_id(fval)[1] + handle_id_fval = int(handle_id_fval) + nfilter_value.append(handle_id_fval) + + filter_value = nfilter_value + except: # single value + filter_value = relay.Node.from_global_id(filter_value)[1] + filter_value = int(filter_value) + except ValueError: + pass + + + if isinstance(filter_value, int) or isinstance(filter_value, str): + filter_array = ScalarQueryBuilder.filter_array + queryBuilder = ScalarQueryBuilder + elif isinstance(filter_value, list) and not (\ + isinstance(filter_value[0], str) or isinstance(filter_value[0], int))\ + or issubclass(type(filter_value), graphene.InputObjectType): + # set of type + is_nested_query = True + of_type = None + + if isinstance(filter_value, list): + of_type = filter_value[0]._of_type + else: + of_type = filter_value._of_type + + filter_array = InputFieldQueryBuilder.filter_array + queryBuilder = InputFieldQueryBuilder + additional_clause = of_type.match_additional_clause + + if additional_clause: + if additional_clause not in raw_additional_clause.keys(): + raw_clause = additional_clause + + # format var name and additional match + if issubclass(of_type, NIObjectType): + neo4j_var = '{}{}'.format(of_type.neo4j_var_name, idxdict['node_idx']) + neo4j_vars[of_type] = neo4j_var + additional_clause = additional_clause.format( + 'n:{}'.format(nodetype), + 'l{}'.format(idxdict['subrel_idx']), + idxdict['node_idx'] + ) + idxdict['node_idx'] = idxdict['node_idx'] + 1 + idxdict['subrel_idx'] = idxdict['subrel_idx'] + 1 + match_additional_nodes.append(additional_clause) + elif issubclass(of_type, NIRelationType): + neo4j_var = '{}{}'.format(of_type.neo4j_var_name, idxdict['rel_idx']) + additional_clause = additional_clause.format( + 'n:{}'.format(nodetype), + idxdict['rel_idx'], + 'z{}'.format(idxdict['subnode_idx']) + ) + idxdict['rel_idx'] = idxdict['rel_idx'] + 1 + idxdict['subnode_idx'] = idxdict['subnode_idx'] + 1 + match_additional_rels.append(additional_clause) + + raw_additional_clause[raw_clause] = neo4j_var + else: + neo4j_var = raw_additional_clause[additional_clause] + else: + filter_array = ScalarQueryBuilder.filter_array + queryBuilder = ScalarQueryBuilder + + filter_field = cls.filter_names[filter_key] + field = filter_field['field'] + + # append field to list to avoid the aditional match + filtered_fields.append(field) + + suffix = filter_field['suffix'] + field_type = filter_field['field_type'] + + + # iterate through the keys of the filter array and extracts + # the predicate building function + for fa_suffix, fa_value in filter_array.items(): + # change id field into handle_id for neo4j db + try: + if field.index('id') == 0: + field = field.replace('id', 'handle_id') + except ValueError: + pass + + if fa_suffix != '': + fa_suffix = '_{}'.format(fa_suffix) + + # get the predicate + if suffix == fa_suffix: + build_predicate_func = fa_value['qpredicate'] + + predicate = build_predicate_func(field, filter_value, field_type, neo4j_var=neo4j_var) + + if predicate: + predicates.append(predicate) + elif predicate == "" and is_nested_query: + # if the predicate comes empty, remove + # index increases and additional matches + if issubclass(of_type, NIObjectType): + idxdict['node_idx'] = idxdict['node_idx'] - 1 + idxdict['subrel_idx'] = idxdict['subrel_idx'] - 1 + del match_additional_nodes[-1] + elif issubclass(of_type, NIRelationType): + idxdict['rel_idx'] = idxdict['rel_idx'] - 1 + idxdict['subnode_idx'] = idxdict['subnode_idx'] - 1 + del match_additional_rels[-1] + + operations[operation]['predicates'] = predicates + + and_query = ' AND '.join(operations['AND']['predicates']) + or_query = ' OR '.join(operations['OR']['predicates']) + + if and_query and or_query: + build_query = '({}) AND ({})'.format( + and_query, + or_query + ) + else: + if and_query: + build_query = and_query + elif or_query: + build_query = or_query + + if build_query != '': + build_query = 'WHERE {}'.format(build_query) + + # remove redundant additional clauses + match_additional_nodes = list(set(match_additional_nodes)) + match_additional_rels = list(set(match_additional_rels)) + + # prepare match clause + node_match_clause = "(n:{label})".format(label=nodetype) + additional_match_str = ', '.join( match_additional_nodes + match_additional_rels) + + if additional_match_str: + node_match_clause = '{}, {}'.format(node_match_clause, additional_match_str) + + # create order query + if orderBy: + emptyFilter = False if filter else True + of_type = cls._order_field_match[orderBy]['input_field'] + + additional_clause = '' + + if hasattr(of_type, 'match_additional_clause'): + additional_clause = of_type.match_additional_clause + + m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) + prop = m[1] + order = m[2] + + order_field_present = False + + if prop in filtered_fields: + order_field_present = True + + if issubclass(of_type, NIObjectType): + if order_field_present: + idxdict['node_idx'] = idxdict['node_idx'] - 1 + idxdict['subrel_idx'] = idxdict['subrel_idx'] - 1 + + neo4j_var = '{}{}'.format(of_type.neo4j_var_name, idxdict['node_idx']) + neo4j_vars[of_type] = neo4j_var + additional_clause = additional_clause.format( + 'n:{}'.format(nodetype), + 'l{}'.format(idxdict['subrel_idx']), + idxdict['node_idx'] + ) + + optional_matches = 'OPTIONAL MATCH {}'.format(additional_clause) + order_query = 'ORDER BY {} {}'.format( + '{}.name'.format(neo4j_var), + cls._desc_suffix if cls._order_field_match[orderBy]['is_desc'] else cls._asc_suffix, + ) + elif issubclass(of_type, NIRelationType): + if order_field_present: + idxdict['rel_idx'] = idxdict['rel_idx'] - 1 + idxdict['subnode_idx'] = idxdict['subnode_idx'] - 1 + + neo4j_var = '{}{}'.format(of_type.neo4j_var_name, idxdict['rel_idx']) + neo4j_vars[of_type] = neo4j_var + additional_clause = additional_clause.format( + 'n:{}'.format(nodetype), + idxdict['rel_idx'], + 'z{}'.format(idxdict['subnode_idx']) + ) + + optional_matches = 'OPTIONAL MATCH {}'.format(additional_clause) + order_query = 'ORDER BY {} {}'.format( + '{}.name'.format(neo4j_var), + cls._desc_suffix if cls._order_field_match[orderBy]['is_desc'] else cls._asc_suffix, + ) + + if order_field_present: + optional_matches = '' + + else: + m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) + prop = m[1] + order = m[2] + + order_query = "ORDER BY n.{} {}".format(prop, order) + + if handle_id_order: + order_nibble = 'ASC' if revert_order else 'DESC' + order_query = "ORDER BY n.handle_id {}".format(order_nibble) + + q = """ + MATCH {node_match_clause} + {optional_matches} + {build_query} + RETURN n + {order_query} + """.format(node_match_clause=node_match_clause, + optional_matches=optional_matches, + build_query=build_query, order_query=order_query) + + logger.debug('Neo4j connection filter query:\n{}\n'.format(q)) + + return q + + @classproperty + def match_additional_clause(cls): + return "({})-[{}]-({}{}:{})".format('{}', '{}', cls.neo4j_var_name, '{}', + cls.NIMetaType.ni_type) + + neo4j_var_name = "m" + + class Meta: + model = NodeHandle + interfaces = (relay.Node, ) + +########## END RELATION AND NODE TYPES + + +########## RELATION FIELD +class NIRelationField(NIBasicField): + ''' + This field can be used in NIObjectTypes to represent a set relationships + ''' + def __init__(self, field_type=graphene.List, manual_resolver=False, + type_args=(NIRelationType,), rel_name=None, **kwargs): + self.field_type = field_type + self.manual_resolver = manual_resolver + self.type_args = type_args + self.rel_name = rel_name + + def get_resolver(self, **kwargs): + # getting nimodel + nimodel = nc.models.BaseRelationshipModel + + if self.type_args != (NIRelationType,): + rel_type = self.type_args[0] + nimeta = getattr(rel_type, 'NIMetaType', None) + if nimeta: + nimodel = getattr(nimeta, 'nimodel', None) + + field_name = kwargs.get('field_name') + rel_name = kwargs.get('rel_name') + + if not field_name: + raise Exception( + 'Field name for field {} should not be empty for a {}'.format( + field_name, self.__class__ + ) + ) + def resolve_node_relation(self, info, **kwargs): + ret = [] + reldicts = self.get_node().relationships.get(rel_name, None) + + if reldicts: + for reldict in reldicts: + relbundle = nc.get_relationship_bundle(nc.graphdb.manager, relationship_id=reldict['relationship_id']) + relation = nimodel(nc.graphdb.manager) + relation.load(relbundle) + ret.append(relation) + + return ret + + return resolve_node_relation +########## END RELATION FIELD + +########## MUTATION FACTORY +class NodeHandler(NIObjectType): + name = NIStringField(type_kwargs={ 'required': True }) + + +class DeleteRelationship(relay.ClientIDMutation): + class Input: + relation_id = graphene.Int(required=True) + + success = graphene.Boolean(required=True) + relation_id = graphene.Int(required=True) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + relation_id = input.get("relation_id", None) + success = False + + try: + relationship = nc.get_relationship_model(nc.graphdb.manager, relation_id) + + # check permissions before delete + start_id = relationship.start['handle_id'] + end_id = relationship.end['handle_id'] + + authorized_start = sriutils.authorice_read_resource( + info.context.user, start_id + ) + + authorized_end = sriutils.authorice_read_resource( + info.context.user, end_id + ) + + if authorized_start and authorized_end: + activitylog.delete_relationship(info.context.user, relationship) + relationship.delete() + + success = True + except nc.exceptions.RelationshipNotFound: + success = True + + return DeleteRelationship(success=success, relation_id=relation_id) + + +class AbstractNIMutation(relay.ClientIDMutation): + errors = graphene.List(ErrorType) + + @classmethod + def __init_subclass_with_meta__( + cls, output=None, input_fields=None, arguments=None, name=None, **options + ): + ''' In this method we'll build an input nested object using the form + ''' + # read form + ni_metaclass = getattr(cls, 'NIMetaClass') + graphql_type = getattr(ni_metaclass, 'graphql_type', None) + django_form = getattr(ni_metaclass, 'django_form', None) + mutation_name = getattr(ni_metaclass, 'mutation_name', None) + is_create = getattr(ni_metaclass, 'is_create', False) + is_delete = getattr(ni_metaclass, 'is_delete', False) + include = getattr(ni_metaclass, 'include', None) + exclude = getattr(ni_metaclass, 'exclude', None) + + if include and exclude: + raise Exception('Only "include" or "exclude" metafields can be defined') + + # build fields into Input + inner_fields = {} + if django_form: + for class_field_name, class_field in django_form.__dict__.items(): + if class_field_name == 'declared_fields' or class_field_name == 'base_fields': + for field_name, field in class_field.items(): + # convert form field into mutation input field + graphene_field = cls.form_to_graphene_field(field) + + if graphene_field: + add_field = False + + if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): + if field not in django_form.Meta.exclude: + add_field = True + else: + add_field = True + + if include: + if field_name not in include: + add_field = False + elif exclude: + if field_name in exclude: + add_field = False + + if add_field: + inner_fields[field_name] = graphene_field + + # add handle_id + if not is_create: + inner_fields['id'] = graphene.ID(required=True) + + # add Input attribute to class + inner_class = type('Input', (object,), inner_fields) + setattr(cls, 'Input', inner_class) + + # add Input to private attribute + if graphql_type: + op_name = 'Create' if is_create else 'Update' + op_name = 'Delete' if is_delete else op_name + type_name = graphql_type.__name__ + inner_input = type('Single{}Input'.format(op_name + type_name), + (graphene.InputObjectType, ), inner_fields) + + setattr(ni_metaclass, '_input_list', graphene.List(inner_input)) + setattr(ni_metaclass, '_payload_list', graphene.List(cls)) + + # add the converted fields to the metaclass so we can get them later + setattr(ni_metaclass, 'inner_fields', inner_fields) + setattr(cls, 'NIMetaClass', ni_metaclass) + + cls.add_return_type(graphql_type) + + super(AbstractNIMutation, cls).__init_subclass_with_meta__( + output, inner_fields, arguments, name=mutation_name, **options + ) + + @classmethod + def add_return_type(cls, graphql_type): + if graphql_type: + setattr(cls, graphql_type.__name__.lower(), graphene.Field(graphql_type)) + + @classmethod + def form_to_graphene_field(cls, form_field, include=None, exclude=None): + '''Django form to graphene field conversor + ''' + graphene_field = None + + # get attributes + graph_kwargs = {} + disabled = False + for attr_name, attr_value in form_field.__dict__.items(): + if attr_name == 'required': + graph_kwargs['required'] = attr_value + elif attr_name == 'disabled': + disabled = attr_value + elif attr_name == 'initial': + graph_kwargs['default_value'] = attr_value + + # compare types + if not disabled: + if isinstance(form_field, forms.BooleanField): + graphene_field = graphene.Boolean(**graph_kwargs) + elif isinstance(form_field, forms.CharField): + graphene_field = graphene.String(**graph_kwargs) + elif isinstance(form_field, forms.ChoiceField): + graphene_field = ChoiceScalar(**graph_kwargs) + elif isinstance(form_field, forms.FloatField): + graphene_field = graphene.Float(**graph_kwargs) + elif isinstance(form_field, forms.IntegerField): + graphene_field = graphene.Int(**graph_kwargs) + elif isinstance(form_field, forms.MultipleChoiceField): + graphene_field = graphene.String(**graph_kwargs) + elif isinstance(form_field, forms.URLField): + graphene_field = graphene.String(**graph_kwargs) + else: + graphene_field = graphene.String(**graph_kwargs) + + if isinstance(form_field, forms.NullBooleanField): + graphene_field = NullBoolean(**graph_kwargs) + + ### fields to be implement: ### + # IPAddrField (CharField) + # JSONField (CharField) + # NodeChoiceField (ModelChoiceField) + # DatePickerField (DateField) + # description_field (CharField) + # relationship_field (ChoiceField / IntegerField) + else: + return None + + return graphene_field + + @classmethod + def get_type(cls): + ni_metaclass = getattr(cls, 'NIMetaClass') + return getattr(ni_metaclass, 'typeclass') + + @classmethod + def from_input_to_request(cls, user, **input): + ''' + Gets the input data from the input inner class, and this is build using + the fields in the django form. It returns a nodehandle of the type + defined by the NIMetaClass + ''' + # get ni metaclass data + ni_metaclass = getattr(cls, 'NIMetaClass') + form_class = getattr(ni_metaclass, 'django_form', None) + request_path = getattr(ni_metaclass, 'request_path', '/') + is_create = getattr(ni_metaclass, 'is_create', False) + inner_fields = getattr(ni_metaclass, 'inner_fields', []) + + graphql_type = getattr(ni_metaclass, 'graphql_type') + nimetatype = getattr(graphql_type, 'NIMetaType') + node_type = getattr(nimetatype, 'ni_type').lower() + node_meta_type = getattr(nimetatype, 'ni_metatype').capitalize() + + # get input values + noninput_fields = list(inner_fields.keys()) + input_class = getattr(cls, 'Input', None) + input_params = {} + if input_class: + for attr_name, attr_field in input_class.__dict__.items(): + attr_value = input.get(attr_name) + + if attr_value != None: + input_params[attr_name] = attr_value + if attr_name in noninput_fields: + noninput_fields.remove(attr_name) + + # if it's an edit mutation add handle_id + # and also add the existent values in the request + if not is_create: + input_params['id'] = input.get('id') + handle_id = None + handle_id = relay.Node.from_global_id(input_params['id'])[1] + + # get previous instance + nh = NodeHandle.objects.get(handle_id=handle_id) + node = nh.get_node() + for noninput_field in noninput_fields: + if noninput_field in node.data: + input_params[noninput_field] = node.data.get(noninput_field) + + # morph ids for relation processors + relations_processors = getattr(ni_metaclass, 'relations_processors', None) + + if relations_processors: + for relation_name in relations_processors.keys(): + relay_id = input.get(relation_name, None) + + if relay_id: + handle_id = relay.Node.from_global_id(relay_id)[1] + input_params[relation_name] = handle_id + + # forge request + request_factory = RequestFactory() + request = request_factory.post(request_path, data=input_params) + request.user = user + + return (request, dict(form_class=form_class, node_type=node_type, + node_meta_type=node_meta_type)) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + if not info.context or not info.context.user.is_authenticated: + raise GraphQLAuthException() + + # convert the input to a request object for the form to processs + reqinput = cls.from_input_to_request(info.context.user, **input) + + # get input context, otherwise get the type context + graphql_type = cls.get_graphql_type() + input_context = input.get('context', graphql_type.get_type_context()) + # add it to the dict + reqinput[1]['input_context'] = input_context + + # call subclass do_request method + has_error, ret = cls.do_request(reqinput[0], **reqinput[1]) + + init_params = {} + + if not has_error: + for key, value in ret.items(): + init_params[key] = value + else: + init_params['errors'] = ret + + return cls(**init_params) + + @classmethod + def get_graphql_type(cls): + ni_metaclass = getattr(cls, 'NIMetaClass') + graphql_type = getattr(ni_metaclass, 'graphql_type', None) + + return graphql_type + + @classmethod + def format_error_array(cls, errordict): + errors = [] + + for key, value in errordict.items(): + errors.append(ErrorType(field=key, messages=[value.as_data()[0].messages[0]])) + + return errors + + @classmethod + def process_relations(cls, request, form, nodehandler): + nimetaclass = getattr(cls, 'NIMetaClass') + relations_processors = getattr(nimetaclass, 'relations_processors', None) + + if relations_processors: + for relation_name, relation_f in relations_processors.items(): + relation_f(request, form, nodehandler, relation_name) + + @classmethod + def process_subentities(cls, request, form, master_nh, context): + nimetaclass = getattr(cls, 'NIMetaClass') + subentity_processors = getattr(nimetaclass, 'subentity_processors', None) + + if subentity_processors: + for sub_name, sub_props in subentity_processors.items(): + type_slug = sub_props['type_slug'] + fields = sub_props['fields'] + meta_type = sub_props['meta_type'] + link_method = sub_props['link_method'] + + node_type = NodeType.objects.get(slug=type_slug) + + # forge attributes object + input_params = {} + sub_handle_id = None + node_name = None + + for fform_name, fform_value in fields.items(): + if fform_name == 'id': + sub_id = form.cleaned_data.get(fform_value, None) + if sub_id: + _type, sub_handle_id = relay.Node.from_global_id(sub_id) + else: + if fform_name == 'name': + node_name = form.cleaned_data.get(fform_value, None) + + input_params[fform_name] = form.cleaned_data.get(fform_value, None) + + nh = None + + # create or edit entity + if node_name: + if not sub_handle_id: # create + nh = NodeHandle( + node_name=node_name, + node_type=node_type, + node_meta_type=meta_type, + creator=request.user, + modifier=request.user, + ) + nh.save() + else: # edit + nh = NodeHandle.objects.get(handle_id=sub_handle_id) + + # add neo4j attributes + for key, value in input_params.items(): + nh.get_node().remove_property(key) + nh.get_node().add_property(key, value) + + # add relation to master node + link_method = getattr(master_nh, link_method, None) + + if link_method: + link_method(nh.handle_id) + + # add to permission context + NodeHandleContext(nodehandle=nh, context=context).save() + + class Meta: + abstract = True + +''' +This classes are used by the Mutation factory but it could be used as the +superclass of a manualy coded class in case it's needed. +''' + +class CreateNIMutation(AbstractNIMutation): + ''' + Implements a creation mutation for a specific NodeType + ''' + class NIMetaClass: + request_path = None + is_create = True + graphql_type = None + include = None + exclude = None + + @classmethod + def get_form_to_nodehandle_func(cls): + return helpers.form_to_generic_node_handle + + @classmethod + def do_request(cls, request, **kwargs): + form_class = kwargs.get('form_class') + context = kwargs.get('input_context') + + nimetaclass = getattr(cls, 'NIMetaClass') + graphql_type = getattr(nimetaclass, 'graphql_type') + property_update = getattr(nimetaclass, 'property_update', None) + relay_extra_ids = getattr(nimetaclass, 'relay_extra_ids', None) + + nimetatype = getattr(graphql_type, 'NIMetaType') + node_type = getattr(nimetatype, 'ni_type').lower() + node_meta_type = getattr(nimetatype, 'ni_metatype').capitalize() + + has_error = False + + # check it can write on this context + authorized = sriutils.authorize_create_resource(request.user, context) + + if not authorized: + raise GraphQLAuthException() + + ## code from role creation + post_data = request.POST.copy() + + # convert relay ids to django ids + if relay_extra_ids: + for extra_id in relay_extra_ids: + rela_id_val = post_data.get(extra_id) + if rela_id_val: + rela_id_val = relay.Node.from_global_id(rela_id_val)[1] + post_data.pop(extra_id) + post_data.update({ extra_id: rela_id_val}) + + form = form_class(post_data) + if form.is_valid(): + try: + form_to_nodehandle = cls.get_form_to_nodehandle_func() + nh = form_to_nodehandle(request, form, + node_type, node_meta_type) + except UniqueNodeError: + has_error = True + return has_error, [ErrorType(field="_", messages=["A {} with that name already exists.".format(node_type)])] + + helpers.form_update_node(request.user, nh.handle_id, form, property_update) + + # add default context + NodeHandleContext(nodehandle=nh, context=context).save() + + # process relations if implemented + if not has_error: + nh_reload, nodehandler = helpers.get_nh_node(nh.handle_id) + cls.process_relations(request, form, nodehandler) + + # process subentities if implemented + if not has_error: + nh_reload, nodehandler = helpers.get_nh_node(nh.handle_id) + cls.process_subentities(request, form, nodehandler, context) + + return has_error, { graphql_type.__name__.lower(): nh } + else: + # get the errors and return them + has_error = True + errordict = cls.format_error_array(form.errors) + return has_error, errordict + + +class CreateUniqueNIMutation(CreateNIMutation): + ''' + Implements a creation mutation for a specific NodeType, the difference + between this and CreateNIMutation is that this mutation create unique nodes + ''' + + @classmethod + def get_form_to_nodehandle_func(cls): + return helpers.form_to_unique_node_handle + + +class UpdateNIMutation(AbstractNIMutation): + ''' + Implements an update mutation for a specific NodeType + ''' + class NIMetaClass: + request_path = None + graphql_type = None + include = None + exclude = None + + @classmethod + def do_request(cls, request, **kwargs): + form_class = kwargs.get('form_class') + context = kwargs.get('input_context') + + nimetaclass = getattr(cls, 'NIMetaClass') + graphql_type = getattr(nimetaclass, 'graphql_type') + property_update = getattr(nimetaclass, 'property_update', None) + relay_extra_ids = getattr(nimetaclass, 'relay_extra_ids', None) + + nimetatype = getattr(graphql_type, 'NIMetaType') + node_type = getattr(nimetatype, 'ni_type').lower() + node_meta_type = getattr(nimetatype, 'ni_metatype').capitalize() + context_method = getattr(nimetatype, 'context_method') + id = request.POST.get('id') + has_error = False + + # check authorization + handle_id = relay.Node.from_global_id(id)[1] + authorized = sriutils.authorice_write_resource(request.user, handle_id) + + if not authorized: + raise GraphQLAuthException() + + nh, nodehandler = helpers.get_nh_node(handle_id) + if request.POST: + post_data = request.POST.copy() + + # convert relay ids to django ids + if relay_extra_ids: + for extra_id in relay_extra_ids: + rela_id_val = post_data.get(extra_id) + if rela_id_val: + rela_id_val = relay.Node.from_global_id(rela_id_val)[1] + post_data.pop(extra_id) + post_data.update({ extra_id: rela_id_val}) + + form = form_class(post_data) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, nodehandler.handle_id, form, property_update) + + # process relations if implemented + cls.process_relations(request, form, nodehandler) + + # process subentities if implemented + cls.process_subentities(request, form, nodehandler, context) + + return has_error, { graphql_type.__name__.lower(): nh } + else: + has_error = True + errordict = cls.format_error_array(form.errors) + return has_error, errordict + else: + # get the errors and return them + has_error = True + errordict = cls.format_error_array(form.errors) + return has_error, errordict + + +class DeleteNIMutation(AbstractNIMutation): + ''' + Implements an delete mutation for a specific NodeType + ''' + class NIMetaClass: + request_path = None + graphql_type = None + is_delete = False + + @classmethod + def add_return_type(cls, graphql_type): + setattr(cls, 'success', graphene.Boolean(required=True)) + + @classmethod + def do_request(cls, request, **kwargs): + id = request.POST.get('id') + handle_id = relay.Node.from_global_id(id)[1] + + if not handle_id or \ + not NodeHandle.objects.filter(handle_id=handle_id).exists(): + + has_error = True + return has_error, [ + ErrorType( + field="_", + messages=["The node doesn't exist".format(node_type)] + ) + ] + + # check authorization + authorized = sriutils.authorice_write_resource(request.user, handle_id) + + if not authorized: + raise GraphQLAuthException() + + nh, node = helpers.get_nh_node(handle_id) + + # delete associated nodes + cls.delete_nodes(nh, request.user) + + # delete node + success = helpers.delete_node(request.user, node.handle_id) + + return not success, {'success': success} + + @classmethod + def delete_nodes(cls, nodehandler, user): + nimetaclass = getattr(cls, 'NIMetaClass') + delete_nodes = getattr(nimetaclass, 'delete_nodes', None) + + if delete_nodes: + for relation_name, relation_f in delete_nodes.items(): + relation_f(nodehandler, relation_name, user) + +class MultipleMutation(relay.ClientIDMutation): + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + if not info.context or not info.context.user.is_authenticated: + raise GraphQLAuthException() + + # get input values + create_inputs = input.get("create_inputs") + update_inputs = input.get("update_inputs") + delete_inputs = input.get("delete_inputs") + detach_inputs = input.get("detach_inputs") + + # get underlying mutations + nimetaclass = getattr(cls, 'NIMetaClass') + create_mutation = getattr(nimetaclass, 'create_mutation', None) + update_mutation = getattr(nimetaclass, 'update_mutation', None) + delete_mutation = getattr(nimetaclass, 'delete_mutation', None) + detach_mutation = getattr(nimetaclass, 'detach_mutation', None) + + ret_created = [] + if create_inputs: + for input in create_inputs: + ret = create_mutation.mutate_and_get_payload(root, info, **input) + ret_created.append(ret) + + ret_updated = [] + if update_inputs: + for input in update_inputs: + ret = update_mutation.mutate_and_get_payload(root, info, **input) + ret_updated.append(ret) + + ret_deleted = [] + if delete_inputs: + for input in delete_inputs: + ret = delete_mutation.mutate_and_get_payload(root, info, **input) + ret_deleted.append(ret) + + ret_detached = [] + if detach_inputs: + for input in detach_inputs: + ret = detach_mutation.mutate_and_get_payload(root, info, **input) + ret_deleted.append(ret) + + return cls( + created=ret_created, updated=ret_updated, + deleted=ret_deleted, detached=ret_detached + ) + + +class CompositeMutation(relay.ClientIDMutation): + @classmethod + def get_link_kwargs(cls, master_input, slave_input): + return {} + + @classmethod + def link_slave_to_master(cls, user, master_nh, slave_nh, **kwargs): + pass + + @classmethod + def forge_payload(cls, **kwargs): + return cls(**kwargs) + + @classmethod + def process_extra_subentities(cls, user, master_nh, root, info, input, context): + pass + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + # check if the user is authenticated + if not info.context or not info.context.user.is_authenticated: + raise GraphQLAuthException() + + # get main entity possible inputs + user = info.context.user + create_input = input.get("create_input") + update_input = input.get("update_input") + + nimetaclass = getattr(cls, 'NIMetaClass') + graphql_type = getattr(nimetaclass, 'graphql_type', None) + graphql_subtype = getattr(nimetaclass, 'graphql_subtype', None) + create_mutation = getattr(nimetaclass, 'create_mutation', None) + update_mutation = getattr(nimetaclass, 'update_mutation', None) + context = getattr(nimetaclass, 'context', None) + + # this handle_id will be set to the created or updated main entity + main_handle_id = None + ret_created = None + ret_updated = None + + ret_subcreated = None + ret_subupdated = None + ret_subdeleted = None + ret_unlinked = None + + has_main_errors = False + + # perform main operation + create = False + + if create_input: + create = True + create_input['context'] = context + ret_created = create_mutation.mutate_and_get_payload(root, info, **create_input) + elif update_input: + update_input['context'] = context + ret_updated = update_mutation.mutate_and_get_payload(root, info, **update_input) + else: + raise Exception('At least an input should be provided') + + main_ret = ret_created if create else ret_updated + main_input = create_input if create else update_input + + # extract handle_id from the returned payload + extract_param = graphql_type.get_from_nimetatype('ni_type').lower() + main_nh = getattr(main_ret, extract_param, None) + main_handle_id = None + + if main_nh: + main_handle_id = main_nh.handle_id + + # check if there's errors in the form + errors = getattr(main_ret, 'errors', None) + + # extra params for return + ret_extra_subentities = {} + + # if everything went fine, proceed with the subentity list + if main_handle_id and not errors: + extract_param = graphql_subtype.get_from_nimetatype('ni_type').lower() + + create_subinputs = input.get("create_subinputs") + update_subinputs = input.get("update_subinputs") + delete_subinputs = input.get("delete_subinputs") + unlink_subinputs = input.get("unlink_subinputs") + + create_submutation = getattr(nimetaclass, 'create_submutation', None) + update_submutation = getattr(nimetaclass, 'update_submutation', None) + delete_submutation = getattr(nimetaclass, 'delete_submutation', None) + unlink_submutation = getattr(nimetaclass, 'unlink_submutation', None) + + if create_subinputs: + ret_subcreated = [] + + for subinput in create_subinputs: + subinput['context'] = context + ret = create_submutation.mutate_and_get_payload(root, info, **subinput) + ret_subcreated.append(ret) + + # link if it's possible + sub_errors = getattr(ret, 'errors', None) + sub_created = getattr(ret, extract_param, None) + + if not sub_errors and sub_created: + link_kwargs = cls.get_link_kwargs(main_input, subinput) + cls.link_slave_to_master(user, main_nh, sub_created, **link_kwargs) + + if update_subinputs: + ret_subupdated = [] + + for subinput in update_subinputs: + subinput['context'] = context + ret = update_submutation.mutate_and_get_payload(root, info, **subinput) + ret_subupdated.append(ret) + + # link if it's possible + sub_errors = getattr(ret, 'errors', None) + sub_edited = getattr(ret, extract_param, None) + + if not sub_errors and sub_edited: + link_kwargs = cls.get_link_kwargs(main_input, subinput) + cls.link_slave_to_master(user, main_nh, sub_edited, **link_kwargs) + + if delete_subinputs: + ret_subdeleted = [] + + for subinput in delete_subinputs: + ret = delete_submutation.mutate_and_get_payload(root, info, **subinput) + ret_subdeleted.append(ret) + + if unlink_subinputs: + ret_unlinked = [] + + for subinput in unlink_subinputs: + ret = unlink_submutation.mutate_and_get_payload(root, info, **subinput) + ret_unlinked.append(ret) + + ret_extra_subentities = \ + cls.process_extra_subentities(user, main_nh, root, info, input, context) + + payload_kwargs = dict( + created=ret_created, updated=ret_updated, + subcreated=ret_subcreated, subupdated=ret_subupdated, + subdeleted=ret_subdeleted, unlinked=ret_unlinked + ) + + if ret_extra_subentities: + payload_kwargs = {**payload_kwargs, **ret_extra_subentities} + + return cls.forge_payload( + **payload_kwargs + ) + + +class NIMutationFactory(): + ''' + The mutation factory takes a django form, a node type and some parameters + more and generates a mutation to create/update/delete nodes. If a higher + degree of control is needed the classes CreateNIMutation, UpdateNIMutation + and DeleteNIMutation could be subclassed to override any method's behaviour. + ''' + + node_type = None + node_meta_type = None + request_path = None + + create_mutation_class = CreateNIMutation + update_mutation_class = UpdateNIMutation + delete_mutation_class = DeleteNIMutation + + def __init_subclass__(cls, **kwargs): + metaclass_name = 'NIMetaClass' + nh_field = 'nodehandle' + + cls._create_mutation = None + cls._update_mutation = None + cls._delete_mutation = None + + # check defined form attributes + ni_metaclass = getattr(cls, metaclass_name) + form = getattr(ni_metaclass, 'form', None) + create_form = getattr(ni_metaclass, 'create_form', None) + update_form = getattr(ni_metaclass, 'update_form', None) + request_path = getattr(ni_metaclass, 'request_path', None) + graphql_type = getattr(ni_metaclass, 'graphql_type', NodeHandler) + create_include = getattr(ni_metaclass, 'create_include', None) + create_exclude = getattr(ni_metaclass, 'create_exclude', None) + update_include = getattr(ni_metaclass, 'update_include', None) + update_exclude = getattr(ni_metaclass, 'update_exclude', None) + property_update = getattr(ni_metaclass, 'property_update', None) + relay_extra_ids = getattr(ni_metaclass, 'relay_extra_ids', None) + + manual_create = getattr(ni_metaclass, 'manual_create', None) + manual_update = getattr(ni_metaclass, 'manual_update', None) + + # check for relationship processors and delete associated nodes functions + relations_processors = getattr(ni_metaclass, 'relations_processors', None) + delete_nodes = getattr(ni_metaclass, 'delete_nodes', None) + subentity_processors = getattr(ni_metaclass, 'subentity_processors', None) + + # we'll retrieve these values NI type/metatype from the GraphQLType + nimetatype = getattr(graphql_type, 'NIMetaType') + node_type = getattr(nimetatype, 'ni_type').lower() + + # specify and set create and update forms + assert form and not create_form and not update_form or\ + create_form and update_form and not form, \ + 'You must specify form or both create_form and edit_form in {}'\ + .format(cls.__name__) + + if form: + create_form = form + update_form = form + + # create mutations + class_name = 'Create{}'.format(node_type.capitalize()) + attr_dict = { + 'django_form': create_form, + 'request_path': request_path, + 'is_create': True, + 'graphql_type': graphql_type, + 'include': create_include, + 'exclude': create_exclude, + 'property_update': property_update, + 'relay_extra_ids': relay_extra_ids, + } + + if relations_processors: + attr_dict['relations_processors'] = relations_processors + + if subentity_processors: + attr_dict['subentity_processors'] = subentity_processors + + create_metaclass = type(metaclass_name, (object,), attr_dict) + + if not manual_create: + cls._create_mutation = type( + class_name, + (cls.create_mutation_class,), + { + metaclass_name: create_metaclass, + }, + ) + else: + cls._create_mutation = manual_create + + class_name = 'Update{}'.format(node_type.capitalize()) + attr_dict['django_form'] = update_form + attr_dict['is_create'] = False + attr_dict['include'] = update_include + attr_dict['exclude'] = update_exclude + + if relations_processors: + attr_dict['relations_processors'] = relations_processors + + update_metaclass = type(metaclass_name, (object,), attr_dict) + + if not manual_update: + cls._update_mutation = type( + class_name, + (cls.update_mutation_class,), + { + metaclass_name: update_metaclass, + }, + ) + else: + cls._update_mutation = manual_update + + class_name = 'Delete{}'.format(node_type.capitalize()) + del attr_dict['django_form'] + del attr_dict['include'] + del attr_dict['exclude'] + del attr_dict['property_update'] + del attr_dict['relay_extra_ids'] + attr_dict['is_delete'] = True + + if relations_processors: + del attr_dict['relations_processors'] + + if delete_nodes: + attr_dict['delete_nodes'] = delete_nodes + + if subentity_processors: + del attr_dict['subentity_processors'] + + delete_metaclass = type(metaclass_name, (object,), attr_dict) + + cls._delete_mutation = type( + class_name, + (cls.delete_mutation_class,), + { + metaclass_name: delete_metaclass, + }, + ) + + # make multiple mutation + class_name = 'Multiple{}'.format(node_type.capitalize()) + + # create input class + multi_attr_input_list = { + 'create_inputs': cls._create_mutation.NIMetaClass._input_list, + 'update_inputs': cls._update_mutation.NIMetaClass._input_list, + 'delete_inputs': cls._delete_mutation.NIMetaClass._input_list, + 'detach_inputs': graphene.List(DeleteRelationship.Input), + } + + inner_class = type('Input', (object,), multi_attr_input_list) + + # metaclass + metaclass_attr = { + 'create_mutation': cls._create_mutation, + 'update_mutation': cls._update_mutation, + 'delete_mutation': cls._delete_mutation, + 'detach_mutation': DeleteRelationship, + } + + meta_class = type('NIMetaClass', (object,), metaclass_attr) + + # create class + multiple_attr_list = { + 'Input': inner_class, + 'created': cls._create_mutation.NIMetaClass._payload_list, + 'updated': cls._update_mutation.NIMetaClass._payload_list, + 'deleted': cls._delete_mutation.NIMetaClass._payload_list, + 'detached': graphene.List(DeleteRelationship), + 'NIMetaClass': meta_class + } + + cls._multiple_mutation = type( + class_name, + (MultipleMutation,), + multiple_attr_list + ) + + + @classmethod + def get_create_mutation(cls, *args, **kwargs): + return cls._create_mutation + + @classmethod + def get_update_mutation(cls, *args, **kwargs): + return cls._update_mutation + + @classmethod + def get_delete_mutation(cls, *args, **kwargs): + return cls._delete_mutation + + @classmethod + def get_multiple_mutation(cls, *args, **kwargs): + return cls._multiple_mutation + +########## END MUTATION FACTORY + +########## EXCEPTION +class GraphQLAuthException(Exception): + ''' + Simple auth exception + ''' + def __init__(self, message=None): + message = 'You must be logged in the system: {}'.format( + ': {}'.format(message) if message else '' + ) + super().__init__(message) + +########## END EXCEPTION diff --git a/src/niweb/apps/noclook/schema/fields.py b/src/niweb/apps/noclook/schema/fields.py new file mode 100644 index 000000000..8f01f763c --- /dev/null +++ b/src/niweb/apps/noclook/schema/fields.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook.models import NodeHandle +from .scalars import ChoiceScalar + +import graphene +import types + + +########## KEYVALUE TYPES +class KeyValue(graphene.Interface): + name = graphene.String(required=True) + value = graphene.String(required=True) + +class DictEntryType(graphene.ObjectType): + ''' + This type represents an key value pair in a dictionary for the data + dict of the norduniclient nodes + ''' + + class Meta: + interfaces = (KeyValue, ) + +def resolve_nidata(self, info, **kwargs): + ''' + Resolvers norduniclient nodes data dictionary + ''' + ret = [] + + alldata = self.get_node().data + for key, value in alldata.items(): + ret.append(DictEntryType(name=key, value=value)) + + return ret + +########## END KEYVALUE TYPES + +class NIBasicField(): + ''' + Super class of the type fields + ''' + def __init__(self, field_type=graphene.String, manual_resolver=False, + type_kwargs=None, **kwargs): + + self.field_type = field_type + self.manual_resolver = manual_resolver + self.type_kwargs = type_kwargs + + def get_resolver(self, **kwargs): + field_name = kwargs.get('field_name') + if not field_name: + raise Exception( + 'Field name for field {} should not be empty for a {}'.format( + field_name, self.__class__ + ) + ) + def resolve_node_value(instance, info, **kwargs): + node = self.get_inner_node(instance) + return node.data.get(field_name) + + return resolve_node_value + + def get_field_type(self): + return self.field_type + + def get_inner_node(self, instance): + if not hasattr(instance, '_node'): + instance._node = instance.get_node() + + return instance._node + +class NIStringField(NIBasicField): + ''' + String type + ''' + pass + +class NIIntField(NIBasicField): + ''' + Int type + ''' + def __init__(self, field_type=graphene.Int, manual_resolver=False, + type_kwargs=None, **kwargs): + super(NIIntField, self).__init__(field_type, manual_resolver, + type_kwargs, **kwargs) + +class NIChoiceField(NIBasicField): + ''' + Choice type + ''' + def __init__(self, field_type=ChoiceScalar, manual_resolver=False, + type_kwargs=None, **kwargs): + super(NIChoiceField, self).__init__(field_type, manual_resolver, + type_kwargs, **kwargs) + + +class NIBooleanField(NIBasicField): + ''' + Boolean type + ''' + def __init__(self, field_type=graphene.Boolean, manual_resolver=False, + type_kwargs=None, **kwargs): + super(NIBooleanField, self).__init__(field_type, manual_resolver, + type_kwargs, **kwargs) + + def get_resolver(self, **kwargs): + field_name = kwargs.get('field_name') + if not field_name: + raise Exception( + 'Field name for field {} should not be empty for a {}'.format( + field_name, self.__class__ + ) + ) + def resolve_node_value(instance, info, **kwargs): + possible_value = self.get_inner_node(instance).data.get(field_name) + if possible_value == None: + possible_value = False + + return possible_value + + return resolve_node_value + + +class NIListField(NIBasicField): + ''' + Object list type + ''' + def __init__(self, field_type=graphene.List, manual_resolver=False, + type_args=None, rel_name=None, rel_method=None, + not_null_list=False, **kwargs): + + self.field_type = field_type + self.manual_resolver = manual_resolver + self.type_args = type_args + self.rel_name = rel_name + self.rel_method = rel_method + self.not_null_list = not_null_list + + def get_resolver(self, **kwargs): + rel_name = kwargs.get('rel_name') + rel_method = kwargs.get('rel_method') + + def resolve_relationship_list(instance, info, **kwargs): + neo4jnode = self.get_inner_node(instance) + relations = getattr(neo4jnode, rel_method)() + nodes = relations.get(rel_name) + + handle_id_list = [] + if nodes: + for node in nodes: + node = node['node'] + node_id = node.data.get('handle_id') + handle_id_list.append(node_id) + + ret = NodeHandle.objects.filter(handle_id__in=handle_id_list).order_by('handle_id') + + return ret + + return resolve_relationship_list + + +class IDRelation(graphene.ObjectType): + entity_id = graphene.ID() + relation_id = graphene.Int() + + +def is_lambda_function(obj): + return isinstance(obj, types.LambdaType) and obj.__name__ == "" + + +class NIRelationListField(NIBasicField): + ''' + ID/relation_id list type + ''' + def __init__(self, field_type=graphene.List, manual_resolver=False, + type_args=(IDRelation,), rel_name=None, rel_method=None, + not_null_list=False, graphene_type=None, **kwargs): + + self.field_type = field_type + self.manual_resolver = manual_resolver + self.type_args = type_args + self.rel_name = rel_name + self.rel_method = rel_method + self.not_null_list = not_null_list + self.graphene_type = graphene_type + + def get_resolver(self, **kwargs): + rel_name = kwargs.get('rel_name') + rel_method = kwargs.get('rel_method') + graphene_type = self.graphene_type + + def resolve_relationship_list(instance, info, **kwargs): + neo4jnode = self.get_inner_node(instance) + relations = getattr(neo4jnode, rel_method)() + nodes = relations.get(rel_name) + + if is_lambda_function(graphene_type): + type_str = str(graphene_type()) + else: + type_str = str(graphene_type) + + handle_id_list = [] + if nodes: + for node in nodes: + relation_id = node['relationship_id'] + node = node['node'] + node_id = node.data.get('handle_id') + id = graphene.relay.Node.to_global_id( + type_str, str(node_id) + ) + id_relation = IDRelation() + id_relation.entity_id = id + id_relation.relation_id = relation_id + handle_id_list.append(id_relation) + + return handle_id_list + + return resolve_relationship_list diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py new file mode 100644 index 000000000..18a3f6cf6 --- /dev/null +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -0,0 +1,1072 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import graphene +import norduniclient as nc +import apps.noclook.vakt.utils as sriutils + +from apps.noclook import activitylog, helpers +from apps.noclook.forms import * +from apps.noclook.models import Dropdown as DropdownModel, Role as RoleModel, \ + DEFAULT_ROLES, DEFAULT_ROLES, DEFAULT_ROLE_KEY, Choice as ChoiceModel +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.shortcuts import get_current_site +from django.test import RequestFactory +from django_comments.forms import CommentForm +from django_comments.models import Comment +from graphene import Field +from graphene_django.forms.mutation import DjangoModelFormMutation, BaseDjangoFormMutation +from django.core.exceptions import ObjectDoesNotExist + +from binascii import Error as BinasciiError + +from .core import NIMutationFactory, CreateNIMutation, CommentType +from .types import * + +logger = logging.getLogger(__name__) + +class NIGroupMutationFactory(NIMutationFactory): + class NIMetaClass: + create_form = NewGroupForm + update_form = EditGroupForm + request_path = '/' + graphql_type = Group + + class Meta: + abstract = False + + +class NIProcedureMutationFactory(NIMutationFactory): + class NIMetaClass: + create_form = NewProcedureForm + update_form = EditProcedureForm + request_path = '/' + graphql_type = Procedure + + class Meta: + abstract = False + + +def empty_processor(request, form, nodehandler, relation_name): + pass + + +def process_works_for(request, form, nodehandler, relation_name): + if relation_name in form.cleaned_data and 'role' in form.cleaned_data and \ + form.cleaned_data[relation_name] and form.cleaned_data['role']: + + organization_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) + role_handle_id = form.cleaned_data['role'] + role = RoleModel.objects.get(handle_id=role_handle_id) + helpers.set_works_for(request.user, nodehandler, organization_nh.handle_id, role.name) + + +def process_member_of(request, form, nodehandler, relation_name): + if relation_name in form.cleaned_data and form.cleaned_data[relation_name]: + group_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) + helpers.set_member_of(request.user, nodehandler, group_nh.handle_id) + + +def process_has_phone(request, form, nodehandler, relation_name): + if relation_name in form.cleaned_data and form.cleaned_data[relation_name]: + contact_id = form.cleaned_data[relation_name] + helpers.add_phone_contact(request.user, nodehandler, contact_id) + + +def process_has_email(request, form, nodehandler, relation_name): + if relation_name in form.cleaned_data and form.cleaned_data[relation_name]: + contact_id = form.cleaned_data[relation_name] + helpers.add_email_contact(request.user, nodehandler, contact_id) + + +def process_has_address(request, form, nodehandler, relation_name): + if relation_name in form.cleaned_data and form.cleaned_data[relation_name]: + organization_id = form.cleaned_data[relation_name] + helpers.add_address_organization(request.user, nodehandler, organization_id) + + +class NIPhoneMutationFactory(NIMutationFactory): + class NIMetaClass: + form = PhoneForm + request_path = '/' + graphql_type = Phone + relations_processors = { + 'contact': process_has_phone, + } + property_update = ['name', 'type'] + + class Meta: + abstract = False + + +class NIEmailMutationFactory(NIMutationFactory): + class NIMetaClass: + form = EmailForm + request_path = '/' + graphql_type = Email + + relations_processors = { + 'contact': process_has_email, + } + property_update = ['name', 'type'] + + class Meta: + abstract = False + + +class NIAddressMutationFactory(NIMutationFactory): + class NIMetaClass: + form = AddressForm + request_path = '/' + graphql_type = Address + + relations_processors = { + 'organization': process_has_address, + } + property_update = ['name', 'phone', 'street', 'postal_code', 'postal_area'] + + class Meta: + abstract = False + + +def delete_outgoing_nodes(nodehandler, relation_name, user): + node = nodehandler.get_node() + relations = node.get_outgoing_relations() + + for relname, link_nodes in relations.items(): + if relname == relation_name: + for link_node in link_nodes: + link_node = link_node['node'] + helpers.delete_node(user, link_node.handle_id) + + +class NIContactMutationFactory(NIMutationFactory): + class NIMetaClass: + form = MailPhoneContactForm + request_path = '/' + graphql_type = Contact + relations_processors = { + 'relationship_works_for': process_works_for, + 'relationship_member_of': process_member_of, + } + + subentity_processors = { + 'email': { + 'form': EmailForm, + 'type_slug': 'email', + 'meta_type': 'Logical', + 'fields': { + 'id': 'email_id', + 'name': 'email', + 'type': 'email_type', + }, + 'link_method': 'add_email', + }, + 'phone': { + 'form': PhoneForm, + 'type_slug': 'phone', + 'meta_type': 'Logical', + 'fields': { + 'id': 'phone_id', + 'name': 'phone', + 'type': 'phone_type', + }, + 'link_method': 'add_phone', + }, + } + + delete_nodes = { + 'Has_email': delete_outgoing_nodes, + 'Has_phone': delete_outgoing_nodes, + } + + property_update = [ + 'first_name', 'last_name', 'contact_type', 'name', 'title', + 'pgp_fingerprint', 'notes' + ] + + relay_extra_ids = ['role', ] + + class Meta: + abstract = False + + +class CreateOrganization(CreateNIMutation): + @classmethod + def do_request(cls, request, **kwargs): + form_class = kwargs.get('form_class') + nimetaclass = getattr(cls, 'NIMetaClass') + graphql_type = getattr(nimetaclass, 'graphql_type') + nimetatype = getattr(graphql_type, 'NIMetaType') + node_type = getattr(nimetatype, 'ni_type').lower() + node_meta_type = getattr(nimetatype, 'ni_metatype').capitalize() + context_method = getattr(nimetatype, 'context_method') + has_error = False + + context = context_method() + + # check it can write on this context + authorized = sriutils.authorize_create_resource(request.user, context) + + if not authorized: + raise GraphQLAuthException() + + # Get needed data from node + if request.POST: + # replace relay ids for handle_id in contacts if present + post_data = request.POST.copy() + + for field, roledict in DEFAULT_ROLES.items(): + if field in post_data: + handle_id = post_data.get(field) + handle_id = relay.Node.from_global_id(handle_id)[1] + post_data.pop(field) + post_data.update({field: handle_id}) + + relay_extra_ids = ('relationship_parent_of', 'relationship_uses_a') + for field in relay_extra_ids: + handle_id = post_data.get(field) + if handle_id: + try: + handle_id = relay.Node.from_global_id(handle_id)[1] + post_data.pop(field) + post_data.update({field: handle_id}) + except BinasciiError: + pass # the id is already in handle_id format + + form = form_class(post_data) + form.strict_validation = True + + if form.is_valid(): + try: + nh = helpers.form_to_generic_node_handle(request, form, + node_type, node_meta_type) + except UniqueNodeError: + has_error = True + return has_error, [ErrorType(field="_", messages=["A {} with that name already exists.".format(node_type)])] + + # Generic node update + # use property keys to avoid inserting contacts as a string property of the node + property_keys = [ + 'name', 'description', 'organization_id', 'type', 'incident_management_info', + 'affiliation_customer', 'affiliation_end_customer', 'affiliation_provider', + 'affiliation_partner', 'affiliation_host_user', 'affiliation_site_owner', + 'website', 'organization_number' + ] + helpers.form_update_node(request.user, nh.handle_id, form, property_keys) + nh_reload, organization = helpers.get_nh_node(nh.handle_id) + + # add default context + NodeHandleContext(nodehandle=nh, context=context).save() + + # specific role setting + for field, roledict in DEFAULT_ROLES.items(): + if field in form.cleaned_data: + contact_id = form.cleaned_data[field] + + role = RoleModel.objects.get(slug=field) + set_contact = helpers.get_contact_for_orgrole(organization.handle_id, role) + + if contact_id: + if set_contact: + if set_contact.handle_id != contact_id: + helpers.unlink_contact_with_role_from_org(request.user, organization, role) + helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + else: + helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + elif set_contact: + helpers.unlink_contact_and_role_from_org(request.user, organization, set_contact.handle_id, role) + + # Set child organizations + if form.cleaned_data['relationship_parent_of']: + organization_nh = NodeHandle.objects.get(handle_id=form.cleaned_data['relationship_parent_of']) + helpers.set_parent_of(request.user, organization, organization_nh.handle_id) + if form.cleaned_data['relationship_uses_a']: + procedure_nh = NodeHandle.objects.get(handle_id=form.cleaned_data['relationship_uses_a']) + helpers.set_uses_a(request.user, organization, procedure_nh.handle_id) + + return has_error, { graphql_type.__name__.lower(): nh } + else: + # get the errors and return them + has_error = True + errordict = cls.format_error_array(form.errors) + return has_error, errordict + else: + # get the errors and return them + has_error = True + errordict = cls.format_error_array(form.errors) + return has_error, errordict + + class NIMetaClass: + django_form = EditOrganizationForm + request_path = '/' + graphql_type = Organization + is_create = True + + relations_processors = { + 'relationship_parent_of': empty_processor, + 'relationship_uses_a': empty_processor, + } + + + +class UpdateOrganization(UpdateNIMutation): + @classmethod + def do_request(cls, request, **kwargs): + form_class = kwargs.get('form_class') + nimetaclass = getattr(cls, 'NIMetaClass') + graphql_type = getattr(nimetaclass, 'graphql_type') + nimetatype = getattr(graphql_type, 'NIMetaType') + node_type = getattr(nimetatype, 'ni_type').lower() + node_meta_type = getattr(nimetatype, 'ni_metatype').capitalize() + id = request.POST.get('id') + has_error = False + + # check authorization + handle_id = relay.Node.from_global_id(id)[1] + authorized = sriutils.authorice_write_resource(request.user, handle_id) + + if not authorized: + raise GraphQLAuthException() + + # Get needed data from node + nh, organization = helpers.get_nh_node(handle_id) + relations = organization.get_relations() + out_relations = organization.get_outgoing_relations() + + if request.POST: + # set handle_id into POST data and remove relay id + post_data = request.POST.copy() + post_data.pop('id') + post_data.update({'handle_id': handle_id}) + + # replace relay ids for handle_id in contacts if present + for field, roledict in DEFAULT_ROLES.items(): + if field in post_data: + handle_id = post_data.get(field) + handle_id = relay.Node.from_global_id(handle_id)[1] + post_data.pop(field) + post_data.update({field: handle_id}) + + relay_extra_ids = ('relationship_parent_of', 'relationship_uses_a') + for field in relay_extra_ids: + handle_id = post_data.get(field) + if handle_id: + handle_id = relay.Node.from_global_id(handle_id)[1] + post_data.pop(field) + post_data.update({field: handle_id}) + + form = form_class(post_data) + form.strict_validation = True + + if form.is_valid(): + # Generic node update + # use property keys to avoid inserting contacts as a string property of the node + property_keys = [ + 'name', 'description', 'organization_id', 'type', 'incident_management_info', + 'affiliation_customer', 'affiliation_end_customer', 'affiliation_provider', + 'affiliation_partner', 'affiliation_host_user', 'affiliation_site_owner', + 'website', 'organization_number' + ] + helpers.form_update_node(request.user, organization.handle_id, form, property_keys) + + # specific role setting + for field, roledict in DEFAULT_ROLES.items(): + if field in form.cleaned_data: + contact_id = form.cleaned_data[field] + role = RoleModel.objects.get(slug=field) + set_contact = helpers.get_contact_for_orgrole(organization.handle_id, role) + + if contact_id: + if set_contact: + if set_contact.handle_id != contact_id: + helpers.unlink_contact_with_role_from_org(request.user, organization, role) + helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + else: + helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + elif set_contact: + helpers.unlink_contact_and_role_from_org(request.user, organization, set_contact.handle_id, role) + + # Set child organizations + if form.cleaned_data['relationship_parent_of']: + organization_nh = NodeHandle.objects.get(handle_id=form.cleaned_data['relationship_parent_of']) + helpers.set_parent_of(request.user, organization, organization_nh.handle_id) + if form.cleaned_data['relationship_uses_a']: + procedure_nh = NodeHandle.objects.get(handle_id=form.cleaned_data['relationship_uses_a']) + helpers.set_uses_a(request.user, organization, procedure_nh.handle_id) + + return has_error, { graphql_type.__name__.lower(): nh } + else: + # get the errors and return them + has_error = True + errordict = cls.format_error_array(form.errors) + return has_error, errordict + else: + # get the errors and return them + has_error = True + errordict = cls.format_error_array(form.errors) + return has_error, errordict + + class NIMetaClass: + django_form = EditOrganizationForm + request_path = '/' + graphql_type = Organization + + +class NIOrganizationMutationFactory(NIMutationFactory): + class NIMetaClass: + create_form = NewOrganizationForm + update_form = EditOrganizationForm + request_path = '/' + graphql_type = Organization + # create_include or create_exclude + + delete_nodes = { + 'Has_address': delete_outgoing_nodes, + } + + manual_create = CreateOrganization + manual_update = UpdateOrganization + + class Meta: + abstract = False + + +class CreateRole(DjangoModelFormMutation): + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + context = sriutils.get_community_context() + + # check it can write on this context + authorized = sriutils.authorize_create_resource(info.context.user, context) + + if not authorized: + raise GraphQLAuthException() + + form = cls.get_form(root, info, **input) + + if form.is_valid(): + return cls.perform_mutate(form, info) + else: + errors = [ + ErrorType(field=key, messages=value) + for key, value in form.errors.items() + ] + + return cls(errors=errors) + + class Meta: + form_class = NewRoleForm + + +class UpdateRole(DjangoModelFormMutation): + class Input: + id = graphene.ID(required=True) + + @classmethod + def get_form_kwargs(cls, root, info, **input): + context = sriutils.get_community_context() + + # check it can write on this context + authorized = sriutils.authorize_create_resource(info.context.user, context) + + if not authorized: + raise GraphQLAuthException() + + kwargs = {"data": input} + + id = input.pop("id", None) + handle_id = relay.Node.from_global_id(id)[1] + if handle_id: + instance = cls._meta.model._default_manager.get(pk=handle_id) + kwargs["instance"] = instance + + return kwargs + + class Meta: + form_class = EditRoleForm + + +class DeleteRole(relay.ClientIDMutation): + class Input: + id = graphene.ID(required=True) + + success = graphene.Boolean(required=True) + id = graphene.ID(required=True) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + id = input.get("id", None) + handle_id = relay.Node.from_global_id(id)[1] + success = False + + context = sriutils.get_community_context() + + # check it can write on this context + authorized = sriutils.authorize_create_resource(info.context.user, context) + + if not authorized: + raise GraphQLAuthException() + + try: + role = RoleModel.objects.get(handle_id=handle_id) + role.delete() + success = True + except ObjectDoesNotExist: + success = False + + return DeleteRole(success=success, id=id) + + +class CreateComment(relay.ClientIDMutation): + class Input: + object_id = graphene.ID(required=True) + comment = graphene.String(required=True) + + comment = Field(CommentType) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + object_id = input.get("object_id") + object_pk = relay.Node.from_global_id(object_id)[1] + + # check it can write for this node + authorized = sriutils.authorice_write_resource(info.context.user, object_pk) + + if not authorized: + raise GraphQLAuthException() + + comment = input.get("comment") + content_type = ContentType.objects.get(app_label="noclook", model="nodehandle") + + request_factory = RequestFactory() + request = request_factory.post('/', data={}) + site = get_current_site(request) + + comment = Comment( + content_type=content_type, + object_pk=object_pk, + site=site, + user=info.context.user, + comment=comment, + ) + comment.save() + + return CreateComment(comment=comment) + +class UpdateComment(relay.ClientIDMutation): + class Input: + id = graphene.ID(required=True) + comment = graphene.String(required=True) + + comment = Field(CommentType) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + id = input.get("id") + id = relay.Node.from_global_id(id)[1] + comment_txt = input.get("comment") + + comment = Comment.objects.get(id=id) + object_pk = comment.object_pk + + # check it can write for this node + authorized = sriutils.authorice_write_resource(info.context.user, object_pk) + + if not authorized: + raise GraphQLAuthException() + + comment.comment = comment_txt + comment.save() + + return UpdateComment(comment=comment) + +class DeleteComment(relay.ClientIDMutation): + class Input: + id = graphene.ID(required=True) + + success = graphene.Boolean(required=True) + id = graphene.ID(required=True) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + relay_id = input.get("id") + id = relay.Node.from_global_id(relay_id)[1] + success = False + + try: + comment = Comment.objects.get(id=id) + object_pk = comment.object_pk + + # check it can write for this node + authorized = sriutils.authorice_write_resource(info.context.user, object_pk) + + if not authorized: + raise GraphQLAuthException() + + comment.delete() + success = True + except ObjectDoesNotExist: + success = False + + return DeleteComment(success=success, id=relay_id) + + +class CreateOptionForDropdown(relay.ClientIDMutation): + class Input: + dropdown_name = graphene.String(required=True) + name = graphene.String(required=True) + value = graphene.String(required=True) + + choice = Field(Choice) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + # only superadmins may add options for dropdowns + authorized = sriutils.authorize_superadmin(info.context.user) + + if not authorized: + raise GraphQLAuthException() + + dropdown_name = input.get("dropdown_name") + name = input.get("name") + value = input.get("value") + dropdown = DropdownModel.objects.get(name=dropdown_name) + + choice = ChoiceModel( + dropdown=dropdown, + name=name, + value=value + ) + choice.save() + + return CreateOptionForDropdown(choice=choice) + + +## Composite mutations +class CompositeGroupMutation(CompositeMutation): + class Input: + create_input = graphene.Field(NIGroupMutationFactory.get_create_mutation().Input) + update_input = graphene.Field(NIGroupMutationFactory.get_update_mutation().Input) + create_subinputs = graphene.List(NIContactMutationFactory.get_create_mutation().Input) + update_subinputs = graphene.List(NIContactMutationFactory.get_update_mutation().Input) + delete_subinputs = graphene.List(NIContactMutationFactory.get_delete_mutation().Input) + unlink_subinputs = graphene.List(DeleteRelationship.Input) + + created = graphene.Field(NIGroupMutationFactory.get_create_mutation()) + updated = graphene.Field(NIGroupMutationFactory.get_update_mutation()) + subcreated = graphene.List(NIContactMutationFactory.get_create_mutation()) + subupdated = graphene.List(NIContactMutationFactory.get_update_mutation()) + subdeleted = graphene.List(NIContactMutationFactory.get_delete_mutation()) + unlinked = graphene.List(DeleteRelationship) + + @classmethod + def link_slave_to_master(cls, user, master_nh, slave_nh): + helpers.set_member_of(user, slave_nh.get_node(), master_nh.handle_id) + + class NIMetaClass: + create_mutation = NIGroupMutationFactory.get_create_mutation() + update_mutation = NIGroupMutationFactory.get_update_mutation() + create_submutation = NIContactMutationFactory.get_create_mutation() + update_submutation = NIContactMutationFactory.get_update_mutation() + delete_submutation = NIContactMutationFactory.get_delete_mutation() + unlink_submutation = DeleteRelationship + graphql_type = Group + graphql_subtype = Contact + context = sriutils.get_community_context() + + +class CompositeOrganizationMutation(CompositeMutation): + class Input: + create_input = graphene.Field(CreateOrganization.Input) + update_input = graphene.Field(UpdateOrganization.Input) + + create_subinputs = graphene.List(NIContactMutationFactory.get_create_mutation().Input) + update_subinputs = graphene.List(NIContactMutationFactory.get_update_mutation().Input) + delete_subinputs = graphene.List(NIContactMutationFactory.get_delete_mutation().Input) + unlink_subinputs = graphene.List(DeleteRelationship.Input) + + create_address = graphene.List(NIAddressMutationFactory.get_create_mutation().Input) + update_address = graphene.List(NIAddressMutationFactory.get_update_mutation().Input) + delete_address = graphene.List(NIAddressMutationFactory.get_delete_mutation().Input) + + created = graphene.Field(CreateOrganization) + updated = graphene.Field(UpdateOrganization) + + subcreated = graphene.List(NIContactMutationFactory.get_create_mutation()) + subupdated = graphene.List(NIContactMutationFactory.get_update_mutation()) + subdeleted = graphene.List(NIContactMutationFactory.get_delete_mutation()) + unlinked = graphene.List(DeleteRelationship) + + address_created = graphene.List(NIAddressMutationFactory.get_create_mutation()) + address_updated = graphene.List(NIAddressMutationFactory.get_update_mutation()) + address_deleted = graphene.List(NIAddressMutationFactory.get_delete_mutation()) + + @classmethod + def get_link_kwargs(cls, master_input, slave_input): + ret = {} + role_id = slave_input.get('role_id', None) + + if role_id: + ret['role_id'] = role_id + + return ret + + @classmethod + def link_slave_to_master(cls, user, master_nh, slave_nh, **kwargs): + role_id = kwargs.get('role_id', None) + role = None + + if role_id: + role_handle_id = relay.Node.from_global_id(role_id)[1] + role = RoleModel.objects.get(handle_id=role_handle_id) + else: + role = RoleModel.objects.get(slug=DEFAULT_ROLE_KEY) + + helpers.link_contact_role_for_organization(user, master_nh.get_node(), slave_nh.handle_id, role) + + @classmethod + def link_address_to_organization(cls, user, master_nh, slave_nh, **kwargs): + helpers.add_address_organization(user, slave_nh.get_node(), master_nh.handle_id) + + @classmethod + def process_extra_subentities(cls, user, main_nh, root, info, input, context): + extract_param = 'address' + ret_subcreated = None + ret_subupdated = None + ret_subdeleted = None + + create_address = input.get("create_address") + update_address = input.get("update_address") + delete_address = input.get("delete_address") + + nimetaclass = getattr(cls, 'NIMetaClass') + address_created = getattr(nimetaclass, 'address_created', None) + address_updated = getattr(nimetaclass, 'address_updated', None) + address_deleted = getattr(nimetaclass, 'address_deleted', None) + + main_handle_id = None + + if main_nh: + main_handle_id = main_nh.handle_id + + if main_handle_id: + if create_address: + ret_subcreated = [] + + for input in create_address: + input['context'] = context + ret = address_created.mutate_and_get_payload(root, info, **input) + ret_subcreated.append(ret) + + # link if it's possible + sub_errors = getattr(ret, 'errors', None) + sub_created = getattr(ret, extract_param, None) + + if not sub_errors and sub_created: + helpers.add_address_organization( + user, sub_created.get_node(), main_handle_id) + + if update_address: + ret_subupdated = [] + + for input in update_address: + input['context'] = context + ret = address_updated.mutate_and_get_payload(root, info, **input) + ret_subupdated.append(ret) + + # link if it's possible + sub_errors = getattr(ret, 'errors', None) + sub_edited = getattr(ret, extract_param, None) + + if not sub_errors and sub_edited: + helpers.add_address_organization( + user, sub_edited.get_node(), main_handle_id) + + if delete_address: + ret_subdeleted = [] + + for input in delete_address: + ret = address_deleted.mutate_and_get_payload(root, info, **input) + ret_subdeleted.append(ret) + + return dict(address_created=ret_subcreated, + address_updated=ret_subupdated, + address_deleted=ret_subdeleted) + + class NIMetaClass: + create_mutation = CreateOrganization + update_mutation = UpdateOrganization + create_submutation = NIContactMutationFactory.get_create_mutation() + update_submutation = NIContactMutationFactory.get_update_mutation() + delete_submutation = NIContactMutationFactory.get_delete_mutation() + unlink_submutation = DeleteRelationship + address_created = NIAddressMutationFactory.get_create_mutation() + address_updated = NIAddressMutationFactory.get_update_mutation() + address_deleted = NIAddressMutationFactory.get_delete_mutation() + graphql_type = Organization + graphql_subtype = Contact + context = sriutils.get_community_context() + + +class RoleRelationMutation(relay.ClientIDMutation): + class Input: + role_id = graphene.ID() + organization_id = graphene.ID(required=True) + relation_id = graphene.Int() + + errors = graphene.List(ErrorType) + rolerelation = Field(RoleRelation) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + if not info.context or not info.context.user.is_authenticated: + raise GraphQLAuthException() + + user = info.context.user + + errors = None + rolerelation = None + + # get input + contact_handle_id = input.get('contact_handle_id', None) + organization_id = input.get('organization_id', None) + role_id = input.get('role_id', None) + relation_id = input.get('relation_id', None) + + if role_id: + role_handle_id = relay.Node.from_global_id(role_id)[1] + else: + default_role = RoleModel.objects.get(slug=DEFAULT_ROLE_KEY) + role_handle_id = default_role.handle_id + + organization_handle_id = relay.Node.from_global_id(organization_id)[1] + + # get entities and check permissions + contact_nh = None + organization_nh = None + role_model = None + + add_error_contact = False + add_error_organization = False + add_error_role = False + + if sriutils.authorice_write_resource(user, contact_handle_id): + try: + contact_nh = NodeHandle.objects.get(handle_id=contact_handle_id) + except: + add_error_contact = True + else: + add_error_contact = True + + if add_error_contact: + error = ErrorType( + field="contact_handle_id", + messages=["The selected contact doesn't exist"] + ) + errors.append(error) + + + if sriutils.authorice_write_resource(user, organization_handle_id): + try: + organization_nh = NodeHandle.objects.get(handle_id=organization_handle_id) + except: + add_error_organization = True + else: + add_error_organization = True + + if add_error_organization: + error = ErrorType( + field="organization_handle_id", + messages=["The selected organization doesn't exist"] + ) + errors.append(error) + + try: + role_model = RoleModel.objects.get(handle_id=role_handle_id) + except: + add_error_role = True + + if add_error_role: + error = ErrorType( + field="role_handle_id", + messages=["The selected role doesn't exist"] + ) + errors.append(error) + + # link contact with organization + if not errors: + contact, rolerelation = helpers.link_contact_role_for_organization( + user, organization_nh.get_node(), contact_nh.handle_id, + role_model, relation_id + ) + + return cls(errors=errors, rolerelation=rolerelation) + + +class CompositeContactMutation(CompositeMutation): + class Input: + create_input = graphene.Field(NIContactMutationFactory.get_create_mutation().Input) + update_input = graphene.Field(NIContactMutationFactory.get_update_mutation().Input) + create_subinputs = graphene.List(NIEmailMutationFactory.get_create_mutation().Input) + update_subinputs = graphene.List(NIEmailMutationFactory.get_update_mutation().Input) + delete_subinputs = graphene.List(NIEmailMutationFactory.get_delete_mutation().Input) + unlink_subinputs = graphene.List(DeleteRelationship.Input) + + create_phones = graphene.List(NIPhoneMutationFactory.get_create_mutation().Input) + update_phones = graphene.List(NIPhoneMutationFactory.get_update_mutation().Input) + delete_phones = graphene.List(NIPhoneMutationFactory.get_delete_mutation().Input) + + link_rolerelations = graphene.List(RoleRelationMutation.Input) + + + created = graphene.Field(NIContactMutationFactory.get_create_mutation()) + updated = graphene.Field(NIContactMutationFactory.get_update_mutation()) + subcreated = graphene.List(NIEmailMutationFactory.get_create_mutation()) + subupdated = graphene.List(NIEmailMutationFactory.get_update_mutation()) + subdeleted = graphene.List(NIEmailMutationFactory.get_delete_mutation()) + unlinked = graphene.List(DeleteRelationship) + phones_created = graphene.List(NIPhoneMutationFactory.get_create_mutation()) + phones_updated = graphene.List(NIPhoneMutationFactory.get_update_mutation()) + phones_deleted = graphene.List(NIPhoneMutationFactory.get_delete_mutation()) + rolerelations = graphene.List(RoleRelationMutation) + + @classmethod + def link_slave_to_master(cls, user, master_nh, slave_nh): + helpers.add_email_contact(user, slave_nh.get_node(), master_nh.handle_id) + + @classmethod + def process_extra_subentities(cls, user, main_nh, root, info, input, context): + extract_param = 'phone' + ret_subcreated = None + ret_subupdated = None + ret_subdeleted = None + ret_rolerelations = None + + create_phones = input.get("create_phones") + update_phones = input.get("update_phones") + delete_phones = input.get("delete_phones") + link_rolerelations = input.get("link_rolerelations") + + nimetaclass = getattr(cls, 'NIMetaClass') + phones_created = getattr(nimetaclass, 'phones_created', None) + phones_updated = getattr(nimetaclass, 'phones_updated', None) + phones_deleted = getattr(nimetaclass, 'phones_deleted', None) + rolerelation_mutation = getattr(nimetaclass, 'rolerelation_mutation', None) + + main_handle_id = None + + if main_nh: + main_handle_id = main_nh.handle_id + + if main_handle_id: + if create_phones: + ret_subcreated = [] + + for input in create_phones: + input['context'] = context + ret = phones_created.mutate_and_get_payload(root, info, **input) + ret_subcreated.append(ret) + + # link if it's possible + sub_errors = getattr(ret, 'errors', None) + sub_created = getattr(ret, extract_param, None) + + if not sub_errors and sub_created: + helpers.add_phone_contact( + user, sub_created.get_node(), main_handle_id) + + if update_phones: + ret_subupdated = [] + + for input in update_phones: + input['context'] = context + ret = phones_updated.mutate_and_get_payload(root, info, **input) + ret_subupdated.append(ret) + + # link if it's possible + sub_errors = getattr(ret, 'errors', None) + sub_edited = getattr(ret, extract_param, None) + + if not sub_errors and sub_edited: + helpers.add_phone_contact( + user, sub_edited.get_node(), main_handle_id) + + if delete_phones: + ret_subdeleted = [] + + for input in delete_phones: + ret = phones_deleted.mutate_and_get_payload(root, info, **input) + ret_subdeleted.append(ret) + + if link_rolerelations: + ret_rolerelations = [] + + for input in link_rolerelations: + input['contact_handle_id'] = main_handle_id + ret = rolerelation_mutation.mutate_and_get_payload(root, info, **input) + ret_rolerelations.append(ret) + + return dict(phones_created=ret_subcreated, + phones_updated=ret_subupdated, + phones_deleted=ret_subdeleted, + rolerelations=ret_rolerelations) + + class NIMetaClass: + create_mutation = NIContactMutationFactory.get_create_mutation() + update_mutation = NIContactMutationFactory.get_update_mutation() + create_submutation = NIEmailMutationFactory.get_create_mutation() + update_submutation = NIEmailMutationFactory.get_update_mutation() + delete_submutation = NIEmailMutationFactory.get_delete_mutation() + unlink_submutation = DeleteRelationship + phones_created = NIPhoneMutationFactory.get_create_mutation() + phones_updated = NIPhoneMutationFactory.get_update_mutation() + phones_deleted = NIPhoneMutationFactory.get_delete_mutation() + rolerelation_mutation = RoleRelationMutation + graphql_type = Contact + graphql_subtype = Email + context = sriutils.get_community_context() + + +class NOCRootMutation(graphene.ObjectType): + create_group = NIGroupMutationFactory.get_create_mutation().Field() + update_group = NIGroupMutationFactory.get_update_mutation().Field() + delete_group = NIGroupMutationFactory.get_delete_mutation().Field() + composite_group = CompositeGroupMutation.Field() + + create_procedure = NIProcedureMutationFactory.get_create_mutation().Field() + update_procedure = NIProcedureMutationFactory.get_update_mutation().Field() + delete_procedure = NIProcedureMutationFactory.get_delete_mutation().Field() + + create_phone = NIPhoneMutationFactory.get_create_mutation().Field() + update_phone = NIPhoneMutationFactory.get_update_mutation().Field() + delete_phone = NIPhoneMutationFactory.get_delete_mutation().Field() + + create_email = NIEmailMutationFactory.get_create_mutation().Field() + update_email = NIEmailMutationFactory.get_update_mutation().Field() + delete_email = NIEmailMutationFactory.get_delete_mutation().Field() + + create_address = NIAddressMutationFactory.get_create_mutation().Field() + update_address = NIAddressMutationFactory.get_update_mutation().Field() + delete_address = NIAddressMutationFactory.get_delete_mutation().Field() + + create_contact = NIContactMutationFactory.get_create_mutation().Field() + update_contact = NIContactMutationFactory.get_update_mutation().Field() + delete_contact = NIContactMutationFactory.get_delete_mutation().Field() + composite_contact = CompositeContactMutation.Field() + + create_organization = CreateOrganization.Field() + update_organization = UpdateOrganization.Field() + delete_organization = NIOrganizationMutationFactory.get_delete_mutation().Field() + composite_organization = CompositeOrganizationMutation.Field() + + create_role = CreateRole.Field() + update_role = UpdateRole.Field() + delete_role = DeleteRole.Field() + + create_comment = CreateComment.Field() + update_comment = UpdateComment.Field() + delete_comment = DeleteComment.Field() + + delete_relationship = DeleteRelationship.Field() + create_option = CreateOptionForDropdown.Field() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py new file mode 100644 index 000000000..069493bfd --- /dev/null +++ b/src/niweb/apps/noclook/schema/query.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import graphene +import norduniclient as nc +import apps.noclook.vakt.utils as sriutils + +from django.apps import apps +from graphql import GraphQLError +from ..models import Dropdown as DropdownModel, Role as RoleModel, DummyDropdown,\ + RoleGroup as RoleGroupModel, DEFAULT_ROLEGROUP_NAME +from .types import * + +def can_load_models(): + can_load = True + + try: + NodeType.objects.all().first() + except: + can_load = False + + return can_load + +class NOCAutoQuery(graphene.ObjectType): + ''' + This class creates a connection and a getById method for each of the types + declared on the graphql_types of the NIMeta class of any subclass. + ''' + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + _nimeta = getattr(cls, 'NIMeta') + graphql_types = getattr(_nimeta, 'graphql_types') + + # add list with pagination resolver + # add by id resolver + for graphql_type in graphql_types: + ## extract values + ni_type = graphql_type.get_from_nimetatype('ni_type') + assert ni_type, '{} has not set its ni_type attribute'.format(cls.__name__) + ni_metatype = graphql_type.get_from_nimetatype('ni_metatype') + assert ni_metatype, '{} has not set its ni_metatype attribute'.format(cls.__name__) + + node_type = NodeType.objects.filter(type=ni_type).first() if can_load_models() else None + + if node_type: + type_name = node_type.type + type_slug = node_type.slug + + # add simple list attribute and resolver + field_name = 'all_{}s'.format(type_slug) + resolver_name = 'resolve_{}'.format(field_name) + + setattr(cls, field_name, graphene.List(graphql_type)) + setattr(cls, resolver_name, graphql_type.get_list_resolver()) + + # add simple counter + field_name = 'count_{}s'.format(type_slug) + resolver_name = 'resolve_{}'.format(field_name) + + setattr(cls, field_name, graphene.Int()) + setattr(cls, resolver_name, graphql_type.get_count_resolver()) + + # add connection attribute + field_name = '{}s'.format(type_slug) + resolver_name = 'resolve_{}'.format(field_name) + + connection_input, connection_order = graphql_type.build_filter_and_order() + connection_meta = type('Meta', (object, ), dict(node=graphql_type)) + connection_class = type( + '{}Connection'.format(graphql_type.__name__), + (graphene.relay.Connection,), + #(connection_type,), + dict(Meta=connection_meta) + ) + + setattr(cls, field_name, graphene.relay.ConnectionField( + connection_class, + filter=graphene.Argument(connection_input), + orderBy=graphene.Argument(connection_order), + )) + setattr(cls, resolver_name, graphql_type.get_connection_resolver()) + + ## build field and resolver byid + field_name = 'get{}ById'.format(type_name) + resolver_name = 'resolve_{}'.format(field_name) + + setattr(cls, field_name, graphene.Field(graphql_type, id=graphene.ID())) + setattr(cls, resolver_name, graphql_type.get_byid_resolver()) + + +class NOCRootQuery(NOCAutoQuery): + getAvailableDropdowns = graphene.List(graphene.String) + getChoicesForDropdown = graphene.List(Choice, name=graphene.String(required=True)) + roles = relay.ConnectionField(RoleConnection, filter=graphene.Argument(RoleFilter), orderBy=graphene.Argument(RoleOrderBy)) + checkExistentOrganizationId = graphene.Boolean(organization_id=graphene.String(required=True), id=graphene.ID()) + + # get roles lookup + getAvailableRoleGroups = graphene.List(RoleGroup) + getRolesFromRoleGroup = graphene.List(Role, name=graphene.String()) + + def resolve_getAvailableDropdowns(self, info, **kwargs): + django_dropdowns = [d.name for d in DropdownModel.objects.all()] + + return django_dropdowns + + def resolve_getChoicesForDropdown(self, info, **kwargs): + # django dropdown resolver + name = kwargs.get('name') + ddqs = DropdownModel.get(name) + + if not isinstance(ddqs, DummyDropdown): + return ddqs.choice_set.order_by('name') + else: + raise Exception(u'Could not find dropdown with name \'{}\'. Please create it using /admin/'.format(name)) + + def resolve_roles(self, info, **kwargs): + filter = kwargs.get('filter') + order_by = kwargs.get('orderBy') + + qs = RoleModel.objects.all() + + if order_by: + if order_by == RoleOrderBy.handle_id_ASC: + qs = qs.order_by('handle_id') + elif order_by == RoleOrderBy.handle_id_DESC: + qs = qs.order_by('-handle_id') + elif order_by == RoleOrderBy.name_ASC: + qs = qs.order_by('name') + elif order_by == RoleOrderBy.name_DESC: + qs = qs.order_by('-name') + + if filter: + if filter.id: + handle_id = relay.Node.from_global_id(filter.id)[1] + qs = qs.filter(handle_id=handle_id) + + if filter.name: + qs = qs.filter(name=filter.name) + + return qs + + def resolve_getAvailableRoleGroups(self, info, **kwargs): + ret = [] + + if info.context and info.context.user.is_authenticated: + # well use the community context to check if the user + # can read the rolegroup list + community_context = sriutils.get_community_context() + authorized = sriutils.authorize_list_module( + info.context.user, community_context + ) + + if not authorized: + raise GraphQLAuthException() + + ret = RoleGroupModel.objects.all() + else: + raise GraphQLAuthException() + + return ret + + + def resolve_getRolesFromRoleGroup(self, info, **kwargs): + ret = [] + name = kwargs.get('name', DEFAULT_ROLEGROUP_NAME) + + if info.context and info.context.user.is_authenticated: + # well use the community context to check if the user + # can read the rolegroup list + community_context = sriutils.get_community_context() + authorized = sriutils.authorize_list_module( + info.context.user, community_context + ) + + if not authorized: + raise GraphQLAuthException() + + role_group = RoleGroupModel.objects.get(name=name) + ret = RoleModel.objects.filter(role_group=role_group) + else: + raise GraphQLAuthException() + + return ret + + def resolve_checkExistentOrganizationId(self, info, **kwargs): + id = kwargs.get('id', None) + handle_id = None + + if id: + _type, handle_id = relay.Node.from_global_id(id) + + organization_id = kwargs.get('organization_id') + + ret = nc.models.OrganizationModel.check_existent_organization_id(organization_id, handle_id, nc.graphdb.manager) + + return ret + + class NIMeta: + graphql_types = [ Group, Address, Phone, Email, Contact, Organization, Procedure, Host ] diff --git a/src/niweb/apps/noclook/schema/querybuilders.py b/src/niweb/apps/noclook/schema/querybuilders.py new file mode 100644 index 000000000..521b06d5b --- /dev/null +++ b/src/niweb/apps/noclook/schema/querybuilders.py @@ -0,0 +1,645 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import graphene + +from collections import OrderedDict +from django.db.models import Q +from django.utils import six + +class classproperty(object): + def __init__(self, f): + self.f = f + def __get__(self, obj, owner): + return self.f(owner) + +class AbstractQueryBuilder: + @staticmethod + def build_match_predicate(field, value, type): + pass + + @staticmethod + def build_not_predicate(field, value, type): + pass + + @staticmethod + def build_in_predicate(field, value, type): + pass + + @staticmethod + def build_not_in_predicate(field, value, type): + pass + + @staticmethod + def build_lt_predicate(field, value, type): + pass + + @staticmethod + def build_lte_predicate(field, value, type): + pass + + @staticmethod + def build_gt_predicate(field, value, type): + pass + + @staticmethod + def build_gte_predicate(field, value, type): + pass + + @staticmethod + def build_contains_predicate(field, value, type): + pass + + @staticmethod + def build_not_contains_predicate(field, value, type): + pass + + @staticmethod + def build_starts_with_predicate(field, value, type): + pass + + @staticmethod + def build_not_starts_with_predicate(field, value, type): + pass + + @staticmethod + def build_ends_with_predicate(field, value, type): + pass + + @staticmethod + def build_not_ends_with_predicate(field, value, type): + pass + + @classproperty + def filter_array(cls): + return OrderedDict([ + ('', { 'wrapper_field': None, 'only_strings': False, 'qpredicate': cls.build_match_predicate }), + ('not', { 'wrapper_field': None, 'only_strings': False, 'qpredicate': cls.build_not_predicate }), + ('in', { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False, 'qpredicate': cls.build_in_predicate }), + ('not_in', { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False, 'qpredicate': cls.build_not_in_predicate }), + ('lt', { 'wrapper_field': None, 'only_strings': False, 'qpredicate': cls.build_lt_predicate }), + ('lte', { 'wrapper_field': None, 'only_strings': False, 'qpredicate': cls.build_lte_predicate }), + ('gt', { 'wrapper_field': None, 'only_strings': False, 'qpredicate': cls.build_gt_predicate }), + ('gte', { 'wrapper_field': None, 'only_strings': False, 'qpredicate': cls.build_gte_predicate }), + + ('contains', { 'wrapper_field': None, 'only_strings': True, 'qpredicate': cls.build_contains_predicate }), + ('not_contains', { 'wrapper_field': None, 'only_strings': True, 'qpredicate': cls.build_not_contains_predicate }), + ('starts_with', { 'wrapper_field': None, 'only_strings': True, 'qpredicate': cls.build_starts_with_predicate }), + ('not_starts_with', { 'wrapper_field': None, 'only_strings': True, 'qpredicate': cls.build_not_starts_with_predicate }), + ('ends_with', { 'wrapper_field': None, 'only_strings': True, 'qpredicate': cls.build_ends_with_predicate }), + ('not_ends_with', { 'wrapper_field': None, 'only_strings': True, 'qpredicate': cls.build_not_ends_with_predicate }), + ]) + +class ScalarQueryBuilder(AbstractQueryBuilder): + @staticmethod + def build_match_predicate(field, value, type, **kwargs): + # string quoting + expression = """n.{field} = {value}""" + if isinstance(value, six.string_types): + value = "'{}'".format(value) + expression = """toLower(n.{field}) = toLower({value})""" + + ret = expression.format(field=field, value=value) + + return ret + + @staticmethod + def build_not_predicate(field, value, type, **kwargs): + # string quoting + expression = """n.{field} <> {value}""" + if isinstance(value, six.string_types): + value = "'{}'".format(value) + expression = """toLower(n.{field}) <> toLower({value})""" + + ret = expression.format(field=field, value=value) + + return ret + + @staticmethod + def build_in_predicate(field, values, type, **kwargs): # a list predicate builder + expression = 'n.{field} IN [{list_string}]' + value_list = [] + string_values = False + for value in values: + if isinstance(value, six.string_types): + value = "toLower('{}')".format(value) + string_values = True + else: + value = str(value) + + value_list.append(value) + + list_string = '{}'.format(', '.join(value_list)) + if string_values: + expression = 'toLower(n.{field}) IN [{list_string}]' + + ret = expression.format(field=field, list_string=list_string) + return ret + + @staticmethod + def build_not_in_predicate(field, values, type, **kwargs): # a list predicate builder + expression = 'NOT n.{field} IN [{list_string}]' + value_list = [] + string_values = False + for value in values: + if isinstance(value, six.string_types): + value = "toLower('{}')".format(value) + string_values = True + else: + value = str(value) + + value_list.append(value) + + list_string = '{}'.format(', '.join(value_list)) + if string_values: + expression = 'NOT toLower(n.{field}) IN [{list_string}]' + + ret = expression.format(field=field, list_string=list_string) + return ret + + @staticmethod + def build_lt_predicate(field, value, type, **kwargs): + expression = """n.{field} < {value}""" + # string quoting + if isinstance(value, six.string_types): + value = "'{}'".format(value) + expression = """toLower(n.{field}) < toLower({value})""" + + ret = expression.format(field=field, value=value) + + return ret + + @staticmethod + def build_lte_predicate(field, value, type, **kwargs): + expression = """n.{field} <= {value}""" + # string quoting + if isinstance(value, six.string_types): + value = "'{}'".format(value) + expression = """toLower(n.{field}) <= toLower({value})""" + + ret = expression.format(field=field, value=value) + + return ret + + @staticmethod + def build_gt_predicate(field, value, type, **kwargs): + expression = """n.{field} > {value}""" + # string quoting + if isinstance(value, six.string_types): + value = "'{}'".format(value) + expression = """toLower(n.{field}) > toLower({value})""" + + ret = expression.format(field=field, value=value) + + return ret + + @staticmethod + def build_gte_predicate(field, value, type, **kwargs): + expression = """n.{field} >= {value}""" + # string quoting + if isinstance(value, six.string_types): + value = "'{}'".format(value) + expression = """toLower(n.{field}) >= toLower({value})""" + + ret = expression.format(field=field, value=value) + + return ret + + @staticmethod + def build_contains_predicate(field, value, type, **kwargs): + expression = """toLower(n.{field}) CONTAINS toLower('{value}')""" + return expression.format(field=field, value=value) + + @staticmethod + def build_not_contains_predicate(field, value, type, **kwargs): + expression = """NOT toLower(n.{field}) CONTAINS toLower('{value}')""" + return expression.format(field=field, value=value) + + @staticmethod + def build_starts_with_predicate(field, value, type, **kwargs): + expression = """toLower(n.{field}) STARTS WITH toLower('{value}')""" + return expression.format(field=field, value=value) + + @staticmethod + def build_not_starts_with_predicate(field, value, type, **kwargs): + expression = """NOT toLower(n.{field}) STARTS WITH toLower('{value}')""" + return expression.format(field=field, value=value) + + @staticmethod + def build_ends_with_predicate(field, value, type, **kwargs): + expression = """toLower(n.{field}) ENDS WITH toLower('{value}')""" + return expression.format(field=field, value=value) + + @staticmethod + def build_not_ends_with_predicate(field, value, type, **kwargs): + expression = """NOT toLower(n.{field}) ENDS WITH toLower('{value}')""" + return expression.format(field=field, value=value) + +class InputFieldQueryBuilder(AbstractQueryBuilder): + standard_expression = """{neo4j_var}.{field} {op} {value}""" + standard_insensitive_expression = """toLower({neo4j_var}.{field}) {op} {value}""" + id_expression = """ID({neo4j_var}) {op} {value}""" + + @classmethod + def format_expression(cls, key, value, neo4j_var, op, add_quotes=True, string_values=False): + # string quoting + is_string = False + + if isinstance(value, str) and add_quotes: + value = "toLower('{}')".format(value) + is_string = True + + if key is 'relation_id': + ret = cls.id_expression.format( + neo4j_var=neo4j_var, + op=op, + value=value, + ) + else: + expression = cls.standard_expression + if is_string or string_values: + expression = cls.standard_insensitive_expression + + ret = expression.format( + neo4j_var=neo4j_var, + field=key, + op=op, + value=value, + ) + + return ret + + @staticmethod + def single_value_predicate(field, value, type, op, not_in=False, **kwargs): + neo4j_var = kwargs.get('neo4j_var') + ret = "" + + for k, v in value.items(): + ret = InputFieldQueryBuilder.format_expression(k, v, neo4j_var, op) + + if not_in: + ret = 'NOT {}'.format(ret) + + return ret + + @classmethod + def multiple_value_predicate(cls, field, values, type, op, not_in=False, **kwargs): # a list predicate builder + neo4j_var = kwargs.get('neo4j_var') + + string_filter = False + all_values = [] + field_name = "" + + for value in values: + for k, v in value.items(): + if isinstance(v, str): + v = "toLower('{}')".format(v) + string_filter = True + + field_name = k + all_values.append(v) + + the_value = "[{}]".format(', '.join([str(x) for x in all_values])) + + ret = InputFieldQueryBuilder.format_expression(field_name, the_value, neo4j_var, op, False, string_filter) + + if not_in: + ret = 'NOT {}'.format(ret) + + return ret + + @staticmethod + def build_match_predicate(field, value, type, **kwargs): + op = "=" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_not_predicate(field, value, type, **kwargs): + op = "<>" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @classmethod + def build_in_predicate(cls, field, values, type, **kwargs): # a list predicate builder + op = "IN" + ret = InputFieldQueryBuilder.multiple_value_predicate( + field, values, type, op, **kwargs + ) + return ret + + @classmethod + def build_not_in_predicate(cls, field, values, type, **kwargs): # a list predicate builder + op = "IN" + ret = InputFieldQueryBuilder.multiple_value_predicate( + field, values, type, op, True, **kwargs + ) + return ret + + @staticmethod + def build_lt_predicate(field, value, type, **kwargs): + op = "<" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_lte_predicate(field, value, type, **kwargs): + op = "<=" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_gt_predicate(field, value, type, **kwargs): + op = ">" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_gte_predicate(field, value, type, **kwargs): + op = ">=" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def has_string_value(subfilter): + # optimistic check: if any of the inner values is not a string + # we won't use it for filtering + ret = True + + for key, val in subfilter.items(): + if not isinstance(val, six.string_types): + ret = False + break + + return ret + + @staticmethod + def build_contains_predicate(field, value, type, **kwargs): + op = "CONTAINS" + ret = "" + + if InputFieldQueryBuilder.has_string_value(value): + ret = InputFieldQueryBuilder.single_value_predicate(field, value, + type, op, **kwargs) + + return ret + + @staticmethod + def build_not_contains_predicate(field, value, type, **kwargs): + op = "CONTAINS" + ret = "" + + if InputFieldQueryBuilder.has_string_value(value): + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, True, **kwargs) + + return ret + + @staticmethod + def build_starts_with_predicate(field, value, type, **kwargs): + op = "STARTS WITH" + ret = "" + + if InputFieldQueryBuilder.has_string_value(value): + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_not_starts_with_predicate(field, value, type, **kwargs): + op = "STARTS WITH" + ret = "" + + if InputFieldQueryBuilder.has_string_value(value): + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, True, **kwargs) + + return ret + + @staticmethod + def build_ends_with_predicate(field, value, type, **kwargs): + op = "ENDS WITH" + ret = "" + + if InputFieldQueryBuilder.has_string_value(value): + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_not_ends_with_predicate(field, value, type, **kwargs): + op = "ENDS WITH" + ret = "" + + if InputFieldQueryBuilder.has_string_value(value): + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, True, **kwargs) + + return ret + + +class DateQueryBuilder(AbstractQueryBuilder): + fields = [ 'created', 'modified' ] + + @classproperty + def suffixes(cls): + ''' + The advantage of this over a static dict is that if the filter_array + is changed in the superclass this would + ''' + ret = OrderedDict() + + thekeys = list(cls.filter_array.keys())[:8] + + for akey in thekeys: + if akey == '': + ret[akey] = cls.build_match_predicate + elif akey == 'not': + ret[akey] = cls.build_not_predicate + elif akey == 'in': + ret[akey] = cls.build_in_predicate + elif akey == 'not_in': + ret[akey] = cls.build_not_in_predicate + elif akey == 'lt': + ret[akey] = cls.build_lt_predicate + elif akey == 'lte': + ret[akey] = cls.build_lte_predicate + elif akey == 'gt': + ret[akey] = cls.build_gt_predicate + elif akey == 'gte': + ret[akey] = cls.build_gte_predicate + + return ret + + @staticmethod + def build_match_predicate(field, value): + kwargs = { '{}__date'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_not_predicate(field, value): + kwargs = { '{}__date'.format(field) : value } + return ~Q(**kwargs) + + @staticmethod + def build_in_predicate(field, value): + kwargs = { '{}__date__in'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_not_in_predicate(field, value): + kwargs = { '{}__date__in'.format(field) : value } + return ~Q(**kwargs) + + @staticmethod + def build_lt_predicate(field, value): + kwargs = { '{}__date__lt'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_lte_predicate(field, value): + kwargs = { '{}__date__lte'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_gt_predicate(field, value): + kwargs = { '{}__date__gt'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_gte_predicate(field, value): + kwargs = { '{}__date__gte'.format(field) : value } + return Q(**kwargs) + + @classproperty + def search_fields_list(cls): + search_fields_list = {} + + for field_name in cls.fields: + for suffix, func in cls.suffixes.items(): + field_wsuffix = field_name + + if suffix != '': + field_wsuffix = '{}_{}'.format(field_name, suffix) + + search_fields_list[field_wsuffix] = { + 'function': func, + 'field': field_name + } + + return search_fields_list + + @classmethod + def filter_queryset(cls, filter_values, qs): + import copy + + cfilter_values = copy.deepcopy(filter_values) + qobj_dict = { + 'AND': [], + 'OR': [] + } + + # iterate operations (AND/OR) and its array of values + if cfilter_values: + for and_or_op, op_filter_list in cfilter_values.items(): + array_idx = 0 + + # iterate through the array of dicts of the operation + for op_filter_values in op_filter_list: + + # iterate through the fields and values in these dicts + for filter_name, filter_value in op_filter_values.items(): + if filter_name in cls.search_fields_list: + # extract values + func = cls.search_fields_list[filter_name]['function'] + field_name = cls.search_fields_list[filter_name]['field'] + + # call function and add q object + qobj = func(field_name, filter_value) + qobj_dict[and_or_op].append(qobj) + + # delete value from filter + del filter_values[and_or_op][array_idx][filter_name] + + array_idx = array_idx + 1 + + # filter the queryset with the q objects + # do AND + qand = None + for qobj in qobj_dict['AND']: + if not qand: + qand = qobj + else: + qand = qand & qobj + + if qand: + qs = qs.filter(qand) + + # do OR + qor = None + for qobj in qobj_dict['OR']: + if not qor: + qor = qobj + else: + qor = qor & qobj + if qor: + qs = qs.filter(qor) + + return qs + +class UserQueryBuilder(DateQueryBuilder): + fields = [ 'creator', 'modifier'] + + @staticmethod + def build_match_predicate(field, value): + kwargs = { '{}'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_not_predicate(field, value): + kwargs = { '{}'.format(field) : value } + return ~Q(**kwargs) + + @staticmethod + def build_in_predicate(field, value): + kwargs = { '{}__in'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_not_in_predicate(field, value): + kwargs = { '{}__in'.format(field) : value } + return ~Q(**kwargs) + + @staticmethod + def build_lt_predicate(field, value): + kwargs = { '{}__lt'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_lte_predicate(field, value): + kwargs = { '{}__lte'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_gt_predicate(field, value): + kwargs = { '{}__gt'.format(field) : value } + return Q(**kwargs) + + @staticmethod + def build_gte_predicate(field, value): + kwargs = { '{}__gte'.format(field) : value } + return Q(**kwargs) diff --git a/src/niweb/apps/noclook/schema/scalars.py b/src/niweb/apps/noclook/schema/scalars.py new file mode 100644 index 000000000..6b2d481f3 --- /dev/null +++ b/src/niweb/apps/noclook/schema/scalars.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from graphql.language.ast import BooleanValue, IntValue, StringValue +from graphene.types import Scalar +from io import StringIO + +import graphene + +class IPAddr(Scalar): + '''IPAddr scalar to be matched with the IPAddrField in a django form''' + @staticmethod + def serialize(value): + # this would be to_python method + if isinstance(value, list): + return value + else: + return none + + @staticmethod + def parse_value(value): + # and this would be the clean method + result = [] + for line in StringIO(value): + ip = line.replace('\n','').strip() + if ip: + try: + ipaddress.ip_address(ip) + result.append(ip) + except ValueError as e: + errors.append(str(e)) + if errors: + raise ValidationError(errors) + return result + + parse_literal = parse_value + + +class JSON(Scalar): + '''JSON scalar to be matched with the JSONField in a django form''' + @staticmethod + def serialize(value): + if value: + value = json.dumps(value) + + return value + + @staticmethod + def parse_value(value): + try: + if value: + value = json.loads(value) + except ValueError: + raise ValidationError(self.error_messages['invalid'], code='invalid') + return value + + +class RoleScalar(Scalar): + '''This is a POC scalar that may be used in the contact mutation input''' + @staticmethod + def serialize(value): + roles_dict = get_roles_dropdown() + + if value in roles_dict.keys(): + return roles_dict[value] + else: + raise Exception('The selected role ("{}") doesn\'t exists' + .format(value)) + + @staticmethod + def parse_value(value): + roles_dict = get_roles_dropdown() + + key = value.replace(' ', '_').lower() + if key in roles_dict.keys(): + return roles_dict[key] + else: + return value + + @staticmethod + def get_roles_dropdown(): + ret = {} + roles = nc.models.RoleRelationship.get_all_roles() + for role in roles: + name = role.replace(' ', '_').lower() + ret[name] = role + + return ret + + +class ChoiceScalar(Scalar): + '''This scalar represents a choice field in query/mutation''' + @staticmethod + def coerce_choice(value): + num = None + try: + num = int(value) + except ValueError: + try: + num = int(float(value)) + except ValueError: + pass + if num: + return graphene.Int.coerce_int(value) + else: + return graphene.String.coerce_string(value) + + + serialize = coerce_choice + parse_value = coerce_choice + + @staticmethod + def parse_literal(ast): + if isinstance(ast, IntValue): + return graphene.Int.parse_literal(ast) + elif isinstance(ast, StringValue): + return graphene.String.parse_literal(ast) + + @classmethod + def get_roles_dropdown(cls): + pass + + +class NullBoolean(graphene.Boolean): + """ + Just like the `Boolean` graphene scalar but it could be set on null/None + """ + @staticmethod + def serialize(value): + if value in (True, 'True', 'true', '1'): + return True + elif value in (False, 'False', 'false', '0'): + return False + else: + return None + + @staticmethod + def parse_value(value): + if value != None: + return bool(value) + + return None + + @staticmethod + def parse_literal(ast): + ret = None + if isinstance(ast, BooleanValue): + ret = ast.value + + return ret diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py new file mode 100644 index 000000000..063ce8e9f --- /dev/null +++ b/src/niweb/apps/noclook/schema/types.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import graphene + +import apps.noclook.vakt.utils as sriutils +from norduniclient.models import RoleRelationship +from graphene import relay, ObjectType, String, Field +from .core import * +from ..models import Dropdown, Choice, Role as RoleModel, RoleGroup as RoleGroupModel + + +class Dropdown(DjangoObjectType): + ''' + This class represents a dropdown to use in forms + ''' + class Meta: + only_fields = ('id', 'name') + model = Dropdown + + +class Choice(DjangoObjectType): + ''' + This class is used for the choices available in a dropdown + ''' + class Meta: + model = Choice + interfaces = (KeyValue, ) + + +class Neo4jChoice(graphene.ObjectType): + class Meta: + interfaces = (KeyValue, ) + + +class RoleGroup(DjangoObjectType): + ''' + This class represents a Role in the relational db + ''' + class Meta: + model = RoleGroupModel + + +class Role(DjangoObjectType): + ''' + This class represents a Role in the relational db + ''' + class Meta: + model = RoleModel + interfaces = (relay.Node, ) + use_connection = False + + +class Group(NIObjectType): + ''' + The group type is used to group contacts + ''' + name = NIStringField(type_kwargs={ 'required': True }) + description = NIStringField() + contacts = NIListField(type_args=(lambda: Contact,), rel_name='Member_of', rel_method='get_relations') + contact_relations = NIRelationListField(rel_name='Member_of', rel_method='get_relations', graphene_type=lambda: Contact) + + class NIMetaType: + ni_type = 'Group' + ni_metatype = NIMETA_LOGICAL + context_method = sriutils.get_community_context + + +class Procedure(NIObjectType): + ''' + The group type is used to group contacts + ''' + name = NIStringField(type_kwargs={ 'required': True }) + description = NIStringField() + + class NIMetaType: + ni_type = 'Procedure' + ni_metatype = NIMETA_LOGICAL + context_method = sriutils.get_community_context + + +class Address(NIObjectType): + ''' + Phone entity to be used inside contact + ''' + name = NIStringField(type_kwargs={ 'required': True }) + phone = NIStringField() + street = NIStringField() + postal_code = NIStringField() + postal_area = NIStringField() + + class Meta: + only_fields = ('handle_id',) + + class NIMetaType: + ni_type = 'Address' + ni_metatype = NIMETA_LOGICAL + context_method = sriutils.get_community_context + + +class Organization(NIObjectType): + ''' + The group type is used to group contacts + ''' + name = NIStringField(type_kwargs={ 'required': True }) + description = NIStringField() + organization_number = NIStringField() + organization_id = NIStringField() + incident_management_info = NIStringField() + type = NIChoiceField() + website = NIStringField() + addresses = NIListField(type_args=(Address,), rel_name='Has_address', rel_method='get_outgoing_relations') + addresses_relations = NIRelationListField(rel_name='Has_address', rel_method='get_outgoing_relations', graphene_type= Address) + affiliation_customer = NIBooleanField() + affiliation_end_customer = NIBooleanField() + affiliation_provider = NIBooleanField() + affiliation_partner = NIBooleanField() + affiliation_host_user = NIBooleanField() + affiliation_site_owner = NIBooleanField() + parent_organization = NIListField(type_args=(lambda: Organization,), rel_name='Parent_of', rel_method='get_relations') + contacts = NIListField(type_args=(lambda: Contact,), rel_name='Works_for', rel_method='get_relations') + contacts_relations = NIRelationListField(rel_name='Works_for', rel_method='get_relations', graphene_type=lambda: Contact) + + class NIMetaType: + ni_type = 'Organization' + ni_metatype = NIMETA_RELATION + context_method = sriutils.get_community_context + + +class RoleRelation(NIRelationType): + name = graphene.String() + start = graphene.Field(lambda: Contact) + end = graphene.Field(Organization) + role_data = graphene.Field(Role) + + def resolve_name(self, info, **kwargs): + return getattr(self, 'name', None) + + def resolve_role_data(self, info, **kwargs): + name = getattr(self, 'name', None) + role_data = RoleModel.objects.get(name=name) + return role_data + + class NIMetaType: + nimodel = RoleRelationship + filter_exclude = ('type') + + +class Phone(NIObjectType): + ''' + Phone entity to be used inside contact + ''' + name = NIStringField(type_kwargs={ 'required': True }) + type = NIChoiceField(type_kwargs={ 'required': True }) + + class Meta: + only_fields = ('handle_id',) + + class NIMetaType: + ni_type = 'Phone' + ni_metatype = NIMETA_LOGICAL + context_method = sriutils.get_community_context + + +class Email(NIObjectType): + ''' + Email entity to be used inside contact + ''' + name = NIStringField(type_kwargs={ 'required': True }) + type = NIChoiceField(type_kwargs={ 'required': True }) + + class Meta: + only_fields = ('handle_id',) + + class NIMetaType: + ni_type = 'Email' + ni_metatype = NIMETA_LOGICAL + context_method = sriutils.get_community_context + + +class Contact(NIObjectType): + ''' + A contact in the SRI system + ''' + name = NIStringField(type_kwargs={ 'required': True }) + first_name = NIStringField(type_kwargs={ 'required': True }) + last_name = NIStringField(type_kwargs={ 'required': True }) + title = NIStringField() + salutation = NIStringField() + contact_type = NIChoiceField() + phones = NIListField(type_args=(Phone,), rel_name='Has_phone', rel_method='get_outgoing_relations') + phones_relations = NIRelationListField(rel_name='Has_phone', rel_method='get_outgoing_relations', graphene_type=Phone) + emails = NIListField(type_args=(Email,), rel_name='Has_email', rel_method='get_outgoing_relations') + emails_relations = NIRelationListField(rel_name='Has_email', rel_method='get_outgoing_relations', graphene_type=Email) + pgp_fingerprint = NIStringField() + member_of_groups = NIListField(type_args=(Group,), rel_name='Member_of', rel_method='get_outgoing_relations') + roles = NIRelationField(rel_name=RoleRelationship.RELATION_NAME, type_args=(RoleRelation, )) + organizations = NIListField(type_args=(Organization,), rel_name='Works_for', rel_method='get_outgoing_relations') + organizations_relations = NIRelationListField(rel_name='Works_for', rel_method='get_outgoing_relations', graphene_type=Organization) + notes = NIStringField() + + class NIMetaType: + ni_type = 'Contact' + ni_metatype = NIMETA_RELATION + context_method = sriutils.get_community_context + + +class Host(NIObjectType): + ''' + A host in the SRI system + ''' + name = NIStringField(type_kwargs={ 'required': True }) + operational_state = NIStringField(type_kwargs={ 'required': True }) + os = NIStringField() + os_version = NIStringField() + vendor = NIStringField() + backup = NIStringField() + managed_by = NIStringField() + ip_addresses = IPAddr() + description = NIStringField() + responsible_group = NIStringField() + support_group = NIStringField() + security_class = NIStringField() + security_comment = NIStringField() + + def resolve_ip_addresses(self, info, **kwargs): + '''Manual resolver for the ip field''' + return self.get_node().data.get('ip_addresses', None) + + class NIMetaType: + ni_type = 'Host' + ni_metatype = NIMETA_LOGICAL + + +class RoleConnection(relay.Connection): + class Meta: + node = Role + + +class RoleFilter(graphene.InputObjectType): + name = graphene.String() + id = graphene.ID() + + +class RoleOrderBy(graphene.Enum): + name_ASC='name_ASC' + name_DESC='name_DESC' + handle_id_ASC='handle_id_ASC' + handle_id_DESC='handle_id_DESC' diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_contact.html b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html new file mode 100644 index 000000000..40d63a8cd --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +

Create new contact

+ {% if form.errors %} +
+

The operation could not be performed because one or more error(s) occurred.

+ Please resubmit the form after making the following changes: + {{ form.errors }} +
+ {% endif %} +
+
{% csrf_token %} +

Main information

+ + {{ form.first_name }} +
+ + {{ form.last_name }} +
+

Additional info (optional)

+ + {{ form.title }} +
+ + {{ form.contact_type }} +
+ + {{ form.pgp_fingerprint }} +
+ + Cancel +
+
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_group.html b/src/niweb/apps/noclook/templates/noclook/create/create_group.html new file mode 100644 index 000000000..3fbc914c8 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_group.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

Create new group

+ {% if form.errors %} +
+

The operation could not be performed because one or more error(s) occurred.

+ Please resubmit the form after making the following changes: + {{ form.errors }} +
+ {% endif %} +
+
{% csrf_token %} +

Main information

+ + {{ form.name }} +
+ + {{ form.description }} +
+ + Cancel +
+
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_organization.html b/src/niweb/apps/noclook/templates/noclook/create/create_organization.html new file mode 100644 index 000000000..27b502a37 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_organization.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +

Create new organization

+ {% if form.errors %} +
+

The operation could not be performed because one or more error(s) occurred.

+ Please resubmit the form after making the following changes: + {{ form.errors }} +
+ {% endif %} +
+
{% csrf_token %} +

Main information

+ + {{ form.name }} +
+ + {{ form.description }} +
+

Additional info (optional)

+ + {{ form.organization_id }} +
+ + {{ form.type }} +
+ + {{ form.incident_management_info }} +
+ + Cancel +
+
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_procedure.html b/src/niweb/apps/noclook/templates/noclook/create/create_procedure.html new file mode 100644 index 000000000..a7de32666 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_procedure.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

Create new procedure

+ {% if form.errors %} +
+

The operation could not be performed because one or more error(s) occurred.

+ Please resubmit the form after making the following changes: + {{ form.errors }} +
+ {% endif %} +
+
{% csrf_token %} +

Main information

+ + {{ form.name }} +
+ + {{ form.description }} +
+ + Cancel +
+
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_role.html b/src/niweb/apps/noclook/templates/noclook/create/create_role.html new file mode 100644 index 000000000..d950afe6b --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_role.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +

Create new role

+ {% if form.errors %} +
+

The operation could not be performed because one or more error(s) occurred.

+ Please resubmit the form after making the following changes: + {{ form.errors }} +
+ {% endif %} +
+
{% csrf_token %} +

Main information

+ + {{ form.name }} +
+ + {{ form.description }} +
+ + Cancel +
+
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/debug.html b/src/niweb/apps/noclook/templates/noclook/debug.html index 877e6f7a7..ee16e049c 100644 --- a/src/niweb/apps/noclook/templates/noclook/debug.html +++ b/src/niweb/apps/noclook/templates/noclook/debug.html @@ -20,7 +20,8 @@

Relationships

- {% for key, value in item.relationship.items %} + {% noclook_as_dict item.relationship as rel_dict %} + {% for key, value in rel_dict.items %} @@ -41,7 +42,8 @@

Relationships

{{ key }}{{ value }}
- {% for key, value in item.relationship.items %} + {% noclook_as_dict item.relationship as rel_dict %} + {% for key, value in rel_dict.items %} diff --git a/src/niweb/apps/noclook/templates/noclook/detail/base_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/base_detail.html index 1c51c40b1..21b525127 100644 --- a/src/niweb/apps/noclook/templates/noclook/detail/base_detail.html +++ b/src/niweb/apps/noclook/templates/noclook/detail/base_detail.html @@ -67,6 +67,7 @@

{{ comment.submit_date|date:"Y-m-d H:i" }} by {{ comme {% block content_footer %} {% if node_handle %}
+ {% block edit_link %}{% endblock %} Add a comment diff --git a/src/niweb/apps/noclook/templates/noclook/detail/contact_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/contact_detail.html new file mode 100644 index 000000000..9edb4ff12 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/detail/contact_detail.html @@ -0,0 +1,8 @@ +{% extends "noclook/detail/detail.html" %} +{% load table_tags %} + +{% block edit_link %} + {% if user.is_staff %} + Edit + {% endif %} +{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/detail/group_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/group_detail.html new file mode 100644 index 000000000..9edb4ff12 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/detail/group_detail.html @@ -0,0 +1,8 @@ +{% extends "noclook/detail/detail.html" %} +{% load table_tags %} + +{% block edit_link %} + {% if user.is_staff %} + Edit + {% endif %} +{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/detail/organization_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/organization_detail.html new file mode 100644 index 000000000..9edb4ff12 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/detail/organization_detail.html @@ -0,0 +1,8 @@ +{% extends "noclook/detail/detail.html" %} +{% load table_tags %} + +{% block edit_link %} + {% if user.is_staff %} + Edit + {% endif %} +{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/detail/procedure_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/procedure_detail.html new file mode 100644 index 000000000..9edb4ff12 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/detail/procedure_detail.html @@ -0,0 +1,8 @@ +{% extends "noclook/detail/detail.html" %} +{% load table_tags %} + +{% block edit_link %} + {% if user.is_staff %} + Edit + {% endif %} +{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html new file mode 100644 index 000000000..7f8187eba --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html @@ -0,0 +1,126 @@ +{% extends "noclook/detail/base_detail.html" %} +{% load table_tags %} +{% load noclook_tags %} + +{% block js %} + {{ block.super }} + + + {% block js_table_covert %} + + {% endblock %} +{% endblock %} + +{% block title %}Role {{ node_handle.name }}{% endblock %} + +{% block before_table %} + +{% endblock %} + +{% block content %} + {{ block.super }} +

Role {{name}}

+ {% if node_handle.description %} +

{{ key }}{{ value }}
+ + + +
description{{ node_handle.description }}
+ {% endif %} +
+ {% if table.no_badges %} + {# Nothing to show #} + {% elif table.badges %} + {% for badge, name in table.badges %} + {{name}} + {% endfor %} + {% elif table.filters %} + {% for badge, name, link, active in table.filters %} + {% if active %} {% endif %}{{name}} + {% endfor %} + {% else %} + {{block.super}} + {% endif %} + {% table_search %} +
+ + + + {% for header in table.headers %} + + {% endfor %} + + + + {% for row in table.rows %} + + {% for col in row.cols %} + + {% endfor %} + + {% endfor %} + +
{{ header }}
{% table_column col %}
+ + +{% endblock %} + + +{% block content_footer %} +
+ {% if user.is_staff %} + Edit + {% endif %} +
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html new file mode 100644 index 000000000..1bf1f0068 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -0,0 +1,45 @@ +{% extends "noclook/edit/base_edit.html" %} +{% load crispy_forms_tags %} +{% load noclook_tags %} +{% load static %} + +{% block js %} +{{ block.super }} + + + +{% endblock %} +{% block content %} +{{ block.super }} +
+ {{ form.first_name | as_crispy_field }} + {{ form.last_name | as_crispy_field }} +
+ +
+ {% accordion 'Additional info (optional)' 'additional-edit' '#edit-accordion' %} + {{ form.title | as_crispy_field }} + {{ form.contact_type | as_crispy_field }} + {{ form.pgp_fingerprint | as_crispy_field}} + {% endaccordion %} + {% include "noclook/edit/includes/works_for_group.html" %} + {% include "noclook/edit/includes/member_of_group.html" %} +{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html new file mode 100644 index 000000000..0ea6737b7 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html @@ -0,0 +1,28 @@ +{% extends "noclook/edit/base_edit.html" %} +{% load crispy_forms_tags %} +{% load noclook_tags %} + +{% block js %} +{{ block.super }} + +{% endblock %} +{% block content %} +{{ block.super }} +
+ {{ form.name | as_crispy_field}} + {{ form.description | as_crispy_field}} +
+ +
+ {% include "noclook/edit/includes/of_member_group.html" %} +{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html new file mode 100644 index 000000000..0e570e6c7 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html @@ -0,0 +1,55 @@ +{% extends "noclook/edit/base_edit.html" %} +{% load crispy_forms_tags %} +{% load noclook_tags %} + +{% block js %} +{{ block.super }} + + + +{% endblock %} +{% block content %} +{{ block.super }} +
+ {{ form.name | as_crispy_field}} + {{ form.description | as_crispy_field}} +
+ +
+ {% accordion 'Additional info (optional)' 'additional-edit' '#edit-accordion' %} + {{ form.organization_id | as_crispy_field}} + {{ form.type | as_crispy_field}} + {{ form.incident_management_info | as_crispy_field}} + {% endaccordion %} + {% accordion 'Contacts (optional)' 'contacts-edit' '#edit-contacts' %} + {{ form.abuse_contact | as_crispy_field}} + {{ form.primary_contact | as_crispy_field}} + {{ form.secondary_contact | as_crispy_field}} + {{ form.it_technical_contact | as_crispy_field}} + {{ form.it_security_contact | as_crispy_field}} + {{ form.it_manager_contact | as_crispy_field}} + {% endaccordion %} + {% include "noclook/edit/includes/parent_of_group.html" %} + {% include "noclook/edit/includes/uses_a_group.html" %} +{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_procedure.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_procedure.html new file mode 100644 index 000000000..821413891 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_procedure.html @@ -0,0 +1,14 @@ +{% extends "noclook/edit/base_edit.html" %} +{% load crispy_forms_tags %} +{% load noclook_tags %} + +{% block js %} +{{ block.super }} +{% endblock %} +{% block content %} +{{ block.super }} +
+ {{ form.name | as_crispy_field}} + {{ form.description | as_crispy_field}} +
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_role.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_role.html new file mode 100644 index 000000000..a1e974e5d --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_role.html @@ -0,0 +1,54 @@ +{% extends "noclook/edit/base_edit.html" %} +{% load crispy_forms_tags %} +{% load noclook_tags %} + +{% block js %} +{{ block.super }} +{% endblock %} + +{% block title %}{{ block.super }} Role "{{ role.name }}" {% endblock %} + +{% block content %} + + + + + +
{% csrf_token %} +

Edit role {{ role.name }}

+ {% if form.errors %} +
+

The operation could not be performed because one or more error(s) occurred.

+ Please resubmit the form after making the following changes: + {{ form.errors }} +
+ {% endif %} +
+ {{ form.name | as_crispy_field}} + {{ form.description | as_crispy_field}} +
+{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/member_of_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/member_of_group.html new file mode 100644 index 000000000..9e58e1c30 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/member_of_group.html @@ -0,0 +1,27 @@ +{% load noclook_tags %} +{% load crispy_forms_tags %} +{% blockvar memberof_title %} + {{ node_handle.node_type }} group (optional) +{% endblockvar %} +{% accordion memberof_title 'memberof-edit' '#edit-accordion' %} + {% if relations.Member_of %} + {% load noclook_tags %} +

Remove from group

+ {% for item in relations.Member_of %} +
+
+ {% noclook_get_type item.node.handle_id as node_type %} + {{ node_type }} {{ item.node.data.name }} +
+
+ Delete +
+
+ {% endfor %} +
+ {% endif %} +

Add contact to group

+
+ {{ form.relationship_member_of | as_crispy_field }} +
+{% endaccordion %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/of_member_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/of_member_group.html new file mode 100644 index 000000000..9d56ffa5d --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/of_member_group.html @@ -0,0 +1,66 @@ +{% load noclook_tags %} +{% load crispy_forms_tags %} +{% blockvar memberof_title %} + {{ node_handle.node_type }} contacts +{% endblockvar %} +{% accordion memberof_title 'memberof-edit' '#edit-accordion' %} + {% if relations.Member_of %} + {% load noclook_tags %} +

Remove contact

+
+
+ + +
+
+ {{ foo }} + {% for contact in contacts %} + +
+
+ {% noclook_get_type contact.handle_id as node_type %} + {{ node_type }} {{ contact.data.name }} +
+
+ Delete +
+
+ {% endfor %} +
+ {% endif %} +

Add contact to group

+
+ {{ form.relationship_member_of | as_crispy_field }} +
+{% endaccordion %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/parent_of_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/parent_of_group.html new file mode 100644 index 000000000..cdc881667 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/parent_of_group.html @@ -0,0 +1,27 @@ +{% load noclook_tags %} +{% load crispy_forms_tags %} +{% blockvar organization_title %} + Parent Organization (optional) +{% endblockvar %} +{% accordion organization_title 'organization-edit' '#edit-accordion' %} + {% if relations.Parent_of %} + {% load noclook_tags %} +

Remove organization

+ {% for item in relations.Parent_of %} +
+
+ {% noclook_get_type item.node.handle_id as node_type %} + {{ node_type }} {{ item.node.data.name }} +
+
+ Delete +
+
+ {% endfor %} +
+ {% endif %} +

Add parent organization

+
+ {{ form.relationship_parent_of | as_crispy_field }} +
+{% endaccordion %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/uses_a_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/uses_a_group.html new file mode 100644 index 000000000..06f5fd26c --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/uses_a_group.html @@ -0,0 +1,27 @@ +{% load noclook_tags %} +{% load crispy_forms_tags %} +{% blockvar procedure_title %} + {{ node_handle.node_type }} Linked Procedure (optional) +{% endblockvar %} +{% accordion procedure_title 'procedure-edit' '#edit-accordion' %} + {% if out_relations.Uses_a %} + {% load noclook_tags %} +

Remove procedure

+ {% for item in out_relations.Uses_a %} +
+
+ {% noclook_get_type item.node.handle_id as node_type %} + {{ node_type }} {{ item.node.data.name }} +
+
+ Delete +
+
+ {% endfor %} +
+ {% endif %} +

Add procedure

+
+ {{ form.relationship_uses_a | as_crispy_field }} +
+{% endaccordion %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/works_for_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/works_for_group.html new file mode 100644 index 000000000..1ae45892d --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/works_for_group.html @@ -0,0 +1,30 @@ +{% load noclook_tags %} +{% load crispy_forms_tags %} +{% blockvar worksfor_title %} + {{ node_handle.node_type }} organization (optional) +{% endblockvar %} +{% accordion worksfor_title 'worksfor-edit' '#edit-accordion' %} + {% if relations.Works_for %} + {% load noclook_tags %} +

Remove organization

+ {% for item in relations.Works_for %} +
+
+ {% noclook_get_type item.node.handle_id as node_type %} + {{ node_type }} {{ item.node.data.name }}{% if item.relationship.name %} (as {{ item.relationship.name }}){% endif %} +
+
+ Delete +
+
+ {% endfor %} +
+ {% endif %} +

Link contact to organization

+
+ {{ form.relationship_works_for | as_crispy_field }} +
+
+ {{ form.role | as_crispy_field }} +
+{% endaccordion %} diff --git a/src/niweb/apps/noclook/templates/noclook/logout.html b/src/niweb/apps/noclook/templates/noclook/logout.html new file mode 100644 index 000000000..fc0aedbbb --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/logout.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}{{ block.super }} | Welcome{% endblock %} + +{% block js %} + + +{% endblock %} + +{% block content %} +
+
+
+
+
+
+
+
+

+ Welcome to NOCLook
+ {{ noclook.brand }} Network Inventory +

+
+
+
+ {% if user.is_authenticated %} + {% csrf_token %} + + + {% endif %} +
+
+{% endblock %} diff --git a/src/niweb/apps/noclook/templatetags/noclook_tags.py b/src/niweb/apps/noclook/templatetags/noclook_tags.py index 76f603f6a..4b61359f4 100644 --- a/src/niweb/apps/noclook/templatetags/noclook_tags.py +++ b/src/niweb/apps/noclook/templatetags/noclook_tags.py @@ -24,6 +24,17 @@ def type_menu(): return {'types': types} +@register.inclusion_tag('type_menu.html') +def menu_mode(): + """ + Adds a the menu items if it's set in the dynamic_preferences + """ + global_preferences = global_preferences_registry.manager() + menu_mode = global_preferences['general__menu_mode'] + + { 'menu_mode': menu_mode } + + @register.simple_tag(takes_context=True) def noclook_node_to_url(context, handle_id): """G @@ -34,7 +45,7 @@ def noclook_node_to_url(context, handle_id): if urls and handle_id in urls: return urls.get(handle_id) else: - return "/nodes/%s" % handle_id + return "/nodes/%s" % handle_id @register.simple_tag(takes_context=True) @@ -68,7 +79,7 @@ def noclook_last_seen_as_td(date): table column. """ if type(date) is datetime: - last_seen = date + last_seen = date else: last_seen = noclook_last_seen_to_dt(date) return {'last_seen': last_seen} @@ -148,7 +159,19 @@ def noclook_report_age(item, old, very_old): return '' -@register.simple_tag +@register.assignment_tag +def noclook_as_dict(obj): + """ + :param obj: Neo4j object + :return: dict + """ + try: + return dict(obj.items()) + except TypeError: + return dict() + + +@register.assignment_tag def noclook_has_rogue_ports(handle_id): """ :param handle_id: unique id @@ -206,12 +229,12 @@ def as_json(value): def hardware_module(module, level=0): result = "" indent = " "*4*level - keys = ["name", - "version", - "part_number", - "serial_number", + keys = ["name", + "version", + "part_number", + "serial_number", "description", - "hardware_description", + "hardware_description", "model_number", "clei_code"] if module: @@ -221,7 +244,7 @@ def hardware_module(module, level=0): result += "\n".join([ hardware_module(mod, level+1) for mod in module.get('modules',[]) ]) result += "\n".join([ hardware_module(mod, level+1) for mod in module.get('sub_modules',[]) ]) result += "\n{0}{1}\n".format(indent,"-"*8) - + return result @@ -238,7 +261,7 @@ def dynamic_ports(context,bulk_ports, *args, **kwargs): port_types = context.request.POST.getlist("port_type") ports = zip(port_names, port_types) bulk_ports.auto_id = False - + export = {} export.update({"bulk_ports": bulk_ports, "ports": ports}) export.update(kwargs) diff --git a/src/niweb/apps/noclook/tests/forms/test_common.py b/src/niweb/apps/noclook/tests/forms/test_common.py new file mode 100644 index 000000000..a1258afa0 --- /dev/null +++ b/src/niweb/apps/noclook/tests/forms/test_common.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from ..neo4j_base import NeoTestCase +from apps.noclook.forms import common as forms +from apps.noclook.models import NodeHandle, NodeType, Dropdown, NordunetUniqueId + + +class CommonFormTest(NeoTestCase): + + def test_new_organization(self): + test_org_id = '5678' + # create a test organization + self.organization1 = self.create_node('organization1', 'organization', meta='Logical') + + # add an organization_id + organization1_data = { + 'organization_id': test_org_id, + } + + for key, value in organization1_data.items(): + self.organization1.get_node().add_property(key, value) + + data1 = { + 'organization_number': '1234', + 'name': 'Lipsum', + 'description': 'Lorem ipsum dolor sit amet, \ + consectetur adipiscing elit.\ + Morbi dignissim vehicula \ + justo sit amet pulvinar. \ + Fusce ipsum nulla, feugiat eu\ + gravida eget, efficitur a risus.', + 'website': 'www.lipsum.com', + 'organization_id': test_org_id, + 'type': 'university_college', + 'incident_management_info': 'They have a form on their website', + } + + form = forms.NewOrganizationForm(data=data1) + self.assertFalse(form.is_valid()) diff --git a/src/niweb/apps/noclook/tests/forms/test_validators.py b/src/niweb/apps/noclook/tests/forms/test_validators.py new file mode 100644 index 000000000..6a44eb749 --- /dev/null +++ b/src/niweb/apps/noclook/tests/forms/test_validators.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook.models import NodeHandle, NodeType +from apps.noclook.forms import common as forms +from apps.noclook.forms.validators import validate_organization, \ + validate_contact, validate_group, \ + validate_procedure +from django.core.exceptions import ValidationError +from pprint import pformat + +from ..neo4j_base import NeoTestCase + +class Neo4jGraphQLTest(NeoTestCase): + def test_validators(self): + # create nodes + self.organization1 = self.create_node('organization1', 'organization', meta='Logical') + self.contact1 = self.create_node('contact1', 'contact', meta='Relation') + self.group1 = self.create_node('group1', 'group', meta='Logical') + self.procedure1 = self.create_node('procedure1', 'procedure', meta='Logical') + + # organization + # test valid organization + valid = True + + try: + validate_organization(self.organization1.handle_id) + except ValidationError: + valid = False + + self.assertTrue(valid) + + # test a non valid organization (a contact for example) + valid = True + + try: + validate_organization(self.contact1.handle_id) + except ValidationError: + valid = False + + self.assertFalse(valid) + + # contact + # test valid contact + valid = True + + try: + validate_contact(self.contact1.handle_id) + except ValidationError: + valid = False + + self.assertTrue(valid) + + # test a non valid contact (a organization for example) + valid = True + + try: + validate_contact(self.organization1.handle_id) + except ValidationError: + valid = False + + self.assertFalse(valid) + + # group + # test valid group + valid = True + + try: + validate_group(self.group1.handle_id) + except ValidationError: + valid = False + + self.assertTrue(valid) + + # test a non valid group (a organization for example) + valid = True + + try: + validate_group(self.organization1.handle_id) + except ValidationError: + valid = False + + self.assertFalse(valid) + + def test_organization_form(self): + # create nodes + self.organization1 = self.create_node('organization1', 'organization', meta='Logical') + self.contact1 = self.create_node('contact1', 'contact', meta='Relation') + self.group1 = self.create_node('group1', 'group', meta='Logical') + self.procedure1 = self.create_node('procedure1', 'procedure', meta='Logical') + + ## organization + data = { + 'handle_id': self.organization1.handle_id, + 'organization_number': '1234', + 'name': 'Lipsum', + 'description': 'Lorem ipsum dolor sit amet, \ + consectetur adipiscing elit.\ + Morbi dignissim vehicula \ + justo sit amet pulvinar. \ + Fusce ipsum nulla, feugiat eu\ + gravida eget, efficitur a risus.', + 'website': 'www.lipsum.com', + 'organization_id': '5678', + 'type': 'university_college', + 'incident_management_info': 'They have a form on their website', + 'relationship_parent_of': self.organization1.handle_id, + } + + # check a valid form + form = forms.EditOrganizationForm(data) + self.assertTrue(form.is_valid(), pformat(form.errors, indent=1)) + + # check a non valid form + data['relationship_parent_of'] = self.contact1.handle_id + form = forms.EditOrganizationForm(data) + self.assertFalse(form.is_valid()) + + # check another valid form + data['relationship_parent_of'] = self.organization1.handle_id + + data['abuse_contact'] = self.contact1.handle_id + data['primary_contact'] = self.contact1.handle_id + data['secondary_contact'] = self.contact1.handle_id + data['it_technical_contact'] = self.contact1.handle_id + data['it_security_contact'] = self.contact1.handle_id + data['it_manager_contact'] = self.contact1.handle_id + form = forms.EditOrganizationForm(data) + form.strict_validation = True + self.assertTrue(form.is_valid(), pformat(form.errors, indent=1)) + + # check another non valid form + data['abuse_contact'] = self.group1.handle_id + data['primary_contact'] = self.procedure1.handle_id + data['secondary_contact'] = self.group1.handle_id + data['it_technical_contact'] = self.procedure1.handle_id + data['it_security_contact'] = self.group1.handle_id + data['it_manager_contact'] = self.procedure1.handle_id + form = forms.EditOrganizationForm(data) + form.strict_validation = True + self.assertFalse(form.is_valid()) + + def test_contact_form(self): + # create nodes + self.organization1 = self.create_node('organization1', 'organization', meta='Logical') + self.contact1 = self.create_node('contact1', 'contact', meta='Relation') + self.group1 = self.create_node('group1', 'group', meta='Logical') + self.procedure1 = self.create_node('procedure1', 'procedure', meta='Logical') + + ## contact + data = { + 'handle_id': self.contact1.handle_id, + 'first_name': 'Alice', + 'last_name': 'Svensson', + 'contact_type': 'person', + 'title': 'PhD', + 'pgp_fingerprint': '-', + 'relationship_works_for': self.organization1.handle_id, + 'relationship_member_of': self.group1.handle_id, + } + + # check a valid form + form = forms.EditContactForm(data) + self.assertTrue(form.is_valid()) + + # check a non valid form + data['relationship_member_of'] = self.contact1.handle_id + form = forms.EditContactForm(data) + self.assertFalse(form.is_valid()) + + def test_group_form(self): + # create nodes + self.organization1 = self.create_node('organization1', 'organization', meta='Logical') + self.contact1 = self.create_node('contact1', 'contact', meta='Relation') + self.group1 = self.create_node('group1', 'group', meta='Logical') + self.procedure1 = self.create_node('procedure1', 'procedure', meta='Logical') + + ## group + data = { + 'handle_id': self.group1.handle_id, + 'name': 'Text providers', + 'description': 'Lorem ipsum dolor sit amet, \ + consectetur adipiscing elit.\ + Morbi dignissim vehicula \ + justo sit amet pulvinar. \ + Fusce ipsum nulla, feugiat eu\ + gravida eget, efficitur a risus.', + 'relationship_member_of': self.contact1.handle_id, + } + + # check a valid form + form = forms.EditGroupForm(data) + self.assertTrue(form.is_valid()) + + # check a non valid form + data['relationship_member_of'] = self.group1.handle_id + form = forms.EditGroupForm(data) + self.assertFalse(form.is_valid()) diff --git a/src/niweb/apps/noclook/tests/management/__init__.py b/src/niweb/apps/noclook/tests/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/niweb/apps/noclook/tests/management/fileutils.py b/src/niweb/apps/noclook/tests/management/fileutils.py new file mode 100644 index 000000000..d3d39e32d --- /dev/null +++ b/src/niweb/apps/noclook/tests/management/fileutils.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import tempfile + +def write_string_to_disk(string): + # get random file + tf = tempfile.NamedTemporaryFile(mode='w+') + + # write text + tf.write(string) + tf.flush() + # return file descriptor + return tf diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py new file mode 100644 index 000000000..59ec26c6a --- /dev/null +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- + +from django.core.management import call_command + +import norduniclient as nc +from norduniclient.exceptions import UniqueNodeError, NodeNotFound +import norduniclient.models as ncmodels + +from apps.noclook.models import NodeHandle, NodeType, User, Role, DEFAULT_ROLE_KEY + +from ..neo4j_base import NeoTestCase +from .fileutils import write_string_to_disk + +__author__ = 'ffuentes' + +class CsvImportTest(NeoTestCase): + cmd_name = 'csvimport' + + organizations_str = """"organization_number";"account_name";"description";"phone";"website";"organization_id";"type";"parent_account" +1;"Tazz";;"453-896-3068";"https://studiopress.com";"DRIVE";"University, College"; +2;"Wikizz";;"531-584-0224";"https://ihg.com";"DRIVE";"University, College"; +3;"Browsecat";;"971-875-7084";"http://skyrock.com";"ROAD";"University, College";"Tazz" +4;"Dabfeed";;"855-843-6570";"http://merriam-webster.com";"LANE";"University, College";"Wikizz" + """ + + contacts_str = """"salutation";"first_name";"last_name";"title";"contact_role";"contact_type";"mailing_street";"mailing_city";"mailing_zip";"mailing_state";"mailing_country";"phone";"mobile";"fax";"email";"other_email";"PGP_fingerprint";"account_name" +"Honorable";"Caesar";"Newby";;"Computer Systems Analyst III";"Person";;;;;"China";"897-979-7799";"501-503-1550";;"cnewby0@joomla.org";"cnewby1@joomla.org";;"Gabtune" +"Mr";"Zilvia";"Linnard";;"Analog Circuit Design manager";"Person";;;;;"Indonesia";"205-934-3477";"473-256-5648";;"zlinnard1@wunderground.com";;;"Babblestorm" +"Honorable";"Reamonn";"Scriviner";;"Tax Accountant";"Person";;;;;"China";"200-111-4607";"419-639-2648";;"rscriviner2@moonfruit.com";;;"Babbleblab" +"Mrs";"Jessy";"Bainton";;"Software Consultant";"Person";;;;;"China";"877-832-9647";"138-608-6235";;"fbainton3@si.edu";;;"Mudo" +"Rev";"Theresa";"Janosevic";;"Physical Therapy Assistant";"Person";;;;;"China";"568-690-1854";"118-569-1303";;"tjanosevic4@umich.edu";;;"Youspan" +"Mrs";"David";"Janosevic";;;"Person";;;;;"United Kingdom";"568-690-1854";"118-569-1303";;"djanosevic4@afaa.co.uk";;;"AsFastAsAFAA" + """ + + secroles_str = """"Organisation";"Contact";"Role" +"Chalmers";"CTH Abuse";"Abuse" +"Chalmers";"CTH IRT";"IRT Gruppfunktion" +"Chalmers";"Hans Nilsson";"Övrig incidentkontakt" +"Chalmers";"Stefan Svensson";"Övrig incidentkontakt" +"Chalmers";"Karl Larsson";"Primär incidentkontakt" + """ + + def setUp(self): + super(CsvImportTest, self).setUp() + # write organizations csv file to disk + self.organizations_file = write_string_to_disk(self.organizations_str) + + # write contacts csv file to disk + self.contacts_file = write_string_to_disk(self.contacts_str) + + # write contacts csv file to disk + self.secroles_file = write_string_to_disk(self.secroles_str) + + # create noclook user + User.objects.get_or_create(username="noclook")[0] + + def tearDown(self): + super(CsvImportTest, self).tearDown() + # close organizations csv file + self.organizations_file.close() + + # close contacts csv file + self.contacts_file.close() + + # close contacts csv file + self.secroles_file.close() + + def test_organizations_import(self): + # call csvimport command (verbose 0) + call_command( + self.cmd_name, + organizations=self.organizations_file, + verbosity=0, + ) + # check one of the organizations is present + qs = NodeHandle.objects.filter(node_name='Browsecat') + self.assertIsNotNone(qs) + organization1 = qs.first() + self.assertIsNotNone(organization1) + self.assertIsInstance(organization1.get_node(), ncmodels.OrganizationModel) + + # check if one of them has a parent organization + relations = organization1.get_node().get_relations() + parent_relation = relations.get('Parent_of', None) + self.assertIsNotNone(parent_relation) + self.assertIsInstance(relations['Parent_of'][0]['node'], ncmodels.RelationModel) + + def test_contacts_import(self): + # call csvimport command (verbose 0) + call_command( + self.cmd_name, + contacts=self.contacts_file, + verbosity=0, + ) + # check one of the contacts is present + full_name = '{} {}'.format('Caesar', 'Newby') + qs = NodeHandle.objects.filter(node_name=full_name) + self.assertIsNotNone(qs) + contact1 = qs.first() + self.assertIsNotNone(contact1) + self.assertIsInstance(contact1.get_node(), ncmodels.ContactModel) + + # check if works for an organization + qs = NodeHandle.objects.filter(node_name='Gabtune') + self.assertIsNotNone(qs) + organization1 = qs.first() + self.assertIsNotNone(organization1) + self.assertIsInstance(organization1.get_node(), ncmodels.OrganizationModel) + + # check if role is created + role1 = ncmodels.RoleRelationship(nc.core.GraphDB.get_instance().manager) + role1.load_from_nodes(contact1.handle_id, organization1.handle_id) + self.assertIsNotNone(role1) + self.assertEquals(role1.name, 'Computer Systems Analyst III') + + roleqs = Role.objects.filter(name=role1.name) + self.assertIsNotNone(roleqs) + self.assertIsNotNone(roleqs.first) + + # check for empty role and if it has the role employee + qs = NodeHandle.objects.filter(node_name='David Janosevic') + self.assertIsNotNone(qs) + contact_employee = qs.first() + self.assertIsNotNone(contact_employee) + employee_role = Role.objects.get(slug=DEFAULT_ROLE_KEY) + relations = contact_employee.get_node().get_outgoing_relations() + self.assertEquals(employee_role.name, relations['Works_for'][0]['relationship']['name']) + + def test_fix_addresss(self): + # call csvimport command (verbose 0) to import test contacts + call_command( + self.cmd_name, + organizations=self.organizations_file, + verbosity=0, + ) + + # check one of the contacts is present + org_name = "Tazz" + qs = NodeHandle.objects.filter(node_name=org_name) + self.assertIsNotNone(qs) + organization1 = qs.first() + self.assertIsNotNone(organization1) + self.assertIsInstance(organization1.get_node(), ncmodels.OrganizationModel) + organization1_node = organization1.get_node() + + # check organization's website and phone + phone1_test = '453-896-3068' + has_phone1 = 'phone' in organization1_node.data + self.assertTrue(has_phone1) + self.assertEquals(organization1_node.data['phone'], phone1_test) + + website1_test = 'https://studiopress.com' + has_website1 = 'website' in organization1_node.data + self.assertTrue(has_website1) + self.assertEquals(organization1_node.data['website'], website1_test) + + call_command( + self.cmd_name, + addressfix=True, + verbosity=0, + ) + + # check the old fields are not present anymore + qs = NodeHandle.objects.filter(node_name=org_name) + self.assertIsNotNone(qs) + organization1 = qs.first() + self.assertIsNotNone(organization1) + self.assertIsInstance(organization1.get_node(), ncmodels.OrganizationModel) + organization1_node = organization1.get_node() + + has_phone = 'phone' in organization1_node.data + self.assertFalse(has_phone) + + relations = organization1_node.get_outgoing_relations() + relation_keys = list(relations.keys()) + has_address = 'Has_address' in relation_keys + self.assertTrue(has_address) + + address_node = relations['Has_address'][0]['node'] + self.assertIsInstance(address_node, ncmodels.AddressModel) + + has_phone = 'phone' in address_node.data + self.assertTrue(has_phone) + + def test_fix_emails_phones(self): + # call csvimport command (verbose 0) to import test contacts + call_command( + self.cmd_name, + contacts=self.contacts_file, + verbosity=0, + ) + + # check one of the contacts is present + full_name = '{} {}'.format('Caesar', 'Newby') + qs = NodeHandle.objects.filter(node_name=full_name) + self.assertIsNotNone(qs) + contact1 = qs.first() + self.assertIsNotNone(contact1) + self.assertIsInstance(contact1.get_node(), ncmodels.ContactModel) + contact_node = contact1.get_node() + + # check user emails in old fields + email1_test = 'cnewby0@joomla.org' + has_email1 = 'email' in contact_node.data + self.assertTrue(has_email1) + self.assertEquals(contact_node.data['email'], email1_test) + + email2_test = 'cnewby1@joomla.org' + has_email2 = 'other_email' in contact_node.data + self.assertTrue(has_email2) + self.assertEquals(contact_node.data['other_email'], email2_test) + + # check user phones in old fields + phone1_test = '897-979-7799' + has_phone1 = 'phone' in contact_node.data + self.assertTrue(has_phone1) + self.assertEquals(contact_node.data['phone'], phone1_test) + + phone2_test = '501-503-1550' + has_phone2 = 'mobile' in contact_node.data + self.assertTrue(has_phone2) + self.assertEquals(contact_node.data['mobile'], phone2_test) + + call_command( + self.cmd_name, + emailphones=True, + verbosity=0, + ) + + # check the old fields are not present anymore + qs = NodeHandle.objects.filter(node_name=full_name) + self.assertIsNotNone(qs) + contact1 = qs.first() + self.assertIsNotNone(contact1) + self.assertIsInstance(contact1.get_node(), ncmodels.ContactModel) + contact_node = contact1.get_node() + + has_phone1 = 'phone' in contact_node.data + self.assertTrue(not has_phone1) + has_phone2 = 'mobile' in contact_node.data + self.assertTrue(not has_phone2) + has_email1 = 'email' in contact_node.data + self.assertTrue(not has_email1) + has_email2 = 'other_email' in contact_node.data + self.assertTrue(not has_email2) + + relations = contact_node.get_outgoing_relations() + relation_keys = list(relations.keys()) + has_phone = 'Has_phone' in relation_keys + has_emails = 'Has_email' in relation_keys + + test_dict = { + 'email': { + 'work': email1_test, + 'personal': email2_test, + }, + 'phone': { + 'work': phone1_test, + 'personal': phone2_test, + } + } + + self.assertTrue(has_phone) + self.assertTrue(has_emails) + + for phone_rel in relations['Has_phone']: + phone_node = phone_rel['node'] + phone_type = phone_node.data['type'] + check_phone = test_dict['phone'][phone_type] + self.assertEquals(check_phone, phone_node.data['name']) + + for email_rel in relations['Has_email']: + email_node = email_rel['node'] + email_type = email_node.data['type'] + check_email = test_dict['email'][email_type] + self.assertEquals(check_email, email_node.data['name']) + + def test_secroles_import(self): + # call csvimport command (verbose 0) + call_command( + self.cmd_name, + secroles=self.secroles_file, + verbosity=0, + ) + + # check if the organization is present + qs = NodeHandle.objects.filter(node_name='Chalmers') + self.assertIsNotNone(qs) + organization1 = qs.first() + self.assertIsNotNone(organization1) + + # check a contact is present + qs = NodeHandle.objects.filter(node_name='Hans Nilsson') + self.assertIsNotNone(qs) + contact1 = qs.first() + self.assertIsNotNone(contact1) + + # check if role is created + role1 = ncmodels.RoleRelationship(nc.core.GraphDB.get_instance().manager) + role1.load_from_nodes(contact1.handle_id, organization1.handle_id) + self.assertIsNotNone(role1) + self.assertEquals(role1.name, 'Övrig incidentkontakt') + + def test_organizations_import(self): + # call csvimport command (verbose 0) + call_command( + self.cmd_name, + organizations=self.organizations_file, + verbosity=0, + ) + + call_command( + self.cmd_name, + addressfix=True, + verbosity=0, + ) + + website_field = 'website' + + organization_type = NodeType.objects.get_or_create(type='Organization', slug='organization')[0] # organization + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + + for organization in all_organizations: + # get the address and check that the website field is not present + website_value = organization.get_node().data.get(website_field, None) + self.assertIsNotNone(website_value) + + relations = organization.get_node().get_outgoing_relations() + address_relations = relations.get('Has_address', None) + orgnode = organization.get_node() + + self.assertIsNotNone(address_relations) + + # check and add it for test + for rel in address_relations: + address_end = rel['relationship'].end_node + self.assertFalse(website_field in address_end._properties) + handle_id = address_end._properties['handle_id'] + address_node = NodeHandle.objects.get(handle_id=handle_id).get_node() + + address_node.add_property(website_field, website_value) + orgnode.remove_property(website_field) + + # fix it + call_command( + self.cmd_name, + movewebsite=True, + verbosity=0, + ) + + # check that it's good again + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + + for organization in all_organizations: + # get the address and check that the website field is not present + website_value = organization.get_node().data.get(website_field, None) + self.assertIsNotNone(website_value) + + relations = organization.get_node().get_outgoing_relations() + address_relations = relations.get('Has_address', None) + orgnode = organization.get_node() + + self.assertIsNotNone(address_relations) + + # check the data is not present on the address + for rel in address_relations: + address_end = rel['relationship'].end_node + self.assertFalse(website_field in address_end._properties) + + + def test_orgid_fix(self): + # call csvimport command (verbose 0) + call_command( + self.cmd_name, + organizations=self.organizations_file, + verbosity=0, + ) + + old_field1 = 'customer_id' + new_field1 = 'organization_id' + + organization_type = NodeType.objects.get_or_create(type='Organization', slug='organization')[0] # organization + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + + for organization in all_organizations: + orgnode = organization.get_node() + org_id_val = orgnode.data.get(new_field1, None) + self.assertIsNotNone(org_id_val) + orgnode.remove_property(new_field1) + orgnode.add_property(old_field1, org_id_val) + + # fix it + call_command( + self.cmd_name, + reorgprops=True, + verbosity=0, + ) + + # check that it's good again + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + + for organization in all_organizations: + # get the address and check that the website field is not present + org_id_val = organization.get_node().data.get(new_field1, None) + self.assertIsNotNone(org_id_val) + + + def write_string_to_disk(self, string): + # get random file + tf = tempfile.NamedTemporaryFile(mode='w+') + + # write text + tf.write(string) + tf.flush() + # return file descriptor + return tf diff --git a/src/niweb/apps/noclook/tests/management/test_datafaker.py b/src/niweb/apps/noclook/tests/management/test_datafaker.py new file mode 100644 index 000000000..b26f07c43 --- /dev/null +++ b/src/niweb/apps/noclook/tests/management/test_datafaker.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import norduniclient as nc + +from apps.noclook.models import NodeHandle, NodeType, Dropdown, Choice +from apps.noclook.management.commands.datafaker import Command as DFCommand +from django.core.management import call_command +from django.test.utils import override_settings +from norduniclient.exceptions import UniqueNodeError, NodeNotFound +import norduniclient.models as ncmodels + +from ..neo4j_base import NeoTestCase + + +class DataFakerTest(NeoTestCase): + cmd_name = DFCommand.cmd_name + test_node_num = 5 + + @override_settings(DEBUG=True) + def test_create_organizations(self): + # check that there's not any node of the generated types + all_node_types = NodeType.objects.filter(type__in=DFCommand.generated_types) + self.assertFalse( + NodeHandle.objects.filter(node_type__in=all_node_types).exists() + ) + + # call organization generator + call_command(self.cmd_name, + **{ + DFCommand.option_organizations: self.test_node_num, + 'verbosity': 0, + } + ) + + # call equipment and cables generator + call_command(self.cmd_name, + **{ + DFCommand.option_equipment: self.test_node_num, + 'verbosity': 0, + } + ) + + # check that there's nodes from the generated types + self.assertTrue( + NodeHandle.objects.filter(node_type__in=all_node_types).exists() + ) + + # delete all + call_command(self.cmd_name, + **{ + DFCommand.option_deleteall: 1, + 'verbosity': 0, + } + ) + + # check there's nothing left + self.assertFalse( + NodeHandle.objects.filter(node_type__in=all_node_types).exists() + ) diff --git a/src/niweb/apps/noclook/tests/management/test_datafixer.py b/src/niweb/apps/noclook/tests/management/test_datafixer.py new file mode 100644 index 000000000..17a5d1445 --- /dev/null +++ b/src/niweb/apps/noclook/tests/management/test_datafixer.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +from django.core.management import call_command + +import norduniclient as nc +from norduniclient.exceptions import UniqueNodeError, NodeNotFound +import norduniclient.models as ncmodels + +from apps.noclook.models import NodeHandle, NodeType, Dropdown, Choice + +from ..neo4j_base import NeoTestCase +from .fileutils import write_string_to_disk +from .test_csvimport import CsvImportTest + +__author__ = 'ffuentes' + +class DataFixerImportTest(NeoTestCase): + cmd_name = 'datafixer' + import_name = 'csvimport' + + def setUp(self): + super(DataFixerImportTest, self).setUp() + self.organizations_file = write_string_to_disk(CsvImportTest.organizations_str) + + def tearDown(self): + super(DataFixerImportTest, self).tearDown() + + def count_different_orgids(self): + q = """ + MATCH (o:Organization) + RETURN COUNT(DISTINCT o.organization_id) AS orgs + """ + res = nc.query_to_dict(nc.graphdb.manager, q) + + return res['orgs'] + + def has_raw_values(self, all_organizations): + has_raw_values = False + + org_types_drop = Dropdown.objects.get(name='organization_types') + org_types = Choice.objects.filter(dropdown=org_types_drop) + + for organization in all_organizations: + orgnode = organization.get_node() + org_type = orgnode.data.get('type', None) + + if org_type and not org_types.filter(value=org_type).exists(): + has_raw_values = True + break + + return has_raw_values + + def test_organizations_import(self): + # call csvimport command (verbose 0) + call_command( + self.import_name, + organizations=self.organizations_file, + verbosity=0, + ) + + # count organizations + organization_type = NodeType.objects.get_or_create( + type='Organization', slug='organization')[0] + all_organizations = NodeHandle.objects.filter(node_type=organization_type) + orgs_num = all_organizations.count() + + # count unique org_ids + orgids = self.count_different_orgids() + self.assertTrue( orgids < orgs_num) + + # check it has raw values + has_raw_values = self.has_raw_values(all_organizations) + self.assertTrue(has_raw_values) + + # call fixer command + call_command( + self.cmd_name, + fixtestdata=True, + verbosity=0, + ) + + # count unique org_ids + orgids = self.count_different_orgids() + + self.assertTrue( orgids == orgs_num) + + # check it hasn't raw values + has_raw_values = self.has_raw_values(all_organizations) + self.assertFalse(has_raw_values) diff --git a/src/niweb/apps/noclook/tests/schema/__init__.py b/src/niweb/apps/noclook/tests/schema/__init__.py new file mode 100644 index 000000000..a11f25771 --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import apps.noclook.vakt.utils as sriutils + +from django.db import connection + +from apps.noclook import helpers +from apps.noclook.models import NodeHandle, Dropdown, Choice, Role, Group, GroupContextAuthzAction, NodeHandleContext +from ..neo4j_base import NeoTestCase + +class TestContext(): + def __init__(self, user, *ignore): + self.user = user + +class Neo4jGraphQLTest(NeoTestCase): + def setUp(self): + super(Neo4jGraphQLTest, self).setUp() + self.context = TestContext(self.user) + + # create group for read in community context + self.group_read = Group( name="Group can read the community context" ) + self.group_read.save() + + # create group for write in community context + self.group_write = Group( name="Group can write for the community context" ) + self.group_write.save() + + # create group for list in community context + self.group_list = Group( name="Group can list for the community context" ) + self.group_list.save() + + # add user to this group + self.group_read.user_set.add(self.user) + self.group_write.user_set.add(self.user) + self.group_list.user_set.add(self.user) + + # get read aa + self.get_read_authaction = sriutils.get_read_authaction() + self.get_write_authaction = sriutils.get_write_authaction() + self.get_list_authaction = sriutils.get_list_authaction() + + # get default context + self.community_ctxt = sriutils.get_community_context() + + # add contexts and profiles + GroupContextAuthzAction( + group = self.group_read, + authzprofile = self.get_read_authaction, + context = self.community_ctxt + ).save() + + GroupContextAuthzAction( + group = self.group_write, + authzprofile = self.get_write_authaction, + context = self.community_ctxt + ).save() + + GroupContextAuthzAction( + group = self.group_list, + authzprofile = self.get_list_authaction, + context = self.community_ctxt + ).save() + + # create nodes + self.organization1 = self.create_node('organization1', 'organization', meta='Logical') + self.organization2 = self.create_node('organization2', 'organization', meta='Logical') + self.contact1 = self.create_node('contact1', 'contact', meta='Relation') + self.contact2 = self.create_node('contact2', 'contact', meta='Relation') + self.group1 = self.create_node('group1', 'group', meta='Logical') + self.group2 = self.create_node('group2', 'group', meta='Logical') + self.role1 = Role(name='role1').save() + self.role2 = Role(name='role2').save() + + # add nodes to the appropiate context + NodeHandleContext(nodehandle=self.organization1, context=self.community_ctxt).save() + NodeHandleContext(nodehandle=self.organization2, context=self.community_ctxt).save() + NodeHandleContext(nodehandle=self.contact1, context=self.community_ctxt).save() + NodeHandleContext(nodehandle=self.contact2, context=self.community_ctxt).save() + NodeHandleContext(nodehandle=self.group1, context=self.community_ctxt).save() + NodeHandleContext(nodehandle=self.group2, context=self.community_ctxt).save() + + # add some data + contact1_data = { + 'first_name': 'Jane', + 'last_name': 'Doe', + 'name': 'Jane Doe', + } + + for key, value in contact1_data.items(): + self.contact1.get_node().add_property(key, value) + + contact2_data = { + 'first_name': 'John', + 'last_name': 'Smith', + 'name': 'John Smith', + } + + for key, value in contact2_data.items(): + self.contact2.get_node().add_property(key, value) + + organization1_data = { + 'type': 'university_college', + 'organization_id': 'ORG1', + 'affiliation_customer': True, + } + + for key, value in organization1_data.items(): + self.organization1.get_node().add_property(key, value) + + organization2_data = { + 'type': 'university_coldep', + 'organization_id': 'ORG2', + 'affiliation_end_customer': True, + } + + for key, value in organization2_data.items(): + self.organization2.get_node().add_property(key, value) + + # create relationships + self.contact1.get_node().add_group(self.group1.handle_id) + self.contact2.get_node().add_group(self.group2.handle_id) + + helpers.link_contact_role_for_organization( + self.context.user, + self.organization1.get_node(), + self.contact1.handle_id, + self.role1 + ) + helpers.link_contact_role_for_organization( + self.context.user, + self.organization2.get_node(), + self.contact2.handle_id, + self.role2 + ) + + # create dummy dropdown + dropdown = Dropdown.objects.get_or_create(name='contact_type')[0] + dropdown.save() + ch1 = Choice.objects.get_or_create(dropdown=dropdown, name='Person', value='person')[0] + ch2 = Choice.objects.get_or_create(dropdown=dropdown, name='Group', value='group')[0] + ch1.save() + ch2.save() + + def tearDown(self): + super(Neo4jGraphQLTest, self).tearDown() + + # reset sql database + NodeHandle.objects.all().delete() + + with connection.cursor() as cursor: + cursor.execute("ALTER SEQUENCE noclook_nodehandle_handle_id_seq RESTART WITH 1") diff --git a/src/niweb/apps/noclook/tests/schema/test_complex.py b/src/niweb/apps/noclook/tests/schema/test_complex.py new file mode 100644 index 000000000..8b5a70e9d --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/test_complex.py @@ -0,0 +1,2149 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook.models import NodeHandle, Dropdown, Choice, Role, Group, \ + GroupContextAuthzAction, NodeHandleContext, DEFAULT_ROLEGROUP_NAME +from collections import OrderedDict +from . import Neo4jGraphQLTest +from niweb.schema import schema +from pprint import pformat +from . import Neo4jGraphQLTest +from graphene import relay + +class GroupComplexTest(Neo4jGraphQLTest): + def test_composite_group(self): + group_name = "The Pendletones" + description_group = "In sodales nisl et turpis sollicitudin, nec \ + feugiat erat egestas. Nam pretium felis vel dolor euismod ornare. \ + Praesent consectetur risus sit amet lectus scelerisque, non \ + sollicitudin dolor luctus. Aliquam pretium neque non purus dictum \ + blandit." + + contact_type = "person" + email_type = "work" + phone_type = "work" + + c1_first_name = "Brian" + c1_last_name = "Wilson" + c1_mail = 'bwilson@pendletones.com' + c1_phone = '555-123456' + + c2_first_name = "Mike" + c2_last_name = "Love" + c2_mail = 'mlove@pendletones.com' + c2_phone = '555-987654' + + c3_first_name = "Murry" + c3_last_name = "Wilson" + c3_mail = 'mwilson@pendletones.com' + c3_phone = '555-987654' + + # Create query + + query = ''' + mutation{{ + composite_group(input:{{ + create_input:{{ + name: "{group_name}" + description: "{description_group}" + }} + create_subinputs:[ + {{ + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{contact_type1}" + email: "{c1_mail}" + email_type: "{email_type1}" + phone: "{c1_phone}" + phone_type: "{phone_type1}" + }} + {{ + first_name: "{c2_first_name}" + last_name: "{c2_last_name}" + contact_type: "{contact_type2}" + email: "{c2_mail}" + email_type: "{email_type2}" + }} + {{ + first_name: "{c3_first_name}" + last_name: "{c3_last_name}" + contact_type: "{contact_type1}" + email: "{c3_mail}" + email_type: "{email_type1}" + phone: "{c3_phone}" + phone_type: "{phone_type1}" + }} + ] + }}){{ + created{{ + errors{{ + field + messages + }} + group{{ + id + name + description + contacts{{ + id + first_name + last_name + }} + }} + }} + subcreated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + emails{{ + id + name + }} + phones{{ + id + name + }} + member_of_groups{{ + name + }} + }} + }} + }} + }} + '''.format(group_name=group_name, description_group=description_group, + c1_first_name=c1_first_name, c1_last_name=c1_last_name, + contact_type1=contact_type, c1_mail=c1_mail, + email_type1=email_type, c1_phone=c1_phone, + phone_type1=phone_type, c2_first_name=c2_first_name, + c2_last_name=c2_last_name, contact_type2=contact_type, + c2_mail=c2_mail, email_type2=email_type, + c3_first_name=c3_first_name, c3_last_name=c3_last_name, + c3_mail=c3_mail, c3_phone=c3_phone) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + created_errors = result.data['composite_group']['created']['errors'] + assert not created_errors, pformat(created_errors, indent=1) + + for subcreated in result.data['composite_group']['subcreated']: + assert not subcreated['errors'] + + # get the ids + result_data = result.data['composite_group'] + group_id = result_data['created']['group']['id'] + c1_id = result_data['subcreated'][0]['contact']['id'] + c1_email_id = result_data['subcreated'][0]['contact']['emails'][0]['id'] + c1_phone_id = result_data['subcreated'][0]['contact']['phones'][0]['id'] + c2_id = result_data['subcreated'][1]['contact']['id'] + c2_email_id = result_data['subcreated'][1]['contact']['emails'][0]['id'] + c3_id = result_data['subcreated'][2]['contact']['id'] + c3_email_id = result_data['subcreated'][2]['contact']['emails'][0]['id'] + c3_phone_id = result_data['subcreated'][2]['contact']['phones'][0]['id'] + + # check the integrity of the data + created_data = result_data['created']['group'] + + # check group + assert created_data['name'] == group_name, \ + "Group name doesn't match \n{} != {}"\ + .format(created_data['name'], group_name) + assert created_data['description'] == description_group, \ + "Group name doesn't match \n{} != {}"\ + .format(created_data['description'], description_group) + + # check members + subcreated_data = result_data['subcreated'] + + # first contact + assert subcreated_data[0]['contact']['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['first_name'], c1_first_name) + assert subcreated_data[0]['contact']['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['last_name'], c1_last_name) + assert subcreated_data[0]['contact']['emails'][0]['name'] == c1_mail, \ + "1st contact's email doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['emails'][0]['name'], c1_mail) + assert subcreated_data[0]['contact']['phones'][0]['name'] == c1_phone, \ + "1st contact's phone doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['phones'][0]['name'], c1_phone) + assert subcreated_data[0]['contact']['member_of_groups'][0]['name'] == group_name, \ + "1st contact's group name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['member_of_groups'][0]['name'], group_name) + + # second contact + assert subcreated_data[1]['contact']['first_name'] == c2_first_name, \ + "2nd contact's first name doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['first_name'], c2_first_name) + assert subcreated_data[1]['contact']['last_name'] == c2_last_name, \ + "2nd contact's last name doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['last_name'], c2_last_name) + assert subcreated_data[1]['contact']['emails'][0]['name'] == c2_mail, \ + "2nd contact's email doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['emails'][0]['name'], c2_mail) + assert subcreated_data[1]['contact']['member_of_groups'][0]['name'] == group_name, \ + "2nd contact's group name doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['member_of_groups'][0]['name'], group_name) + + # third contact + assert subcreated_data[2]['contact']['first_name'] == c3_first_name, \ + "3rd contact's first name doesn't match \n{} != {}"\ + .format(subcreated_data[2]['contact']['first_name'], c3_first_name) + assert subcreated_data[2]['contact']['last_name'] == c3_last_name, \ + "3rd contact's last name doesn't match \n{} != {}"\ + .format(subcreated_data[2]['contact']['last_name'], c3_last_name) + assert subcreated_data[2]['contact']['emails'][0]['name'] == c3_mail, \ + "3rd contact's email doesn't match \n{} != {}"\ + .format(subcreated_data[2]['contact']['emails'][0]['name'], c3_mail) + assert subcreated_data[2]['contact']['phones'][0]['name'] == c3_phone, \ + "3rd contact's phone doesn't match \n{} != {}"\ + .format(subcreated_data[2]['contact']['phones'][0]['name'], c3_phone) + assert subcreated_data[2]['contact']['member_of_groups'][0]['name'] == group_name, \ + "3rd contact's group name doesn't match \n{} != {}"\ + .format(subcreated_data[2]['contact']['member_of_groups'][0]['name'], group_name) + + ## edit + group_name = "The Beach Boys" + c1_mail = 'bwilson@beachboys.com' + c1_phone = '555-123456' + c2_mail = 'mlove@beachboys.com' + c2_phone = '555-987654' + phone_type2 = 'personal' + + c4_first_name = "Carl" + c4_last_name = "Wilson" + c4_mail = 'cwilson@beachboys.com' + c4_phone = '555-000000' + + # Update query + + query = ''' + mutation {{ + composite_group(input: {{ + update_input: {{ + id: "{group_id}", + name: "{group_name}" + description: "{description_group}" + }} + create_subinputs:[ + {{ + first_name: "{c4_first_name}" + last_name: "{c4_last_name}" + contact_type: "{contact_type}" + email: "{c4_mail}" + email_type: "{email_type}" + phone: "{c4_phone}" + phone_type: "{phone_type2}" + }} + ] + update_subinputs:[ + {{ + id: "{c1_id}" + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{contact_type}" + email_id: "{c1_email_id}" + email: "{c1_mail}" + email_type: "{email_type}" + email_id: "{c1_email_id}" + phone: "{c1_phone}" + phone_type: "{phone_type2}" + phone_id: "{c1_phone_id}" + }} + {{ + id: "{c2_id}" + first_name: "{c2_first_name}" + last_name: "{c2_last_name}" + contact_type: "{contact_type}" + email_id: "{c2_email_id}" + email: "{c2_mail}" + email_type: "{email_type}" + phone: "{c2_phone}" + phone_type: "{phone_type2}" + }} + ] + delete_subinputs:[ + {{ + id: "{c3_id}" + }} + ] + }}) + {{ + updated{{ + errors{{ + field + messages + }} + group{{ + id + name + description + }} + }} + subcreated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + member_of_groups{{ + name + }} + }} + }} + subupdated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + member_of_groups{{ + name + }} + }} + }} + subdeleted{{ + errors{{ + field + messages + }} + success + }} + }} + }} + '''.format(group_id=group_id, group_name=group_name, + description_group=description_group, + c4_first_name=c4_first_name, c4_last_name=c4_last_name, + contact_type=contact_type, c4_mail=c4_mail, + email_type=email_type, c4_phone=c4_phone, + phone_type2=phone_type2, c1_id=c1_id, + c1_first_name=c1_first_name, c1_last_name=c1_last_name, + c1_email_id=c1_email_id, c1_mail=c1_mail, + c1_phone=c1_phone, c1_phone_id=c1_phone_id, + c2_id=c2_id, c2_first_name=c2_first_name, + c2_last_name=c2_last_name, c2_email_id=c2_email_id, + c2_mail=c2_mail, c2_phone=c2_phone, c3_id=c3_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + updated_errors = result.data['composite_group']['updated']['errors'] + assert not updated_errors, pformat(updated_errors, indent=1) + + for subcreated in result.data['composite_group']['subcreated']: + assert not subcreated['errors'] + + for subupdated in result.data['composite_group']['subupdated']: + assert not subupdated['errors'] + + for subdeleted in result.data['composite_group']['subdeleted']: + assert not subdeleted['errors'] + + # get the ids + result_data = result.data['composite_group'] + c4_id = result_data['subcreated'][0]['contact']['id'] + + # check the integrity of the data + updated_data = result_data['updated']['group'] + + # check group + assert updated_data['name'] == group_name, \ + "Group name doesn't match \n{} != {}"\ + .format(updated_data['name'], group_name) + assert updated_data['description'] == description_group, \ + "Group name doesn't match \n{} != {}"\ + .format(updated_data['description'], description_group) + + # check members + subcreated_data = result_data['subcreated'] + subupdated_data = result_data['subupdated'] + subdeleted_data = result_data['subdeleted'] + + # fourth contact + assert subcreated_data[0]['contact']['first_name'] == c4_first_name, \ + "4th contact's first name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['first_name'], c4_first_name) + assert subcreated_data[0]['contact']['last_name'] == c4_last_name, \ + "4th contact's last name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['last_name'], c4_last_name) + assert subcreated_data[0]['contact']['emails'][0]['name'] == c4_mail, \ + "4th contact's email doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['emails'][0]['name'], c4_mail) + assert subcreated_data[0]['contact']['phones'][0]['name'] == c4_phone, \ + "4th contact's phone doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['phones'][0]['name'], c4_phone) + assert subcreated_data[0]['contact']['member_of_groups'][0]['name'] == group_name, \ + "4th contact's group name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['member_of_groups'][0]['name'], group_name) + + # first contact + assert subupdated_data[0]['contact']['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(subupdated_data[0]['contact']['first_name'], c1_first_name) + assert subupdated_data[0]['contact']['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(subupdated_data[0]['contact']['last_name'], c1_last_name) + assert subupdated_data[0]['contact']['emails'][0]['name'] == c1_mail, \ + "1st contact's email doesn't match \n{} != {}"\ + .format(subupdated_data[0]['contact']['emails'][0]['name'], c1_mail) + assert subupdated_data[0]['contact']['phones'][0]['name'] == c1_phone, \ + "1st contact's phone doesn't match \n{} != {}"\ + .format(subupdated_data[0]['contact']['phones'][0]['name'], c1_phone) + assert subupdated_data[0]['contact']['member_of_groups'][0]['name'] == group_name, \ + "1st contact's group name doesn't match \n{} != {}"\ + .format(subupdated_data[0]['contact']['member_of_groups'][0]['name'], group_name) + + # second contact + assert subupdated_data[1]['contact']['first_name'] == c2_first_name, \ + "2nd contact's first name doesn't match \n{} != {}"\ + .format(subupdated_data[1]['contact']['first_name'], c2_first_name) + assert subupdated_data[1]['contact']['last_name'] == c2_last_name, \ + "2nd contact's last name doesn't match \n{} != {}"\ + .format(subupdated_data[1]['contact']['last_name'], c2_last_name) + assert subupdated_data[1]['contact']['emails'][0]['name'] == c2_mail, \ + "2nd contact's email doesn't match \n{} != {}"\ + .format(subupdated_data[1]['contact']['emails'][0]['name'], c2_mail) + assert subupdated_data[1]['contact']['phones'][0]['name'] == c2_phone, \ + "1st contact's phone doesn't match \n{} != {}"\ + .format(subupdated_data[1]['contact']['phones'][0]['name'], c2_phone) + assert subupdated_data[1]['contact']['member_of_groups'][0]['name'] == group_name, \ + "2nd contact's group name doesn't match \n{} != {}"\ + .format(subupdated_data[1]['contact']['member_of_groups'][0]['name'], group_name) + + # third contact + assert subdeleted_data[0]['success'], "The requested contact couldn't be deleted" + + +class OrganizationComplexTest(Neo4jGraphQLTest): + def test_composite_organization(self): + org_name = "PyPI" + org_type = "partner" + org_id = "AABA" + parent_org_id = relay.Node.to_global_id('Organization', str(self.organization1.handle_id)) + org_web = "pypi.org" + org_num = "55446" + + contact_type = "person" + email_type = "work" + phone_type = "work" + + c1_first_name = "Janet" + c1_last_name = "Doe" + c1_email = "jdoe@pypi.org" + c1_phone = "+34600123456" + + c2_first_name = "Brian" + c2_last_name = "Smith" + c2_email = "bsmith@pypi.org" + c2_phone = "+34600789456" + + org_addr_name = "Main" + org_addr_st = "Fake St. 123" + org_addr_pcode = "21500" + org_addr_parea = "Huelva" + + org_addr_name2 = "Second" + org_addr_st2 = "Real St. 456" + org_addr_pcode2 = "41000" + org_addr_parea2 = "Sevilla" + + # Create query + + query = ''' + mutation{{ + composite_organization(input:{{ + create_input:{{ + name: "{org_name}" + type: "{org_type}" + affiliation_site_owner: true + organization_id: "{org_id}" + relationship_parent_of: "{parent_org_id}" + website: "{org_web}" + organization_number: "{org_num}" + }} + create_subinputs:[ + {{ + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{contact_type}" + email: "{c1_email}" + email_type: "{email_type}" + phone:"{c1_phone}" + phone_type: "{phone_type}" + }} + {{ + first_name: "{c2_first_name}" + last_name: "{c2_last_name}" + contact_type: "{contact_type}" + email: "{c2_email}" + email_type: "{email_type}" + phone:"{c2_phone}" + phone_type: "{phone_type}" + }} + ] + create_address:[ + {{ + name: "{org_addr_name}" + street: "{org_addr_st}" + postal_code: "{org_addr_pcode}" + postal_area: "{org_addr_parea}" + }} + {{ + name: "{org_addr_name2}" + street: "{org_addr_st2}" + postal_code: "{org_addr_pcode2}" + postal_area: "{org_addr_parea2}" + }} + ] + }}){{ + created{{ + errors{{ + field + messages + }} + organization{{ + id + type + name + description + addresses{{ + id + name + street + postal_code + postal_area + }} + contacts{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + }} + }} + }} + subcreated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + organizations{{ + id + name + }} + roles{{ + relation_id + name + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + }} + address_created{{ + errors{{ + field + messages + }} + address{{ + id + name + street + postal_code + postal_area + }} + }} + }} + }} + '''.format(org_name=org_name, org_type=org_type, org_id=org_id, + parent_org_id=parent_org_id, org_web=org_web, org_num=org_num, + c1_first_name=c1_first_name, c1_last_name=c1_last_name, + contact_type=contact_type, c1_email=c1_email, + email_type=email_type, c1_phone=c1_phone, + phone_type=phone_type, c2_first_name=c2_first_name, + c2_last_name=c2_last_name, c2_email=c2_email, + c2_phone=c2_phone, org_addr_name=org_addr_name, + org_addr_st=org_addr_st, org_addr_pcode=org_addr_pcode, + org_addr_parea=org_addr_parea, org_addr_name2=org_addr_name2, + org_addr_st2=org_addr_st2, org_addr_pcode2=org_addr_pcode2, + org_addr_parea2=org_addr_parea2) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + created_errors = result.data['composite_organization']['created']['errors'] + assert not created_errors, pformat(created_errors, indent=1) + + for subcreated in result.data['composite_organization']['subcreated']: + assert not subcreated['errors'] + + for subcreated in result.data['composite_organization']['address_created']: + assert not subcreated['errors'] + + # get the ids + result_data = result.data['composite_organization'] + organization_id = result_data['created']['organization']['id'] + c1_id = result_data['subcreated'][0]['contact']['id'] + c1_email_id = result_data['subcreated'][0]['contact']['emails'][0]['id'] + c1_phone_id = result_data['subcreated'][0]['contact']['phones'][0]['id'] + c1_org_rel_id = result_data['subcreated'][0]['contact']['roles'][0]['relation_id'] + c2_id = result_data['subcreated'][1]['contact']['id'] + address1_id = result_data['address_created'][0]['address']['id'] + address2_id = result_data['address_created'][1]['address']['id'] + + # check the integrity of the data + created_data = result_data['created']['organization'] + + # check organization + assert created_data['name'] == org_name, \ + "Organization name doesn't match \n{} != {}"\ + .format(created_data['name'], org_name) + assert created_data['type'] == org_type, \ + "Organization type doesn't match \n{} != {}"\ + .format(created_data['type'], org_type) + + # check subnodes + # address + address_node = created_data['addresses'][0] + assert address_node['name'] == org_addr_name, \ + "Address' name doesn't match \n{} != {}"\ + .format(address_node['name'], org_addr_name) + assert address_node['street'] == org_addr_st, \ + "Address' street doesn't match \n{} != {}"\ + .format(address_node['street'], org_addr_st) + assert address_node['postal_code'] == org_addr_pcode, \ + "Address' postal code doesn't match \n{} != {}"\ + .format(address_node['postal_code'], org_addr_pcode) + assert address_node['postal_area'] == org_addr_parea, \ + "Address' postal area doesn't match \n{} != {}"\ + .format(address_node['postal_area'], org_addr_parea) + + address_node = created_data['addresses'][1] + assert address_node['name'] == org_addr_name2, \ + "Address' 2 name doesn't match \n{} != {}"\ + .format(address_node['name'], org_addr_name2) + assert address_node['street'] == org_addr_st2, \ + "Address' 2 street doesn't match \n{} != {}"\ + .format(address_node['street'], org_addr_st2) + assert address_node['postal_code'] == org_addr_pcode2, \ + "Address' 2 postal code doesn't match \n{} != {}"\ + .format(address_node['postal_code'], org_addr_pcode2) + assert address_node['postal_area'] == org_addr_parea2, \ + "Address' 2 postal area doesn't match \n{} != {}"\ + .format(address_node['postal_area'], org_addr_parea2) + + # contacts + subcreated_data = result_data['subcreated'] + + assert subcreated_data[0]['contact']['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['first_name'], c1_first_name) + assert subcreated_data[0]['contact']['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['last_name'], c1_last_name) + assert subcreated_data[0]['contact']['emails'][0]['name'] == c1_email, \ + "1st contact's email doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['emails'][0]['name'], c1_email) + assert subcreated_data[0]['contact']['phones'][0]['name'] == c1_phone, \ + "1st contact's phone doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['phones'][0]['name'], c1_phone) + assert subcreated_data[0]['contact']['organizations'][0]['name'] == org_name, \ + "1st contact's organization name doesn't match \n{} != {}"\ + .format(subcreated_data[0]['contact']['organizations'][0]['name'], org_name) + + assert subcreated_data[1]['contact']['first_name'] == c2_first_name, \ + "2nd contact's first name doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['first_name'], c2_first_name) + assert subcreated_data[1]['contact']['last_name'] == c2_last_name, \ + "2nd contact's last name doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['last_name'], c2_last_name) + assert subcreated_data[1]['contact']['emails'][0]['name'] == c2_email, \ + "2nd contact's email doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['emails'][0]['name'], c2_email) + assert subcreated_data[1]['contact']['phones'][0]['name'] == c2_phone, \ + "2nd contact's phone doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['phones'][0]['name'], c2_phone) + assert subcreated_data[1]['contact']['organizations'][0]['name'] == org_name, \ + "2nd contact's organization name doesn't match \n{} != {}"\ + .format(subcreated_data[1]['contact']['organizations'][0]['name'], org_name) + + # Update query + org_name = 'FSF' + c1_email = "jdoe@fsf.org" + c2_email = "bsmith@fsf.org" + + c3_first_name = "Stella" + c3_last_name = "Svennson" + c3_email = "ssvensson@fsf.org" + c3_phone = "+34600555123" + + org_addr_name3 = "Thrid" + org_addr_st3 = "Imaginary St. 789" + org_addr_pcode3 = "41001" + org_addr_parea3 = "Sevilla" + + parent_org_id = relay.Node.to_global_id('Organization', str(self.organization2.handle_id)) + + nondefault_role = Role.objects.all().first() + nondefault_roleid = relay.Node.to_global_id("Role", nondefault_role.handle_id) + + query = ''' + mutation{{ + composite_organization(input:{{ + update_input: {{ + id: "{org_id}" + name: "{org_name}" + type: "{org_type}" + affiliation_site_owner: false + affiliation_partner: true + organization_id: "{org_id}" + relationship_parent_of: "{parent_org_id}" + website: "{org_web}" + organization_number: "{org_num}" + }} + create_subinputs:[{{ + first_name: "{c3_first_name}" + last_name: "{c3_last_name}" + contact_type: "{contact_type}" + email: "{c3_email}" + email_type: "{email_type}" + phone: "{c3_phone}" + phone_type: "{phone_type}" + role_id: "{nondefault_roleid}" + }}] + update_subinputs:[{{ + id: "{c1_id}" + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{contact_type}" + email: "{c1_email}" + email_type: "{email_type}" + email_id: "{c1_email_id}" + phone: "{c1_phone}" + phone_type: "{phone_type}" + role_id: "{nondefault_roleid}" + }}] + delete_subinputs:[{{ + id: "{c2_id}" + }}] + create_address:[{{ + name: "{org_addr_name3}" + street: "{org_addr_st3}" + postal_code: "{org_addr_pcode3}" + postal_area: "{org_addr_parea3}" + }}] + update_address:[{{ + id: "{address1_id}" + name: "{org_addr_name}" + street: "{org_addr_st}" + postal_code: "{org_addr_pcode}" + postal_area: "{org_addr_parea}" + }}] + delete_address:[{{ + id: "{address2_id}" + }}] + unlink_subinputs:[{{ + relation_id: {c1_org_rel_id} + }}] + }}){{ + updated{{ + errors{{ + field + messages + }} + organization{{ + id + type + name + description + addresses{{ + id + name + street + postal_code + postal_area + }} + contacts{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + organizations{{ + id + name + }} + roles{{ + relation_id + name + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + }} + }} + subcreated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + organizations{{ + id + name + }} + roles{{ + relation_id + name + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + }} + subupdated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + organizations{{ + id + name + }} + roles{{ + relation_id + name + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + }} + subdeleted{{ + errors{{ + field + messages + }} + success + }} + address_created{{ + errors{{ + field + messages + }} + address{{ + id + name + street + postal_code + postal_area + }} + }} + address_updated{{ + errors{{ + field + messages + }} + address{{ + id + name + street + postal_code + postal_area + }} + }} + address_deleted{{ + errors{{ + field + messages + }} + success + }} + }} + }} + '''.format(org_id=organization_id, org_name=org_name, + org_type=org_type, parent_org_id=parent_org_id, + org_web=org_web, org_num=org_num, c3_first_name=c3_first_name, + c3_last_name=c3_last_name, contact_type=contact_type, + c3_email=c3_email, email_type=email_type, c3_phone=c3_phone, + phone_type=phone_type, nondefault_roleid=nondefault_roleid, + c1_id=c1_id, c1_first_name=c1_first_name, + c1_last_name=c1_last_name, c1_email=c1_email, + c1_email_id=c1_email_id, c1_phone=c1_phone, + c2_id=c2_id, org_addr_name3=org_addr_name3, + org_addr_st3=org_addr_st3, org_addr_pcode3=org_addr_pcode3, + org_addr_parea3=org_addr_parea3, address1_id=address1_id, + org_addr_name=org_addr_name, org_addr_st=org_addr_st, + org_addr_pcode=org_addr_pcode, org_addr_parea=org_addr_parea, + address2_id=address2_id, c1_org_rel_id=c1_org_rel_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + updated_errors = result.data['composite_organization']['updated']['errors'] + assert not updated_errors, pformat(updated_errors, indent=1) + + for subcreated in result.data['composite_organization']['subcreated']: + assert not subcreated['errors'] + + for subupdated in result.data['composite_organization']['subupdated']: + assert not subupdated['errors'] + + for subdeleted in result.data['composite_organization']['subdeleted']: + assert not subdeleted['errors'] + + for subcreated in result.data['composite_organization']['address_created']: + assert not subcreated['errors'] + + for subupdated in result.data['composite_organization']['address_updated']: + assert not subupdated['errors'] + + for subdeleted in result.data['composite_organization']['address_deleted']: + assert not subdeleted['errors'] + + # get the ids + result_data = result.data['composite_organization'] + address3_id = result_data['address_created'][0]['address']['id'] + c3_id = result_data['subcreated'][0]['contact']['id'] + + # check the integrity of the data + updated_data = result_data['updated']['organization'] + + # check organization + assert updated_data['name'] == org_name, \ + "Organization name doesn't match \n{} != {}"\ + .format(updated_data['name'], org_name) + assert updated_data['type'] == org_type, \ + "Organization type doesn't match \n{} != {}"\ + .format(updated_data['type'], org_type) + + # check subnodes (address and contacts) + address_node_1 = None + address_node_3 = None + + for address_node in updated_data['addresses']: + if address_node['id'] == address1_id: + address_node_1 = address_node + elif address_node['id'] == address3_id: + address_node_3 = address_node + + self.assertIsNotNone(address_node_1) + + assert address_node_1['name'] == org_addr_name, \ + "Created address' name doesn't match \n{} != {}"\ + .format(address_node_1['name'], org_addr_name) + assert address_node_1['street'] == org_addr_st, \ + "Created address' street doesn't match \n{} != {}"\ + .format(address_node_1['street'], org_addr_st) + assert address_node_1['postal_code'] == org_addr_pcode, \ + "Created address' postal code doesn't match \n{} != {}"\ + .format(address_node_1['postal_code'], org_addr_pcode) + assert address_node_1['postal_area'] == org_addr_parea, \ + "Created address' postal area doesn't match \n{} != {}"\ + .format(address_node_1['postal_area'], org_addr_parea) + + self.assertIsNotNone(address_node_3) + + assert address_node_3['name'] == org_addr_name3, \ + "Created address' name doesn't match \n{} != {}"\ + .format(address_node_3['name'], org_addr_name3) + assert address_node_3['street'] == org_addr_st3, \ + "Created address' street doesn't match \n{} != {}"\ + .format(address_node_3['street'], org_addr_st3) + assert address_node_3['postal_code'] == org_addr_pcode3, \ + "Created address' postal code doesn't match \n{} != {}"\ + .format(address_node_3['postal_code'], org_addr_pcode3) + assert address_node_3['postal_area'] == org_addr_parea3, \ + "Created address' postal area doesn't match \n{} != {}"\ + .format(address_node_3['postal_area'], org_addr_parea3) + + contact_1 = None + contact_3 = None + + for contact_node in updated_data['contacts']: + if contact_node['id'] == c1_id: + contact_1 = contact_node + elif contact_node['id'] == c3_id: + contact_3 = contact_node + + self.assertIsNotNone(contact_1) + assert contact_1['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(contact_1['first_name'], c1_first_name) + assert contact_1['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(contact_1['last_name'], c1_last_name) + assert contact_1['emails'][0]['name'] == c1_email, \ + "1st contact's email doesn't match \n{} != {}"\ + .format(contact_1['emails'][0]['name'], c1_email) + assert contact_1['phones'][0]['name'] == c1_phone, \ + "1st contact's phone doesn't match \n{} != {}"\ + .format(contact_1['phones'][0]['name'], c1_phone) + assert contact_1['organizations'][0]['name'] == org_name, \ + "1st contact's organization name doesn't match \n{} != {}"\ + .format(contact_1['organizations'][0]['name'], org_name) + assert contact_1['roles'][0]['name'] == nondefault_role.name, \ + "1st contact's role name doesn't match \n{} != {}"\ + .format(contact_1['roles'][0]['name'], nondefault_role.name) + assert len(contact_1['roles']) == 1, "1st contact has two roles" + + self.assertIsNotNone(contact_3) + assert contact_3['first_name'] == c3_first_name, \ + "3rd contact's first name doesn't match \n{} != {}"\ + .format(contact_3['first_name'], c3_first_name) + assert contact_3['last_name'] == c3_last_name, \ + "3rd contact's last name doesn't match \n{} != {}"\ + .format(contact_3['last_name'], c3_last_name) + assert contact_3['emails'][0]['name'] == c3_email, \ + "3rd contact's email doesn't match \n{} != {}"\ + .format(contact_3['emails'][0]['name'], c3_email) + assert contact_3['phones'][0]['name'] == c3_phone, \ + "3rd contact's phone doesn't match \n{} != {}"\ + .format(contact_3['phones'][0]['name'], c3_phone) + assert contact_3['organizations'][0]['name'] == org_name, \ + "3rd contact's organization name doesn't match \n{} != {}"\ + .format(contact_3['organizations'][0]['name'], org_name) + assert contact_3['roles'][0]['name'] == nondefault_role.name, \ + "3rd contact's role name doesn't match \n{} != {}"\ + .format(contact_3['roles'][0]['name'], nondefault_role.name) + assert len(contact_3['roles']) == 1, "1st contact has two roles" + + # check for deleted address and contact + c2_handle_id = relay.Node.from_global_id(c2_id)[1] + c2_handle_id = int(c2_handle_id) + assert not NodeHandle.objects.filter(handle_id=c2_handle_id).exists(), \ + "Second contact of this organization should have been deleted" + + address2_handle_id = relay.Node.from_global_id(address2_id)[1] + address2_handle_id = int(address2_handle_id) + assert not NodeHandle.objects.filter(handle_id=address2_handle_id).exists(), \ + "Second address of this organization should have been deleted" + + +class ContactsComplexTest(Neo4jGraphQLTest): + def test_multiple_mutation(self): + c1_first_name = "Jane" + c1_last_name = "Doe" + c1_contact_type = "person" + c1_email = "jdoe@pypi.org" + c1_email_type = "work" + c2_email = "jdoe@myemail.org" + c2_email_type = "personal" + c1_phone = "+34600123456" + c1_phone_type = "work" + c2_phone = "+34600789456" + c2_phone_type = "personal" + + role_id = relay.Node.to_global_id( + 'Role', str(Role.objects.all().first().handle_id)) + organization_id = relay.Node.to_global_id( + 'Organization', str(self.organization1.handle_id)) + + query = ''' + mutation{{ + composite_contact(input:{{ + create_input:{{ + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{c1_contact_type}" + }} + create_subinputs:[ + {{ + name: "{c1_email}" + type: "{c1_email_type}" + }} + {{ + name: "{c2_email}" + type: "{c2_email_type}" + }} + ] + create_phones:[ + {{ + name: "{c1_phone}" + type: "{c1_phone_type}" + }} + {{ + name: "{c2_phone}" + type: "{c2_phone_type}" + }} + ] + link_rolerelations:[ + {{ + role_id: "{role_id}" + organization_id: "{organization_id}" + }} + ] + }}){{ + created{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + }} + }} + subcreated{{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + phones_created{{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + rolerelations{{ + errors{{ + field + messages + }} + rolerelation{{ + relation_id + type + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + }} + }} + '''.format(c1_first_name=c1_first_name, c1_last_name=c1_last_name, + c1_contact_type=c1_contact_type, c1_email=c1_email, + c1_email_type=c1_email_type, c2_email=c2_email, + c2_email_type=c2_email_type, c1_phone=c1_phone, + c1_phone_type=c1_phone_type, c2_phone=c2_phone, + c2_phone_type=c2_phone_type, role_id=role_id, + organization_id=organization_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + created_errors = result.data['composite_contact']['created']['errors'] + assert not created_errors, pformat(created_errors, indent=1) + + for subcreated in result.data['composite_contact']['subcreated']: + assert not subcreated['errors'] + + for subcreated in result.data['composite_contact']['phones_created']: + assert not subcreated['errors'] + + for subcreated in result.data['composite_contact']['rolerelations']: + assert not subcreated['errors'] + + # get the ids + result_data = result.data['composite_contact'] + c1_id = result_data['created']['contact']['id'] + c1_email_id = result_data['subcreated'][0]['email']['id'] + c1_email_id2 = result_data['subcreated'][1]['email']['id'] + c1_phone_id = result_data['phones_created'][0]['phone']['id'] + c1_phone_id2 = result_data['phones_created'][1]['phone']['id'] + role_relation_id = result_data['rolerelations'][0]['rolerelation']['relation_id'] + + # check the integrity of the data + created_data = result_data['created']['contact'] + + # check contact + assert created_data['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(created_data['first_name'], c1_first_name) + assert created_data['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(created_data['last_name'], c1_last_name) + + # check email + created_email_data = result_data['subcreated'][0]['email'] + + assert c1_email_id == created_data['emails'][0]['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_email_id, created_data['emails'][0]['id']) + assert c1_email == created_email_data['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c1_email, created_email_data['name']) + assert c1_email_type == created_email_data['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c1_email_type, created_email_data['type']) + + created_email_data = result_data['subcreated'][1]['email'] + + assert c1_email_id2 == created_data['emails'][1]['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_email_id2, created_data['emails'][1]['id']) + assert c2_email == created_email_data['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c2_email, created_email_data['name']) + assert c2_email_type == created_email_data['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c2_email_type, created_email_data['type']) + + # check phone + created_phone_data = result_data['phones_created'][0]['phone'] + + assert c1_phone_id == created_data['phones'][0]['id'], \ + "Contact's phone id doesn't match \n{} != {}"\ + .format(c1_phone_id, created_data['phones'][0]['id']) + assert c1_phone == created_phone_data['name'], \ + "Contact's phone doesn't match \n{} != {}"\ + .format(c1_phone, created_phone_data['name']) + assert c1_phone_type == created_phone_data['type'], \ + "Contact's phone type doesn't match \n{} != {}"\ + .format(c1_phone_type, created_phone_data['type']) + + # check rolerelation + rolerelation = result_data['rolerelations'][0]['rolerelation'] + + assert c1_id == rolerelation['start']['id'], \ + "Contact's id doesn't match with the one present in the relation \n\ + {} != {}".format(c1_id , rolerelation['start']['id'],) + assert organization_id == rolerelation['end']['id'], \ + "Organization's id doesn't match with the one present in the relation\n\ + {} != {}".format(organization_id , rolerelation['end']['id'],) + + # Update mutation + c1_first_name = "Anne" + c1_last_name = "Doe" + c1_email = "adoe@pypi.org" + c1_phone = "+34600000789" + + c3_email = "adoe@myemail.org" + c3_email_type = "personal" + c3_phone = "+34600111222" + c3_phone_type = "personal" + + role_id = relay.Node.to_global_id('Role', str(Role.objects.all().last().handle_id)) + organization_id = relay.Node.to_global_id('Organization', str(self.organization2.handle_id)) + + query = ''' + mutation{{ + composite_contact(input:{{ + update_input:{{ + id: "{c1_id}" + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{c1_contact_type}" + }} + create_subinputs:[{{ + name: "{c3_email}" + type: "{c3_email_type}" + }}] + update_subinputs:[{{ + id: "{c1_email_id}" + name: "{c1_email}" + type: "{c1_email_type}" + }}] + delete_subinputs:[{{ + id: "{c1_email_id2}" + }}] + create_phones:[{{ + name: "{c3_phone}" + type: "{c3_phone_type}" + }}] + update_phones:[{{ + id: "{c1_phone_id}" + name: "{c1_phone}" + type: "{c1_phone_type}" + }}] + link_rolerelations:[{{ + role_id: "{role_id}" + organization_id: "{organization_id}" + }}] + delete_phones:[{{ + id: "{c1_phone_id2}" + }}] + unlink_subinputs:[{{ + relation_id: {role_relation_id} + }}] + }}){{ + updated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + roles{{ + relation_id + start{{ + id + first_name + }} + end{{ + id + name + }} + }} + }} + }} + subcreated{{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + subupdated{{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + phones_created{{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + phones_updated{{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + rolerelations{{ + errors{{ + field + messages + }} + rolerelation{{ + relation_id + type + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + unlinked{{ + success + relation_id + }} + }} + }} + '''.format(c1_id=c1_id, c1_first_name=c1_first_name, + c1_last_name=c1_last_name, c1_contact_type=c1_contact_type, + c3_email=c3_email, c3_email_type=c3_email_type, + c1_email_id=c1_email_id, c1_email=c1_email, + c1_email_type=c1_email_type, c1_email_id2=c1_email_id2, + c3_phone=c3_phone, c3_phone_type=c3_phone_type, + c1_phone_id=c1_phone_id, c1_phone=c1_phone, + c1_phone_type=c1_phone_type, role_id=role_id, + organization_id=organization_id, c1_phone_id2=c1_phone_id2, + role_relation_id=role_relation_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + updated_errors = result.data['composite_contact']['updated']['errors'] + assert not updated_errors, pformat(updated_errors, indent=1) + + for subcreated in result.data['composite_contact']['subcreated']: + assert not subcreated['errors'] + + for subupdated in result.data['composite_contact']['subupdated']: + assert not subupdated['errors'] + + for subcreated in result.data['composite_contact']['phones_created']: + assert not subcreated['errors'] + + for subupdated in result.data['composite_contact']['phones_updated']: + assert not subupdated['errors'] + + for subcreated in result.data['composite_contact']['rolerelations']: + assert not subcreated['errors'] + + # get the ids + result_data = result.data['composite_contact'] + c1_email_id3 = result_data['subcreated'][0]['email']['id'] + c1_phone_id3 = result_data['phones_created'][0]['phone']['id'] + role_relation_id2 = result_data['rolerelations'][0]['rolerelation']['relation_id'] + + # check the integrity of the data + updated_data = result_data['updated']['contact'] + + # check contact + assert updated_data['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(updated_data['first_name'], c1_first_name) + assert updated_data['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(updated_data['last_name'], c1_last_name) + + # get emails and check them + email1_node = None + email3_node = None + + for email_node in updated_data['emails']: + if email_node['id'] == c1_email_id: + email1_node = email_node + elif email_node['id'] == c1_email_id3: + email3_node = email_node + + self.assertIsNotNone(email1_node) + + assert c1_email_id == email1_node['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_email_id, email1_node['id']) + assert c1_email == email1_node['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c1_email, email1_node['name']) + assert c1_email_type == email1_node['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c1_email_type, email1_node['type']) + + self.assertIsNotNone(email3_node) + + assert c1_email_id3 == email3_node['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_phone_id3, email3_node['id']) + assert c3_email == email3_node['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c3_email, email3_node['name']) + assert c3_email_type == email3_node['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c3_email_type, email3_node['type']) + + + # get phones and check them + phone1_node = None + phone3_node = None + + for phone_node in updated_data['phones']: + if phone_node['id'] == c1_phone_id: + phone1_node = phone_node + elif phone_node['id'] == c1_phone_id3: + phone3_node = phone_node + + self.assertIsNotNone(phone1_node) + + assert c1_phone_id == phone1_node['id'], \ + "Contact's phone id doesn't match \n{} != {}"\ + .format(c1_phone_id, phone1_node['id']) + assert c1_phone == phone1_node['name'], \ + "Contact's phone doesn't match \n{} != {}"\ + .format(c1_phone, phone1_node['name']) + assert c1_phone_type == phone1_node['type'], \ + "Contact's phone type doesn't match \n{} != {}"\ + .format(c1_phone_type, phone1_node['type']) + + self.assertIsNotNone(phone3_node) + + assert c1_phone_id3 == phone3_node['id'], \ + "Contact's phone id doesn't match \n{} != {}"\ + .format(c1_phone_id, phone3_node['id']) + assert c3_phone == phone3_node['name'], \ + "Contact's phone doesn't match \n{} != {}"\ + .format(c3_phone, phone3_node['name']) + assert c3_phone_type == phone3_node['type'], \ + "Contact's phone type doesn't match \n{} != {}"\ + .format(c3_phone_type, phone3_node['type']) + + # check rolerelation + assert len(result_data['rolerelations']) == 1, \ + 'This contact should only have one role' + rolerelation = result_data['rolerelations'][0]['rolerelation'] + + assert c1_id == rolerelation['start']['id'], \ + "Contact's id doesn't match with the one present in the relation \n\ + {} != {}".format(c1_id , rolerelation['start']['id'],) + assert organization_id == rolerelation['end']['id'], \ + "Organization's id doesn't match with the one present in the relation\n\ + {} != {}".format(organization_id , rolerelation['end']['id'],) + + # check for deleted email and phone + c1_email_hid2 = relay.Node.from_global_id(c1_email_id2)[1] + c1_email_hid2 = int(c1_email_hid2) + assert not NodeHandle.objects.filter(handle_id=c1_email_hid2).exists(), \ + "This email node should had been deleted" + + c1_phone_hid2 = relay.Node.from_global_id(c1_phone_id2)[1] + c1_phone_hid2 = int(c1_phone_hid2) + assert not NodeHandle.objects.filter(handle_id=c1_phone_hid2).exists(), \ + "This phone node should had been deleted" + + def test_multiple_mutation_2(self): + c1_first_name = "Jane" + c1_last_name = "Doe" + c1_contact_type = "person" + c1_email = "jdoe@pypi.org" + c1_email_type = "work" + c2_email = "jdoe@myemail.org" + c2_email_type = "personal" + c1_phone = "+34600123456" + c1_phone_type = "work" + c2_phone = "+34600789456" + c2_phone_type = "personal" + + role_id = relay.Node.to_global_id('Role', str(Role.objects.all().first().handle_id)) + organization_id = relay.Node.to_global_id('Organization', str(self.organization1.handle_id)) + + query = ''' + mutation{{ + composite_contact(input:{{ + create_input:{{ + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{c1_contact_type}" + }} + create_subinputs:[ + {{ + name: "{c1_email}" + type: "{c1_email_type}" + }} + {{ + name: "{c2_email}" + type: "{c2_email_type}" + }} + ] + create_phones:[ + {{ + name: "{c1_phone}" + type: "{c1_phone_type}" + }} + {{ + name: "{c2_phone}" + type: "{c2_phone_type}" + }} + ] + link_rolerelations:[ + {{ + role_id: "{role_id}" + organization_id: "{organization_id}" + }} + ] + }}){{ + created{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + }} + }} + subcreated{{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + phones_created{{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + rolerelations{{ + errors{{ + field + messages + }} + rolerelation{{ + relation_id + type + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + }} + }} + '''.format(c1_first_name=c1_first_name, c1_last_name=c1_last_name, + c1_contact_type=c1_contact_type, c1_email=c1_email, + c1_email_type=c1_email_type, c2_email=c2_email, + c2_email_type=c2_email_type, c1_phone=c1_phone, + c1_phone_type=c1_phone_type, c2_phone=c2_phone, + c2_phone_type=c2_phone_type, role_id=role_id, + organization_id=organization_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + created_errors = result.data['composite_contact']['created']['errors'] + assert not created_errors, pformat(created_errors, indent=1) + + for subcreated in result.data['composite_contact']['subcreated']: + assert not subcreated['errors'] + + for subcreated in result.data['composite_contact']['phones_created']: + assert not subcreated['errors'] + + # get the ids + result_data = result.data['composite_contact'] + c1_id = result_data['created']['contact']['id'] + c1_email_id = result_data['subcreated'][0]['email']['id'] + c1_email_id2 = result_data['subcreated'][1]['email']['id'] + c1_phone_id = result_data['phones_created'][0]['phone']['id'] + c1_phone_id2 = result_data['phones_created'][1]['phone']['id'] + role_relation_id = result_data['rolerelations'][0]['rolerelation']['relation_id'] + + # check the integrity of the data + created_data = result_data['created']['contact'] + + # check contact + assert created_data['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(created_data['first_name'], c1_first_name) + assert created_data['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(created_data['last_name'], c1_last_name) + + # check email + created_email_data = result_data['subcreated'][0]['email'] + + assert c1_email_id == created_data['emails'][0]['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_email_id, created_data['emails'][0]['id']) + assert c1_email == created_email_data['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c1_email, created_email_data['name']) + assert c1_email_type == created_email_data['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c1_email_type, created_email_data['type']) + + created_email_data = result_data['subcreated'][1]['email'] + + assert c1_email_id2 == created_data['emails'][1]['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_email_id2, created_data['emails'][1]['id']) + assert c2_email == created_email_data['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c2_email, created_email_data['name']) + assert c2_email_type == created_email_data['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c2_email_type, created_email_data['type']) + + # check phone + created_phone_data = result_data['phones_created'][0]['phone'] + + assert c1_phone_id == created_data['phones'][0]['id'], \ + "Contact's phone id doesn't match \n{} != {}"\ + .format(c1_phone_id, created_data['phones'][0]['id']) + assert c1_phone == created_phone_data['name'], \ + "Contact's phone doesn't match \n{} != {}"\ + .format(c1_phone, created_phone_data['name']) + assert c1_phone_type == created_phone_data['type'], \ + "Contact's phone type doesn't match \n{} != {}"\ + .format(c1_phone_type, created_phone_data['type']) + + # assert role relation + self.assertIsNotNone(role_relation_id, 'Role relation shouldn\'t be none') + + # Update mutation + c1_first_name = "Anne" + c1_last_name = "Doe" + c1_email = "adoe@pypi.org" + c1_phone = "+34600000789" + + c3_email = "adoe@myemail.org" + c3_email_type = "personal" + c3_phone = "+34600111222" + c3_phone_type = "personal" + + role_id = relay.Node.to_global_id('Role', str(Role.objects.all().last().handle_id)) + + query = ''' + mutation{{ + composite_contact(input:{{ + update_input:{{ + id: "{c1_id}" + first_name: "{c1_first_name}" + last_name: "{c1_last_name}" + contact_type: "{c1_contact_type}" + }} + create_subinputs:[{{ + name: "{c3_email}" + type: "{c3_email_type}" + }}] + update_subinputs:[{{ + id: "{c1_email_id}" + name: "{c1_email}" + type: "{c1_email_type}" + }}] + delete_subinputs:[{{ + id: "{c1_email_id2}" + }}] + create_phones:[{{ + name: "{c3_phone}" + type: "{c3_phone_type}" + }}] + update_phones:[{{ + id: "{c1_phone_id}" + name: "{c1_phone}" + type: "{c1_phone_type}" + }}] + link_rolerelations:[{{ + role_id: "{role_id}" + organization_id: "{organization_id}" + relation_id: {role_relation_id} + }}] + delete_phones:[{{ + id: "{c1_phone_id2}" + }}] + }}){{ + updated{{ + errors{{ + field + messages + }} + contact{{ + id + first_name + last_name + contact_type + emails{{ + id + name + type + }} + phones{{ + id + name + type + }} + roles{{ + relation_id + start{{ + id + first_name + }} + end{{ + id + name + }} + }} + }} + }} + subcreated{{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + subupdated{{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + phones_created{{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + phones_updated{{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + rolerelations{{ + errors{{ + field + messages + }} + rolerelation{{ + relation_id + type + start{{ + id + first_name + last_name + }} + end{{ + id + name + }} + }} + }} + }} + }} + '''.format(c1_id=c1_id, c1_first_name=c1_first_name, + c1_last_name=c1_last_name, c1_contact_type=c1_contact_type, + c3_email=c3_email, c3_email_type=c3_email_type, + c1_email_id=c1_email_id, c1_email=c1_email, + c1_email_type=c1_email_type, c1_email_id2=c1_email_id2, + c3_phone=c3_phone, c3_phone_type=c3_phone_type, + c1_phone_id=c1_phone_id, c1_phone=c1_phone, + c1_phone_type=c1_phone_type, role_id=role_id, + organization_id=organization_id, + role_relation_id=role_relation_id, c1_phone_id2=c1_phone_id2) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check for errors + updated_errors = result.data['composite_contact']['updated']['errors'] + assert not updated_errors, pformat(updated_errors, indent=1) + + for subcreated in result.data['composite_contact']['subcreated']: + assert not subcreated['errors'] + + for subupdated in result.data['composite_contact']['subupdated']: + assert not subupdated['errors'] + + for subcreated in result.data['composite_contact']['phones_created']: + assert not subcreated['errors'] + + for subupdated in result.data['composite_contact']['phones_updated']: + assert not subupdated['errors'] + + for subcreated in result.data['composite_contact']['rolerelations']: + assert not subcreated['errors'] + + # get the ids + result_data = result.data['composite_contact'] + c1_email_id3 = result_data['subcreated'][0]['email']['id'] + c1_phone_id3 = result_data['phones_created'][0]['phone']['id'] + role_relation_id2 = result_data['rolerelations'][0]['rolerelation']['relation_id'] + + # check the integrity of the data + updated_data = result_data['updated']['contact'] + + # check contact + assert updated_data['first_name'] == c1_first_name, \ + "1st contact's first name doesn't match \n{} != {}"\ + .format(updated_data['first_name'], c1_first_name) + assert updated_data['last_name'] == c1_last_name, \ + "1st contact's last name doesn't match \n{} != {}"\ + .format(updated_data['last_name'], c1_last_name) + + # get emails and check them + email1_node = None + email3_node = None + + for email_node in updated_data['emails']: + if email_node['id'] == c1_email_id: + email1_node = email_node + elif email_node['id'] == c1_email_id3: + email3_node = email_node + + self.assertIsNotNone(email1_node) + + assert c1_email_id == email1_node['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_email_id, email1_node['id']) + assert c1_email == email1_node['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c1_email, email1_node['name']) + assert c1_email_type == email1_node['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c1_email_type, email1_node['type']) + + self.assertIsNotNone(email3_node) + + assert c1_email_id3 == email3_node['id'], \ + "Contact's email id doesn't match \n{} != {}"\ + .format(c1_phone_id3, email3_node['id']) + assert c3_email == email3_node['name'], \ + "Contact's email doesn't match \n{} != {}"\ + .format(c3_email, email3_node['name']) + assert c3_email_type == email3_node['type'], \ + "Contact's email type doesn't match \n{} != {}"\ + .format(c3_email_type, email3_node['type']) + + + # get phones and check them + phone1_node = None + phone3_node = None + + for phone_node in updated_data['phones']: + if phone_node['id'] == c1_phone_id: + phone1_node = phone_node + elif phone_node['id'] == c1_phone_id3: + phone3_node = phone_node + + self.assertIsNotNone(phone1_node) + + assert c1_phone_id == phone1_node['id'], \ + "Contact's phone id doesn't match \n{} != {}"\ + .format(c1_phone_id, phone1_node['id']) + assert c1_phone == phone1_node['name'], \ + "Contact's phone doesn't match \n{} != {}"\ + .format(c1_phone, phone1_node['name']) + assert c1_phone_type == phone1_node['type'], \ + "Contact's phone type doesn't match \n{} != {}"\ + .format(c1_phone_type, phone1_node['type']) + + self.assertIsNotNone(phone3_node) + + assert c1_phone_id3 == phone3_node['id'], \ + "Contact's phone id doesn't match \n{} != {}"\ + .format(c1_phone_id, phone3_node['id']) + assert c3_phone == phone3_node['name'], \ + "Contact's phone doesn't match \n{} != {}"\ + .format(c3_phone, phone3_node['name']) + assert c3_phone_type == phone3_node['type'], \ + "Contact's phone type doesn't match \n{} != {}"\ + .format(c3_phone_type, phone3_node['type']) + + # check rolerelation + assert len(result_data['rolerelations']) == 1, \ + 'This contact should only have one role' + rolerelation = result_data['rolerelations'][0]['rolerelation'] + + # check number of roles + roles_data = result_data['updated']['contact']['roles'] + assert len(roles_data) == 1, \ + 'This contact should only have one role' + + assert c1_id == rolerelation['start']['id'], \ + "Contact's id doesn't match with the one present in the relation \n\ + {} != {}".format(c1_id , rolerelation['start']['id'],) + assert organization_id == rolerelation['end']['id'], \ + "Organization's id doesn't match with the one present in the relation\n\ + {} != {}".format(organization_id , rolerelation['end']['id'],) + + # check for deleted email and phone + c1_email_hid2 = relay.Node.from_global_id(c1_email_id2)[1] + c1_email_hid2 = int(c1_email_hid2) + assert not NodeHandle.objects.filter(handle_id=c1_email_hid2).exists(), \ + "This email node should had been deleted" + + c1_phone_hid2 = relay.Node.from_global_id(c1_phone_id2)[1] + c1_phone_hid2 = int(c1_phone_hid2) + assert not NodeHandle.objects.filter(handle_id=c1_phone_hid2).exists(), \ + "This phone node should had been deleted" diff --git a/src/niweb/apps/noclook/tests/schema/test_connections.py b/src/niweb/apps/noclook/tests/schema/test_connections.py new file mode 100644 index 000000000..a5020bd2f --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/test_connections.py @@ -0,0 +1,469 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from collections import OrderedDict +from pprint import pformat +from . import Neo4jGraphQLTest +from niweb.schema import schema + +class OrganizationConnectionTest(Neo4jGraphQLTest): + def test_organizations_order(self): + ## order by name + query = ''' + { + organizations( orderBy:name_ASC ){ + edges{ + node{ + name + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', + 'organization1')]))]), + OrderedDict([('node', + OrderedDict([('name', + 'organization2')]))])])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + query = ''' + { + organizations( orderBy:name_DESC ){ + edges{ + node{ + name + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', + 'organization2')]))]), + OrderedDict([('node', + OrderedDict([('name', + 'organization1')]))])])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + ## order by organization id + query = ''' + { + organizations( + orderBy: organization_id_ASC + ){ + edges{ + node{ + node_name + organization_id + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('node_name', + 'organization1'), + ('organization_id', + 'ORG1')]))]), + OrderedDict([('node', + OrderedDict([('node_name', + 'organization2'), + ('organization_id', + 'ORG2')]))])])]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + query = ''' + { + organizations( + orderBy: organization_id_DESC + ){ + edges{ + node{ + node_name + organization_id + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('node_name', + 'organization2'), + ('organization_id', + 'ORG2')]))]), + OrderedDict([('node', + OrderedDict([('node_name', + 'organization1'), + ('organization_id', + 'ORG1')]))])])]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + ## order by type + query = ''' + { + organizations( orderBy: type_ASC ){ + edges{ + node{ + node_name + type + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('node_name', + 'organization2'), + ('type', + 'university_coldep')]))]), + OrderedDict([('node', + OrderedDict([('node_name', + 'organization1'), + ('type', + 'university_college')]))])])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + query = ''' + { + organizations( orderBy: type_DESC ){ + edges{ + node{ + name + type + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', + 'organization1'), + ('type', + 'university_college')]))]), + OrderedDict([('node', + OrderedDict([('name', + 'organization2'), + ('type', + 'university_coldep')]))]), + ])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + def test_organizations_filter(self): + # filter by name + query = ''' + { + organizations( + filter:{ + AND:[{ + name: "organization1" + }] + } + ){ + edges{ + node{ + name + type + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', + 'organization1'), + ('type', + 'university_college')]))]) + ])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + # filter by organization_id + query = ''' + { + organizations( + filter:{ + AND:[{ + organization_id: "ORG1" + }] + } + ){ + edges{ + node{ + name + type + } + } + } + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + # filter by type + query = ''' + { + organizations( + filter:{ + AND:[{ + type: "university_college" + }] + } + ){ + edges{ + node{ + name + type + } + } + } + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + +class ContactConnectionTest(Neo4jGraphQLTest): + def test_organizations_order(self): + ## order by name + query = ''' + { + contacts( orderBy: name_ASC){ + edges{ + node{ + name + } + } + } + } + ''' + + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([ + ('name', + 'Jane Doe')]))]), + OrderedDict([('node', + OrderedDict([ + ('name', + 'John ' + 'Smith')]))])])]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + query = ''' + { + contacts( orderBy: name_DESC){ + edges{ + node{ + name + } + } + } + } + ''' + + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([ + ('name', + 'John ' + 'Smith')]))]), + OrderedDict([('node', + OrderedDict([ + ('name', + 'Jane Doe')]))]), + ])]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + ## order by organization + query = ''' + { + contacts( orderBy: organizations_ASC){ + edges{ + node{ + name + organizations{ + name + } + } + } + } + } + ''' + + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', 'Jane Doe'), + ('organizations', + [OrderedDict([('name', + 'organization1')])])]))]), + OrderedDict([('node', + OrderedDict([('name', 'John Smith'), + ('organizations', + [OrderedDict([('name', + 'organization2')])])]))])])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + query = ''' + { + contacts( orderBy: organizations_DESC){ + edges{ + node{ + name + organizations{ + name + } + } + } + } + } + ''' + + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', 'John Smith'), + ('organizations', + [OrderedDict([('name', + 'organization2')])])]))]), + OrderedDict([('node', + OrderedDict([('name', 'Jane Doe'), + ('organizations', + [OrderedDict([('name', + 'organization1')])])]))]), + ])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), + pformat(result.data, indent=1) + ) + + +class ConnectionTest(Neo4jGraphQLTest): + def test_filter(self): + ## create ## + query = ''' + { + groups(filter: {AND: [ + { + name: "group1" + } + ]}){ + edges{ + node{ + handle_id + name + outgoing { + name + relation { + id + } + } + } + } + } + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py new file mode 100644 index 000000000..429126ec2 --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -0,0 +1,1249 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from collections import OrderedDict +from graphene import relay +from niweb.schema import schema +from pprint import pformat +from . import Neo4jGraphQLTest + +class JWTTest(Neo4jGraphQLTest): + def test_jwt_mutations(self): + ### jwt mutations + ## get token + test_username="test user" + query = ''' + mutation{{ + token_auth(input: {{ username: "{user}", password: "{password}" }}) {{ + token + }} + }} + '''.format(user=test_username, password="test") + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + token = result.data['token_auth']['token'] + + ## verify token + query = ''' + mutation{{ + verify_token(input: {{ token: "{token}" }}) {{ + payload + }} + }} + '''.format(token=token) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data['verify_token']['payload']['username'] == test_username, \ + "The username from the jwt token doesn't match" + + ## refresh token + query = ''' + mutation{{ + refresh_token(input: {{ token: "{token}" }}) {{ + token + payload + }} + }} + '''.format(token=token) + result = schema.execute(query, context=self.context) + assert not result.errors, result.data['refresh_token']['payload'] + assert result.data['refresh_token']['payload']['username'] == test_username, \ + "The username from the jwt token doesn't match" + assert result.data['refresh_token']['token'], result.data['refresh_token']['token'] + +class SingleTest(Neo4jGraphQLTest): + def test_single_mutations(self): + ### Simple entity ### + ## create ## + new_group_name = "New test group" + query = ''' + mutation create_test_group {{ + create_group(input: {{name: "{new_group_name}"}}){{ + group {{ + handle_id + name + }} + clientMutationId + }} + }} + '''.format(new_group_name=new_group_name) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + create_group_result = result.data + + # query the api to get the handle_id of the new group + query = ''' + query {{ + groups(filter:{{ AND:[{{ name: "{new_group_name}" }}]}}){{ + edges{{ + node{{ + id + name + }} + }} + }} + }} + '''.format(new_group_name=new_group_name) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + group_handle_id = result.data['groups']['edges'][0]['node']['id'] + + expected = OrderedDict([('groups', + OrderedDict([('edges', + [ + OrderedDict([ + ('node', OrderedDict([ + ('id', group_handle_id), + ('name', new_group_name) + ])) + ]) + ] + )]) + )]) + + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(expected, indent=1), pformat(result.data, indent=1)) + + ## update ## + query = """ + mutation update_test_group {{ + update_group(input: {{ id: "{id}", name: "A test group"}} ){{ + group {{ + id + name + }} + clientMutationId + }} + }} + """.format(id=group_handle_id) + + expected = OrderedDict([ + ('update_group', + OrderedDict([ + ('group', + OrderedDict([ + ('id', group_handle_id), + ('name', 'A test group') + ])), + ('clientMutationId', None) + ]) + ) + ]) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected , pformat(result.data, indent=1) + + ## delete ## + query = """ + mutation delete_test_group {{ + delete_group(input: {{ id: "{id}" }}){{ + success + }} + }} + """.format(id=group_handle_id) + + expected = OrderedDict([ + ('delete_group', + OrderedDict([ + ('success', True), + ]) + ) + ]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected + + ### Composite entities (Contact) ### + # get the first organization + query= """ + { + organizations(orderBy: handle_id_ASC, first: 1) { + edges { + node { + id + } + } + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + organization_id = result.data['organizations']['edges'][0]['node']['id'] + + # get the first group + # query the api to get the handle_id of the new group + query= """ + { + groups(orderBy: handle_id_ASC, first: 1) { + edges { + node { + id + } + } + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + group_handle_id = result.data['groups']['edges'][0]['node']['id'] + + # get IT-manager role + query = ''' + { + roles(filter: {name: "NOC Manager"}){ + edges{ + node{ + id + name + } + } + } + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + role_id = result.data['roles']['edges'][0]['node']['id'] + + ## create ## + note_txt = "Lorem ipsum dolor sit amet" + + query = """ + mutation create_test_contact {{ + create_contact( + input: {{ + first_name: "Jane" + last_name: "Smith" + title: "" + contact_type: "person" + relationship_works_for: "{organization_id}" + role: "{role_id}" + relationship_member_of: "{group_handle_id}" + notes: "{note_txt}" + }} + ){{ + errors{{ + field + messages + }} + contact{{ + id + name + first_name + last_name + title + contact_type + notes + roles{{ + name + end{{ + id + node_name + }} + }} + member_of_groups{{ + name + }} + }} + }} + }} + """.format(organization_id=organization_id, + role_id=role_id, group_handle_id=group_handle_id, + note_txt=note_txt) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert not result.data['create_contact']['errors'], \ + pformat(result.data['create_contact']['errors'], indent=1) + contact_id = result.data['create_contact']['contact']['id'] + expected = OrderedDict([('create_contact', + OrderedDict([('errors', None), + ('contact', + OrderedDict([('id', contact_id), + ('name', 'Jane Smith'), + ('first_name', 'Jane'), + ('last_name', 'Smith'), + ('title', None), + ('contact_type', 'person'), + ('notes', note_txt), + ('roles', + [OrderedDict([('name', 'NOC Manager'), + ('end', + OrderedDict([('id', + str(organization_id)), + ('node_name', + 'organization1')]))])]), + ('member_of_groups', + [OrderedDict([('name', + 'group1')])])]))]))]) + + assert result.data == expected, pformat(result.data, indent=1) + + ## update ## + query = """ + mutation update_test_contact {{ + update_contact( + input: {{ + id: "{contact_id}" + first_name: "Janet" + last_name: "Doe" + contact_type: "person" + relationship_works_for: "{organization_id}" + role: "{role_id}" + relationship_member_of: "{group_handle_id}" + }} + ){{ + contact{{ + id + name + first_name + last_name + title + contact_type + roles{{ + name + end{{ + id + node_name + }} + }} + member_of_groups{{ + name + }} + }} + }} + }} + """.format(contact_id=contact_id, organization_id=organization_id, + role_id=role_id, group_handle_id=group_handle_id) + + expected = OrderedDict([('update_contact', + OrderedDict([('contact', + OrderedDict([('id', contact_id), + ('name', 'Janet Doe'), + ('first_name', 'Janet'), + ('last_name', 'Doe'), + ('title', None), + ('contact_type', 'person'), + ('roles', + [OrderedDict([('name', 'NOC Manager'), + ('end', + OrderedDict([('id', + organization_id), + ('node_name', + 'organization1')]))])]), + ('member_of_groups', + [OrderedDict([('name', + 'group1')])])]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + # test error output + query = ''' + mutation{{ + update_contact(input:{{ + id: "{contact_id}", + first_name: "Janet" + last_name: "Janet" + contact_type: "doesnt_exists" + }}){{ + contact{{ + id + name + }} + errors{{ + field + messages + }} + }} + }} + '''.format(contact_id=contact_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert 'errors' in result.data['update_contact'], pformat(result.data, indent=1) + + # test another erroneous form + fake_org_id = relay.Node.to_global_id('Organization', "-1") + query = ''' + mutation{{ + update_contact(input:{{ + id: "{contact_id}", + first_name: "Janet" + last_name: "Janet" + contact_type: "person" + relationship_works_for: "{organization_id}" + }}){{ + contact{{ + id + name + }} + errors{{ + field + messages + }} + }} + }} + '''.format(contact_id=contact_id, organization_id=fake_org_id) + result = schema.execute(query, context=self.context) + assert 'errors' in result.data['update_contact'], pformat(result.data, indent=1) + + ## delete ## + query = """ + mutation delete_test_contact {{ + delete_contact(input: {{ id: "{contact_id}" }}){{ + success + }} + }} + """.format(contact_id=contact_id) + + expected = OrderedDict([ + ('delete_contact', + OrderedDict([ + ('success', True), + ]) + ) + ]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + +class MultipleEntityTest(Neo4jGraphQLTest): + def test_multiple_entity_mutations(self): + ### Composite entities (Organization) ### + # get the first organization + query= """ + { + organizations(orderBy: handle_id_ASC, first: 1) { + edges { + node { + id + } + } + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + organization_id = result.data['organizations']['edges'][0]['node']['id'] + + # get the first two contacts + query= """ + { + contacts(orderBy: handle_id_ASC, first: 2){ + edges{ + node{ + id + } + } + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact_1_id = result.data['contacts']['edges'][0]['node']['id'] + contact_2_id = result.data['contacts']['edges'][1]['node']['id'] + + assert contact_1_id != contact_2_id, 'The contact ids are equal' + + incident_management_info = "Nullam eleifend ultrices risus, ac dignissim sapien mollis id. Aenean ante nibh, pharetra ac accumsan eget, suscipit eget purus. Ut sit amet diam in arcu dapibus ultricies. Phasellus a consequat eros. Proin cursus commodo consequat. Fusce nisl metus, egestas eu blandit sit amet, condimentum vitae felis." + website = "www.demo.org" + organization_number = "1234A" + + query = """ + mutation{{ + create_organization( + input: {{ + name: "Another org", + description: "This is the description of the new organization", + incident_management_info: "{incident_management_info}", + relationship_parent_of: "{organization_id}", + abuse_contact: "{contact_1_id}", + primary_contact: "{contact_2_id}", + secondary_contact: "{contact_1_id}", + it_technical_contact: "{contact_2_id}", + it_security_contact: "{contact_1_id}", + it_manager_contact: "{contact_2_id}", + affiliation_provider: true, + affiliation_customer: true, + website: "{website}", + organization_number: "{organization_number}" + }} + ){{ + errors{{ + field + messages + }} + organization{{ + id + name + description + incident_management_info + affiliation_provider + affiliation_customer + website + organization_number + incoming{{ + name + relation{{ + id + start{{ + id + node_name + }} + end{{ + id + node_name + }} + }} + }} + }} + }} + }} + """.format(organization_id=organization_id, + contact_1_id=contact_1_id, contact_2_id=contact_2_id, + incident_management_info=incident_management_info, + website=website, organization_number=organization_number) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + form_errors = result.data['create_organization']['errors'] + assert not form_errors, pformat(form_errors, indent=1) + organization_id_2 = result.data['create_organization']['organization']['id'] + incoming_relations = result.data['create_organization']['organization']['incoming'] + + expected = OrderedDict([('create_organization', + OrderedDict([('errors', None), + ('organization', + OrderedDict([('id', organization_id_2), + ('name', 'Another org'), + ('description', + 'This is the description of the new ' + 'organization'), + ('incident_management_info', + incident_management_info), + ('affiliation_provider', True), + ('affiliation_customer', True), + ('website', website), + ('organization_number', organization_number), + ('incoming', incoming_relations)]))]))]) + + found_offspring = False + for relation in incoming_relations: + for k, relation_dict in relation.items(): + if k == 'name' and relation_dict == 'Parent_of': + start_id = relay.Node.from_global_id(relation['relation']['start']['id'])[1] + org_id = relay.Node.from_global_id(organization_id)[1] + + if start_id == org_id: + found_offspring = True + + assert found_offspring, pformat(result.data, indent=1) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{}\n!=\n{}'.format( + pformat(result.data, indent=1), pformat(expected, indent=1)) + + ## edit organization + query = """ + mutation{{ + update_organization( + input: {{ + id: "{organization_id}" + name: "Another org", + description: "This is the description of the new organization", + abuse_contact: "{contact_1_id}", + primary_contact: "{contact_2_id}", + secondary_contact: "{contact_1_id}", + it_technical_contact: "{contact_2_id}", + it_security_contact: "{contact_1_id}", + it_manager_contact: "{contact_2_id}", + affiliation_provider: false, + affiliation_partner: true, + website: "{website}", + organization_number: "{organization_number}" + }} + ){{ + organization{{ + id + name + description + incident_management_info + affiliation_provider + affiliation_partner + affiliation_customer + website + organization_number + }} + }} + }} + """.format(organization_id=organization_id_2, + contact_1_id=contact_1_id, contact_2_id=contact_2_id, + website=website, organization_number=organization_number) + + expected = OrderedDict([('update_organization', + OrderedDict([('organization', + OrderedDict([('id', organization_id_2), + ('name', 'Another org'), + ('description', + 'This is the description of the new ' + 'organization'), + ('incident_management_info', + incident_management_info), + ('affiliation_provider', False), + ('affiliation_partner', True), + ('affiliation_customer', True), + ('website', website), + ('organization_number', organization_number)]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + ### Phone/Email tests ### + + ## create ## + phone_num = "823-971-5606" + phone_type = "work" + query = """ + mutation{{ + create_phone(input:{{ + type: "{phone_type}", + name: "{phone_num}", + contact: "{contact_id}" + }}){{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + }} + """.format(phone_type=phone_type, phone_num=phone_num, + contact_id=contact_1_id) + + expected = OrderedDict([('create_phone', + OrderedDict([('errors', None), + ('phone', + OrderedDict([('id', None), + ('name', phone_num), + ('type', phone_type)]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + phone_id_str = result.data['create_phone']['phone']['id'] + expected['create_phone']['phone']['id'] = phone_id_str + + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## read ## + query = """ + {{ + getContactById(id: "{contact_id}"){{ + id + phones{{ + id + name + type + }} + }} + }} + """.format(contact_id=contact_1_id) + + expected = OrderedDict([('getContactById', + OrderedDict([('id', contact_1_id), + ('phones', + [OrderedDict([('id', phone_id_str), + ('name', phone_num), + ('type', phone_type)])])]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## update ## + new_phone_num = "617-372-0822" + query = """ + mutation{{ + update_phone(input:{{ + id: "{phone_id}" + type: "{phone_type}", + name: "{phone_num}", + contact: "{contact_id}" + }}){{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + }} + """.format(phone_id=phone_id_str, phone_type=phone_type, + phone_num=new_phone_num, contact_id=contact_1_id) + + expected = OrderedDict([('update_phone', + OrderedDict([('errors', None), + ('phone', + OrderedDict([('id', phone_id_str), + ('name', new_phone_num), + ('type', phone_type)]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## delete ## + query = """ + mutation{{ + delete_phone(input: {{ + id: "{phone_id}" + }}){{ + errors{{ + field + messages + }} + success + }} + }} + """.format(phone_id=phone_id_str) + + expected = OrderedDict([('delete_phone', + OrderedDict([('errors', None), + ('success', True) + ]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## Email ## + ## create ## + email_str = "ssvensson@sunet.se" + email_type = "work" + query = """ + mutation{{ + create_email(input:{{ + type: "{email_type}", + name: "{email_str}", + contact: "{contact_id}" + }}){{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + }} + """.format(email_type=email_type, email_str=email_str, + contact_id=contact_1_id) + + expected = OrderedDict([('create_email', + OrderedDict([('errors', None), + ('email', + OrderedDict([('id', None), + ('name', email_str), + ('type', email_type)]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + email_id_str = result.data['create_email']['email']['id'] + expected['create_email']['email']['id'] = email_id_str + + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## read ## + query = """ + {{ + getContactById(id: "{contact_id}"){{ + id + emails{{ + id + name + type + }} + }} + }} + """.format(contact_id=contact_1_id) + + expected = OrderedDict([('getContactById', + OrderedDict([('id', str(contact_1_id)), + ('emails', + [OrderedDict([('id', email_id_str), + ('name', email_str), + ('type', email_type)])])]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## update ## + new_email = "617-372-0822" + query = """ + mutation{{ + update_email(input:{{ + id: "{email_id}" + type: "{email_type}", + name: "{email_str}", + contact: "{contact_id}" + }}){{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + }} + """.format(email_id=email_id_str, email_type=email_type, + email_str=new_email, contact_id=contact_1_id) + + expected = OrderedDict([('update_email', + OrderedDict([('errors', None), + ('email', + OrderedDict([('id', email_id_str), + ('name', new_email), + ('type', email_type)]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## delete ## + query = """ + mutation{{ + delete_email(input: {{ + id: "{email_id}" + }}){{ + errors{{ + field + messages + }} + success + }} + }} + """.format(email_id=email_id_str) + + expected = OrderedDict([('delete_email', + OrderedDict([('errors', None), + ('success', True) + ]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## Address ## + ## create ## + address_name = "New address" + address_website = "emergya.com" + address_phone = "617-372-0822" + address_street = "Fake st 123" + address_postal_code = "12345" + address_postal_area = "Sevilla" + + query = """ + mutation{{ + create_address(input:{{ + organization: "{organization_id}", + name: "{address_name}", + phone: "{address_phone}", + street: "{address_street}", + postal_code: "{address_postal_code}", + postal_area: "{address_postal_area}" + }}){{ + errors{{ + field + messages + }} + address{{ + id + name + phone + street + postal_code + postal_area + }} + }} + }} + """.format(organization_id=organization_id, address_name=address_name, + address_website=address_website, address_phone=address_phone, + address_street=address_street, + address_postal_code=address_postal_code, + address_postal_area=address_postal_area) + + expected = OrderedDict([('create_address', + OrderedDict([('errors', None), + ('address', + OrderedDict([('id', None), + ('name', address_name), + ('phone', address_phone), + ('street', address_street), + ('postal_code', address_postal_code), + ('postal_area', address_postal_area), + ]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + address_id_str = result.data['create_address']['address']['id'] + expected['create_address']['address']['id'] = address_id_str + + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## read ## + query = """ + {{ + getOrganizationById(id: "{organization_id}"){{ + id + addresses{{ + id + name + phone + street + postal_code + postal_area + }} + }} + }} + """.format(organization_id=organization_id) + + expected = OrderedDict([('getOrganizationById', + OrderedDict([('id', organization_id), + ('addresses', + [OrderedDict([('id', address_id_str), + ('name', address_name), + ('phone', address_phone), + ('street', address_street), + ('postal_code', address_postal_code), + ('postal_area', address_postal_area) + ])])]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## update ## + new_website = "www.emergyadigital.com" + query = """ + mutation{{ + update_address(input:{{ + id: "{address_id}", + organization: "{organization_id}", + name: "{address_name}", + phone: "{address_phone}", + street: "{address_street}", + postal_code: "{address_postal_code}", + postal_area: "{address_postal_area}" + }}){{ + errors{{ + field + messages + }} + address{{ + id + name + phone + street + postal_code + postal_area + }} + }} + }} + """.format(address_id=address_id_str, + organization_id=organization_id, address_name=address_name, + address_phone=address_phone, address_street=address_street, + address_postal_code=address_postal_code, + address_postal_area=address_postal_area) + + expected = OrderedDict([('update_address', + OrderedDict([('errors', None), + ('address', + OrderedDict([('id', address_id_str), + ('name', address_name), + ('phone', address_phone), + ('street', address_street), + ('postal_code', address_postal_code), + ('postal_area', address_postal_area)]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + + ## delete ## + query = """ + mutation{{ + delete_address(input: {{ + id: "{address_id}" + }}){{ + errors{{ + field + messages + }} + success + }} + }} + """.format(address_id=address_id_str) + + expected = OrderedDict([('delete_address', + OrderedDict([('errors', None), + ('success', True) + ]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) +class CommentsTest(Neo4jGraphQLTest): + def test_comments_mutations(self): + ### Comments tests ### + organization_id = relay.Node.to_global_id('Organization', + str(self.organization1.handle_id)) + + ## create ## + query = """ + mutation{{ + create_comment( + input:{{ + object_id: "{organization_id}", + comment: "This comment was added using the graphql api" + }} + ){{ + comment{{ + object_id + comment + is_public + }} + }} + }} + """.format(organization_id=organization_id) + + expected = OrderedDict([('create_comment', + OrderedDict([('comment', + OrderedDict([('object_id', organization_id), + ('comment', + 'This comment was added using the ' + 'graphql api'), + ('is_public', True)]))]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + ## read ## + query = """ + {{ + getOrganizationById(id: "{organization_id}"){{ + comments{{ + id + comment + }} + }} + }} + """.format(organization_id=organization_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + comment_id = result.data['getOrganizationById']['comments'][0]['id'] + + ## update ## + query = """ + mutation{{ + update_comment( + input:{{ + id: "{comment_id}", + comment: "This comment was added using SRI's graphql api" + }} + ){{ + comment{{ + id + comment + is_public + }} + }} + }} + """.format(comment_id=comment_id) + + expected = OrderedDict([('update_comment', + OrderedDict([('comment', + OrderedDict([('id', comment_id), + ('comment', + 'This comment was added using SRI\'s ' + 'graphql api'), + ('is_public', True)]))]))]) + + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + ## delete ## + query = """ + mutation{{ + delete_comment(input:{{ + id: "{comment_id}" + }}){{ + success + id + }} + }} + """.format(comment_id=comment_id) + + expected = OrderedDict([ + ('delete_comment', + OrderedDict([('success', True), ('id', comment_id)]) + ) + ]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + +class ValidationTest(Neo4jGraphQLTest): + def test_node_validation(self): + # add an abuse contact first + organization_id = relay.Node.to_global_id('Organization', + str(self.organization1.handle_id)) + organization2_id = relay.Node.to_global_id('Organization', + str(self.organization2.handle_id)) + contact_1 = relay.Node.to_global_id('Contact', + str(self.contact1.handle_id)) + + query = ''' + mutation{{ + update_organization(input: {{ + id: "{organization_id}" + name: "Organization 1" + }}){{ + errors{{ + field + messages + }} + organization{{ + id + name + incoming{{ + name + relation{{ + nidata{{ + name + value + }} + start{{ + id + node_name + }} + }} + }} + }} + }} + }} + '''.format(organization_id=organization_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert not result.data['update_organization']['errors'], \ + pformat(result.data['update_organization']['errors'], indent=1) + + # add a non valid contact (an organization) + query = ''' + mutation{{ + update_organization(input: {{ + id: "{organization_id}" + name: "Organization 1" + relationship_parent_of: "{organization_2}" + }}){{ + errors{{ + field + messages + }} + organization{{ + id + name + incoming{{ + name + relation{{ + nidata{{ + name + value + }} + start{{ + id + node_name + }} + }} + }} + }} + }} + }} + '''.format(organization_id=organization_id, organization_2=contact_1) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data['update_organization']['errors'], \ + print('{}\n{}'.format(pformat(result.data, indent=1), pformat(result.errors, indent=1))) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py new file mode 100644 index 000000000..f0498ce2c --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -0,0 +1,1508 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook.models import NodeHandle, Dropdown, Choice, Role, Group, \ + GroupContextAuthzAction, NodeHandleContext, DEFAULT_ROLEGROUP_NAME +from collections import OrderedDict +from django.utils.dateparse import parse_datetime +from graphene import relay +from niweb.schema import schema +from pprint import pformat +from . import Neo4jGraphQLTest + +from datetime import datetime + +class SimpleListTest(Neo4jGraphQLTest): + def test_simple_list(self): + # query all available types + test_types = { + 'organization': [self.organization1, self.organization2], + 'contact': [self.contact1, self.contact2], + 'group': [self.group1, self.group2], + } + + for name, nodes in test_types.items(): + query = ''' + {{ + all_{}s {{ + handle_id + node_name + }} + }} + '''.format(name) + + node_list = [] + + for node in nodes: + node_dict = OrderedDict([ + ('handle_id', str(node.handle_id)), + ('node_name', node.node_name) + ]) + node_list.append(node_dict) + + expected = OrderedDict([ + ('all_{}s'.format(name), node_list) + ]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + assert result.data == expected, '{} \n != {}'.format( + pformat(result.data, indent=1), + pformat(expected, indent=1) + ) + +class ConnectionsTest(Neo4jGraphQLTest): + def test_connections(self): + # test contacts: slicing and ordering + query = ''' + query getLastTwoContacts { + contacts(first: 2, orderBy: handle_id_DESC) { + edges { + node { + name + first_name + last_name + member_of_groups { + name + } + roles{ + name + } + } + } + } + } + ''' + + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', 'John Smith'), + ('first_name', 'John'), + ('last_name', 'Smith'), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])]), + ('roles', + [OrderedDict([('name', + 'role2')])])]))]), + OrderedDict([('node', + OrderedDict([('name', 'Jane Doe'), + ('first_name', 'Jane'), + ('last_name', 'Doe'), + ('member_of_groups', + [OrderedDict([('name', + 'group1')])]), + ('roles', + [OrderedDict([('name', + 'role1')])])]))])])]))]) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + # AND filter with subentities + query = ''' + query { + contacts(filter: {AND: [ + { + member_of_groups: { name: "Group2" }, + roles: { name: "Role2"} + } + ]}){ + edges{ + node{ + name + roles{ + name + } + member_of_groups{ + name + } + } + } + } + } + ''' + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', 'John Smith'), + ('roles', + [OrderedDict([('name', + 'role2')])]), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])])]))])])]))]) + + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + query = ''' + query { + contacts(orderBy: handle_id_DESC, filter: {AND: [ + { + member_of_groups_in: [{ name: "Group1" }, { name: "gRoup2" }], + roles_in: [{ name: "ROLE1" }, { name: "role2" }] + } + ]}){ + edges{ + node{ + name + member_of_groups{ + name + } + roles{ + name + } + } + } + } + } + ''' + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', 'John Smith'), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])]), + ('roles', + [OrderedDict([('name', + 'role2')])])]))]), + OrderedDict([('node', + OrderedDict([('name', 'Jane Doe'), + ('member_of_groups', + [OrderedDict([('name', + 'group1')])]), + ('roles', + [OrderedDict([('name', + 'role1')])])]))])])]))]) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + query = ''' + query { + contacts(filter: {AND: [ + { + member_of_groups: { name: "Group2" }, + roles: { name: "role2" } + } + ]}){ + edges{ + node{ + name + roles{ + name + } + member_of_groups{ + name + } + } + } + } + } + ''' + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([ + ('name', 'John Smith'), + ('roles', + [OrderedDict([('name', + 'role2')])]), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])])]))])])]))]) + + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + # filter by ScalarChoice + query = ''' + { + getAvailableDropdowns + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert 'organization_types' in result.data['getAvailableDropdowns'], pformat(result.data, indent=1) + + query = ''' + { + getChoicesForDropdown(name: "organization_types"){ + name + value + } + } + ''' + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + found = False + test_org_type = 'university_college' + for pair in result.data['getChoicesForDropdown']: + if pair['value'] == test_org_type: + found = True + break + + assert found, pformat(result.data, indent=1) + + query = ''' + { + organizations(filter:{ + AND: [ + { type: "university_college" } + ] + }){ + edges{ + node{ + name + type + } + } + } + } + ''' + + expected = OrderedDict([('organizations', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([ + ('name', + 'organization1'), + ('type', + 'university_college')]))])])]))]) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + # filter tests + query = ''' + { + groups(first: 10, filter:{ + AND:[{ + name: "group1", name_not: "group2", + name_not_in: ["group2"] + }] + }, orderBy: handle_id_ASC){ + edges{ + node{ + name + } + } + } + } + ''' + expected = OrderedDict([('groups', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', + 'group1')] + ))])] + )])) + ]) + + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + query = ''' + { + groups(first: 10, filter:{ + OR:[{ + name_in: ["group1", "group2"] + },{ + name: "group2", + }] + }, orderBy: handle_id_ASC){ + edges{ + node{ + name + } + } + } + } + ''' + expected = OrderedDict([('groups', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', 'group1')]))]), + OrderedDict([('node', + OrderedDict([('name', + 'group2')]))])])]))]) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + # test date and user filters + + # but first get the user and date + query = ''' + { + groups(first:2){ + edges{ + node{ + handle_id + node_name + created + modified + creator{ + username + } + } + } + } + } + ''' + result = schema.execute(query, context=self.context) + + node = result.data['groups']['edges'][0]['node'] + username = node['creator']['username'] + created = node['created'] + modified = node['modified'] + created_dt = parse_datetime(created) + modified_dt = parse_datetime(modified) + handle_id = int(node['handle_id']) + + # modify the second group to add an hour so it can be filtered + node2 = result.data['groups']['edges'][1]['node'] + handle_id2 = int(node2['handle_id']) + group2 = NodeHandle.objects.get(handle_id=handle_id2) + + query = ''' + { + groups(first:2){ + edges{ + node{ + handle_id + node_name + created + modified + creator{ + username + } + } + } + } + } + ''' + result = schema.execute(query, context=self.context) + + node2 = result.data['groups']['edges'][1]['node'] + created2 = node2['created'] + modified2 = node2['modified'] + + # test date filters: AND + query = ''' + {{ + groups(first: 10, filter:{{ + AND:[{{ + created: "{adate}", + created_in: ["{adate}"] + }}] + }}, orderBy: handle_id_ASC){{ + edges{{ + node{{ + name + }} + }} + }} + }} + '''.format(adate=created, nodate=created2) + expected = OrderedDict([('groups', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('name', + 'group1')] + ))]), + OrderedDict([('node', + OrderedDict([('name', + 'group2')] + ))]),] + )])) + ]) + + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + query = ''' + {{ + groups(first: 10, filter:{{ + AND:[{{ + modified: "{adate}", + modified_in: ["{adate}"] + }}] + }}, orderBy: handle_id_ASC){{ + edges{{ + node{{ + name + }} + }} + }} + }} + '''.format(adate=created) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + # test date filters: OR + query = ''' + {{ + groups(first: 10, filter:{{ + OR:[{{ + created: "{adate}", + created_in: ["{adate}"] + }}] + }}, orderBy: handle_id_ASC){{ + edges{{ + node{{ + name + }} + }} + }} + }} + '''.format(adate=created) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + query = ''' + {{ + groups(first: 10, filter:{{ + OR:[{{ + modified: "{adate}", + modified_in: ["{adate}"] + }}] + }}, orderBy: handle_id_ASC){{ + edges{{ + node{{ + name + }} + }} + }} + }} + '''.format(adate=created) + + result = schema.execute(query, context=self.context) + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) + + +class DropdownTest(Neo4jGraphQLTest): + def test_dropdown(self): + query = ''' + { + getAvailableDropdowns + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert 'contact_type' in result.data['getAvailableDropdowns'], pformat(result.data, indent=1) + + query = ''' + query{ + getChoicesForDropdown(name:"contact_type"){ + name + value + } + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + +class RelationResolversTest(Neo4jGraphQLTest): + def test_relation_resolvers(self): + ## get aux entities types + # get phone types + query = """ + { + getChoicesForDropdown(name: "contact_type"){ + value + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact_type = result.data['getChoicesForDropdown'][-1]['value'] + + # get phone types + query = """ + { + getChoicesForDropdown(name: "phone_type"){ + value + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + phone_type = result.data['getChoicesForDropdown'][-1]['value'] + + # get email types + query = """ + { + getChoicesForDropdown(name: "email_type"){ + value + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + email_type = result.data['getChoicesForDropdown'][-1]['value'] + + + # get the first group + # query the api to get the handle_id of the new group + query= """ + { + groups(orderBy: handle_id_ASC, first: 1) { + edges { + node { + id + } + } + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + group_handle_id = result.data['groups']['edges'][0]['node']['id'] + + # get the two contacts + query= """ + { + contacts(orderBy: handle_id_ASC, first: 2){ + edges{ + node{ + id + first_name + last_name + contact_type + } + } + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact_1_id = result.data['contacts']['edges'][0]['node']['id'] + contact_1_fname = result.data['contacts']['edges'][0]['node']['first_name'] + contact_1_lname = result.data['contacts']['edges'][0]['node']['last_name'] + contact_2_id = result.data['contacts']['edges'][1]['node']['id'] + contact_2_fname = result.data['contacts']['edges'][1]['node']['first_name'] + contact_2_lname = result.data['contacts']['edges'][1]['node']['last_name'] + + assert contact_1_id != contact_2_id, 'The contact ids are equal' + + # create a phone for the first contact + phone_number = '453-896-3068' + query = """ + mutation{{ + create_phone(input:{{ + contact: "{contact_1_id}", + name: "{phone_number}" + type: "{phone_type}", + }}){{ + errors{{ + field + messages + }} + phone{{ + id + name + type + }} + }} + }} + """.format(contact_1_id=contact_1_id, phone_number=phone_number, + phone_type=phone_type) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert not result.data['create_phone']['errors'], \ + pformat(result.data['create_phone']['errors'], indent=1) + + phone_id = result.data['create_phone']['phone']['id'] + + # create an email for the first contact + email_dir = "cnewby1@joomla.org" + query = """ + mutation{{ + create_email(input:{{ + contact: "{contact_1_id}", + name: "{email_dir}" + type: "{email_type}", + }}){{ + errors{{ + field + messages + }} + email{{ + id + name + type + }} + }} + }} + """.format(contact_1_id=contact_1_id, email_dir=email_dir, + email_type=email_type) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert not result.data['create_email']['errors'], \ + pformat(result.data['create_email']['errors'], indent=1) + + email_id = result.data['create_email']['email']['id'] + + # check the contact has the right phone and email set + query = """ + {{ + getContactById(id: "{contact_1_id}"){{ + id + name + phones{{ + id + }} + emails{{ + id + }} + }} + }} + """.format(contact_1_id=contact_1_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + tphone_id = result.data['getContactById']['phones'][0]['id'] + temail_id = result.data['getContactById']['emails'][0]['id'] + + assert phone_id == tphone_id, \ + "Phone id don't match: {} != {}".format(phone_id, tphone_id) + + assert email_id == temail_id, \ + "Email id don't match: {} != {}".format(email_id, temail_id) + + # associate first contact to group + query = """ + mutation{{ + update_contact(input:{{ + id: "{contact_1_id}", + first_name: "{contact_1_fname}", + last_name: "{contact_1_lname}", + contact_type: "{contact_1_ctype}", + relationship_member_of: "{group_handle_id}" + }}){{ + errors{{ + field + messages + }} + contact{{ + handle_id + member_of_groups{{ + id + }} + }} + }} + }} + """.format(contact_1_id=contact_1_id, contact_1_fname=contact_1_fname, + contact_1_lname=contact_1_lname, + contact_1_ctype=contact_type, + group_handle_id=group_handle_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert not result.data['update_contact']['errors'], \ + pformat(result.data['update_contact']['errors'], indent=1) + + t_group_handle_id = \ + result.data['update_contact']['contact']['member_of_groups'][0]['id'] + assert t_group_handle_id == group_handle_id + + # get the first organization + query = """ + { + organizations(orderBy: handle_id_ASC, first: 1) { + edges { + node { + id + } + } + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + organization_id = result.data['organizations']['edges'][0]['node']['id'] + + # create address for organization + street_str = "Calle Luis de Morales, 32, 5º, Puerta 5" + postal_c_str = "41018" + postal_a_str = "Seville" + query = """ + mutation{{ + create_address(input:{{ + organization: "{organization_id}", + name: "New address", + phone: "{phone_number}", + street: "{street_str}", + postal_code: "{postal_c_str}", + postal_area: "{postal_a_str}", + }}){{ + errors{{ + field + messages + }} + address{{ + id + name + phone + street + postal_code + postal_area + }} + }} + }} + """.format(organization_id=organization_id, + phone_number=phone_number, street_str=street_str, + postal_c_str=postal_c_str, postal_a_str=postal_a_str) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + assert not result.data['create_address']['errors'], \ + pformat(result.data['create_address']['errors'], indent=1) + + address_id = result.data['create_address']['address']['id'] + + # check the address has been added + query = """ + {{ + getOrganizationById(id: "{organization_id}"){{ + id + name + addresses{{ + id + name + phone + street + postal_code + postal_area + }} + }} + }} + """.format(organization_id=organization_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + taddress_id = result.data['getOrganizationById']['addresses'][0]['id'] + taddress_name = result.data['getOrganizationById']['addresses'][0]['name'] + taddress_phone = result.data['getOrganizationById']['addresses'][0]['phone'] + taddress_street = result.data['getOrganizationById']['addresses'][0]['street'] + taddress_postal_code = result.data['getOrganizationById']['addresses'][0]['postal_code'] + taddress_postal_area = result.data['getOrganizationById']['addresses'][0]['postal_area'] + + assert address_id == taddress_id, \ + "Address id don't match: {} != {}".format(address_id, taddress_id) + assert "New address" == taddress_name, \ + "Address name don't match: {}".format(taddress_name) + assert phone_number == taddress_phone, \ + "Address phone don't match: {} != {}".format(phone_number, taddress_phone) + assert street_str == taddress_street, \ + "Address string don't match: {} != {}".format(phone_number, taddress_street) + assert postal_c_str == taddress_postal_code, \ + "Address string don't match: {} != {}".format(postal_c_str, taddress_postal_code) + assert postal_a_str == taddress_postal_area, \ + "Address string don't match: {} != {}".format(postal_a_str, taddress_postal_area) + + # get relation from Group - Contact + query = """ + {{ + getGroupById(id: "{group_handle_id}"){{ + id + name + contacts{{ + id + name + }} + incoming{{ + name + relation{{ + relation_id + start{{ + id + node_name + }} + end{{ + id + node_name + }} + }} + }} + }} + }} + """.format(group_handle_id=group_handle_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + start_id = result.data['getGroupById']['incoming'][0]['relation']['start']['id'] + end_id = result.data['getGroupById']['incoming'][0]['relation']['end']['id'] + relation_id = result.data['getGroupById']['incoming'][0]['relation']['relation_id'] + + start_handle_id = relay.Node.from_global_id(start_id)[1] + end_handle_id = relay.Node.from_global_id(end_id)[1] + + c1_handle_id = relay.Node.from_global_id(contact_1_id)[1] + g_handle_id = relay.Node.from_global_id(group_handle_id)[1] + + assert start_handle_id == c1_handle_id, \ + "Contact id don't match: {} != {}".format(start_handle_id, c1_handle_id) + + assert end_handle_id == g_handle_id, \ + "Group id don't match: {} != {}".format(end_handle_id, g_handle_id) + + assert relation_id, "Relation id is null" + + # delete relationship + query = """ + mutation{{ + delete_relationship(input:{{ + relation_id: {relation_id} + }}){{ + success + }} + }} + """.format(relation_id=relation_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check that it doesn't exist anymore + query = """ + {{ + getContactById(id: "{contact_1_id}"){{ + id + member_of_groups{{ + id + }} + }} + }} + """.format(contact_1_id=contact_1_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact_groups = result.data['getContactById']['member_of_groups'] + assert not contact_groups, "Groups should be empty \n{}".format( + pformat(contact_groups, indent=1) + ) + + # get relation from Contact - Email + query = """ + {{ + getContactById(id: "{contact_1_id}"){{ + id + outgoing{{ + name + relation{{ + relation_id + start{{ + id + node_name + }} + end{{ + id + node_name + }} + }} + }} + }} + }} + """.format(contact_1_id=contact_1_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + idx = 0 + for relation in result.data['getContactById']['outgoing']: + if relation['name'] == 'Has_email': + break + idx = idx + 1 + + start_id = result.data['getContactById']['outgoing'][idx]['relation']['start']['id'] + end_id = result.data['getContactById']['outgoing'][idx]['relation']['end']['id'] + relation_id = result.data['getContactById']['outgoing'][idx]['relation']['relation_id'] + + start_handle_id = relay.Node.from_global_id(start_id)[1] + end_handle_id = relay.Node.from_global_id(end_id)[1] + + c1_handle_id = relay.Node.from_global_id(contact_1_id)[1] + email_handle_id = relay.Node.from_global_id(email_id)[1] + + assert start_handle_id == c1_handle_id, \ + "Contact id don't match: {} != {}".format(start_handle_id, c1_handle_id) + + assert end_handle_id == email_handle_id, \ + "Email id don't match: {} != {}".format(end_handle_id, email_handle_id) + + # delete relationship + query = """ + mutation{{ + delete_relationship(input:{{ + relation_id: {relation_id} + }}){{ + success + }} + }} + """.format(relation_id=relation_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check that it doesn't exist anymore + query = """ + {{ + getContactById(id: "{contact_1_id}"){{ + id + emails{{ + id + }} + }} + }} + """.format(contact_1_id=contact_1_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact_emails = result.data['getContactById']['emails'] + assert not contact_emails, "Emails should be empty \n{}".format( + pformat(contact_emails, indent=1) + ) + + # get relation from Contact - Phone + query = """ + {{ + getContactById(id: "{contact_1_id}"){{ + id + outgoing{{ + name + relation{{ + relation_id + start{{ + id + node_name + }} + end{{ + id + node_name + }} + }} + }} + }} + }} + """.format(contact_1_id=contact_1_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + idx = 0 + for relation in result.data['getContactById']['outgoing']: + if relation['name'] == 'Has_phone': + break + idx = idx + 1 + + start_id = result.data['getContactById']['outgoing'][idx]['relation']['start']['id'] + end_id = result.data['getContactById']['outgoing'][idx]['relation']['end']['id'] + relation_id = result.data['getContactById']['outgoing'][idx]['relation']['relation_id'] + + start_handle_id = relay.Node.from_global_id(start_id)[1] + end_handle_id = relay.Node.from_global_id(end_id)[1] + + c1_handle_id = relay.Node.from_global_id(contact_1_id)[1] + p_handle_id = relay.Node.from_global_id(phone_id)[1] + + assert start_handle_id == c1_handle_id, \ + "Contact id don't match: {} != {}".format(start_handle_id, c1_handle_id) + + assert end_handle_id == p_handle_id, \ + "Phone id don't match: {} != {}".format(end_handle_id, p_handle_id) + + # delete relationship + query = """ + mutation{{ + delete_relationship(input:{{ + relation_id: {relation_id} + }}){{ + success + }} + }} + """.format(relation_id=relation_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check that it doesn't exist anymore + query = """ + {{ + getContactById(id: "{contact_1_id}"){{ + id + phones{{ + id + }} + }} + }} + """.format(contact_1_id=contact_1_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact_phones = result.data['getContactById']['phones'] + assert not contact_phones, "Phones should be empty \n{}".format( + pformat(contact_phones, indent=1) + ) + + # get relation from Organization - Address + query = """ + {{ + getOrganizationById(id: "{organization_id}"){{ + id + outgoing{{ + name + relation{{ + relation_id + start{{ + id + node_name + }} + end{{ + id + node_name + }} + }} + }} + }} + }} + """.format(organization_id=organization_id) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + idx = 0 + for relation in result.data['getOrganizationById']['outgoing']: + if relation['name'] == 'Has_address': + break + idx = idx + 1 + + start_id = result.data['getOrganizationById']['outgoing'][idx]['relation']['start']['id'] + end_id = result.data['getOrganizationById']['outgoing'][idx]['relation']['end']['id'] + relation_id = result.data['getOrganizationById']['outgoing'][idx]['relation']['relation_id'] + + start_handle_id = relay.Node.from_global_id(start_id)[1] + end_handle_id = relay.Node.from_global_id(end_id)[1] + + org1_handle_id = relay.Node.from_global_id(organization_id)[1] + addr_handle_id = relay.Node.from_global_id(address_id)[1] + + assert start_handle_id == org1_handle_id, \ + "Contact id don't match: {} != {}".format(start_handle_id, org1_handle_id) + + assert end_handle_id == addr_handle_id, \ + "Phone id don't match: {} != {}".format(end_handle_id, addr_handle_id) + + # delete relationship + query = """ + mutation{{ + delete_relationship(input:{{ + relation_id: {relation_id} + }}){{ + success + }} + }} + """.format(relation_id=relation_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + # check that it doesn't exist anymore + query = """ + {{ + getOrganizationById(id: "{organization_id}"){{ + id + addresses{{ + handle_id + }} + }} + }} + """.format(organization_id=organization_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + organization_addresses = result.data['getOrganizationById']['addresses'] + assert not organization_addresses, "Address array should be empty \n{}".format( + pformat(organization_addresses, indent=1) + ) + + +class CascadeDeleteTest(Neo4jGraphQLTest): + def test_cascade_delete(self): + ## get aux entities types + # get contact types + query = """ + { + getChoicesForDropdown(name: "organization_types"){ + value + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + organization_type = result.data['getChoicesForDropdown'][-1]['value'] + + # get contact types + query = """ + { + getChoicesForDropdown(name: "contact_type"){ + value + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact_type = result.data['getChoicesForDropdown'][-1]['value'] + + # get phone types + query = """ + { + getChoicesForDropdown(name: "phone_type"){ + value + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + phone_type = result.data['getChoicesForDropdown'][-1]['value'] + + # get email types + query = """ + { + getChoicesForDropdown(name: "email_type"){ + value + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + email_type = result.data['getChoicesForDropdown'][-1]['value'] + + # create new organization + query = """ + mutation{ + create_organization( + input:{ + name: "Emergya" + description: "A test organization" + } + ){ + errors{ + field + messages + } + organization{ + id + name + } + } + } + """ + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + organization3_id = result.data['create_organization']['organization']['id'] + + # create a new contact for this organization + query = """ + mutation{{ + create_contact(input:{{ + first_name: "Jasmine" + last_name: "Svensson" + contact_type: "person" + relationship_works_for: "{organization_id}" + }}){{ + errors{{ + field + messages + }} + contact{{ + id + name + }} + }} + }} + """.format(organization_id=organization3_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + contact3_id = result.data['create_contact']['contact']['id'] + + # add email and get id + query = """ + mutation{{ + create_email(input:{{ + name: "jsvensson@emergya.com" + type: "work" + contact: "{contact_id}" + }}){{ + errors{{ + field + messages + }} + email{{ + id + name + }} + }} + }} + """.format(contact_id=contact3_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + email3_id = result.data['create_email']['email']['id'] + + # add phone and get id + query = """ + mutation{{ + create_phone(input:{{ + name: "+34606000606" + type: "work" + contact: "{contact_id}" + }}){{ + errors{{ + field + messages + }} + phone{{ + id + name + }} + }} + }} + """.format(contact_id=contact3_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + phone3_id = result.data['create_phone']['phone']['id'] + + # delete contact + query = """ + mutation{{ + delete_contact(input:{{ id: "{contact_id}" }}){{ + errors{{ + field + messages + }} + success + }} + }} + """.format(contact_id=contact3_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + errors = result.data['delete_contact']['errors'] + success = result.data['delete_contact']['success'] + assert not errors, pformat(errors, indent=1) + assert success, pformat(success, indent=1) + + # check organization still exists + query = """ + {{ + getOrganizationById( id: "{organization_id}" ){{ + id + name + }} + }} + """.format(organization_id=organization3_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + edges = result.data['getOrganizationById'] + assert edges, \ + "Organization query is empty:\n {}".format(pformat(edges, indent=1)) + test_org_id = result.data['getOrganizationById']['id'] + assert test_org_id == organization3_id, \ + print("Organization doesn't exists") + + # check email and phone are deleted + query = """ + {{ + getEmailById( id: "{email_id}" ){{ + handle_id + name + }} + }} + """.format(email_id=email3_id) + result = schema.execute(query, context=self.context) + + expected_error = [()] + assert result.errors, pformat(result, indent=1) + + query = """ + {{ + getPhoneById( id: "{phone_id}" ){{ + id + name + }} + }} + """.format(phone_id=phone3_id) + result = schema.execute(query, context=self.context) + assert result.errors, pformat(result, indent=1) + + # create address + address_name = "New address" + address_website = "emergya.com" + address_phone = "617-372-0822" + address_street = "Fake st 123" + address_postal_code = "12345" + address_postal_area = "Sevilla" + + query = """ + mutation{{ + create_address(input:{{ + organization: "{organization_id}", + name: "{address_name}", + phone: "{address_phone}", + street: "{address_street}", + postal_code: "{address_postal_code}", + postal_area: "{address_postal_area}" + }}){{ + errors{{ + field + messages + }} + address{{ + id + name + phone + street + postal_code + postal_area + }} + }} + }} + """.format(organization_id=organization3_id, address_name=address_name, + address_phone=address_phone, address_street=address_street, + address_postal_code=address_postal_code, + address_postal_area=address_postal_area) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + address_id_str = result.data['create_address']['address']['id'] + + # delete organization + query = """ + mutation{{ + delete_organization(input:{{ id: "{organization_id}" }}){{ + errors{{ + field + messages + }} + success + }} + }} + """.format(organization_id=organization3_id) + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + errors = result.data['delete_organization']['errors'] + success = result.data['delete_organization']['success'] + assert not errors, pformat(errors, indent=1) + assert success, pformat(success, indent=1) + + # check address is deleted + query = """ + {{ + getAddressById( id: {address_id_str} ){{ + edges{{ + node{{ + id + name + }} + }} + }} + }} + """.format(address_id_str=address_id_str) + result = schema.execute(query, context=self.context) + assert result.errors, pformat(result, indent=1) + + +class RoleGroupTest(Neo4jGraphQLTest): + def test_rolegroup(self): + query = ''' + { + getAvailableRoleGroups{ + name + } + } + ''' + + expected = [] + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + found = False + + for rolegroups in result.data['getAvailableRoleGroups']: + for k, gname in rolegroups.items(): + if gname == DEFAULT_ROLEGROUP_NAME: + found = True + + assert found, pformat(result.data, indent=1) + + query = """ + { + getRolesFromRoleGroup{ + handle_id + name + slug + description + } + } + """ + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + no_args_roles = result.data['getRolesFromRoleGroup'] + + query = ''' + {{ + getRolesFromRoleGroup(name: "{default_rolegroup}"){{ + handle_id + name + slug + description + }} + }} + '''.format(default_rolegroup=DEFAULT_ROLEGROUP_NAME) + + result = schema.execute(query, context=self.context) + assert not result.errors, pformat(result.errors, indent=1) + + args_roles = result.data['getRolesFromRoleGroup'] + + assert args_roles == no_args_roles, "{}\n!=\n{}".format( + pformat(no_args_roles, indent=1), + pformat(args_roles, indent=1) + ) diff --git a/src/niweb/apps/noclook/tests/stressload/__init__.py b/src/niweb/apps/noclook/tests/stressload/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py new file mode 100644 index 000000000..57b78c789 --- /dev/null +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -0,0 +1,470 @@ +# -*- coding: utf-8 -*- + +__author__ = 'ffuentes' + +from collections import OrderedDict +from faker import Faker +from apps.nerds.lib.consumer_util import get_user +from apps.noclook import helpers +from apps.noclook.models import NodeHandle, NodeType, Dropdown, Choice, NodeHandleContext +from django.contrib.auth.models import User +from django.template.defaultfilters import slugify +import norduniclient as nc +from norduniclient import META_TYPES + +import apps.noclook.vakt.utils as sriutils +import random + +class FakeDataGenerator: + def __init__(self, seed=None): + locales = OrderedDict([ + ('en_GB', 1), + ('sv_SE', 2), + ]) + self.fake = Faker(locales) + + if seed: + self.fake.seed_instance(seed) + + def escape_quotes(self, str_in): + return str_in.replace("'", "\'") + + def company_name(self): + return self.escape_quotes( self.fake.company() ) + + def first_name(self): + return self.escape_quotes( self.fake.first_name() ) + + def last_name(self): + return self.escape_quotes( self.fake.last_name() ) + + def rand_person_or_company_name(self): + person_name = '{} {}'.format(self.first_name(), self.last_name()) + company_name = self.company_name() + name = random.choice((person_name, company_name)) + + return name + + @staticmethod + def clean_rogue_nodetype(): + NodeType.objects.filter(type="").delete() + + +class CommunityFakeDataGenerator(FakeDataGenerator): + def create_fake_contact(self): + salutations = ['Ms.', 'Mr.', 'Dr.', 'Mrs.', 'Mx.'] + contact_types_drop = Dropdown.objects.get(name='contact_type') + contact_types = Choice.objects.filter(dropdown=contact_types_drop) + contact_types = [x.value for x in contact_types] + + contact_dict = { + 'salutation': random.choice(salutations), + 'first_name': self.first_name(), + 'last_name': self.last_name(), + 'title': '', + 'contact_role': self.fake.job(), + 'contact_type': random.choice(contact_types), + 'mailing_street': self.fake.address().replace('\n', ' '), + 'mailing_city': self.fake.city(), + 'mailing_zip': self.fake.postcode(), + 'mailing_state': self.fake.state(), + 'mailing_country': self.fake.country(), + 'phone': self.fake.phone_number(), + 'mobile': self.fake.phone_number(), + 'fax': self.fake.phone_number(), + 'email': self.fake.ascii_company_email(), + 'other_email': self.fake.ascii_company_email(), + 'PGP_fingerprint': self.fake.sha256(False), + 'account_name': '', + } + + return contact_dict + + def create_fake_organization(self): + organization_name = self.company_name() + organization_id = organization_name.upper() + + org_types_drop = Dropdown.objects.get(name='organization_types') + org_types = Choice.objects.filter(dropdown=org_types_drop) + org_types = [x.value for x in org_types] + + organization_dict = { + 'organization_number': '', + 'account_name': organization_name, + 'description': self.fake.catch_phrase(), + 'phone': self.fake.phone_number(), + 'website': self.fake.url(), + 'organization_id': organization_id, + 'type': random.choice(org_types), + 'parent_account': '', + } + + return organization_dict + + def create_fake_group(self): + group_dict = { + 'name': self.fake.sentence(), + 'description': self.fake.paragraph(), + } + + return group_dict + + +class NetworkFakeDataGenerator(FakeDataGenerator): + def __init__(self, seed=None): + super().__init__() + + self.user = get_user() + + # set vars + self.max_cable_providers = 5 + self.max_ports_total = 40 + + def add_network_context(self, nh): + net_ctx = sriutils.get_network_context() + NodeHandleContext(nodehandle=nh, context=net_ctx).save() + + @staticmethod + def get_nodetype(type_name): + return NodeType.objects.get_or_create(type=type_name, slug=slugify(type_name))[0] + + def get_or_create_node(self, node_name, type_name, meta_type): + node_type = NetworkFakeDataGenerator.get_nodetype(type_name) + + # create object + nh = NodeHandle.objects.get_or_create( + node_name=node_name, + node_type=node_type, + node_meta_type=meta_type, + creator=self.user, + modifier=self.user + )[0] + + return nh + + def get_dropdown_keys(self, dropdown_name): + return [ x[0] for x in Dropdown.get(dropdown_name).as_choices()[1:] ] + + ## Organizations + def create_customer(self): + # create object + name = self.rand_person_or_company_name() + customer = self.get_or_create_node( + name, 'Customer', META_TYPES[2]) # Relation + + # add context + self.add_network_context(customer) + + data = { + 'url': self.fake.url(), + 'description': self.fake.paragraph(), + } + + for key, value in data.items(): + customer.get_node().add_property(key, value) + + return customer + + def create_end_user(self): + # create object + name = self.rand_person_or_company_name() + enduser = self.get_or_create_node( + name, 'End User', META_TYPES[2]) # Relation + + # add context + self.add_network_context(enduser) + + data = { + 'url': self.fake.url(), + 'description': self.fake.paragraph(), + } + + for key, value in data.items(): + enduser.get_node().add_property(key, value) + + return enduser + + def create_peering_partner(self): + # create object + name = self.company_name() + peering_partner = self.get_or_create_node( + name, 'Peering Partner', META_TYPES[2]) # Relation + + data = { + 'as_number' : str(random.randint(0, 99999)).zfill(5), + } + + for key, value in data.items(): + peering_partner.get_node().add_property(key, value) + + # add context + self.add_network_context(peering_partner) + + return peering_partner + + def create_peering_group(self): + # create object + name = self.company_name() + peering_group = self.get_or_create_node( + name, 'Peering Group', META_TYPES[1]) # Logical + + # add context + self.add_network_context(peering_group) + + return peering_group + + def create_provider(self): + provider = self.get_or_create_node( + self.company_name(), 'Provider', META_TYPES[2]) # Relation + + # add context + self.add_network_context(provider) + + data = { + 'url' : self.fake.url(), + 'description': self.fake.paragraph(), + } + + for key, value in data.items(): + provider.get_node().add_property(key, value) + + return provider + + def create_site_owner(self): + # create object + name = self.rand_person_or_company_name() + siteowner = self.get_or_create_node( + name, 'Site Owner', META_TYPES[2]) # Relation + + # add context + self.add_network_context(siteowner) + + data = { + 'url': self.fake.url(), + 'description': self.fake.paragraph(), + } + + for key, value in data.items(): + siteowner.get_node().add_property(key, value) + + return siteowner + + ## Equipment and cables + + def create_port(self): + # create object + port = self.get_or_create_node( + str(random.randint(0, 50000)), 'Port', META_TYPES[0]) + + # add context + self.add_network_context(port) + + # add data + port_types = self.get_dropdown_keys('port_types') + + data = { + 'port_type' : random.choice(port_types), + 'description' : self.fake.paragraph(), + #'relationship_parent' : None, # not used for the moment + } + + for key, value in data.items(): + port.get_node().add_property(key, value) + + return port + + def create_cable(self): + # create object + cable = self.get_or_create_node( + self.fake.hostname(), 'Cable', META_TYPES[0]) + + # add context + self.add_network_context(cable) + + # add data + cable_types = self.get_dropdown_keys('cable_types') + + # check if there's any provider or if we should create one + provider_type = NetworkFakeDataGenerator.get_nodetype('Provider') + providers = NodeHandle.objects.filter(node_type=provider_type) + + max_providers = self.max_cable_providers + provider = None + + if not providers or len(providers) < max_providers: + provider = self.create_provider() + else: + provider = random.choice(list(providers)) + + # add ports + port_a = None + port_b = None + + port_type = NetworkFakeDataGenerator.get_nodetype('Port') + total_ports = NodeHandle.objects.filter(node_type=port_type).count() + + if total_ports < self.max_ports_total: + port_a = self.create_port() + port_b = self.create_port() + else: + all_ports = list(NodeHandle.objects.filter(node_type=port_type)) + port_a = random.choice(all_ports) + port_b = random.choice(all_ports) + + + data = { + 'cable_type' : random.choice(cable_types), + 'description' : self.fake.paragraph(), + 'relationship_provider' : provider.handle_id, + 'relationship_end_a' : port_a.handle_id, + 'relationship_end_b' : port_b.handle_id, + } + + for key, value in data.items(): + cable.get_node().add_property(key, value) + + # add relationship to provider + helpers.set_provider(self.user, cable.get_node(), provider.handle_id) + helpers.set_connected_to(self.user, cable.get_node(), port_a.handle_id) + helpers.set_connected_to(self.user, cable.get_node(), port_b.handle_id) + + return cable + + def create_host(self, name=None, type_name="Host", metatype=META_TYPES[0]): + # create object + if not name: + name = self.fake.hostname() + + host = self.get_or_create_node( + name, type_name, metatype) + + # add context + self.add_network_context(host) + + # add data + num_ips = random.randint(0,4) + ip_adresses = [self.fake.ipv4()] + + for i in range(num_ips): + ip_adresses.append(self.fake.ipv4()) + + operational_states = self.get_dropdown_keys('operational_states') + managed_by = self.get_dropdown_keys('host_management_sw') + responsible_group = self.get_dropdown_keys('responsible_groups') + support_group = self.get_dropdown_keys('responsible_groups') + backup_systems = ['TSM', 'IP nett'] + security_class = self.get_dropdown_keys('security_classes') + os_options = ( + ('GNU/Linux', ('Ubuntu', 'Debian', 'Fedora', 'Arch')), + ('Microsoft Windows', ('8', '10', 'X')) + ) + os_choice = random.choice(os_options) + + data = { + 'ip_addresses' : ip_adresses, + 'rack_units': random.randint(1,10), + 'rack_position': random.randint(1,10), + 'description': self.fake.paragraph(), + 'operational_state': random.choice(operational_states), + 'managed_by': random.choice(managed_by), + 'responsible_group': random.choice(responsible_group), + 'support_group': random.choice(support_group), + 'backup': random.choice(backup_systems), + 'security_class': random.choice(security_class), + 'security_comment': self.fake.paragraph(), + 'os': os_choice[0], + 'os_version': random.choice(os_choice[1]), + 'model': self.fake.license_plate(), + 'vendor': self.company_name(), + 'service_tag': self.fake.license_plate(), + } + + for key, value in data.items(): + host.get_node().add_property(key, value) + + return host + + def create_router(self): + # create object + router_name = '{}-{}'.format( + self.fake.safe_color_name(), self.fake.ean8()) + router = self.get_or_create_node( + router_name, 'Router', META_TYPES[0]) + + # add context + self.add_network_context(router) + + # add data + operational_states = self.get_dropdown_keys('operational_states') + + data = { + 'rack_units': random.randint(1,10), + 'rack_position': random.randint(1,10), + 'operational_state': random.choice(operational_states), + 'description': self.fake.paragraph(), + 'model': self.fake.license_plate(), + 'version': '{}.{}'.format(random.randint(0,20), random.randint(0,99)), + } + + for key, value in data.items(): + router.get_node().add_property(key, value) + + return router + + def create_switch(self): + # create object + switch_name = '{}-{}'.format( + self.fake.safe_color_name(), self.fake.ean8()) + switch = self.create_host(switch_name, "Switch") + + data = { + 'max_number_of_ports': random.randint(5,25), + } + + for key, value in data.items(): + switch.get_node().add_property(key, value) + + +class DataRelationMaker: + def __init__(self): + self.user = get_user() + + +class LogicalDataRelationMaker(DataRelationMaker): + def add_part_of(self, logical_nh, physical_nh): + physical_node = physical_nh.get_node() + logical_handle_id = logical_nh.handle_id + helpers.set_part_of(self.user, physical_node, logical_handle_id) + + +class RelationDataRelationMaker(DataRelationMaker): + def add_provides(self, relation_nh, phylogical_nh): + the_node = phylogical_nh.get_node() + relation_handle_id = relation_nh.handle_id + helpers.set_provider(self.user, the_node, relation_handle_id) + + def add_owns(self, relation_nh, physical_nh): + physical_node = physical_nh.get_node() + relation_handle_id = relation_nh.handle_id + helpers.set_owner(self.user, physical_node, relation_handle_id) + + def add_responsible_for(self, relation_nh, location_nh): + location_node = location_nh.get_node() + relation_handle_id = relation_nh.handle_id + helpers.set_responsible_for(self.user, location_node, relation_handle_id) + + +class PhysicalDataRelationMaker(DataRelationMaker): + def add_parent(self, physical_nh, physical_parent_nh): + handle_id = physical_nh.handle_id + parent_handle_id = physical_parent_nh.handle_id + + q = """ + MATCH (n:Node:Physical {handle_id: {handle_id}}), + (p:Node:Physical {parent_handle_id: {parent_handle_id}}) + MERGE (n)<-[r:Has]-(p) + RETURN n, r, p + """ + + result = nc.query_to_dict(nc.graphdb.manager, q, + handle_id=handle_id, parent_handle_id=parent_handle_id) diff --git a/src/niweb/apps/noclook/tests/stressload/test_stress.py b/src/niweb/apps/noclook/tests/stressload/test_stress.py new file mode 100644 index 000000000..9ac637935 --- /dev/null +++ b/src/niweb/apps/noclook/tests/stressload/test_stress.py @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- + +__author__ = 'ffuentes' + +from abc import ABC, abstractmethod +from apps.noclook.models import User, NodeType, NodeHandle, Role +from apps.noclook.helpers import set_member_of, set_works_for +from apps.nerds.lib.consumer_util import get_user +from django.core.management import call_command +from niweb.schema import schema + +from .data_generator import FakeDataGenerator +from ..management.fileutils import write_string_to_disk +from ..neo4j_base import NeoTestCase + +import logging +import norduniclient as nc +import random +import sys +import timeit +import os +import unittest + +#logging.basicConfig( stream=sys.stderr ) +logger = logging.getLogger('StressTest') +skip_reason = "This test should be run explicitly.:\ + STRESS_TEST=1 ./manage.py test apps/noclook/tests/stressload/" + +class AbstractStressTest(ABC): + import_cmd = 'csvimport' + org_csv_head = '"organization_number";"account_name";"description";"phone";"website";"organization_id";"type";"parent_account"' + con_csv_head = '"salutation";"first_name";"last_name";"title";"contact_role";"contact_type";"mailing_street";"mailing_city";"mailing_zip";"mailing_state";"mailing_country";"phone";"mobile";"fax";"email";"other_email";"PGP_fingerprint";"account_name"' + + log_file = 'stress_log.txt' + + setup_code = """ +from apps.noclook.models import Group, GroupContextAuthzAction +from apps.nerds.lib.consumer_util import get_user +from django.contrib.auth.models import User +from niweb.schema import schema + +import apps.noclook.vakt.utils as sriutils + +class TestContext(): + def __init__(self, user, *ignore): + self.user = user + +user = get_user() + +context = TestContext(user) + +group_read = Group.objects.get_or_create( name="Group can read the community context" )[0] +group_write = Group.objects.get_or_create( name="Group can write for the community context" )[0] +group_list = Group.objects.get_or_create( name="Group can list for the community context" )[0] + +# add user to this group +group_read.user_set.add(user) +group_write.user_set.add(user) +group_list.user_set.add(user) + +# get read aa +get_read_authaction = sriutils.get_read_authaction() +get_write_authaction = sriutils.get_write_authaction() +get_list_authaction = sriutils.get_list_authaction() + +# get default context +community_ctxt = sriutils.get_default_context() + +# add contexts and profiles +GroupContextAuthzAction.objects.get_or_create( + group = group_read, + authzprofile = get_read_authaction, + context = community_ctxt +)[0] + +GroupContextAuthzAction.objects.get_or_create( + group = group_write, + authzprofile = get_write_authaction, + context = community_ctxt +)[0] + +GroupContextAuthzAction.objects.get_or_create( + group = group_list, + authzprofile = get_list_authaction, + context = community_ctxt +)[0] + +query='''{query_value}''' + """ + + def load_nodes(self): + generator = FakeDataGenerator() + + # create organization's file + org_list = [] + + for i in range(self.organization_num): + organization = generator.create_fake_organization() + organization['organization_number'] = str(i+1) + str_value = '"{}"'.format('";"'.join(organization.values())) + org_list.append(str_value) + + org_str = '{}\n{}'.format(self.org_csv_head, '\n'.join(org_list)) + + # write string to file + org_file = write_string_to_disk(org_str) + + # create contacts's file + con_list = [] + + for i in range(self.contact_num): + contact = generator.create_fake_contact() + str_value = '"{}"'.format('";"'.join(contact.values())) + con_list.append(str_value) + + con_str = '{}\n{}'.format(self.con_csv_head, '\n'.join(con_list)) + + # write string to file + con_file = write_string_to_disk(con_str) + + # import organizations file + call_command( + self.import_cmd, + organizations=org_file, + verbosity=0, + ) + + # import contacts file + call_command( + self.import_cmd, + contacts=con_file, + verbosity=0, + ) + + # call data modifiers + call_command(self.import_cmd, emailphones=True, verbosity=0) + call_command(self.import_cmd, addressfix=True, verbosity=0) + call_command(self.import_cmd, movewebsite=True, verbosity=0) + call_command(self.import_cmd, reorgprops=True, verbosity=0) + + # create groups and add members + group_list = [] + group_type = NodeType.objects.filter(type='Group').first() + contact_type = NodeType.objects.filter(type='Contact').first() + user = get_user() + + contact_ids = [ x.handle_id for x in NodeHandle.objects.filter(node_type=contact_type)] + + for i in range(self.group_num): + group_dict = generator.create_fake_group() + + # create group + group = NodeHandle.objects.get_or_create( + node_name = group_dict['name'], + node_type = group_type, + node_meta_type = nc.META_TYPES[1], + creator = user, + modifier = user, + )[0] + group_node = group.get_node() + group_node.add_property('description', group_dict['description']) + + # add contacts (get them randomly) + hids = random.sample( + contact_ids, min(len(contact_ids), self.contacts_per_group) + ) + gcontacts = NodeHandle.objects.filter(handle_id__in=hids) + + for contact in gcontacts: + set_member_of(user, contact.get_node(), group.handle_id) + + # add members to organizations + organization_type = NodeType.objects.filter(type='Organization').first() + role_ids = [x.handle_id for x in Role.objects.all()] + for organization in NodeHandle.objects.filter(node_type=organization_type): + hids = random.sample( + contact_ids, min(len(contact_ids), self.contacts_per_organization) + ) + ocontacts = NodeHandle.objects.filter(handle_id__in=hids) + + for contact in ocontacts: + rand_role = Role.objects.get(handle_id = random.choice(role_ids)) + set_works_for(user, contact.get_node(), organization.handle_id, + rand_role.name) + + def empty_file(self): + open('/app/niweb/logs/{}'.format(self.log_file), 'w').close() + + def write_to_log_file(self, to_write): + with open('/app/niweb/logs/{}'.format(self.log_file), 'a') as f: + f.write(to_write) + + def test_lists(self): + organizations_query = ''' + {{ + ...OrganizationList_organizations_1tT5Hu + ...OrganizationList_organization_types + }} + + fragment OrganizationList_organization_types on Query {{ + getChoicesForDropdown(name: "organization_types") {{ + name + value + id + }} + }} + + fragment OrganizationList_organizations_1tT5Hu on Query {{ + organizations(filter: {filter}, orderBy: {order_by}) {{ + edges {{ + node {{ + handle_id + ...OrganizationRow_organization + id + __typename + }} + cursor + }} + pageInfo {{ + hasNextPage + endCursor + }} + }} + }} + + fragment OrganizationRow_organization on Organization {{ + handle_id + name + type + organization_id + affiliation_customer + affiliation_end_customer + affiliation_host_user + affiliation_partner + affiliation_provider + affiliation_site_owner + parent_organization {{ + organization_id + id + }} + incoming {{ + name + relation {{ + type + start {{ + handle_id + node_name + id + }} + id + }} + }} + }} + ''' + + # order by id: native django order + by_id_query = organizations_query.format(filter={}, order_by='handle_id_DESC') + setup_code = self.setup_code.format(query_value=by_id_query) + + mark1 = timeit.Timer("""result = schema.execute(query, context=context); assert result.data""", \ + setup=setup_code).timeit(1) + + test_result = "Organization list resolution with default order took {} seconds\n".format(mark1) + self.write_to_log_file(test_result) + + # order by id: native django order + name_query = organizations_query.format(filter={}, order_by='name_DESC') + setup_code = self.setup_code.format(query_value=name_query) + mark2 = timeit.Timer("""result = schema.execute(query, context=context); assert result.data""", \ + setup=setup_code).timeit(1) + + test_result = "Organization list resolution with name order took {} seconds\n".format(mark2) + self.write_to_log_file(test_result) + + contacts_query = ''' + query SearchContactsAllQuery{{ + ...ContactList_contacts_1tT5Hu + ...ContactList_organization_types + ...ContactList_roles_default + }} + + fragment ContactList_contacts_1tT5Hu on Query {{ + contacts(filter: {filter}, orderBy: {order_by}) {{ + edges {{ + node {{ + handle_id + ...ContactRow_contact + id + __typename + }} + cursor + }} + pageInfo {{ + endCursor + hasNextPage + hasPreviousPage + startCursor + }} + }} + }} + + fragment ContactList_organization_types on Query {{ + getChoicesForDropdown(name: "organization_types") {{ + name + value + id + }} + }} + + fragment ContactList_roles_default on Query {{ + getRolesFromRoleGroup {{ + handle_id + name + }} + }} + + fragment ContactRow_contact on Contact {{ + handle_id + first_name + last_name + contact_type + modified + roles {{ + name + end {{ + name + id + }} + }} + }} + ''' + + # order by id: native django order + by_id_query = contacts_query.format(filter={}, order_by='handle_id_DESC') + setup_code = self.setup_code.format(query_value=by_id_query) + + mark1 = timeit.Timer("""result = schema.execute(query, context=context); assert result.data""", \ + setup=setup_code).timeit(1) + + test_result = "Contact list resolution with default order took {} seconds\n".format(mark1) + self.write_to_log_file(test_result) + + # order by id: native django order + name_query = contacts_query.format(filter={}, order_by='name_DESC') + setup_code = self.setup_code.format(query_value=name_query) + mark2 = timeit.Timer("""result = schema.execute(query, context=context); assert result.data""", \ + setup=setup_code).timeit(1) + + test_result = "Contact list resolution with name order took {} seconds\n".format(mark2) + self.write_to_log_file(test_result) + + groups_query = ''' + query SearchGroupAllQuery{{ + ...GroupList_groups_1tT5Hu + }} + + fragment GroupList_groups_1tT5Hu on Query {{ + groups(filter: {filter}, orderBy: {order_by}) {{ + edges {{ + node {{ + handle_id + ...GroupRow_group + id + __typename + }} + cursor + }} + pageInfo {{ + hasNextPage + endCursor + }} + }} + }} + + fragment GroupRow_group on Group {{ + handle_id + name + description + }} + ''' + + # order by id: native django order + by_id_query = groups_query.format(filter={}, order_by='handle_id_DESC') + setup_code = self.setup_code.format(query_value=by_id_query) + + mark1 = timeit.Timer("""result = schema.execute(query, context=context); assert result.data""", \ + setup=setup_code).timeit(1) + + test_result = "Group list resolution with default order took {} seconds\n".format(mark1) + self.write_to_log_file(test_result) + + # order by id: native django order + name_query = groups_query.format(filter={}, order_by='name_DESC') + setup_code = self.setup_code.format(query_value=name_query) + mark2 = timeit.Timer("""result = schema.execute(query, context=context); assert result.data""", \ + setup=setup_code).timeit(1) + + test_result = "Group list resolution with name order took {} seconds\n".format(mark2) + self.write_to_log_file(test_result) + +@unittest.skipUnless(int(os.environ.get('STRESS_TEST', '0')) >= 1, skip_reason) +class LowStressTest(NeoTestCase, AbstractStressTest): + contact_num = 5 + organization_num = 5 + group_num = 3 + contacts_per_group = 3 + contacts_per_organization = 3 + + def setUp(self): + super(LowStressTest, self).setUp() + NodeHandle.objects.all().delete() + self.load_nodes() + + +@unittest.skipUnless(int(os.environ.get('STRESS_TEST', '0')) >= 2, skip_reason) +class MidStressTest(NeoTestCase, AbstractStressTest): + contact_num = 50 + organization_num = 50 + group_num = 10 + contacts_per_group = 25 + contacts_per_organization = 10 + + def setUp(self): + super(MidStressTest, self).setUp() + NodeHandle.objects.all().delete() + self.load_nodes() + + +@unittest.skipUnless(int(os.environ.get('STRESS_TEST', '0')) >= 3, skip_reason) +class HighStressTest(NeoTestCase, AbstractStressTest): + contact_num = 500 + organization_num = 500 + group_num = 100 + contacts_per_group = 100 + contacts_per_organization = 50 + + def setUp(self): + super(HighStressTest, self).setUp() + NodeHandle.objects.all().delete() + self.load_nodes() diff --git a/src/niweb/apps/noclook/tests/test_createforms.py b/src/niweb/apps/noclook/tests/test_createforms.py index 038ef25e1..e9dd155e4 100644 --- a/src/niweb/apps/noclook/tests/test_createforms.py +++ b/src/niweb/apps/noclook/tests/test_createforms.py @@ -8,6 +8,7 @@ from django.test import TestCase, Client from django.contrib.auth.models import User from django.template.defaultfilters import slugify +from django.utils import six from dynamic_preferences.registries import global_preferences_registry from apps.noclook.models import NodeHandle, NodeType, UniqueIdGenerator, ServiceType, ServiceClass from apps.noclook import forms, helpers @@ -61,6 +62,10 @@ def setUp(self): modifier=self.user, ) + contact_node_type, created = NodeType.objects.get_or_create(type='Contact', slug="contact") + organization_node_type, created = NodeType.objects.get_or_create(type='Organization', slug="organization") + group_node_type, created = NodeType.objects.get_or_create(type='Group', slug="group") + def tearDown(self): with nc.graphdb.manager.session as s: s.run("MATCH (a:Node) OPTIONAL MATCH (a)-[r]-(b) DELETE a, b, r") @@ -310,6 +315,66 @@ def test_NewOpticalMultiplexSectionForm_full(self): self.assertEqual(len(nh.get_node().relationships), 1) + def test_NewOrganizationForm_full(self): + node_type = 'Organization' + data = { + 'name': 'test organization', + 'description': 'SE', + 'organization_id': 'STDH', + 'type': 'university_college', + } + resp = self.client.post('/new/{}/'.format(slugify(node_type)), data) + positive_status = True if resp.status_code == 302 or resp.status_code == 200 else False + self.assertTrue(positive_status) + self.assertEqual(NodeType.objects.get(type=node_type).nodehandle_set.count(), 1) + nh = NodeType.objects.get(type=node_type).nodehandle_set.get(node_name='test organization') + self.assertDictContainsSubset(data, nh.get_node().data) + + def test_NewContactForm_full(self): + node_type = 'Contact' + country_codes = forms.country_codes() + if six.PY3: + country_codes = list(country_codes) + + country_code = country_codes[0] + data = { + 'first_name': 'Stefan', + 'last_name': 'Listrom', + 'contact_type': 'person', + } + resp = self.client.post('/new/{}/'.format(slugify(node_type)), data) + positive_status = True if resp.status_code == 302 or resp.status_code == 200 else False + self.assertTrue(positive_status) + self.assertEqual(NodeType.objects.get(type=node_type).nodehandle_set.count(), 1) + nh = NodeType.objects.get(type=node_type).nodehandle_set.get(node_name='Stefan Listrom') + self.assertDictContainsSubset(data, nh.get_node().data) + + def test_NewProcedureForm_full(self): + node_type = 'Procedure' + data = { + 'name': 'Reboot', + 'description': 'Lorem ipsum dolor sit amet', + } + resp = self.client.post('/new/{}/'.format(slugify(node_type)), data) + self.assertEqual(resp.status_code, 302) + self.assertEqual(NodeType.objects.get(type=node_type).nodehandle_set.count(), 1) + nh = NodeType.objects.get(type=node_type).nodehandle_set.get(node_name='Reboot') + data['description'] = 'Lorem ipsum dolor sit amet' + self.assertDictContainsSubset(data, nh.get_node().data) + + def test_NewGroupForm_full(self): + group_name = 'New users' + node_type = 'Group' + data = { + 'name': group_name, + } + resp = self.client.post('/new/{}/'.format(slugify(node_type)), data) + self.assertEqual(resp.status_code, 302) + self.assertEqual(NodeType.objects.get(type=node_type).nodehandle_set.count(), 1) + nh = NodeType.objects.get(type=node_type).nodehandle_set.get(node_name=group_name) + data['name'] = group_name + self.assertDictContainsSubset(data, nh.get_node().data) + class NordunetNewForms(FormTestCase): diff --git a/src/niweb/apps/noclook/tests/test_helpers.py b/src/niweb/apps/noclook/tests/test_helpers.py index 57b6bc793..06533d5b2 100644 --- a/src/niweb/apps/noclook/tests/test_helpers.py +++ b/src/niweb/apps/noclook/tests/test_helpers.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from .neo4j_base import NeoTestCase from apps.noclook import helpers +from apps.noclook.models import Role from actstream.models import actor_stream from norduniclient.exceptions import UniqueNodeError import norduniclient as nc @@ -8,6 +9,23 @@ class Neo4jHelpersTest(NeoTestCase): + def setUp(self): + super(Neo4jHelpersTest, self).setUp() + + organization = self.create_node('organization1', 'organization', meta='Logical') + self.organization_node = organization.get_node() + + parent_org = self.create_node('parent organization', 'organization', meta='Logical') + self.parent_org = parent_org.get_node() + + contact = self.create_node('contact1', 'contact', meta='Relation') + self.contact_node = contact.get_node() + + contact2 = self.create_node('contact2', 'contact', meta='Relation') + self.contact2_node = contact2.get_node() + + self.role = Role.objects.get_or_create(name="IT-manager")[0] + def test_delete_node_utf8(self): nh = self.create_node(u'æøå-ftw', 'site') node = nh.get_node() @@ -33,6 +51,88 @@ def test_create_unique_node_handle_case_insensitive(self): 'host', 'Physical') + def test_link_contact_role_for_organization(self): + self.assertEqual(len(self.organization_node.relationships), 0) + + contact, role = helpers.link_contact_role_for_organization( + self.user, + self.organization_node, + self.contact_node.handle_id, + self.role + ) + + self.assertEqual(role.name, self.role.name) + self.assertEqual( + contact.get_node(), + self.organization_node.get_relations().get('Works_for')[0].get('node') + ) + + def test_update_contact_organization(self): + contact, role = helpers.link_contact_role_for_organization( + self.user, + self.organization_node, + self.contact_node.handle_id, + self.role + ) + self.assertEqual(len(self.organization_node.relationships), 1) + + anther_role = Role.objects.get_or_create(name="NOC Manager")[0] + relationship_id = \ + self.organization_node.get_relations()\ + .get('Works_for')[0]['relationship_id'] + + contact, role = helpers.link_contact_role_for_organization( + self.user, + self.organization_node, + self.contact_node.handle_id, + anther_role, + relationship_id + ) + + self.assertEqual(len(self.organization_node.relationships), 1) + self.assertEqual(role.name, anther_role.name) + + def test_add_parent(self): + self.assertEqual(self.organization_node.get_relations(), {}) + relationship, created = helpers.set_parent_of( + self.user, self.organization_node, self.parent_org.handle_id) + + self.assertEqual( + self.organization_node.get_relations()['Parent_of'][0]['node'], + self.parent_org + ) + + def test_works_for_role(self): + self.assertEqual(self.organization_node.get_relations(), {}) + relationship, created = helpers.set_works_for( + self.user, + self.contact_node, + self.organization_node.handle_id, + self.role.name + ) + self.assertEqual( + self.organization_node.get_relations()['Works_for'][0]['relationship_id'], + relationship.id + ) + + contact = helpers.get_contact_for_orgrole( + self.organization_node.handle_id, + self.role + ) + + self.assertEqual(contact.get_node(), self.contact_node) + + helpers.unlink_contact_with_role_from_org( + self.user, + self.organization_node, + self.role + ) + contact = helpers.get_contact_for_orgrole( + self.organization_node.handle_id, + self.role + ) + self.assertEqual(contact, None) + def test_relationship_to_str_with_id(self): nh1 = self.create_node('Router1', 'router') router1 = nh1.get_node() diff --git a/src/niweb/apps/noclook/tests/vakt/test_rules.py b/src/niweb/apps/noclook/tests/vakt/test_rules.py new file mode 100644 index 000000000..4838f4678 --- /dev/null +++ b/src/niweb/apps/noclook/tests/vakt/test_rules.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +from apps.noclook.tests.neo4j_base import NeoTestCase +from apps.noclook.models import Context, AuthzAction, GroupContextAuthzAction, NodeHandleContext +import apps.noclook.vakt.rules as srirules +from django.contrib.auth.models import User, Group + + +class SRIVaktRulesTest(NeoTestCase): + def setUp(self): + super(SRIVaktRulesTest, self).setUp() + + # create users + self.user1 = User( + first_name="Kate", last_name="Svensson", email="ksvensson@sunet.se", + is_staff=False, is_active=True, username="ksvensson", + ) + self.user1.save() + + self.user2 = User( + first_name="Jane", last_name="Atkins", email="jatkins@sunet.se", + is_staff=False, is_active=True, username="jatkins", + ) + self.user2.save() + + self.user3 = User( + first_name="Sven", last_name="Svensson", email="ssvensson@sunet.se", + is_staff=False, is_active=True, username="ssvensson", + ) + self.user3.save() + + # create authzactions + self.authzaction1 = AuthzAction(name="Read action") + self.authzaction2 = AuthzAction(name="Write action") + self.authzaction3 = AuthzAction(name="Admin action") + + self.authzaction1.save() + self.authzaction2.save() + self.authzaction3.save() + + # create groups + self.group1 = Group( name="Group with 2 contexts" ) + self.group2 = Group( name="Group with 1 context" ) + self.group3 = Group( name="Group without contexts" ) + + self.group1.save() + self.group2.save() + self.group3.save() + + # add users to groups + self.group1.user_set.add(self.user1) + self.group2.user_set.add(self.user2) + self.group3.user_set.add(self.user3) + + # create contexts + self.context1 = Context( name="Context 1" ) + self.context2 = Context( name="Context 2" ) + + self.context1.save() + self.context2.save() + + # associate context and groups + # first group can read/write/admin in the first context + GroupContextAuthzAction( + group = self.group1, + authzprofile = self.authzaction1, + context = self.context1 + ).save() + + GroupContextAuthzAction( + group = self.group1, + authzprofile = self.authzaction2, + context = self.context1 + ).save() + + GroupContextAuthzAction( + group = self.group1, + authzprofile = self.authzaction3, + context = self.context1 + ).save() + + # first group can read/write/admin in the second context + GroupContextAuthzAction( + group = self.group1, + authzprofile = self.authzaction1, + context = self.context2 + ).save() + + GroupContextAuthzAction( + group = self.group1, + authzprofile = self.authzaction2, + context = self.context2 + ).save() + + GroupContextAuthzAction( + group = self.group1, + authzprofile = self.authzaction3, + context = self.context2 + ).save() + + # second group read/write in the first context + GroupContextAuthzAction( + group = self.group2, + authzprofile = self.authzaction1, + context = self.context1 + ).save() + + GroupContextAuthzAction( + group = self.group2, + authzprofile = self.authzaction2, + context = self.context1 + ).save() + + # second group reads in the second context + GroupContextAuthzAction( + group = self.group2, + authzprofile = self.authzaction1, + context = self.context2 + ).save() + + # the third group can only read in the first context + GroupContextAuthzAction( + group = self.group3, + authzprofile = self.authzaction1, + context = self.context1 + ).save() + + # create some nodes + self.organization = self.create_node('organization1', 'organization', meta='Logical') + self.contact1 = self.create_node('contact1', 'contact', meta='Relation') + self.contact2 = self.create_node('contact2', 'contact', meta='Relation') + + # the organization belongs to both modules + NodeHandleContext( + nodehandle=self.organization, + context = self.context1 + ).save() + + NodeHandleContext( + nodehandle=self.organization, + context = self.context2 + ).save() + + # the first contact belongs to the first module + NodeHandleContext( + nodehandle=self.contact1, + context = self.context1 + ).save() + + # the second contact belongs to the first module + NodeHandleContext( + nodehandle=self.contact2, + context = self.context2 + ).save() + + def test_has_auth_action(self): + ### rule creation + # rule: read in context1 + self.rule11 = srirules.HasAuthAction( + self.authzaction1, + self.context1 + ) + + # rule: write in context1 + self.rule21 = srirules.HasAuthAction( + self.authzaction2, + self.context1 + ) + + # rule: admin in context1 + self.rule31 = srirules.HasAuthAction( + self.authzaction3, + self.context1 + ) + + # rule: read in context2 + self.rule12 = srirules.HasAuthAction( + self.authzaction1, + self.context2 + ) + + # rule: write in context2 + self.rule22 = srirules.HasAuthAction( + self.authzaction2, + self.context2 + ) + + # rule: admin in context2 + self.rule32 = srirules.HasAuthAction( + self.authzaction3, + self.context2 + ) + + ### test: all users should read in context1 + user1_readsc1 = self.rule11.satisfied(self.user1) + user2_readsc1 = self.rule11.satisfied(self.user2) + user3_readsc1 = self.rule11.satisfied(self.user3) + + self.assertTrue(user1_readsc1) + self.assertTrue(user2_readsc1) + self.assertTrue(user3_readsc1) + + ### test: only users 1 and 2 should be able to write in context1 + user1_writec1 = self.rule21.satisfied(self.user1) + user2_writec1 = self.rule21.satisfied(self.user2) + user3_writec1 = self.rule21.satisfied(self.user3) + + self.assertTrue(user1_writec1) + self.assertTrue(user2_writec1) + self.assertFalse(user3_writec1) + + ### test: only user 1 should be able to admin in context1 + user1_adminc1 = self.rule31.satisfied(self.user1) + user2_adminc1 = self.rule31.satisfied(self.user2) + user3_adminc1 = self.rule31.satisfied(self.user3) + + self.assertTrue(user1_adminc1) + self.assertFalse(user2_adminc1) + self.assertFalse(user3_adminc1) + + ### test: only users 1 and 2 should read in context2 + user1_readsc2 = self.rule12.satisfied(self.user1) + user2_readsc2 = self.rule12.satisfied(self.user2) + user3_readsc2 = self.rule12.satisfied(self.user3) + + self.assertTrue(user1_readsc2) + self.assertTrue(user2_readsc2) + self.assertFalse(user3_readsc2) + + ### test: only user 1 should be able to write in context2 + user1_writec2 = self.rule22.satisfied(self.user1) + user2_writec2 = self.rule22.satisfied(self.user2) + user3_writec2 = self.rule22.satisfied(self.user3) + + self.assertTrue(user1_writec2) + self.assertFalse(user2_writec2) + self.assertFalse(user3_writec2) + + ### test: only user 1 should be able to admin in context2 + user1_adminc2 = self.rule32.satisfied(self.user1) + user2_adminc2 = self.rule32.satisfied(self.user2) + user3_adminc2 = self.rule32.satisfied(self.user3) + + self.assertTrue(user1_adminc2) + self.assertFalse(user2_adminc2) + self.assertFalse(user3_adminc2) + + def test_belongs_tocontext(self): + ### rule creation + self.resource_rule1 = srirules.BelongsContext(self.context1) + self.resource_rule2 = srirules.BelongsContext(self.context2) + + # test: organization belongs to both contexts + rule1_satisfied = self.resource_rule1.satisfied(self.organization) + rule2_satisfied = self.resource_rule2.satisfied(self.organization) + + self.assertTrue(rule1_satisfied) + self.assertTrue(rule2_satisfied) + + # test: contact1 belong only to the first context + rule1_satisfied = self.resource_rule1.satisfied(self.contact1) + rule2_satisfied = self.resource_rule2.satisfied(self.contact1) + + self.assertTrue(rule1_satisfied) + self.assertFalse(rule2_satisfied) + + # test: contact2 belong only to the second context + rule1_satisfied = self.resource_rule1.satisfied(self.contact2) + rule2_satisfied = self.resource_rule2.satisfied(self.contact2) + + self.assertFalse(rule1_satisfied) + self.assertTrue(rule2_satisfied) + + # final check: an empty nodehandle should return true + # because that's how we'll check for a creation of a resource + rule1_satisfied = self.resource_rule1.satisfied(None) + self.assertTrue(rule1_satisfied) + + + def test_contains_rule(self): + contains_1 = srirules.ContainsElement('needle') + + haystack1 = ('hay', 'fork', 'needle', 'grass') + haystack2 = ('sand', 'water', 'sun', 'sky') + + rule_satisfied1 = contains_1.satisfied(haystack1) + rule_satisfied2 = contains_1.satisfied(haystack2) + + self.assertTrue(rule_satisfied1) + self.assertFalse(rule_satisfied2) diff --git a/src/niweb/apps/noclook/tests/vakt/test_utils.py b/src/niweb/apps/noclook/tests/vakt/test_utils.py new file mode 100644 index 000000000..e0bd1f18d --- /dev/null +++ b/src/niweb/apps/noclook/tests/vakt/test_utils.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +from apps.noclook.tests.neo4j_base import NeoTestCase +from django.contrib.auth.models import User, Group +from apps.noclook.models import GroupContextAuthzAction, NodeHandleContext +import apps.noclook.vakt.utils as sriutils + +class SRIVaktUtilsTest(NeoTestCase): + def setUp(self): + super(SRIVaktUtilsTest, self).setUp() + + # get contexts + self.network_ctxt = sriutils.get_network_context() + self.community_ctxt = sriutils.get_community_context() + self.contracts_ctxt = sriutils.get_contracts_context() + + # get auth actions + self.get_read_authaction = sriutils.get_read_authaction() + self.get_write_authaction = sriutils.get_write_authaction() + self.get_list_authaction = sriutils.get_list_authaction() + self.get_admin_authaction = sriutils.get_admin_authaction() + + # create some nodes + self.organization1 = self.create_node('organization1', 'organization', meta='Logical') + self.organization2 = self.create_node('organization2', 'organization', meta='Logical') + self.contact1 = self.create_node('contact1', 'contact', meta='Relation') + self.contact2 = self.create_node('contact2', 'contact', meta='Relation') + + # add context to resources + # organization1 belongs to the three modules + NodeHandleContext( + nodehandle=self.organization1, + context = self.network_ctxt + ).save() + + NodeHandleContext( + nodehandle=self.organization1, + context = self.community_ctxt + ).save() + + NodeHandleContext( + nodehandle=self.organization1, + context = self.contracts_ctxt + ).save() + + # organization2 belongs only to the network module + NodeHandleContext( + nodehandle=self.organization2, + context = self.network_ctxt + ).save() + + # the first contact belongs to the community module + NodeHandleContext( + nodehandle=self.contact1, + context = self.community_ctxt + ).save() + + # the second contact belongs to the contracts module + NodeHandleContext( + nodehandle=self.contact2, + context = self.contracts_ctxt + ).save() + + ### create users and groups and add permissions + self.user1 = User( + first_name="Kate", last_name="Svensson", email="ksvensson@sunet.se", + is_staff=False, is_active=True, username="ksvensson", + ) + self.user1.save() + + self.user2 = User( + first_name="Jane", last_name="Atkins", email="jatkins@sunet.se", + is_staff=False, is_active=True, username="jatkins", + ) + self.user2.save() + + self.user3 = User( + first_name="Sven", last_name="Svensson", email="ssvensson@sunet.se", + is_staff=False, is_active=True, username="ssvensson", + ) + self.user3.save() + + # create groups + self.group1 = Group( name="Group can read the three contexts" ) + self.group2 = Group( name="Group can write for the community and contracts" ) + self.group3 = Group( name="Group can admin for the community module" ) + self.group4 = Group( name="Group can list for the community and contracts" ) + + self.group1.save() + self.group2.save() + self.group3.save() + self.group4.save() + + # add contexts and actions + # first group + contexts = [self.network_ctxt, self.community_ctxt, self.contracts_ctxt] + + for context in contexts: + GroupContextAuthzAction( + group = self.group1, + authzprofile = self.get_read_authaction, + context = context + ).save() + + # second group + contexts = [self.community_ctxt, self.contracts_ctxt] + + for context in contexts: + GroupContextAuthzAction( + group = self.group2, + authzprofile = self.get_write_authaction, + context = context + ).save() + + # third group + GroupContextAuthzAction( + group = self.group3, + authzprofile = self.get_admin_authaction, + context = self.community_ctxt + ).save() + + for context in contexts: + GroupContextAuthzAction( + group = self.group4, + authzprofile = self.get_list_authaction, + context = context + ).save() + + # add users to groups + self.group1.user_set.add(self.user1) + self.group1.user_set.add(self.user2) + self.group1.user_set.add(self.user3) + + self.group2.user_set.add(self.user1) + self.group2.user_set.add(self.user2) + + self.group3.user_set.add(self.user1) + + self.group4.user_set.add(self.user1) + self.group4.user_set.add(self.user2) + self.group4.user_set.add(self.user3) + + def test_read_resource(self): + # check if the three users can read the organization1 + result_auth_u1 = sriutils.authorice_read_resource(self.user1, self.organization1.handle_id) + result_auth_u2 = sriutils.authorice_read_resource(self.user2, self.organization1.handle_id) + result_auth_u3 = sriutils.authorice_read_resource(self.user3, self.organization1.handle_id) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertTrue(result_auth_u3) + + # check if the three users can read the organization2 + result_auth_u1 = sriutils.authorice_read_resource(self.user1, self.organization2.handle_id) + result_auth_u2 = sriutils.authorice_read_resource(self.user2, self.organization2.handle_id) + result_auth_u3 = sriutils.authorice_read_resource(self.user3, self.organization2.handle_id) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertTrue(result_auth_u3) + + # check if the three users can read the contact1 + result_auth_u1 = sriutils.authorice_read_resource(self.user1, self.contact1.handle_id) + result_auth_u2 = sriutils.authorice_read_resource(self.user2, self.contact1.handle_id) + result_auth_u3 = sriutils.authorice_read_resource(self.user3, self.contact1.handle_id) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertTrue(result_auth_u3) + + # check if the three users can read the contact2 + result_auth_u1 = sriutils.authorice_read_resource(self.user1, self.contact2.handle_id) + result_auth_u2 = sriutils.authorice_read_resource(self.user2, self.contact2.handle_id) + result_auth_u3 = sriutils.authorice_read_resource(self.user3, self.contact2.handle_id) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertTrue(result_auth_u3) + + + def test_write_resource(self): + # check that only user1 and user2 (from the group2) can write for the resource + result_auth_u1 = sriutils.authorice_write_resource(self.user1, self.organization1.handle_id) + result_auth_u2 = sriutils.authorice_write_resource(self.user2, self.organization1.handle_id) + result_auth_u3 = sriutils.authorice_write_resource(self.user3, self.organization1.handle_id) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertFalse(result_auth_u3) + + # check that nobody can write the resource since it's in the network module + result_auth_u1 = sriutils.authorice_write_resource(self.user1, self.organization2.handle_id) + result_auth_u2 = sriutils.authorice_write_resource(self.user2, self.organization2.handle_id) + result_auth_u3 = sriutils.authorice_write_resource(self.user3, self.organization2.handle_id) + + self.assertFalse(result_auth_u1) + self.assertFalse(result_auth_u2) + self.assertFalse(result_auth_u3) + + # check that only user1 and user2 (from the group2) can write for the resource + result_auth_u1 = sriutils.authorice_write_resource(self.user1, self.contact1.handle_id) + result_auth_u2 = sriutils.authorice_write_resource(self.user2, self.contact1.handle_id) + result_auth_u3 = sriutils.authorice_write_resource(self.user3, self.contact1.handle_id) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertFalse(result_auth_u3) + + # check that only user1 and user2 (from the group2) can write for the resource + result_auth_u1 = sriutils.authorice_write_resource(self.user1, self.contact2.handle_id) + result_auth_u2 = sriutils.authorice_write_resource(self.user2, self.contact2.handle_id) + result_auth_u3 = sriutils.authorice_write_resource(self.user3, self.contact2.handle_id) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertFalse(result_auth_u3) + + + def test_create_resource(self): + # check that only user1 and user2 (from the group2) can create resources in the community module + result_auth_u1 = sriutils.authorize_create_resource(self.user1, self.community_ctxt) + result_auth_u2 = sriutils.authorize_create_resource(self.user2, self.community_ctxt) + result_auth_u3 = sriutils.authorize_create_resource(self.user3, self.community_ctxt) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertFalse(result_auth_u3) + + # check that only user1 and user2 (from the group2) can create resources in the contracts modules + result_auth_u1 = sriutils.authorize_create_resource(self.user1, self.contracts_ctxt) + result_auth_u2 = sriutils.authorize_create_resource(self.user2, self.contracts_ctxt) + result_auth_u3 = sriutils.authorize_create_resource(self.user3, self.contracts_ctxt) + + self.assertTrue(result_auth_u1) + self.assertTrue(result_auth_u2) + self.assertFalse(result_auth_u3) + + + # check that none of them can create resources in the network module + result_auth_u1 = sriutils.authorize_create_resource(self.user1, self.network_ctxt) + result_auth_u2 = sriutils.authorize_create_resource(self.user2, self.network_ctxt) + result_auth_u3 = sriutils.authorize_create_resource(self.user3, self.network_ctxt) + + self.assertFalse(result_auth_u1) + self.assertFalse(result_auth_u2) + self.assertFalse(result_auth_u3) + + + def test_admin(self): + # check that only user1 have admin rights over the community module + result_auth_u1 = sriutils.authorize_admin_module(self.user1, self.community_ctxt) + result_auth_u2 = sriutils.authorize_admin_module(self.user2, self.community_ctxt) + result_auth_u3 = sriutils.authorize_admin_module(self.user3, self.community_ctxt) + + self.assertTrue(result_auth_u1) + self.assertFalse(result_auth_u2) + self.assertFalse(result_auth_u3) + + # check that nobody has admin rights over any other module + result_auth_u1 = sriutils.authorize_admin_module(self.user1, self.network_ctxt) + result_auth_u2 = sriutils.authorize_admin_module(self.user2, self.network_ctxt) + result_auth_u3 = sriutils.authorize_admin_module(self.user3, self.network_ctxt) + + self.assertFalse(result_auth_u1) + self.assertFalse(result_auth_u2) + self.assertFalse(result_auth_u3) + + result_auth_u1 = sriutils.authorize_admin_module(self.user1, self.contracts_ctxt) + result_auth_u2 = sriutils.authorize_admin_module(self.user2, self.contracts_ctxt) + result_auth_u3 = sriutils.authorize_admin_module(self.user3, self.contracts_ctxt) + + self.assertFalse(result_auth_u1) + self.assertFalse(result_auth_u2) + self.assertFalse(result_auth_u3) + + + def test_list(self): + # check that the user1 can't list network context elements + result_auth_u1 = sriutils.authorize_list_module(self.user1, self.network_ctxt) + self.assertFalse(result_auth_u1) + + # check that the user1 can list community context elements + result_auth_u2 = sriutils.authorize_list_module(self.user2, self.community_ctxt) + self.assertTrue(result_auth_u2) + + # check that the user1 can list contracts context elements + result_auth_u3 = sriutils.authorize_list_module(self.user3, self.contracts_ctxt) + self.assertTrue(result_auth_u3) diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index 352572624..be4432131 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -5,6 +5,8 @@ from .views import other, create, edit, import_nodes, report, detail, redirect, debug, list as _list urlpatterns = [ + url(r'^csrf/$', other.csrf), + url(r'^login/$', auth_views.LoginView.as_view()), url(r'^$', other.index), # Log out url(r'^logout/$', other.logout_page), @@ -49,6 +51,7 @@ url(r'^reserve-id/(?P[-\w]+)/$', create.reserve_id_sequence), # -- edit views + url(r'^role/(?P\d+)/delete$', edit.delete_role), url(r'^(?P[-\w]+)/(?P\d+)/edit$', edit.edit_node, name='generic_edit'), url(r'^port/(?P\d+)/edit_connect$', edit.connect_port), url(r'^(?P[-\w]+)/(?P\d+)/edit/disable-noclook-auto-manage/$', edit.disable_noclook_auto_manage), @@ -88,6 +91,9 @@ url(r'^optical-multiplex-section/$', _list.list_optical_multiplex_section), url(r'^optical-link/$', _list.list_optical_links), url(r'^optical-node/$', _list.list_optical_nodes), + url(r'^organization/$', _list.list_organizations), + url(r'^contact/$', _list.list_contacts), + url(r'^role/$', _list.list_roles), url(r'^outlet/$', _list.list_outlet), url(r'^patch-panel/$', _list.list_patch_panels), url(r'^router/$', _list.list_routers), @@ -109,6 +115,7 @@ url(r'^peering-group/(?P\d+)/$', detail.peering_group_detail, name='peering_group_detail'), url(r'^optical-node/(?P\d+)/$', detail.optical_node_detail), url(r'^cable/(?P\d+)/$', detail.cable_detail), + url(r'^group/(?P\d+)/$', detail.group_detail), url(r'^host/(?P\d+)/$', detail.host_detail, name='detail_host'), url(r'^host-service/(?P\d+)/$', detail.host_service_detail), url(r'^host-provider/(?P\d+)/$', detail.host_provider_detail), @@ -119,15 +126,19 @@ url(r'^patch-panel/(?P\d+)/$', detail.patch_panel_detail), url(r'^port/(?P\d+)/$', detail.port_detail), url(r'^site/(?P\d+)/$', detail.site_detail), + url(r'^role/(?P\d+)/$', detail.role_detail), url(r'^rack/(?P\d+)/$', detail.rack_detail), url(r'^room/(?P\d+)/$', detail.room_detail), url(r'^site-owner/(?P\d+)/$', detail.site_owner_detail), url(r'^service/(?P\d+)/$', detail.service_detail), url(r'^optical-link/(?P\d+)/$', detail.optical_link_detail), url(r'^optical-path/(?P\d+)/$', detail.optical_path_detail), + url(r'^organization/(?P\d+)/$', detail.organization_detail), url(r'^end-user/(?P\d+)/$', detail.end_user_detail), + url(r'^contact/(?P\d+)/$', detail.contact_detail), url(r'^customer/(?P\d+)/$', detail.customer_detail), url(r'^provider/(?P\d+)/$', detail.provider_detail), + url(r'^procedure/(?P\d+)/$', detail.procedure_detail), url(r'^unit/(?P\d+)/$', detail.unit_detail), url(r'^external-equipment/(?P\d+)/$', detail.external_equipment_detail), url(r'^optical-multiplex-section/(?P\d+)/$', detail.optical_multiplex_section_detail), diff --git a/src/niweb/apps/noclook/vakt/rules.py b/src/niweb/apps/noclook/vakt/rules.py new file mode 100644 index 000000000..f3361f5d0 --- /dev/null +++ b/src/niweb/apps/noclook/vakt/rules.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from vakt.rules.base import Rule +from apps.noclook.models import NodeHandle, GroupContextAuthzAction, NodeHandleContext + +class HasAuthAction(Rule): + def __init__(self, authzaction, context): + self.authzaction = authzaction + self.context = context + + def satisfied(self, user, inquiry=None): + satisfied = False + + # get user groups + groups = user.groups.all() + + # get all authzactions for these groups and context + gcaas = GroupContextAuthzAction.objects.filter( + group__in=groups, + context=self.context, + authzprofile=self.authzaction + ) + + if gcaas: + satisfied = True + + return satisfied + + +class BelongsContext(Rule): + def __init__(self, context): + self.context = context + + def satisfied(self, nodehandle, inquiry=None): + satisfied = False + + if nodehandle: + possible_contexts = nodehandle.contexts.filter(pk=self.context.pk) + if possible_contexts: + satisfied = True + else: + # if the nodehandle comes empty it is a node creation request + # so other rules may apply but not this one + satisfied = True + + return satisfied + + +class ContainsElement(Rule): + def __init__(self, elem): + self.elem = elem + + def satisfied(self, list, inquiry=None): + satisfied = self.elem in list + + return satisfied diff --git a/src/niweb/apps/noclook/vakt/utils.py b/src/niweb/apps/noclook/vakt/utils.py new file mode 100644 index 000000000..cf3fe2f5a --- /dev/null +++ b/src/niweb/apps/noclook/vakt/utils.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import logging + +from apps.noclook.models import NodeHandle, AuthzAction, GroupContextAuthzAction, NodeHandleContext, Context +from djangovakt.storage import DjangoStorage +from vakt import Guard, RulesChecker, Inquiry + +READ_AA_NAME = 'read' +WRITE_AA_NAME = 'write' +ADMIN_AA_NAME = 'admin' +LIST_AA_NAME = 'list' + +NETWORK_CTX_NAME = 'Network' +COMMUNITY_CTX_NAME = 'Community' +CONTRACTS_CTX_NAME = 'Contracts' + + +logger = logging.getLogger(__name__) + +def trim_readable_queryset(qs, user): + ''' + This function trims a Queryset of nodes to keep only those the user has + rights to read + ''' + logger.debug('Authorizing user to read a set of nodes') + + # get all readable contexts for this user + user_groups = user.groups.all() + read_aa = get_read_authaction() + + gcaas = GroupContextAuthzAction.objects.filter( + group__in=user.groups.all(), + authzprofile=read_aa + ) + + readable_contexts = [] + for gcaa in gcaas: + readable_contexts.append(gcaa.context) + + # queryset only will match nodes that the user can read + if readable_contexts: + # the hard way + readable_ids = NodeHandleContext.objects.filter( + context__in=readable_contexts + ).values_list('nodehandle_id', flat=True) + + qs = qs.filter(handle_id__in=readable_ids) + else: + # the user doesn't have rights to any context + qs.none() + + return qs + + +def get_vakt_storage_and_guard(): + storage = DjangoStorage() + guard = Guard(storage, RulesChecker()) + + return storage, guard + + +def get_authaction_by_name(name, aamodel=AuthzAction): + authzaction = aamodel.objects.get(name=name) + return authzaction + + +def get_read_authaction(aamodel=AuthzAction): + return get_authaction_by_name(READ_AA_NAME, aamodel) + + +def get_write_authaction(aamodel=AuthzAction): + return get_authaction_by_name(WRITE_AA_NAME, aamodel) + + +def get_admin_authaction(aamodel=AuthzAction): + return get_authaction_by_name(ADMIN_AA_NAME, aamodel) + + +def get_list_authaction(aamodel=AuthzAction): + return get_authaction_by_name(LIST_AA_NAME, aamodel) + + +def get_context_by_name(name, cmodel=Context): + try: + context = cmodel.objects.get(name=name) + except: + context = None + + return context + + +def get_network_context(cmodel=Context): + return get_context_by_name(NETWORK_CTX_NAME, cmodel) + + +def get_community_context(cmodel=Context): + return get_context_by_name(COMMUNITY_CTX_NAME, cmodel) + + +def get_contracts_context(cmodel=Context): + return get_context_by_name(CONTRACTS_CTX_NAME, cmodel) + + +def get_default_context(cmodel=Context): + return get_community_context(cmodel) + +def authorize_aa_resource(user, handle_id, get_aa_func): + ''' + This function checks if an user is authorized to do a specific action over + a node specified by its handle_id. It forges an inquiry and check it against + vakt's guard. + ''' + ret = False # deny by default + + # get storage and guard + storage, guard = get_vakt_storage_and_guard() + + # get authaction + authaction = get_aa_func() + + # get contexts for this resource + nodehandle = NodeHandle.objects.prefetch_related('contexts').get(handle_id=handle_id) + contexts = [ c.name for c in nodehandle.contexts.all() ] + + # forge read resource inquiry + inquiry = Inquiry( + action=authaction.name, + resource=nodehandle, + subject=user, + context={'module': contexts} + ) + + ret = guard.is_allowed(inquiry) + + return ret + + +def authorice_read_resource(user, handle_id): + logger.debug('Authorizing user to read a node with id {}'.format(handle_id)) + return authorize_aa_resource(user, handle_id, get_read_authaction) + + +def authorice_write_resource(user, handle_id): + logger.debug('Authorizing user to write a node with id {}'.format(handle_id)) + return authorize_aa_resource(user, handle_id, get_write_authaction) + + +def authorize_aa_operation(user, context, get_aa_func): + ''' + This function authorizes an action within a particular context, it checks + if the user can perform that action within this SRI module + ''' + ret = False # deny by default + + # get storage and guard + storage, guard = get_vakt_storage_and_guard() + + # get authaction + authaction = get_aa_func() + + # forge read resource inquiry + inquiry = Inquiry( + action=authaction.name, + resource=None, + subject=user, + context={'module': (context.name,)} + ) + + ret = guard.is_allowed(inquiry) + + return ret + + +def authorize_create_resource(user, context): + ''' + This function authorizes the creation of a resource within a particular + context, it checks if the user can write within this SRI module + ''' + logger.debug('Authorizing user to create a node within the module {}'\ + .format(context.name)) + return authorize_aa_operation(user, context, get_write_authaction) + + +def authorize_admin_module(user, context): + ''' + This function checks if the user can perform admin actions inside a module + ''' + logger.debug('Authorizing user to admin the module {}'\ + .format(context.name)) + return authorize_aa_operation(user, context, get_admin_authaction) + + +def authorize_list_module(user, context): + ''' + This function checks if the user can perform admin actions inside a module + ''' + logger.debug('Authorizing user to admin the module {}'\ + .format(context.name)) + return authorize_aa_operation(user, context, get_list_authaction) + +def authorize_superadmin(user, cmodel=Context): + ''' + This function checks if the user can perform admin actions inside a module + ''' + logger.debug('Authorizing user {} as a superadmin'.format(user.username)) + + is_superadmin = True + all_contexts = cmodel.objects.all() + + for context in all_contexts: + if not authorize_aa_operation(user, context, get_admin_authaction): + is_superadmin = False + break + + return is_superadmin diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 4e0c94789..8ab15abb6 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -5,11 +5,13 @@ @author: lundberg """ +from django.apps import apps from django.contrib.auth.decorators import login_required from django.contrib.admin.views.decorators import staff_member_required from django.contrib import messages from django.http import Http404 from django.shortcuts import render, redirect +from dynamic_preferences.registries import global_preferences_registry from apps.noclook import forms from apps.noclook.forms import common as common_forms from apps.noclook.models import NodeHandle, Dropdown, SwitchType @@ -17,33 +19,44 @@ from apps.noclook import unique_ids from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible - -TYPES = [ - ("customer", "Customer"), - ("cable", "Cable"), - ("end-user", "End User"), - ("external-cable", "External Cable"), - ("external-equipment", "External Equipment"), - ("host", "Host"), - ("optical-link", "Optical Link"), - ("optical-path", "Optical Path"), - ("service", "Service"), - ("odf", "ODF"), - ("optical-filter", "Optical Filter"), - ("optical-multiplex-section", "Optical Multiplex Section"), - ("optical-node", "Optical Node"), - ("outlet", "Outlet"), - ("patch-panel", "Patch Panel"), - ("port", "Port"), - ("provider", "Provider"), - ("rack", "Rack"), - ("room", "Room"), - ("site", "Site"), - ("site-owner", "Site Owner"), - ("switch", "Switch"), -] -if helpers.app_enabled("apps.scan"): - TYPES.append(("/scan/queue", "Host scan")) +try: + global_preferences = global_preferences_registry.manager() + menu_mode = global_preferences['general__menu_mode'] +except: + menu_mode = 'ni' + +if menu_mode == 'ni': + TYPES = [ + ("customer", "Customer"), + ("cable", "Cable"), + ("end-user", "End User"), + ("external-cable", "External Cable"), + ("external-equipment", "External Equipment"), + ("host", "Host"), + ("optical-link", "Optical Link"), + ("optical-path", "Optical Path"), + ("service", "Service"), + ("odf", "ODF"), + ("optical-filter", "Optical Filter"), + ("optical-multiplex-section", "Optical Multiplex Section"), + ("optical-node", "Optical Node"), + ("port", "Port"), + ("provider", "Provider"), + ("rack", "Rack"), + ("site", "Site"), + ("site-owner", "Site Owner"), + ] + if helpers.app_enabled("apps.scan"): + TYPES.append(("/scan/queue", "Host scan")) +elif menu_mode == 'sri': + TYPES = [ + ("contact", "Contact"), + ("organization", "Organization"), + ("group", "Group"), + ("role", "Role"), + ] +else: + raise NotImplementedError('menu mode {} not implemented'.format(menu_mode)) # Create functions @@ -658,10 +671,92 @@ def new_switch(request, **kwargs): return render(request, 'noclook/create/create_switch.html', {'form': form }) +@staff_member_required +def new_organization(request, **kwargs): + if request.POST: + form = forms.NewOrganizationForm(request.POST) + if form.is_valid(): + try: + nh = helpers.form_to_unique_node_handle(request, form, 'organization', 'Relation') + except UniqueNodeError: + form.add_error('name', 'An Organization with that name already exists.') + return render(request, 'noclook/create/create_organization.html', {'form': form}) + + # use property keys to avoid inserting contacts as a string property of the node + property_keys = [ + 'name', 'description', 'phone', 'website', 'organization_id', 'type', 'incident_management_info', + ] + helpers.form_update_node(request.user, nh.handle_id, form, property_keys) + + return redirect(nh.get_absolute_url()) + else: + form = forms.NewOrganizationForm() + return render(request, 'noclook/create/create_organization.html', {'form': form}) + + +@staff_member_required +def new_contact(request, **kwargs): + if request.POST: + form = forms.NewContactForm(request.POST) + if form.is_valid(): + nh = helpers.form_to_generic_node_handle(request, form, 'contact', 'Relation') + helpers.form_update_node(request.user, nh.handle_id, form) + return redirect(nh.get_absolute_url()) + else: + form = forms.NewContactForm() + return render(request, 'noclook/create/create_contact.html', {'form': form}) + + +@staff_member_required +def new_procedure(request, **kwargs): + if request.POST: + form = forms.NewProcedureForm(request.POST) + if form.is_valid(): + try: + nh = helpers.form_to_unique_node_handle(request, form, 'procedure', 'Logical') + except UniqueNodeError: + form.add_error('name', 'A Procedure with that name already exists.') + return render(request, 'noclook/create/create_procedure.html', {'form': form}) + helpers.form_update_node(request.user, nh.handle_id, form) + return redirect(nh.get_absolute_url()) + else: + form = forms.NewProcedureForm() + return render(request, 'noclook/create/create_procedure.html', {'form': form}) + + +@staff_member_required +def new_group(request, **kwargs): + if request.POST: + form = forms.NewGroupForm(request.POST) + if form.is_valid(): + try: + nh = helpers.form_to_unique_node_handle(request, form, 'group', 'Logical') + except UniqueNodeError: + form.add_error('name', 'A Group with that name already exists.') + return render(request, 'noclook/create/create_group.html', {'form': form}) + helpers.form_update_node(request.user, nh.handle_id, form) + return redirect(nh.get_absolute_url()) + else: + form = forms.NewGroupForm() + return render(request, 'noclook/create/create_group.html', {'form': form}) + +@staff_member_required +def new_role(request, **kwargs): + if request.POST: + form = forms.NewRoleForm(request.POST) + if form.is_valid(): + role = form.save() + return redirect(role.get_absolute_url()) + else: + form = forms.NewGroupForm() + return render(request, 'noclook/create/create_role.html', {'form': form}) + NEW_FUNC = { 'cable': new_cable, 'cable_csv': new_cable_csv, + 'contact': new_contact, 'customer': new_customer, + 'group': new_group, 'end-user': new_end_user, 'external-equipment': new_external_equipment, 'external-cable': new_external_cable, @@ -675,11 +770,14 @@ def new_switch(request, **kwargs): 'patch-panel': new_patch_panel, 'port': new_port, 'provider': new_provider, + 'procedure': new_procedure, 'rack': new_rack, + 'role': new_role, 'room': new_room, 'service': new_service, 'site': new_site, 'site-owner': new_site_owner, 'switch' : new_switch, 'optical-node': new_optical_node, + 'organization': new_organization, } diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 47b8b9fa0..cd515a72d 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- from django.contrib.auth.decorators import login_required +from django.http import Http404 from django.shortcuts import render, get_object_or_404 import ipaddress import json import logging -from apps.noclook.models import NodeHandle +from apps.noclook.models import NodeHandle, Role from apps.noclook import helpers from apps.noclook.views.helpers import Table, TableRow import norduniclient as nc @@ -800,3 +801,83 @@ def switch_detail(request, handle_id): 'host_services': host_services, 'connections': connections, 'dependent': dependent, 'dependencies': dependencies, 'relations': relations, 'location_path': location_path, 'history': True, 'urls': urls, 'scan_enabled': scan_enabled, 'hardware_modules': hardware_modules}) + + +@login_required +def organization_detail(request, handle_id): + nh = get_object_or_404(NodeHandle, pk=handle_id) + # Get node from neo4j-database + node = nh.get_node() + # Get location + location_path = node.get_location_path() + return render(request, 'noclook/detail/organization_detail.html', + {'node_handle': nh, 'node': node, 'location_path': location_path}) + + +@login_required +def contact_detail(request, handle_id): + nh = get_object_or_404(NodeHandle, pk=handle_id) + # Get node from neo4j-database + node = nh.get_node() + # Get location + location_path = node.get_location_path() + return render(request, 'noclook/detail/contact_detail.html', + {'node_handle': nh, 'node': node, 'location_path': location_path}) + + +@login_required +def procedure_detail(request, handle_id): + nh = get_object_or_404(NodeHandle, pk=handle_id) + # Get node from neo4j-database + node = nh.get_node() + # Get location + location_path = node.get_location_path() + return render(request, 'noclook/detail/procedure_detail.html', + {'node_handle': nh, 'node': node, 'location_path': location_path}) + + +@login_required +def group_detail(request, handle_id): + nh = get_object_or_404(NodeHandle, pk=handle_id) + # Get node from neo4j-database + node = nh.get_node() + # Get location + location_path = node.get_location_path() + return render(request, 'noclook/detail/group_detail.html', + {'node_handle': nh, 'node': node, 'location_path': location_path}) + +def _contact_with_role_table(con, org=None): + contact_link = { + 'url': u'/contact/{}/'.format(con.handle_id), + 'name': u'{}'.format(con.node_name) + } + + organization_link = { + 'url': u'/organization/{}/'.format(org.handle_id), + 'name': u'{}'.format(org.node_name) + } + + row = TableRow(contact_link, organization_link) + return row + +@login_required +def role_detail(request, handle_id): + role = get_object_or_404(Role, pk=handle_id) + + con_list = nc.models.RoleRelationship.get_contacts_with_role_name(role.name) + urls = [] + rows = [] + + for x, y in con_list: + con_node = NodeHandle.objects.get(handle_id=x.data['handle_id']) + org_node = NodeHandle.objects.get(handle_id=y.data['handle_id']) + urls.append((con_node.get_absolute_url(), org_node.get_absolute_url())) + rows.append(_contact_with_role_table(con_node, org_node)) + + table = Table('Name', 'Organization') + table.rows = rows + table.no_badges=True + + return render(request, 'noclook/detail/role_detail.html', + {'table': table, 'name': role.name, 'slug': 'role', + 'urls': urls, 'node_handle': role }) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index fc65f941d..369048351 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -10,9 +10,11 @@ from django.contrib.auth.decorators import login_required from django.contrib.admin.views.decorators import staff_member_required from django.http import Http404, JsonResponse +from django.utils import six from django.shortcuts import get_object_or_404, render, redirect import json -from apps.noclook.models import NodeHandle, Dropdown, UniqueIdGenerator, NordunetUniqueId +from apps.noclook.models import NodeHandle, Dropdown, UniqueIdGenerator,\ + NordunetUniqueId, Role, RoleGroup, DEFAULT_ROLES from django.core.exceptions import ObjectDoesNotExist from apps.noclook import forms from apps.noclook import activitylog @@ -1193,14 +1195,176 @@ def disable_noclook_auto_manage(request, slug, handle_id): return redirect(nh.get_absolute_url()) +@staff_member_required +def edit_organization(request, handle_id): + # Get needed data from node + nh, organization = helpers.get_nh_node(handle_id) + relations = organization.get_relations() + out_relations = organization.get_outgoing_relations() + if request.POST: + form = forms.EditOrganizationForm(request.POST) + if form.is_valid(): + # Generic node update + # use property keys to avoid inserting contacts as a string property of the node + property_keys = [ + 'name', 'description', 'phone', 'website', 'organization_id', 'type', 'incident_management_info', + ] + helpers.form_update_node(request.user, organization.handle_id, form, property_keys) + + # specific role setting + for field, roledict in DEFAULT_ROLES.items(): + if field in form.cleaned_data: + contact_id = form.cleaned_data[field] + role = Role.objects.get(slug=field) + set_contact = helpers.get_contact_for_orgrole(organization.handle_id, role) + + if contact_id: + if set_contact: + if set_contact.handle_id != contact_id: + helpers.unlink_contact_with_role_from_org(request.user, organization, role) + helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + else: + helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + elif set_contact: + helpers.unlink_contact_and_role_from_org(request.user, organization, set_contact.handle_id, role) + + + # Set child organizations + if form.cleaned_data['relationship_parent_of']: + organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) + helpers.set_parent_of(request.user, organization, organization_nh.handle_id) + if form.cleaned_data['relationship_uses_a']: + procedure_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_uses_a']) + helpers.set_uses_a(request.user, organization, procedure_nh.handle_id) + if 'saveanddone' in request.POST: + return redirect(nh.get_absolute_url()) + else: + return redirect('%sedit' % nh.get_absolute_url()) + else: + form = forms.EditOrganizationForm(organization.data) + return render(request, 'noclook/edit/edit_organization.html', + {'node_handle': nh, 'form': form, 'relations': relations, 'out_relations': out_relations, 'node': organization}) + +@staff_member_required +def edit_contact(request, handle_id): + # Get needed data from node + nh, contact = helpers.get_nh_node(handle_id) + relations = contact.get_outgoing_relations() + if request.POST: + form = forms.EditContactForm(request.POST) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, contact.handle_id, form) + # Set relationships + if form.cleaned_data['relationship_works_for']: + organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_works_for']) + role_handle_id = form.cleaned_data['role'] + role = Role.objects.get(handle_id=role_handle_id) + helpers.set_works_for(request.user, contact, organization_nh.handle_id, role.name) + if form.cleaned_data['relationship_member_of']: + group_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_member_of']) + helpers.set_member_of(request.user, contact, group_nh.handle_id) + if 'saveanddone' in request.POST: + return redirect(nh.get_absolute_url()) + else: + return redirect('%sedit' % nh.get_absolute_url()) + else: + form = forms.EditContactForm(contact.data) + return render(request, 'noclook/edit/edit_contact.html', + {'node_handle': nh, 'form': form, 'relations': relations, 'node': contact}) + + +@staff_member_required +def edit_procedure(request, handle_id): + # Get needed data from node + nh, procedure = helpers.get_nh_node(handle_id) + if request.POST: + form = forms.EditProcedureForm(request.POST) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, procedure.handle_id, form) + if 'saveanddone' in request.POST: + return redirect(nh.get_absolute_url()) + else: + return redirect('%sedit' % nh.get_absolute_url()) + else: + form = forms.EditProcedureForm(procedure.data) + return render(request, 'noclook/edit/edit_procedure.html', + {'node_handle': nh, 'form': form, 'node': procedure}) + + +@staff_member_required +def edit_group(request, handle_id): + # Get needed data from node + nh, group = helpers.get_nh_node(handle_id) + relations = group.get_relations() + if request.POST: + form = forms.EditGroupForm(request.POST) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, group.handle_id, form) + if form.cleaned_data['relationship_member_of']: + contact_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_member_of']) + helpers.set_of_member(request.user, group, contact_nh.handle_id) + if 'saveanddone' in request.POST: + return redirect(nh.get_absolute_url()) + else: + return redirect('%sedit' % nh.get_absolute_url()) + else: + form = forms.EditGroupForm(group.data) + + contacts = [] + + if 'Member_of' in relations: + for x in relations['Member_of']: + contact = x['node'] + contact.relationship_id = x['relationship_id'] + contacts.append(contact) + + contacts = sorted(contacts, key=lambda x: x.data['name'], reverse=False) + + return render(request, 'noclook/edit/edit_group.html', + {'node_handle': nh, 'form': form, 'node': group, 'relations': relations, 'contacts': contacts }) + + +@staff_member_required +def edit_role(request, handle_id): + # Get needed data from node + role = get_object_or_404(Role, pk=handle_id) + + if request.POST: + form = forms.EditRoleForm(request.POST, instance=role) + if form.is_valid(): + role = form.save() + return redirect(role.get_absolute_url()) + else: + return redirect('%s/edit' % role.get_absolute_url()) + else: + form = forms.EditRoleForm(instance=role) + return render(request, 'noclook/edit/edit_role.html', + {'form': form, 'role': role}) + + +@staff_member_required +def delete_role(request, handle_id): + """ + Removes the role and all the relationships with its handle_id. + """ + redirect_url = '/role/' + role = get_object_or_404(Role, handle_id=handle_id) + role.delete() + return redirect(redirect_url) + EDIT_FUNC = { 'cable': edit_cable, 'customer': edit_customer, + 'contact': edit_contact, 'end-user': edit_end_user, 'external-equipment': edit_external_equipment, 'firewall': edit_firewall, 'service': edit_service, + 'group': edit_group, 'host': edit_host, 'odf': edit_odf, 'optical-filter': edit_optical_fillter, @@ -1215,10 +1379,13 @@ def disable_noclook_auto_manage(request, slug, handle_id): 'peering-group': edit_peering_group, 'port': edit_port, 'provider': edit_provider, + 'procedure': edit_procedure, 'rack': edit_rack, + 'role': edit_role, 'router': edit_router, 'site': edit_site, 'room': edit_room, 'site-owner': edit_site_owner, 'switch': edit_switch, + 'organization': edit_organization, } diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index 435247c35..473872afd 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -3,7 +3,7 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, render -from apps.noclook.models import NodeType, NodeHandle +from apps.noclook.models import NodeType, NodeHandle, Role from apps.noclook.views.helpers import Table, TableRow from apps.noclook.helpers import get_node_urls, neo4j_data_age import norduniclient as nc @@ -682,6 +682,130 @@ def list_sites(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Sites', 'urls': urls}) +def _organization_table(org, parent_orgs): + organization_link = { + 'url': u'/organization/{}/'.format(org.get('handle_id')), + 'name': u'{}'.format(org.get('name', '')) + } + + parent_org_link = '' + + parent_links = [] + if parent_orgs: + for parent_org in parent_orgs: + parent_org_link = { + 'url': u'/organization/{}/'.format(parent_org.get('handle_id')), + 'name': u'{}'.format(parent_org.get('name', '')) + } + parent_links.append(parent_org_link) + + name = org.get('organization_id') + row = TableRow(organization_link, parent_links) + return row + +@login_required +def list_organizations(request): + q = """ + MATCH (org:Organization) + OPTIONAL MATCH (parent_org)-[:Parent_of]->(org:Organization) + RETURN org, parent_org + ORDER BY org.name + """ + + org_list = nc.query_to_list(nc.graphdb.manager, q) + urls = get_node_urls(org_list) + + table = Table('Name', 'Parent Org.') + table.rows = [] + orgs_dict = {} + + for item in org_list: + org = item['org'] + parent_org = item['parent_org'] + org_handle_id = org.get('handle_id') + + if org_handle_id not in orgs_dict: + orgs_dict[org_handle_id] = { 'org': org, 'parent_orgs': [] } + + if item['parent_org']: + orgs_dict[org_handle_id]['parent_orgs'].append(item['parent_org']) + + for org_dict in orgs_dict.values(): + table.rows.append(_organization_table(org_dict['org'], org_dict['parent_orgs'])) + + table.no_badges=True + + return render(request, 'noclook/list/list_generic.html', + {'table': table, 'name': 'Organizations', 'urls': urls}) + +def _contact_table(con, org_name): + contact_link = { + 'url': u'/contact/{}/'.format(con.get('handle_id')), + 'name': u'{}'.format(con.get('name', '')) + } + if not org_name: + org_name = '' + + row = TableRow(contact_link, org_name) + return row + +@login_required +def list_contacts(request): + q = """ + MATCH (con:Contact) + OPTIONAL MATCH (con)-[:Works_for]->(org:Organization) + RETURN con.handle_id AS con_handle_id, con, count(DISTINCT org), org + """ + + con_list = nc.query_to_list(nc.graphdb.manager, q) + contact_list = {} + + for row in con_list: + con_handle_id = row['con_handle_id'] + org_list = [] + + if con_handle_id in contact_list.keys(): + org_list = contact_list[con_handle_id]['org_list'] + + new_org_name = '' + if 'org' in row and row['org']: + new_org_name = row['org'].get('name', '') + + org_list.append(new_org_name) + + contact_list[con_handle_id] = { + 'con': row['con'], + 'org_list': org_list, + 'org': ', '.join(org_list) + } + + con_list = contact_list.values() + urls = get_node_urls(con_list) + + table = Table('Name', 'Organization') + table.rows = [_contact_table(item['con'], item['org']) for item in con_list] + table.no_badges=True + + return render(request, 'noclook/list/list_generic.html', + {'table': table, 'name': 'Contacts', 'urls': urls}) + +def _role_table(role): + role_link = { + 'url': u'/role/{}/'.format(role.handle_id), + 'name': u'{}'.format(role.name) + } + return TableRow(role_link, role.description) + +@login_required +def list_roles(request): + role_list = Role.objects.all() + + table = Table('Name', 'Description') + table.rows = [_role_table(role) for role in role_list] + table.no_badges=True + + return render(request, 'noclook/list/list_generic.html', + {'table': table, 'name': 'Roles', 'urls': role_list}) def _pdu_table(pdu): row = TableRow(pdu, pdu.get('type'), pdu.get('description')) diff --git a/src/niweb/apps/noclook/views/other.py b/src/niweb/apps/noclook/views/other.py index 1f799f18d..09c073d87 100644 --- a/src/niweb/apps/noclook/views/other.py +++ b/src/niweb/apps/noclook/views/other.py @@ -4,9 +4,11 @@ # -*- coding: utf-8 -*- from django.contrib.auth.decorators import login_required from django.contrib.auth import logout -from django.http import HttpResponse, Http404 +from django.http import HttpResponse, Http404, JsonResponse from django.shortcuts import get_object_or_404, render, redirect from django.conf import settings +from django.middleware.csrf import get_token +from graphql_jwt.settings import jwt_settings from re import escape as re_escape import json @@ -16,6 +18,10 @@ import norduniclient as nc +def csrf(request): + return JsonResponse({'csrfToken': get_token(request)}) + + def index(request): return render(request, 'noclook/index.html', {}) @@ -25,8 +31,13 @@ def logout_page(request): """ Log users out and redirects them to the index. """ + response = render(request, 'noclook/logout.html', {'cookie_domain': settings.COOKIE_DOMAIN }) + response.delete_cookie(jwt_settings.JWT_COOKIE_NAME) + request.session.flush() + logout(request) - return redirect('/') + + return response # Visualization views diff --git a/src/niweb/apps/noclook/views/redirect.py b/src/niweb/apps/noclook/views/redirect.py index fbee30fe2..8bb5d68f6 100644 --- a/src/niweb/apps/noclook/views/redirect.py +++ b/src/niweb/apps/noclook/views/redirect.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from time import sleep + from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, get_object_or_404 @@ -11,9 +13,15 @@ def node_redirect(request, handle_id): nh = get_object_or_404(NodeHandle, pk=handle_id) return redirect(nh.get_absolute_url()) -from time import sleep + @login_required def node_slow_redirect(request, handle_id): nh = get_object_or_404(NodeHandle, pk=handle_id) sleep(10) return redirect(nh.get_absolute_url()) + + +@login_required +def redirect_back(request): + nexturl = request.GET.get('next') + return redirect(nexturl) diff --git a/src/niweb/apps/scan/migrations/0002_auto_20190121_0856.py b/src/niweb/apps/scan/migrations/0002_auto_20190121_0856.py new file mode 100644 index 000000000..fcb07219e --- /dev/null +++ b/src/niweb/apps/scan/migrations/0002_auto_20190121_0856.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-01-21 08:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scan', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='queueitem', + name='status', + field=models.CharField(choices=[(b'QUEUED', b'Queued'), (b'PROCESSING', b'Processing'), (b'DONE', b'Done'), (b'FAILED', b'Failed')], default=b'QUEUED', max_length=255), + ), + ] diff --git a/src/niweb/apps/scan/migrations/0003_auto_20190410_1341.py b/src/niweb/apps/scan/migrations/0003_auto_20190410_1341.py new file mode 100644 index 000000000..b47cfc143 --- /dev/null +++ b/src/niweb/apps/scan/migrations/0003_auto_20190410_1341.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-10 13:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scan', '0002_auto_20190121_0856'), + ] + + operations = [ + migrations.AlterField( + model_name='queueitem', + name='status', + field=models.CharField(choices=[('QUEUED', 'Queued'), ('PROCESSING', 'Processing'), ('DONE', 'Done'), ('FAILED', 'Failed')], default='QUEUED', max_length=255), + ), + migrations.AlterField( + model_name='queueitem', + name='type', + field=models.CharField(choices=[('Host', 'Host')], max_length=255), + ), + ] diff --git a/src/niweb/apps/scan/migrations/0004_merge_20190903_1136.py b/src/niweb/apps/scan/migrations/0004_merge_20190903_1136.py new file mode 100644 index 000000000..4407d8579 --- /dev/null +++ b/src/niweb/apps/scan/migrations/0004_merge_20190903_1136.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.24 on 2019-09-03 11:36 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scan', '0003_auto_20190410_1341'), + ('scan', '0002_auto_20190320_1246'), + ] + + operations = [ + ] diff --git a/src/niweb/apps/userprofile/migrations/0002_auto_20191108_1258.py b/src/niweb/apps/userprofile/migrations/0002_auto_20191108_1258.py new file mode 100644 index 000000000..fcbbe75ae --- /dev/null +++ b/src/niweb/apps/userprofile/migrations/0002_auto_20191108_1258.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.25 on 2019-11-08 12:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='landing_page', + field=models.CharField(choices=[('network', 'Network'), ('services', 'Services'), ('community', 'Community')], default='community', max_length=255), + ), + migrations.AddField( + model_name='userprofile', + name='view_community', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='userprofile', + name='view_network', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='userprofile', + name='view_services', + field=models.BooleanField(default=True), + ), + ] diff --git a/src/niweb/apps/userprofile/migrations/0003_userprofile_avatar.py b/src/niweb/apps/userprofile/migrations/0003_userprofile_avatar.py new file mode 100644 index 000000000..7ccd3b4ea --- /dev/null +++ b/src/niweb/apps/userprofile/migrations/0003_userprofile_avatar.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-11-11 08:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0002_auto_20191108_1258'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='avatar', + field=models.ImageField(null=True, upload_to=''), + ), + ] diff --git a/src/niweb/apps/userprofile/migrations/0004_auto_20191122_1213.py b/src/niweb/apps/userprofile/migrations/0004_auto_20191122_1213.py new file mode 100644 index 000000000..c1785c9b6 --- /dev/null +++ b/src/niweb/apps/userprofile/migrations/0004_auto_20191122_1213.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-11-22 12:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0003_userprofile_avatar'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='modified', + field=models.DateTimeField(auto_now=True, null=True), + ), + ] diff --git a/src/niweb/apps/userprofile/migrations/0005_userprofile_email.py b/src/niweb/apps/userprofile/migrations/0005_userprofile_email.py new file mode 100644 index 000000000..b297ef02d --- /dev/null +++ b/src/niweb/apps/userprofile/migrations/0005_userprofile_email.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-11-22 12:48 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0004_auto_20191122_1213'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='email', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/src/niweb/apps/userprofile/models.py b/src/niweb/apps/userprofile/models.py index e92735d60..065b6c145 100644 --- a/src/niweb/apps/userprofile/models.py +++ b/src/niweb/apps/userprofile/models.py @@ -8,10 +8,25 @@ @python_2_unicode_compatible class UserProfile(models.Model): + LANDING_CHOICES = ( + ('network', 'Network'), + ('services', 'Services'), + ('community', 'Community'), + ) + user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE) + email = models.CharField(max_length=255, blank=True, null=True) display_name = models.CharField(max_length=255, blank=True, null=True) - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) + avatar = models.ImageField(null=True) + created = models.DateTimeField(auto_now_add=True, blank=True, null=True) + modified = models.DateTimeField(auto_now=True, blank=True, null=True) + + landing_page = models.CharField(max_length=255, + choices=LANDING_CHOICES, + default='community') + view_network = models.BooleanField(default=True) + view_services = models.BooleanField(default=True) + view_community = models.BooleanField(default=True) def __str__(self): return "%s [%s]" % (self.user.username, self.display_name) @@ -26,4 +41,14 @@ def url(self): @receiver(post_save, sender=User) def create_user_profile(sender, **kwargs): user = kwargs['instance'] - UserProfile.objects.get_or_create(user=user) + user_profile, created = UserProfile.objects.get_or_create(user=user) + if(created): + user_profile.display_name = "%s %s" % (user.first_name, user.last_name) + user.email = user_profile.email + user.save() + +@receiver(post_save, sender=UserProfile) +def create_user_profile(sender, **kwargs): + profile = kwargs['instance'] + profile.user.email = profile.email + profile.user.save() diff --git a/src/niweb/apps/userprofile/resources.py b/src/niweb/apps/userprofile/resources.py new file mode 100644 index 000000000..4f950bf0d --- /dev/null +++ b/src/niweb/apps/userprofile/resources.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from django.contrib.auth.models import User +from tastypie.resources import Resource, ModelResource +from tastypie import fields +from tastypie.authentication import SessionAuthentication +from tastypie.authorization import Authorization +from apps.userprofile.models import UserProfile + +class UserProfileResource(ModelResource): + + class Meta: + queryset = UserProfile.objects.all() + resource_name = 'userprofile' + authentication = SessionAuthentication() + authorization = Authorization() + excludes = ['created', 'modified'] + filtering = {} + + user_id = fields.IntegerField('user_id') + email = fields.CharField('email') + display_name = fields.CharField('display_name') + # avatar = fields.FileField('avatar') + landing_page = fields.CharField('landing_page') + view_network = fields.BooleanField('view_network') + view_services = fields.BooleanField('view_services') + view_community = fields.BooleanField('view_community') diff --git a/src/niweb/apps/userprofile/templatetags/userprofile_tags.py b/src/niweb/apps/userprofile/templatetags/userprofile_tags.py index 97ffc91c8..f4b6f8c01 100644 --- a/src/niweb/apps/userprofile/templatetags/userprofile_tags.py +++ b/src/niweb/apps/userprofile/templatetags/userprofile_tags.py @@ -11,7 +11,10 @@ def userprofile_link(user): # if user is django user, find profile.. if isinstance(user, User): - userprofile = UserProfile.objects.get(user=user) + userprofile = UserProfile.objects.get_or_create( + user=user, + email=user.email, + )[0] elif isinstance(user, UserProfile): # if profile just do the url lookup userprofile = user diff --git a/src/niweb/apps/userprofile/urls.py b/src/niweb/apps/userprofile/urls.py index bae0d96dd..c5872efa7 100644 --- a/src/niweb/apps/userprofile/urls.py +++ b/src/niweb/apps/userprofile/urls.py @@ -14,4 +14,5 @@ urlpatterns = [ url(r'^$', views.list_userprofiles), url(r'^(?P\d+)/$', views.userprofile_detail, name='userprofile_detail'), + url(r'^whoami/$', views.whoami, name='whoami'), ] diff --git a/src/niweb/apps/userprofile/views.py b/src/niweb/apps/userprofile/views.py index 1c4ab3b57..5d0458f22 100644 --- a/src/niweb/apps/userprofile/views.py +++ b/src/niweb/apps/userprofile/views.py @@ -1,6 +1,7 @@ from apps.userprofile.models import UserProfile from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.http import JsonResponse, HttpResponse from django.shortcuts import render, get_object_or_404 from actstream.models import actor_stream @@ -16,7 +17,8 @@ def list_userprofiles(request): def userprofile_detail(request, userprofile_id): profile = get_object_or_404(UserProfile, pk=userprofile_id) activities = actor_stream(profile.user) - paginator = Paginator(activities, 50, allow_empty_first_page=True) # Show 50 activities per page + # Show 50 activities per page + paginator = Paginator(activities, 50, allow_empty_first_page=True) page = request.GET.get('page') try: activities = paginator.page(page) @@ -28,4 +30,35 @@ def userprofile_detail(request, userprofile_id): activities = paginator.page(paginator.num_pages) total_activities = '{:,}'.format(activities.paginator.count) return render(request, 'userprofile/userprofile_detail.html', - {'profile': profile, 'activities': activities, 'total_activities': total_activities}) + {'profile': profile, 'activities': activities, 'total_activities': total_activities}) + + display_name = fields.CharField('display_name') + email = fields.CharField('user__email') + # avatar = fields.FileField('avatar') + landing_page = fields.CharField('landing_page') + view_network = fields.BooleanField('view_network') + view_services = fields.BooleanField('view_services') + view_community = fields.BooleanField('view_community') + + +@login_required +def whoami(request): + if request.method == 'GET': + user_profile = getattr(request.user, 'profile', None) + + if not user_profile: + user_profile = UserProfile(user=request.user, email=request.user.email) + user_profile.save() + + user = { + 'userid': request.user.pk, + 'display_name': user_profile.display_name, + 'email': request.user.email, + 'landing_page': user_profile.landing_page, + 'landing_choices': user_profile.LANDING_CHOICES, + 'view_network': user_profile.view_network, + 'view_services': user_profile.view_services, + 'view_community': user_profile.view_community + } + return JsonResponse(user) + return httpResponse(status_code=405) diff --git a/src/niweb/niweb/assets/css/select2.min.css b/src/niweb/niweb/assets/css/select2.min.css new file mode 100644 index 000000000..1e2c9bf60 --- /dev/null +++ b/src/niweb/niweb/assets/css/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/src/niweb/niweb/assets/js/jquery/js.cookie.min.js b/src/niweb/niweb/assets/js/jquery/js.cookie.min.js new file mode 100644 index 000000000..aa1c3b630 --- /dev/null +++ b/src/niweb/niweb/assets/js/jquery/js.cookie.min.js @@ -0,0 +1,2 @@ +/*! js-cookie v3.0.0-beta.3 | MIT */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,r=e.Cookies=t();r.noConflict=function(){return e.Cookies=n,r}}())}(this,function(){"use strict";var e={read:function(e){return e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}};function t(e){for(var t=1;t0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}if((o||q)&&p){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),o)for(l=o.length;l>0;l-=1)if((e=p[o.slice(0,l).join("/")])&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&q&&q[d]&&(i=q[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){var d=w.call(arguments,0);return"string"!=typeof d[0]&&1===d.length&&d.push(null),o.apply(b,d.concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){r[a]=b}}function j(a){if(e(s,a)){var c=s[a];delete s[a],u[a]=!0,n.apply(b,c)}if(!e(r,a)&&!e(u,a))throw new Error("No "+a);return r[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return a?k(a):[]}function m(a){return function(){return t&&t.config&&t.config[a]||{}}}var n,o,p,q,r={},s={},t={},u={},v=Object.prototype.hasOwnProperty,w=[].slice,x=/\.js$/;p=function(a,b){var c,d=k(a),e=d[0],g=b[1];return a=d[1],e&&(e=f(e,g),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(g)):f(a,g):(a=f(a,g),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},q={require:function(a){return g(a)},exports:function(a){var b=r[a];return void 0!==b?b:r[a]={}},module:function(a){return{id:a,uri:"",exports:r[a],config:m(a)}}},n=function(a,c,d,f){var h,k,m,n,o,t,v,w=[],x=typeof d;if(f=f||a,t=l(f),"undefined"===x||"function"===x){for(c=!c.length&&d.length?["require","exports","module"]:c,o=0;o0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;h":">",'"':""","'":"'","/":"/"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c.__cache={};var e=0;return c.GetUniqueElementId=function(a){var b=a.getAttribute("data-select2-id");return null==b&&(a.id?(b=a.id,a.setAttribute("data-select2-id",b)):(a.setAttribute("data-select2-id",++e),b=e.toString())),b},c.StoreData=function(a,b,d){var e=c.GetUniqueElementId(a);c.__cache[e]||(c.__cache[e]={}),c.__cache[e][b]=d},c.GetData=function(b,d){var e=c.GetUniqueElementId(b);return d?c.__cache[e]&&null!=c.__cache[e][d]?c.__cache[e][d]:a(b).data(d):c.__cache[e]},c.RemoveData=function(a){var b=c.GetUniqueElementId(a);null!=c.__cache[b]&&delete c.__cache[b]},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('
    ');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('
  • '),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c0?b.first().trigger("mouseenter"):a.first().trigger("mouseenter"),this.ensureHighlightVisible()},c.prototype.setClasses=function(){var c=this;this.data.current(function(d){var e=a.map(d,function(a){return a.id.toString()});c.$results.find(".select2-results__option[aria-selected]").each(function(){var c=a(this),d=b.GetData(this,"data"),f=""+d.id;null!=d.element&&d.element.selected||null==d.element&&a.inArray(f,e)>-1?c.attr("aria-selected","true"):c.attr("aria-selected","false")})})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(c){var d=document.createElement("li");d.className="select2-results__option";var e={role:"treeitem","aria-selected":"false"};c.disabled&&(delete e["aria-selected"],e["aria-disabled"]="true"),null==c.id&&delete e["aria-selected"],null!=c._resultId&&(d.id=c._resultId),c.title&&(d.title=c.title),c.children&&(e.role="group",e["aria-label"]=c.text,delete e["aria-selected"]);for(var f in e){var g=e[f];d.setAttribute(f,g)}if(c.children){var h=a(d),i=document.createElement("strong");i.className="select2-results__group";a(i);this.template(c,i);for(var j=[],k=0;k",{class:"select2-results__options select2-results__options--nested"});n.append(j),h.append(i),h.append(n)}else this.template(c,d);return b.StoreData(d,"data",c),d},c.prototype.bind=function(c,d){var e=this,f=c.id+"-results";this.$results.attr("id",f),c.on("results:all",function(a){e.clear(),e.append(a.data),c.isOpen()&&(e.setClasses(),e.highlightFirstItem())}),c.on("results:append",function(a){e.append(a.data),c.isOpen()&&e.setClasses()}),c.on("query",function(a){e.hideMessages(),e.showLoading(a)}),c.on("select",function(){c.isOpen()&&(e.setClasses(),e.highlightFirstItem())}),c.on("unselect",function(){c.isOpen()&&(e.setClasses(),e.highlightFirstItem())}),c.on("open",function(){e.$results.attr("aria-expanded","true"),e.$results.attr("aria-hidden","false"),e.setClasses(),e.ensureHighlightVisible()}),c.on("close",function(){e.$results.attr("aria-expanded","false"),e.$results.attr("aria-hidden","true"),e.$results.removeAttr("aria-activedescendant")}),c.on("results:toggle",function(){var a=e.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),c.on("results:select",function(){var a=e.getHighlightedResults();if(0!==a.length){var c=b.GetData(a[0],"data");"true"==a.attr("aria-selected")?e.trigger("close",{}):e.trigger("select",{data:c})}}),c.on("results:previous",function(){var a=e.getHighlightedResults(),b=e.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var d=c-1;0===a.length&&(d=0);var f=b.eq(d);f.trigger("mouseenter");var g=e.$results.offset().top,h=f.offset().top,i=e.$results.scrollTop()+(h-g);0===d?e.$results.scrollTop(0):h-g<0&&e.$results.scrollTop(i)}}),c.on("results:next",function(){var a=e.getHighlightedResults(),b=e.$results.find("[aria-selected]"),c=b.index(a),d=c+1;if(!(d>=b.length)){var f=b.eq(d);f.trigger("mouseenter");var g=e.$results.offset().top+e.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=e.$results.scrollTop()+h-g;0===d?e.$results.scrollTop(0):h>g&&e.$results.scrollTop(i)}}),c.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),c.on("results:message",function(a){e.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=e.$results.scrollTop(),c=e.$results.get(0).scrollHeight-b+a.deltaY,d=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=e.$results.height();d?(e.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(e.$results.scrollTop(e.$results.get(0).scrollHeight-e.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(c){var d=a(this),f=b.GetData(this,"data");if("true"===d.attr("aria-selected"))return void(e.options.get("multiple")?e.trigger("unselect",{originalEvent:c,data:f}):e.trigger("close",{}));e.trigger("select",{originalEvent:c,data:f})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(c){var d=b.GetData(this,"data");e.getHighlightedResults().removeClass("select2-results__option--highlighted"),e.trigger("results:focus",{data:d,element:a(this)})})},c.prototype.getHighlightedResults=function(){return this.$results.find(".select2-results__option--highlighted")},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),c<=2?this.$results.scrollTop(0):(g>this.$results.outerHeight()||g<0)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var c=a('');return this._tabindex=0,null!=b.GetData(this.$element[0],"old-tabindex")?this._tabindex=b.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),c.attr("title",this.$element.attr("title")),c.attr("tabindex",this._tabindex),this.$selection=c,c},d.prototype.bind=function(a,b){var d=this,e=(a.id,a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(c){a(document.body).on("mousedown.select2."+c.id,function(c){var d=a(c.target),e=d.closest(".select2");a(".select2.select2-container--open").each(function(){a(this),this!=e[0]&&b.GetData(this,"element").select2("close")})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){b.find(".selection").append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html(''),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("focus",function(b){a.isOpen()||c.$selection.focus()})},e.prototype.clear=function(){var a=this.$selection.find(".select2-selection__rendered");a.empty(),a.removeAttr("title")},e.prototype.display=function(a,b){var c=this.options.get("templateSelection");return this.options.get("escapeMarkup")(c(a,b))},e.prototype.selectionContainer=function(){return a("")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.attr("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('
      '),a},d.prototype.bind=function(b,e){var f=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){f.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!f.options.get("disabled")){var d=a(this),e=d.parent(),g=c.GetData(e[0],"data");f.trigger("unselect",{originalEvent:b,data:g})}})},d.prototype.clear=function(){var a=this.$selection.find(".select2-selection__rendered");a.empty(),a.removeAttr("title")},d.prototype.display=function(a,b){var c=this.options.get("templateSelection");return this.options.get("escapeMarkup")(c(a,b))},d.prototype.selectionContainer=function(){return a('
    • ×
    • ')},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d1||c)return a.call(this,b);this.clear();var d=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(d)},b}),b.define("select2/selection/allowClear",["jquery","../keys","../utils"],function(a,b,c){function d(){}return d.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},d.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var d=this.$selection.find(".select2-selection__clear");if(0!==d.length){b.stopPropagation();var e=c.GetData(d[0],"data"),f=this.$element.val();this.$element.val(this.placeholder.id);var g={data:e};if(this.trigger("clear",g),g.prevented)return void this.$element.val(f);for(var h=0;h0||0===d.length)){var e=a('×');c.StoreData(e[0],"data",d),this.$selection.find(".select2-selection__rendered").prepend(e)}},d}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,d,e){var f=this;a.call(this,d,e),d.on("open",function(){f.$search.trigger("focus")}),d.on("close",function(){f.$search.val(""),f.$search.removeAttr("aria-activedescendant"),f.$search.trigger("focus")}),d.on("enable",function(){f.$search.prop("disabled",!1),f._transferTabIndex()}),d.on("disable",function(){f.$search.prop("disabled",!0)}),d.on("focus",function(a){f.$search.trigger("focus")}),d.on("results:focus",function(a){f.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){f.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){f._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){if(a.stopPropagation(),f.trigger("keypress",a),f._keyUpPrevented=a.isDefaultPrevented(),a.which===c.BACKSPACE&&""===f.$search.val()){var d=f.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var e=b.GetData(d[0],"data");f.searchRemoveChoice(e),a.preventDefault()}}});var g=document.documentMode,h=g&&g<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){if(h)return void f.$selection.off("input.search input.searchcheck");f.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(h&&"input"===a.type)return void f.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&f.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{a=.75*(this.$search.val().length+1)+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],g=["opening","closing","selecting","unselecting","clearing"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"}}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),null!=c.id?d+="-"+c.id.toString():d+="-"+a.generateChars(4),d},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){"status"in d&&(0===d.status||"0"===d.status)||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h0&&b.term.length>this.maximumInputLength)return void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}});a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;if(d.maximumSelectionLength>0&&f>=d.maximumSelectionLength)return void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}});a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val(""),e.$search.blur()}),c.on("focus",function(){c.isOpen()||e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){e.showSearch(a)?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){e.$results.offset().top+e.$results.outerHeight(!1)+50>=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1)&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
    • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){b.StoreData(this,"select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(c){var d=b.GetData(this,"select2-scroll-position");a(this).scrollTop(d.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id;this.$container.parents().filter(b.hasScroll).off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),null==l.tokenSeparators&&null==l.tokenizer||(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){null==c(d,e.children[g])&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var h=b(e.text).toUpperCase(),i=b(d.term).toUpperCase();return h.indexOf(i)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(!0,this.defaults,f)},new D}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),d.GetData(a[0],"select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),d.StoreData(a[0],"data",d.GetData(a[0],"select2Tags")),d.StoreData(a[0],"tags",!0)),d.GetData(a[0],"ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",d.GetData(a[0],"ajaxUrl")),d.StoreData(a[0],"ajax-Url",d.GetData(a[0],"ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,d.GetData(a[0])):d.GetData(a[0]);var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,d){null!=c.GetData(a[0],"select2")&&c.GetData(a[0],"select2").destroy(),this.$element=a,this.id=this._generateId(a),d=d||{},this.options=new b(d,a),e.__super__.constructor.call(this);var f=a.attr("tabindex")||0;c.StoreData(a[0],"old-tabindex",f),a.attr("tabindex","-1");var g=this.options.get("dataAdapter");this.dataAdapter=new g(a,this.options);var h=this.render();this._placeContainer(h);var i=this.options.get("selectionAdapter");this.selection=new i(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,h);var j=this.options.get("dropdownAdapter");this.dropdown=new j(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,h);var k=this.options.get("resultsAdapter");this.results=new k(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var l=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){l.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),c.StoreData(a[0],"select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return e<=0?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;h=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting",clear:"clearing"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),null!=a&&0!==a.length||(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",c.GetData(this.$element[0],"old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),c.RemoveData(this.$element[0]),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),c.StoreData(b[0],"element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(a,b,c,d,e){if(null==a.fn.select2){var f=["open","close","destroy"];a.fn.select2=function(b){if("object"==typeof(b=b||{}))return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d,g=Array.prototype.slice.call(arguments,1);return this.each(function(){var a=e.GetData(this,"select2");null==a&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2."),d=a[b].apply(a,g)}),a.inArray(b,f)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c}); \ No newline at end of file diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py new file mode 100644 index 000000000..d3c5715e4 --- /dev/null +++ b/src/niweb/niweb/schema.py @@ -0,0 +1,24 @@ +import graphene +import graphql_jwt +from apps.noclook.schema import NOCSCHEMA_QUERIES, NOCSCHEMA_MUTATIONS,\ + NOCSCHEMA_TYPES + +ALL_TYPES = NOCSCHEMA_TYPES # + OTHER_APP_TYPES +ALL_QUERIES = NOCSCHEMA_QUERIES +ALL_MUTATIONS = NOCSCHEMA_MUTATIONS + +class Query(*ALL_QUERIES, graphene.ObjectType): + pass + +class Mutation(*ALL_MUTATIONS, graphene.ObjectType): + token_auth = graphql_jwt.relay.ObtainJSONWebToken.Field() + verify_token = graphql_jwt.relay.Verify.Field() + refresh_token = graphql_jwt.relay.Refresh.Field() + #revoke_token = graphql_jwt.relay.Revoke.Field() + +schema = graphene.Schema( + query=Query, + mutation=Mutation, + auto_camelcase=False, + types=ALL_TYPES + ) diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index a17bd65c5..3bd8f6787 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from datetime import timedelta from os.path import abspath, basename, dirname, join, normpath from os import environ from sys import path @@ -65,7 +66,7 @@ ########## ALLOWED HOSTS CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '').split() +ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split() ########## END ALLOWED HOST CONFIGURATION ########## MANAGER CONFIGURATION @@ -166,7 +167,7 @@ normpath(join(DJANGO_ROOT, 'templates')), ], 'APP_DIRS': True, - 'OPTIONS': { + 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.debug', @@ -195,18 +196,19 @@ ########## MIDDLEWARE CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#middleware-classes MIDDLEWARE = ( - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', + 'apps.noclook.middleware.SRIJWTAuthMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', ) ########## END MIDDLEWARE CONFIGURATION ########## AUTHENTICATION BACKENDS CONFIGURATION AUTHENTICATION_BACKENDS = ( + 'graphql_jwt.backends.JSONWebTokenBackend', 'django.contrib.auth.backends.ModelBackend', ) if SAML_ENABLED: @@ -247,6 +249,9 @@ 'crispy_forms', 'dynamic_preferences', 'attachments', + 'graphene_django', + 'corsheaders', + 'graphql_jwt.refresh_token.apps.RefreshTokenConfig', ) LOCAL_APPS = ( @@ -254,6 +259,7 @@ 'apps.noclook', 'apps.scan', 'apps.nerds', + 'djangovakt', ) OPTIONAL_APPS = environ.get('OPTIONAL_APPS', '').split() @@ -268,6 +274,13 @@ 'USE_JSONFIELD': True, 'GFK_FETCH_DEPTH': 1, } + +GRAPHENE = { + 'SCHEMA': 'niweb.schema.schema', + 'MIDDLEWARE': [ + 'graphql_jwt.middleware.JSONWebTokenMiddleware', + ], +} ########## END APP CONFIGURATION @@ -344,3 +357,33 @@ # See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application WSGI_APPLICATION = 'wsgi.application' ########## END WSGI CONFIGURATION + +########## GRAPHQL JWT CONFIGURATION +GRAPHQL_JWT = { + 'JWT_VERIFY_EXPIRATION': True, + 'JWT_EXPIRATION_DELTA': timedelta(minutes=5), + 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7), +} +########## END GRAPHQL JWT CONFIGURATION + +########## SESSION_COOKIE_DOMAIN +SESSION_EXPIRE_AT_BROWSER_CLOSE = True +SESSION_COOKIE_HTTPONLY = False + +COOKIE_DOMAIN = environ.get('COOKIE_DOMAIN') +SESSION_COOKIE_DOMAIN = COOKIE_DOMAIN +CSRF_COOKIE_DOMAIN = COOKIE_DOMAIN + +CORS_ALLOW_CREDENTIALS = True +CORS_ORIGIN_ALLOW_ALL = False +CORS_ORIGIN_WHITELIST = [ + 'https://{}'.format( environ.get('SRI_FRONTEND_URL', 'sri.sunet.se') ) +] +CSRF_TRUSTED_ORIGINS = [ + environ.get('SRI_FRONTEND_URL', 'sri.sunet.se'), +] +########## END SESSION_COOKIE_DOMAIN + +########## GRAPHQL CONFIGURATION +USE_GRAPHIQL = False +########## END GRAPHQL CONFIGURATION diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 46b923ec7..00c885322 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -36,6 +36,10 @@ ########## END DEBUG CONFIGURATION +########## SESSION_COOKIE_DOMAIN +SESSION_COOKIE_HTTPONLY = False +########## END SESSION_COOKIE_DOMAIN + ########## EMAIL CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -76,3 +80,9 @@ ########## END TESTING GOOGLE_MAPS_API_KEY = environ.get('GOOGLE_MAPS_API_KEY', 'no-apikey') +CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_CREDENTIALS = True + +########## GRAPHQL CONFIGURATION +USE_GRAPHIQL = True +########## END GRAPHQL CONFIGURATION diff --git a/src/niweb/niweb/settings/prod.py b/src/niweb/niweb/settings/prod.py index 95356129e..5d11f16f4 100644 --- a/src/niweb/niweb/settings/prod.py +++ b/src/niweb/niweb/settings/prod.py @@ -28,7 +28,6 @@ ########## END GENERAL CONFIGURATION # djangosaml2 settings -SESSION_EXPIRE_AT_BROWSER_CLOSE = True SAML_CREATE_UNKNOWN_USER = True SAML_ATTRIBUTE_MAPPING = { diff --git a/src/niweb/niweb/templates/base.html b/src/niweb/niweb/templates/base.html index 7b197e7e6..56d3f2b4f 100644 --- a/src/niweb/niweb/templates/base.html +++ b/src/niweb/niweb/templates/base.html @@ -1,230 +1,232 @@ - -{% load static %} - - - - - - - - - NOCLook{% block title %}{% endblock %} - - - - - - - - {% block js %}{% endblock %} - {% if noclook.link_color %} - - {% endif %} - - - -
      - {% load noclook_tags %} -
      - -
      -
      - - {% include "logo.svg" %} - - {% block menu %} - {% load noclook_tags %} - - {% endblock %} -
      -
      -
      -
      - {% if user.is_authenticated %} - - {% endif %} -
      -
      - {% if user.is_authenticated %} - {% load userprofile_tags %} -
      Logged in as {% userprofile_link user %} Log out
      - {% endif %} -
      -
      - {% if page_flash.message %} -
      {{ page_flash.message }}
      - {% endif %} - - {% if messages %} - {% for message in messages %} -
      {{ message }}
      - {% endfor %} - {% endif %} - - {% block content %}{% endblock %} - - {% block content_footer %}{% endblock %} - - {% if node %} - -
      -


      -
      -
      - -
      -
      -

      Debug

      - loading Loading debug info... -
      -
      -
      -
      -
      - {% endif %} -
      -
      -
      - - + +{% load static %} + + + + + + + + + NOCLook{% block title %}{% endblock %} + + + + + + + + {% block js %}{% endblock %} + {% if noclook.link_color %} + + {% endif %} + + + +
      + {% load noclook_tags %} +
      + +
      +
      + + {% block menu %} + {% load noclook_tags %} + + {% endblock %} +
      +
      +
      +
      + {% if user.is_authenticated %} + + {% endif %} +
      +
      + {% if user.is_authenticated %} + {% load userprofile_tags %} +
      Logged in as {% userprofile_link user %} Log out
      + {% endif %} +
      +
      + {% if page_flash.message %} +
      {{ page_flash.message }}
      + {% endif %} + + {% if messages %} + {% for message in messages %} +
      {{ message }}
      + {% endfor %} + {% endif %} + + {% block content %}{% endblock %} + + {% block content_footer %}{% endblock %} + + {% if node %} + +
      +


      +
      +
      + +
      +
      +

      Debug

      + loading Loading debug info... +
      +
      +
      +
      +
      + {% endif %} +
      +
      +
      + + diff --git a/src/niweb/niweb/urls.py b/src/niweb/niweb/urls.py index aa0fcff12..25d6fcbc0 100644 --- a/src/niweb/niweb/urls.py +++ b/src/niweb/niweb/urls.py @@ -2,7 +2,12 @@ from django.conf.urls import include, url from tastypie.api import Api import apps.noclook.api.resources as niapi +import apps.userprofile.resources as profile_api from django.contrib.auth import views as auth_views +from django.views.decorators.csrf import csrf_exempt +from apps.noclook.schema import AuthGraphQLView +from graphql_jwt.decorators import jwt_cookie +from apps.noclook.views.redirect import redirect_back from django.conf.urls.static import static # Uncomment the next two lines to enable the admin: @@ -19,6 +24,7 @@ def if_installed(appname, *args, **kwargs): v1_api = Api(api_name='v1') # Resources +v1_api.register(profile_api.UserProfileResource()) v1_api.register(niapi.NodeTypeResource()) v1_api.register(niapi.RelationshipResource()) v1_api.register(niapi.UserResource()) @@ -81,6 +87,12 @@ def if_installed(appname, *args, **kwargs): # Tastypie URLs url(r'^api/', include(v1_api.urls)), + # Authn + url(r'^authn$', redirect_back, name='check_authn'), + + # GraphQL endpoint + url(r'^graphql/', csrf_exempt(jwt_cookie(AuthGraphQLView.as_view(graphiql=settings.USE_GRAPHIQL)))), + # Django Generic Comments url(r'^comments/', include('django_comments.urls')),