From a7dfd4a45136c858647c67135e6f0085ea10515f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 27 Feb 2019 11:44:42 +0100 Subject: [PATCH 001/520] Wip version of the salesforce csv importer --- .../noclook/management/commands/csvimport.py | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/niweb/apps/noclook/management/commands/csvimport.py 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..1521e5b8f --- /dev/null +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from django.core.management.base import BaseCommand, CommandError +from pprint import pprint +from apps.noclook.models import User, NodeType, NodeHandle, NODE_META_TYPE_CHOICES + +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('-d', "--delimiter", nargs='?', default=';', + help='Delimiter to use use. Default ";".') + + def handle(self, *args, **options): + ## (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 + user = User.objects.filter(username='admin').first() + + # 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 + + # process organizations + if options['organizations']: + csv_organizations = options['organizations'] + node_list = self.read_csv(csv_organizations) + + # dj: organization exist?: create or get (using just the name) + # n4: add attributes + # dj: if parent organization: create or get (using just the name) + # n4: add relation between org and parent_org + # Print iterations progress + + options['organizations'].close() + + # process contacts + if options['contacts']: + node_type = NodeType.objects.filter(type=self.new_types[2]).first() # contact + relation_meta_type = NODE_META_TYPE_CHOICES[2][1] # relation + logical_meta_type = NODE_META_TYPE_CHOICES[1][1] # logical + node_list = self.read_csv(csv_contacts) + + for node in node_list: + full_name = '{} {}'.format( + node['first_name'], + node['last_name'] + ) + + # dj: contact exists?: create or get + new_contact = None + qs = NodeHandle.objects.filter( + node_name = full_name, + node_type = node_type + ) + + if qs: + new_contact = qs.first() + else: + new_contact = NodeHandle( + node_name = full_name, + node_type = node_type, + node_meta_type = relation_meta_type, + creator = user, + modifier = user, + ) + + new_contact.save() + + # n4: add attributes + graph_node = new_contact.get_node() + + graph_node.add_property('name', full_name) + for key in node.keys(): + if key not in ['node_type', 'role', 'name', 'account_name'] and node[key]: + graph_node.add_property(key, node[key]) + + # dj: role exist?: create or get + role_name = node['contact_role'] + + if role_name: + role_type = NodeType.objects.filter(type=self.new_types[4]).first() # role + new_role = None + qs = NodeHandle.objects.filter( + node_name = node['contact_role'], + node_type = role_type + ) + + if qs: + new_role = qs.first() + else: + new_role = NodeHandle( + node_name = node['contact_role'], + node_type = role_type, + node_meta_type = logical_meta_type, + creator = user, + modifier = user, + ) + + new_role.save() + + # n4: add relation between role and contact + graph_node.add_role(new_role.pk) + + # dj: organization exist?: create or get + # n4: add relation between role and organization + # Print iterations progress + + csv_contacts.close() + + def count_lines(self, 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 = '', suffix = '', 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('') From aaf46c65802e7749a821aa52f6d1a11164a700cf Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Sun, 3 Mar 2019 22:01:13 +0100 Subject: [PATCH 002/520] first functional version of the import management command --- .../noclook/management/commands/csvimport.py | 153 ++++++++++++------ 1 file changed, 107 insertions(+), 46 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 1521e5b8f..e1958d5df 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +from apps.noclook.models import User, NodeType, NodeHandle, NODE_META_TYPE_CHOICES from django.core.management.base import BaseCommand, CommandError from pprint import pprint -from apps.noclook.models import User, NodeType, NodeHandle, NODE_META_TYPE_CHOICES +from time import sleep import argparse import norduniclient as nc @@ -26,6 +27,9 @@ def add_arguments(self, parser): help='Delimiter to use use. Default ";".') def handle(self, *args, **options): + relation_meta_type = NODE_META_TYPE_CHOICES[2][1] # relation + logical_meta_type = NODE_META_TYPE_CHOICES[1][1] # logical + ## (We'll use handle_id on to get the node on cql code) # check if new types exists if options['verbosity'] > 0: @@ -47,7 +51,7 @@ def handle(self, *args, **options): csv_organizations = None csv_contacts = None - user = User.objects.filter(username='admin').first() + self.user = User.objects.filter(username='admin').first() # IMPORT ORGANIZATIONS if options['organizations']: @@ -73,24 +77,64 @@ def handle(self, *args, **options): total_lines = total_lines + con_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) - # dj: organization exist?: create or get (using just the name) - # n4: add attributes - # dj: if parent organization: create or get (using just the name) - # n4: add relation between org and parent_org - # Print iterations progress + for node in node_list: + account_name = node['account_name'] + + # dj: organization exist?: create or get (using just the name) + + #def get_or_create(self, node_name, node_type, node_meta_type): + new_organization = self.get_or_create( + account_name, + node_type, + relation_meta_type + ) + + # n4: add attributes + graph_node = new_organization.get_node() + + graph_node.add_property('name', account_name) + 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 = self.get_or_create( + parent_org_name, + node_type, + relation_meta_type + ) + + parent_node = parent_organization.get_node() + graph_node.add_property('name', parent_org_name) + + # n4: add relation between org and parent_org + graph_node.set_parent(parent_organization.pk) - options['organizations'].close() + # 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 - relation_meta_type = NODE_META_TYPE_CHOICES[2][1] # relation - logical_meta_type = NODE_META_TYPE_CHOICES[1][1] # logical node_list = self.read_csv(csv_contacts) for node in node_list: @@ -100,25 +144,12 @@ def handle(self, *args, **options): ) # dj: contact exists?: create or get - new_contact = None - qs = NodeHandle.objects.filter( - node_name = full_name, - node_type = node_type + new_contact = self.get_or_create( + full_name, + node_type, + relation_meta_type ) - if qs: - new_contact = qs.first() - else: - new_contact = NodeHandle( - node_name = full_name, - node_type = node_type, - node_meta_type = relation_meta_type, - creator = user, - modifier = user, - ) - - new_contact.save() - # n4: add attributes graph_node = new_contact.get_node() @@ -132,34 +163,64 @@ def handle(self, *args, **options): if role_name: role_type = NodeType.objects.filter(type=self.new_types[4]).first() # role - new_role = None - qs = NodeHandle.objects.filter( - node_name = node['contact_role'], - node_type = role_type + new_role = self.get_or_create( + role_name, + role_type, + logical_meta_type ) - if qs: - new_role = qs.first() - else: - new_role = NodeHandle( - node_name = node['contact_role'], - node_type = role_type, - node_meta_type = logical_meta_type, - creator = user, - modifier = user, - ) - - new_role.save() - # n4: add relation between role and contact graph_node.add_role(new_role.pk) # dj: organization exist?: create or get - # n4: add relation between role and organization + organization_name = node['account_name'] + + if organization_name: + org_type = NodeType.objects.filter(type=self.new_types[0]).first() # organization + + new_org = self.get_or_create( + organization_name, + org_type, + relation_meta_type + ) + + # n4: add relation between role and organization + graph_node.add_organization(new_org.pk) + # Print iterations progress + if options['verbosity'] > 0: + imported_lines = imported_lines + 1 + self.printProgressBar(imported_lines, total_lines) csv_contacts.close() + # replace all the duplicate code + def get_or_create(self, node_name, node_type, node_meta_type): + new_node = None + + if not node_name: + raise Exception('Empty node_name') + + qs = NodeHandle.objects.filter( + node_name = node_name, + node_type = node_type + ) + + if qs: + new_node = qs.first() + else: + new_node = NodeHandle( + node_name = node_name, + node_type = node_type, + node_meta_type = node_meta_type, + creator = self.user, + modifier = self.user, + ) + + new_node.save() + + return new_node + def count_lines(self, file): num_lines = 0 try: @@ -201,7 +262,7 @@ def read_csv(self, f, delim=';', empty_keys=True): line = self.normalize_whitespace(f.readline()) return node_list - def printProgressBar (self, iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█'): + 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) From 678de3f3b8ca06c54ef3fb4a37055b22139e9f7a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 4 Mar 2019 18:08:02 +0100 Subject: [PATCH 003/520] Small bugfixes --- src/niweb/apps/noclook/management/commands/csvimport.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index e1958d5df..b8f1bb43d 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -93,8 +93,6 @@ def handle(self, *args, **options): account_name = node['account_name'] # dj: organization exist?: create or get (using just the name) - - #def get_or_create(self, node_name, node_type, node_meta_type): new_organization = self.get_or_create( account_name, node_type, @@ -120,7 +118,7 @@ def handle(self, *args, **options): ) parent_node = parent_organization.get_node() - graph_node.add_property('name', parent_org_name) + parent_node.add_property('name', parent_org_name) # n4: add relation between org and parent_org graph_node.set_parent(parent_organization.pk) @@ -155,7 +153,7 @@ def handle(self, *args, **options): graph_node.add_property('name', full_name) for key in node.keys(): - if key not in ['node_type', 'role', 'name', 'account_name'] and node[key]: + if key not in ['node_type', 'contact_role', 'name', 'account_name'] and node[key]: graph_node.add_property(key, node[key]) # dj: role exist?: create or get From bb820a1718ca9ddc46a987ec5e44fed3f8e59867 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 5 Mar 2019 14:25:54 +0100 Subject: [PATCH 004/520] Some small bugfixes and test suite for the custom csvimport command. --- .../noclook/management/commands/csvimport.py | 12 ++- .../apps/noclook/tests/management/__init__.py | 0 .../tests/management/test_csvimport.py | 97 +++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/niweb/apps/noclook/tests/management/__init__.py create mode 100644 src/niweb/apps/noclook/tests/management/test_csvimport.py diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index b8f1bb43d..be3e1c2fd 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -30,6 +30,10 @@ def handle(self, *args, **options): relation_meta_type = NODE_META_TYPE_CHOICES[2][1] # relation logical_meta_type = NODE_META_TYPE_CHOICES[1][1] # 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: @@ -51,7 +55,7 @@ def handle(self, *args, **options): csv_organizations = None csv_contacts = None - self.user = User.objects.filter(username='admin').first() + self.user = User.objects.all().first() # IMPORT ORGANIZATIONS if options['organizations']: @@ -87,7 +91,7 @@ def handle(self, *args, **options): # contact node_type = NodeType.objects.filter(type=self.new_types[0]).first() csv_organizations = options['organizations'] - node_list = self.read_csv(csv_organizations) + node_list = self.read_csv(csv_organizations, delim=self.delimiter) for node in node_list: account_name = node['account_name'] @@ -133,7 +137,7 @@ def handle(self, *args, **options): # process contacts if options['contacts']: node_type = NodeType.objects.filter(type=self.new_types[2]).first() # contact - node_list = self.read_csv(csv_contacts) + node_list = self.read_csv(csv_contacts, delim=self.delimiter) for node in node_list: full_name = '{} {}'.format( @@ -171,7 +175,7 @@ def handle(self, *args, **options): graph_node.add_role(new_role.pk) # dj: organization exist?: create or get - organization_name = node['account_name'] + organization_name = node.get('account_name', None) if organization_name: org_type = NodeType.objects.filter(type=self.new_types[0]).first() # organization 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/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py new file mode 100644 index 000000000..6c8fd1432 --- /dev/null +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -0,0 +1,97 @@ +# -*- 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 + +from ..neo4j_base import NeoTestCase + +import tempfile + +__author__ = 'ffuentes' + +class CsvImportTest(NeoTestCase): + cmd_name = 'csvimport' + + organizations_str = """"account_id","account_name","description","phone","website","customer_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,,,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,Franny,Bainton,,Software Consultant,Person,,,,,China,877-832-9647,138-608-6235,,fbainton3@si.edu,,,Mudo +Rev,Kiri,Janosevic,,Physical Therapy Assistant,Person,,,,,China,568-690-1854,118-569-1303,,kjanosevic4@umich.edu,,,Youspan + """ + + def setUp(self): + super(CsvImportTest, self).setUp() + # write organizations csv file to disk + self.organizations_file = self.write_string_to_disk(self.organizations_str) + + # write contacts csv file to disk + self.contacts_file = self.write_string_to_disk(self.contacts_str) + + def tearDown(self): + super(CsvImportTest, self).tearDown() + # close organizations csv file + self.organizations_file.close() + + # close contacts csv file + self.contacts_file.close() + + def test_organizations_import(self): + # call csvimport command (verbose 0) + call_command( + self.cmd_name, + organizations=self.organizations_file, + verbosity=0, + delimiter=',' + ) + # check one of the organizations is present + qs = NodeHandle.objects.filter(node_name='Browsecat') + self.assertIsNotNone(qs) + organization1 = qs.first() + self.assertIsNotNone(organization1) + + # check if one of them has a parent organization + relations = organization1.get_node().get_relations() + #raise Exception(' '.join(relations.keys())) + 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, + delimiter=',' + ) + # 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) + + # check if it has a role assigned + # check if works for an organization + + def write_string_to_disk(self, string): + # get random file + tf = tempfile.NamedTemporaryFile() + # write text + tf.write(string) + tf.flush() + # return file descriptor + return tf From c09893a106d31f2afa0de2d295841dc9c8e49b14 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 5 Mar 2019 17:42:59 +0100 Subject: [PATCH 005/520] Full test case --- .../tests/management/test_csvimport.py | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index 6c8fd1432..c2d20b7ec 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -17,19 +17,19 @@ class CsvImportTest(NeoTestCase): cmd_name = 'csvimport' - organizations_str = """"account_id","account_name","description","phone","website","customer_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" + organizations_str = """"account_id";"account_name";"description";"phone";"website";"customer_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,,,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,Franny,Bainton,,Software Consultant,Person,,,,,China,877-832-9647,138-608-6235,,fbainton3@si.edu,,,Mudo -Rev,Kiri,Janosevic,,Physical Therapy Assistant,Person,,,,,China,568-690-1854,118-569-1303,,kjanosevic4@umich.edu,,,Youspan + 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";;;"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";"Franny";"Bainton";;"Software Consultant";"Person";;;;;"China";"877-832-9647";"138-608-6235";;"fbainton3@si.edu";;;"Mudo" +"Rev";"Kiri";"Janosevic";;"Physical Therapy Assistant";"Person";;;;;"China";"568-690-1854";"118-569-1303";;"kjanosevic4@umich.edu";;;"Youspan" """ def setUp(self): @@ -54,7 +54,6 @@ def test_organizations_import(self): self.cmd_name, organizations=self.organizations_file, verbosity=0, - delimiter=',' ) # check one of the organizations is present qs = NodeHandle.objects.filter(node_name='Browsecat') @@ -64,10 +63,9 @@ def test_organizations_import(self): # check if one of them has a parent organization relations = organization1.get_node().get_relations() - #raise Exception(' '.join(relations.keys())) parent_relation = relations.get('Parent_of', None) - #self.assertIsNotNone(parent_relation) - #self.assertIsInstance(relations['Parent_of'][0]['node'], ncmodels.RelationModel) + self.assertIsNotNone(parent_relation) + self.assertIsInstance(relations['Parent_of'][0]['node'], ncmodels.RelationModel) def test_contacts_import(self): # call csvimport command (verbose 0) @@ -75,7 +73,6 @@ def test_contacts_import(self): self.cmd_name, contacts=self.contacts_file, verbosity=0, - delimiter=',' ) # check one of the contacts is present full_name = '{} {}'.format('Caesar', 'Newby') @@ -85,7 +82,26 @@ def test_contacts_import(self): self.assertIsNotNone(contact1) # check if it has a role assigned + qs = NodeHandle.objects.filter(node_name='Computer Systems Analyst III') + self.assertIsNotNone(qs) + role1 = qs.first() + self.assertIsNotNone(role1) + + relations = role1.get_node().get_relations() + relation = relations.get('Is', None) + self.assertIsNotNone(relation) + self.assertIsInstance(relations['Is'][0]['node'], ncmodels.RelationModel) + # check if works for an organization + qs = NodeHandle.objects.filter(node_name='Gabtune') + self.assertIsNotNone(qs) + organization1 = qs.first() + self.assertIsNotNone(organization1) + + relations = organization1.get_node().get_relations() + relation = relations.get('Works_for', None) + self.assertIsNotNone(relation) + self.assertIsInstance(relations['Works_for'][0]['node'], ncmodels.RelationModel) def write_string_to_disk(self, string): # get random file From ee24da93d2e46ccc5705ac2487da3ef1e2a0374a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 6 Mar 2019 10:50:51 +0100 Subject: [PATCH 006/520] Default added to status field migration --- .../migrations/0002_auto_20190121_0856.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/niweb/apps/scan/migrations/0002_auto_20190121_0856.py 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), + ), + ] From 683a4373bea77a15a10e1ed83f02a4f9c9ad78b8 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 6 Mar 2019 18:00:27 +0100 Subject: [PATCH 007/520] Defects detected on the --- .../noclook/management/commands/csvimport.py | 88 +++++++------------ 1 file changed, 34 insertions(+), 54 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index be3e1c2fd..00e061750 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -27,8 +27,8 @@ def add_arguments(self, parser): help='Delimiter to use use. Default ";".') def handle(self, *args, **options): - relation_meta_type = NODE_META_TYPE_CHOICES[2][1] # relation - logical_meta_type = NODE_META_TYPE_CHOICES[1][1] # logical + relation_meta_type = 'Relation' + logical_meta_type = 'Logical' self.delimiter = ';' if options['delimiter']: @@ -55,7 +55,7 @@ def handle(self, *args, **options): csv_organizations = None csv_contacts = None - self.user = User.objects.all().first() + self.user = User.objects.filter(username='noclook').first() # IMPORT ORGANIZATIONS if options['organizations']: @@ -97,16 +97,17 @@ def handle(self, *args, **options): account_name = node['account_name'] # dj: organization exist?: create or get (using just the name) - new_organization = self.get_or_create( - account_name, - node_type, - relation_meta_type + new_organization = NodeHandle.get_or_create( + node_name = account_name, + node_type = node_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, ) # n4: add attributes graph_node = new_organization.get_node() - graph_node.add_property('name', account_name) for key in node.keys(): if key not in ['account_name', 'parent_account'] and node[key]: graph_node.add_property(key, node[key]) @@ -115,17 +116,18 @@ def handle(self, *args, **options): if key == 'parent_account' and node['parent_account']: parent_org_name = node['parent_account'] - parent_organization = self.get_or_create( - parent_org_name, - node_type, - relation_meta_type + parent_organization = NodeHandle.get_or_create( + node_name = parent_org_name, + node_type = node_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, ) parent_node = parent_organization.get_node() - parent_node.add_property('name', parent_org_name) # n4: add relation between org and parent_org - graph_node.set_parent(parent_organization.pk) + graph_node.set_parent(parent_organization.handle_id) # Print iterations progress if options['verbosity'] > 0: @@ -146,16 +148,17 @@ def handle(self, *args, **options): ) # dj: contact exists?: create or get - new_contact = self.get_or_create( - full_name, - node_type, - relation_meta_type + new_contact = NodeHandle.get_or_create( + node_name = full_name, + node_type = node_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, ) # n4: add attributes graph_node = new_contact.get_node() - graph_node.add_property('name', full_name) for key in node.keys(): if key not in ['node_type', 'contact_role', 'name', 'account_name'] and node[key]: graph_node.add_property(key, node[key]) @@ -165,10 +168,12 @@ def handle(self, *args, **options): if role_name: role_type = NodeType.objects.filter(type=self.new_types[4]).first() # role - new_role = self.get_or_create( - role_name, - role_type, - logical_meta_type + new_role = NodeHandle.get_or_create( + node_name = role_name, + node_type = role_type, + node_meta_type = logical_meta_type, + creator = self.user, + modifier = self.user, ) # n4: add relation between role and contact @@ -180,10 +185,12 @@ def handle(self, *args, **options): if organization_name: org_type = NodeType.objects.filter(type=self.new_types[0]).first() # organization - new_org = self.get_or_create( - organization_name, - org_type, - relation_meta_type + new_org = NodeHandle.get_or_create( + node_name = organization_name, + node_type = org_type, + node_meta_type = relation_meta_type, + creator = self.user, + modifier = self.user, ) # n4: add relation between role and organization @@ -196,33 +203,6 @@ def handle(self, *args, **options): csv_contacts.close() - # replace all the duplicate code - def get_or_create(self, node_name, node_type, node_meta_type): - new_node = None - - if not node_name: - raise Exception('Empty node_name') - - qs = NodeHandle.objects.filter( - node_name = node_name, - node_type = node_type - ) - - if qs: - new_node = qs.first() - else: - new_node = NodeHandle( - node_name = node_name, - node_type = node_type, - node_meta_type = node_meta_type, - creator = self.user, - modifier = self.user, - ) - - new_node.save() - - return new_node - def count_lines(self, file): num_lines = 0 try: From 03d78109f2a0fd82a20d5a894436615732da41d3 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 8 Mar 2019 09:29:07 +0100 Subject: [PATCH 008/520] Bugfixes on script and tests. --- .../noclook/management/commands/csvimport.py | 22 ++++++++++--------- .../tests/management/test_csvimport.py | 5 ++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 00e061750..4cb317d28 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -57,6 +57,8 @@ def handle(self, *args, **options): csv_contacts = None self.user = User.objects.filter(username='noclook').first() + from pprint import pprint + # IMPORT ORGANIZATIONS if options['organizations']: # py: count lines @@ -97,13 +99,13 @@ def handle(self, *args, **options): account_name = node['account_name'] # dj: organization exist?: create or get (using just the name) - new_organization = NodeHandle.get_or_create( + 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() @@ -116,13 +118,13 @@ def handle(self, *args, **options): if key == 'parent_account' and node['parent_account']: parent_org_name = node['parent_account'] - parent_organization = NodeHandle.get_or_create( + 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() @@ -148,13 +150,13 @@ def handle(self, *args, **options): ) # dj: contact exists?: create or get - new_contact = NodeHandle.get_or_create( + 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() @@ -168,13 +170,13 @@ def handle(self, *args, **options): if role_name: role_type = NodeType.objects.filter(type=self.new_types[4]).first() # role - new_role = NodeHandle.get_or_create( + new_role = NodeHandle.objects.get_or_create( node_name = role_name, node_type = role_type, node_meta_type = logical_meta_type, creator = self.user, modifier = self.user, - ) + )[0] # n4: add relation between role and contact graph_node.add_role(new_role.pk) @@ -185,13 +187,13 @@ def handle(self, *args, **options): if organization_name: org_type = NodeType.objects.filter(type=self.new_types[0]).first() # organization - new_org = NodeHandle.get_or_create( + 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] # n4: add relation between role and organization graph_node.add_organization(new_org.pk) diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index c2d20b7ec..5add84d7a 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -6,7 +6,7 @@ from norduniclient.exceptions import UniqueNodeError, NodeNotFound import norduniclient.models as ncmodels -from apps.noclook.models import NodeHandle +from apps.noclook.models import NodeHandle, User from ..neo4j_base import NeoTestCase @@ -40,6 +40,9 @@ def setUp(self): # write contacts csv file to disk self.contacts_file = self.write_string_to_disk(self.contacts_str) + # create noclook user + User.objects.get_or_create(username="noclook")[0] + def tearDown(self): super(CsvImportTest, self).tearDown() # close organizations csv file From cd20dfa0a43eed5651efbc9075ac5c4d88986dde Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 8 Mar 2019 10:56:00 +0100 Subject: [PATCH 009/520] Use of an utils function to get the right user on the csv import. --- src/niweb/apps/noclook/management/commands/csvimport.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 4cb317d28..ec6872dee 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -2,6 +2,7 @@ __author__ = 'ffuentes' from apps.noclook.models import User, NodeType, NodeHandle, NODE_META_TYPE_CHOICES +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 @@ -55,9 +56,7 @@ def handle(self, *args, **options): csv_organizations = None csv_contacts = None - self.user = User.objects.filter(username='noclook').first() - - from pprint import pprint + self.user = get_user() # IMPORT ORGANIZATIONS if options['organizations']: From f284aa0e0dffcf241c303a71a844e1f6a44c2f66 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 13 Mar 2019 13:03:40 +0100 Subject: [PATCH 010/520] Organization detail/new/edit views --- src/niweb/apps/noclook/forms/common.py | 18 ++++++++- src/niweb/apps/noclook/helpers.py | 15 ++++++++ .../noclook/create/create_organization.html | 37 +++++++++++++++++++ .../templates/noclook/detail/base_detail.html | 1 + .../noclook/detail/organization_detail.html | 8 ++++ .../noclook/edit/edit_organization.html | 34 +++++++++++++++++ .../edit/includes/parent_of_group.html | 27 ++++++++++++++ src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/create.py | 18 +++++++++ src/niweb/apps/noclook/views/detail.py | 10 +++++ src/niweb/apps/noclook/views/edit.py | 25 +++++++++++++ 11 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_organization.html create mode 100644 src/niweb/apps/noclook/templates/noclook/detail/organization_detail.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/parent_of_group.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 764609935..8d9315512 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -1,7 +1,7 @@ from datetime import datetime from django import forms 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 @@ -782,3 +782,19 @@ def form_to_csv(form, headers): cleaned = form.cleaned_data raw = form.data return u",".join([cleaned.get(h) or raw.get(h, '') for h in headers]) + +class NewOrganizationForm(forms.Form): + account_id = forms.CharField(required=False) + name = forms.CharField() + description = description_field('organization') + phone = forms.CharField(required=False) + website = forms.CharField(required=False) + customer_id = forms.CharField(required=False) + type = forms.CharField(required=False) + +class EditOrganizationForm(NewOrganizationForm): + def __init__(self, *args, **kwargs): + super(EditOrganizationForm, self).__init__(*args, **kwargs) + self.fields['relationship_parent_of'].choices = get_node_type_tuples('Organization') + + relationship_parent_of = relationship_field('children', True) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 3ed75e50b..70dbcfa32 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -880,3 +880,18 @@ def attachment_content(attachment): with open(file_name, 'r') as f: content = f.read() return content + +def set_parent_of(user, node, child_org_id): + """ + :param user: Django user + :param node: norduniclient model + :param responsible_for_id: unique id + :return: norduniclient model, boolean + """ + result = node.set_child(child_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 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..ebc4ea40f --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_organization.html @@ -0,0 +1,37 @@ +{% 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.phone }} +
+ + {{ form.website }} +
+ + {{ form.customer_id }} +
+ + {{ form.type }} + + Cancel +
+
+{% endblock %} 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 d0f334575..c1d5ddf03 100644 --- a/src/niweb/apps/noclook/templates/noclook/detail/base_detail.html +++ b/src/niweb/apps/noclook/templates/noclook/detail/base_detail.html @@ -64,6 +64,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/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/edit/edit_organization.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html new file mode 100644 index 000000000..3f13d7806 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html @@ -0,0 +1,34 @@ +{% 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.phone | as_crispy_field}} + {{ form.website | as_crispy_field}} + {{ form.customer_id | as_crispy_field}} + {{ form.type | as_crispy_field}} + {% endaccordion %} + {% include "noclook/edit/includes/parent_of_group.html" %} +{% endblock %} 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..1251e5bdf --- /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 responsible_title %} + {{ node_handle.node_type }} Child Organization (optional) +{% endblockvar %} +{% accordion responsible_title 'responsible-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 child organization

+
+ {{ form.relationship_parent_of | as_crispy_field }} +
+{% endaccordion %} diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index 2a9106185..0d98f0a47 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -115,6 +115,7 @@ 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'^customer/(?P\d+)/$', detail.customer_detail), url(r'^provider/(?P\d+)/$', detail.provider_detail), diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 3107620c1..e22136ecc 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -37,6 +37,7 @@ ("rack", "Rack"), ("site", "Site"), ("site-owner", "Site Owner"), + ("organization", "Organization"), ] if helpers.app_enabled("apps.scan"): TYPES.append(("/scan/queue", "Host scan")) @@ -557,6 +558,22 @@ def reserve_id_sequence(request, slug=None): return render(request, 'noclook/edit/reserve_id.html', {'form': form, 'slug': slug}) +@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}) + helpers.form_update_node(request.user, nh.handle_id, form) + return redirect(nh.get_absolute_url()) + else: + form = forms.NewOrganizationForm() + return render(request, 'noclook/create/create_organization.html', {'form': form}) + NEW_FUNC = { 'cable': new_cable, 'cable_csv': new_cable_csv, @@ -577,4 +594,5 @@ def reserve_id_sequence(request, slug=None): 'site': new_site, 'site-owner': new_site_owner, '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 c776dcf6d..98e8d9798 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -617,3 +617,13 @@ 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}) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 269e4c4af..d1df9d916 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -929,6 +929,30 @@ 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() + if request.POST: + form = forms.EditOrganizationForm(request.POST) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, organization.handle_id, form) + # Set site owner + if form.cleaned_data['relationship_parent_of']: + responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) + helpers.set_parent_of(request.user, organization, responsible_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, 'node': organization}) + + EDIT_FUNC = { 'cable': edit_cable, @@ -953,4 +977,5 @@ def disable_noclook_auto_manage(request, slug, handle_id): 'site': edit_site, 'site-owner': edit_site_owner, 'switch': edit_switch, + 'organization': edit_organization, } From 7677e5ac0e059bcf1d0ea11e4f99ec87963bae46 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 14 Mar 2019 11:48:54 +0100 Subject: [PATCH 011/520] Added new/edit/detail for contact --- src/niweb/apps/noclook/forms/common.py | 45 ++++++++++++++++++- src/niweb/apps/noclook/helpers.py | 45 +++++++++++++++++++ .../noclook/create/create_contact.html | 44 ++++++++++++++++++ .../noclook/detail/contact_detail.html | 8 ++++ .../templates/noclook/edit/edit_contact.html | 44 ++++++++++++++++++ .../noclook/edit/edit_organization.html | 2 +- .../noclook/edit/includes/is_group.html | 27 +++++++++++ .../edit/includes/member_of_group.html | 27 +++++++++++ .../edit/includes/parent_of_group.html | 4 +- .../edit/includes/works_for_group.html | 27 +++++++++++ src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/create.py | 19 ++++++++ src/niweb/apps/noclook/views/detail.py | 12 +++++ src/niweb/apps/noclook/views/edit.py | 33 +++++++++++++- 14 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_contact.html create mode 100644 src/niweb/apps/noclook/templates/noclook/detail/contact_detail.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/member_of_group.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/works_for_group.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 8d9315512..99ce22e75 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -783,6 +783,7 @@ def form_to_csv(form, headers): raw = form.data return u",".join([cleaned.get(h) or raw.get(h, '') for h in headers]) + class NewOrganizationForm(forms.Form): account_id = forms.CharField(required=False) name = forms.CharField() @@ -790,11 +791,51 @@ class NewOrganizationForm(forms.Form): phone = forms.CharField(required=False) website = forms.CharField(required=False) customer_id = forms.CharField(required=False) - type = forms.CharField(required=False) + type = forms.CharField(required=False) # possible ChoiceField + class EditOrganizationForm(NewOrganizationForm): def __init__(self, *args, **kwargs): super(EditOrganizationForm, self).__init__(*args, **kwargs) self.fields['relationship_parent_of'].choices = get_node_type_tuples('Organization') - relationship_parent_of = relationship_field('children', True) + relationship_parent_of = relationship_field('organization', True) + + +class NewContactForm(forms.Form): + def __init__(self, *args, **kwargs): + super(NewContactForm, self).__init__(*args, **kwargs) + self.fields['mailing_country'].choices = country_codes() + + first_name = forms.CharField() + last_name = forms.CharField() + contact_type = forms.CharField(required=False) # possible ChoiceField + mobile = forms.CharField(required=False) + mailing_country = forms.ChoiceField(widget=forms.widgets.Select) + phone = forms.CharField(required=False) + salutation = forms.CharField(required=False) + email = forms.CharField(required=False) + + 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") + cleaned_data['name'] = '{} {}'.format(first_name, last_name) + + return cleaned_data + + +class EditContactForm(NewContactForm): + def __init__(self, *args, **kwargs): + super(EditContactForm, self).__init__(*args, **kwargs) + self.fields['relationship_works_for'].choices = get_node_type_tuples('Organization') + self.fields['relationship_member_of'].choices = get_node_type_tuples('Group') + self.fields['relationship_is'].choices = get_node_type_tuples('Role') + + relationship_works_for = relationship_field('organization', True) + relationship_member_of = relationship_field('group', True) + relationship_is = relationship_field('role', True) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 70dbcfa32..d5bb7558e 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -895,3 +895,48 @@ def set_parent_of(user, node, child_org_id): if created: activitylog.create_relationship(user, relationship) return relationship, created + +def set_works_for(user, node, child_org_id): + """ + :param user: Django user + :param node: norduniclient model + :param child_org_id: unique id + :return: norduniclient model, boolean + """ + result = node.add_organization(child_org_id) + relationship_id = result.get('Works_for')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Works_for')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) + return relationship, created + +def set_member_of(user, node, child_org_id): + """ + :param user: Django user + :param node: norduniclient model + :param child_org_id: unique id + :return: norduniclient model, boolean + """ + result = node.add_group(child_org_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_is(user, node, child_org_id): + """ + :param user: Django user + :param node: norduniclient model + :param child_org_id: unique id + :return: norduniclient model, boolean + """ + result = node.add_role(child_org_id) + relationship_id = result.get('Is')[0].get('relationship_id') + relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) + created = result.get('Is')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) + return relationship, created 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..5fe7b5951 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html @@ -0,0 +1,44 @@ +{% 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.first_name }} +
+ + {{ form.last_name }} +
+ + {{ form.contact_type }} +
+ + {{ form.mailing_country }} +
+

Additional info (optional)

+ + {{ form.salutation }} +
+ + {{ form.phone }} +
+ + {{ form.mobile }} +
+ + {{ form.email }} +
+ + Cancel +
+
+{% endblock %} 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/edit/edit_contact.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html new file mode 100644 index 000000000..486e2ee77 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -0,0 +1,44 @@ +{% extends "noclook/edit/base_edit.html" %} +{% load crispy_forms_tags %} +{% load noclook_tags %} + +{% 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.contact_type | as_crispy_field}} + {{ form.mobile | as_crispy_field}} + {{ form.mailing_country | as_crispy_field}} + {{ form.phone | as_crispy_field}} + {{ form.salutation | as_crispy_field}} + {{ form.email | as_crispy_field}} + {% endaccordion %} + {% include "noclook/edit/includes/works_for_group.html" %} + {% include "noclook/edit/includes/member_of_group.html" %} + {% include "noclook/edit/includes/is_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 index 3f13d7806..eab1df6ee 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html @@ -9,7 +9,7 @@ function(){ // Responsible for // Populate first level combo box - responsibleCategories = [ + organizationCategories = [ ['organization', 'Organizations'] ]; } diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html new file mode 100644 index 000000000..b4d430c95 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html @@ -0,0 +1,27 @@ +{% load noclook_tags %} +{% load crispy_forms_tags %} +{% blockvar is_title %} + {{ node_handle.node_type }} role (optional) +{% endblockvar %} +{% accordion is_title 'is-edit' '#edit-accordion' %} + {% if relations.Is %} + {% load noclook_tags %} +

Remove role

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

Add role

+
+ {{ form.relationship_is | as_crispy_field }} +
+{% endaccordion %} 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..c4174ea16 --- /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.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 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 index 1251e5bdf..c1e1c80f2 100644 --- 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 @@ -1,7 +1,7 @@ {% load noclook_tags %} {% load crispy_forms_tags %} {% blockvar responsible_title %} - {{ node_handle.node_type }} Child Organization (optional) + {{ node_handle.node_type }} Parent Organization (optional) {% endblockvar %} {% accordion responsible_title 'responsible-edit' '#edit-accordion' %} {% if relations.Parent_of %} @@ -20,7 +20,7 @@

Remove organization

{% endfor %}
{% endif %} -

Add child organization

+

Add parent organization

{{ form.relationship_parent_of | as_crispy_field }}
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..8f1fb971e --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/works_for_group.html @@ -0,0 +1,27 @@ +{% 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 }} +
+
+ Delete +
+
+ {% endfor %} +
+ {% endif %} +

Link contact to organization

+
+ {{ form.relationship_works_for | as_crispy_field }} +
+{% endaccordion %} diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index 0d98f0a47..225e68b36 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -117,6 +117,7 @@ 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'^unit/(?P\d+)/$', detail.unit_detail), diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index e22136ecc..3ce2a9f29 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -21,6 +21,7 @@ TYPES = [ ("customer", "Customer"), ("cable", "Cable"), + ("contact", "Contact"), ("end-user", "End User"), ("external-cable", "External Cable"), ("external-equipment", "External Equipment"), @@ -574,9 +575,27 @@ def new_organization(request, **kwargs): 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(): + try: + nh = helpers.form_to_unique_node_handle(request, form, 'contact', 'Relation') + except UniqueNodeError: + form.add_error('name', 'A Contact with that name already exists.') + return render(request, 'noclook/create/create_contact.html', {'form': form}) + 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}) + NEW_FUNC = { 'cable': new_cable, 'cable_csv': new_cable_csv, + 'contact': new_contact, 'customer': new_customer, 'end-user': new_end_user, 'external-equipment': new_external_equipment, diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 98e8d9798..0a92d91fc 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -618,6 +618,7 @@ def switch_detail(request, handle_id): '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) @@ -627,3 +628,14 @@ def organization_detail(request, handle_id): 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}) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index d1df9d916..daaa39a43 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -939,7 +939,7 @@ def edit_organization(request, handle_id): if form.is_valid(): # Generic node update helpers.form_update_node(request.user, organization.handle_id, form) - # Set site owner + # Set child organizations if form.cleaned_data['relationship_parent_of']: responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) helpers.set_parent_of(request.user, organization, responsible_nh.handle_id) @@ -952,11 +952,42 @@ def edit_organization(request, handle_id): return render(request, 'noclook/edit/edit_organization.html', {'node_handle': nh, 'form': form, 'relations': relations, 'node': organization}) +@staff_member_required +def edit_contact(request, handle_id): + from pprint import pprint + # Get needed data from node + nh, contact = helpers.get_nh_node(handle_id) + relations = contact.get_outgoing_relations() + pprint(vars(contact)) + 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 works for organization + if form.cleaned_data['relationship_works_for']: + responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_works_for']) + helpers.set_works_for(request.user, contact, responsible_nh.handle_id) + if form.cleaned_data['relationship_member_of']: + responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_member_of']) + helpers.set_member_of(request.user, contact, responsible_nh.handle_id) + if form.cleaned_data['relationship_is']: + responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_is']) + helpers.set_is(request.user, contact, responsible_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}) EDIT_FUNC = { 'cable': edit_cable, 'customer': edit_customer, + 'contact': edit_contact, 'end-user': edit_end_user, 'external-equipment': edit_external_equipment, 'firewall': edit_firewall, From 7aa45a609b618ee0480db789d8ecfa3707306c2a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 14 Mar 2019 13:05:57 +0100 Subject: [PATCH 012/520] Added new/edit/detail for role --- src/niweb/apps/noclook/forms/common.py | 8 +++++++ .../templates/noclook/create/create_role.html | 22 +++++++++++++++++++ .../templates/noclook/detail/role_detail.html | 8 +++++++ .../templates/noclook/edit/edit_role.html | 13 +++++++++++ src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/create.py | 18 +++++++++++++++ src/niweb/apps/noclook/views/detail.py | 11 ++++++++++ src/niweb/apps/noclook/views/edit.py | 21 ++++++++++++++++-- 8 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_role.html create mode 100644 src/niweb/apps/noclook/templates/noclook/detail/role_detail.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_role.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 99ce22e75..a4b4cc55d 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -839,3 +839,11 @@ def __init__(self, *args, **kwargs): relationship_works_for = relationship_field('organization', True) relationship_member_of = relationship_field('group', True) relationship_is = relationship_field('role', True) + + +class NewRoleForm(forms.Form): + name = forms.CharField() + + +class EditRoleForm(NewRoleForm): + pass 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..4a5c38512 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_role.html @@ -0,0 +1,22 @@ +{% 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 }} +
+ + Cancel +
+
+{% 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..9edb4ff12 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/detail/role_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/edit/edit_role.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_role.html new file mode 100644 index 000000000..a1097b335 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_role.html @@ -0,0 +1,13 @@ +{% 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}} +
+{% endblock %} diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index 225e68b36..b7302f12d 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -111,6 +111,7 @@ url(r'^port/(?P\d+)/$', detail.port_detail), url(r'^site/(?P\d+)/$', detail.site_detail), url(r'^rack/(?P\d+)/$', detail.rack_detail), + url(r'^role/(?P\d+)/$', detail.role_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), diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 3ce2a9f29..e5bf353a8 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -36,6 +36,7 @@ ("port", "Port"), ("provider", "Provider"), ("rack", "Rack"), + ("role", "Role"), ("site", "Site"), ("site-owner", "Site Owner"), ("organization", "Organization"), @@ -592,6 +593,22 @@ def new_contact(request, **kwargs): form = forms.NewContactForm() return render(request, 'noclook/create/create_contact.html', {'form': form}) +@staff_member_required +def new_role(request, **kwargs): + if request.POST: + form = forms.NewRoleForm(request.POST) + if form.is_valid(): + try: + nh = helpers.form_to_unique_node_handle(request, form, 'role', 'Logical') + except UniqueNodeError: + form.add_error('name', 'A Role with that name already exists.') + return render(request, 'noclook/create/create_role.html', {'form': form}) + helpers.form_update_node(request.user, nh.handle_id, form) + return redirect(nh.get_absolute_url()) + else: + form = forms.NewRoleForm() + return render(request, 'noclook/create/create_role.html', {'form': form}) + NEW_FUNC = { 'cable': new_cable, 'cable_csv': new_cable_csv, @@ -609,6 +626,7 @@ def new_contact(request, **kwargs): 'port': new_port, 'provider': new_provider, 'rack': new_rack, + 'role': new_role, 'service': new_service, 'site': new_site, 'site-owner': new_site_owner, diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 0a92d91fc..8f236c9e1 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -639,3 +639,14 @@ def contact_detail(request, handle_id): 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 role_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/role_detail.html', + {'node_handle': nh, 'node': node, 'location_path': location_path}) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index daaa39a43..95852d91c 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -954,11 +954,9 @@ def edit_organization(request, handle_id): @staff_member_required def edit_contact(request, handle_id): - from pprint import pprint # Get needed data from node nh, contact = helpers.get_nh_node(handle_id) relations = contact.get_outgoing_relations() - pprint(vars(contact)) if request.POST: form = forms.EditContactForm(request.POST) if form.is_valid(): @@ -984,6 +982,24 @@ def edit_contact(request, handle_id): {'node_handle': nh, 'form': form, 'relations': relations, 'node': contact}) +@staff_member_required +def edit_role(request, handle_id): + # Get needed data from node + nh, role = helpers.get_nh_node(handle_id) + if request.POST: + form = forms.EditRoleForm(request.POST) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, role.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.EditRoleForm(role.data) + return render(request, 'noclook/edit/edit_role.html', + {'node_handle': nh, 'form': form, 'node': role}) + EDIT_FUNC = { 'cable': edit_cable, 'customer': edit_customer, @@ -1005,6 +1021,7 @@ def edit_contact(request, handle_id): 'provider': edit_provider, 'rack': edit_rack, 'router': edit_router, + 'role': edit_role, 'site': edit_site, 'site-owner': edit_site_owner, 'switch': edit_switch, From bfe8315b0c93391238b0cce9c27093d88201abc3 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 15 Mar 2019 10:51:57 +0100 Subject: [PATCH 013/520] Procedure added to noclook --- src/niweb/apps/noclook/forms/common.py | 11 ++++++ src/niweb/apps/noclook/helpers.py | 35 +++++++++++++------ .../noclook/create/create_contact.html | 2 +- .../noclook/create/create_procedure.html | 25 +++++++++++++ .../templates/noclook/create/create_role.html | 2 +- .../noclook/detail/procedure_detail.html | 8 +++++ .../noclook/edit/edit_organization.html | 4 +++ .../noclook/edit/edit_procedure.html | 14 ++++++++ .../edit/includes/parent_of_group.html | 4 +-- .../noclook/edit/includes/uses_a_group.html | 27 ++++++++++++++ src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/create.py | 20 +++++++++++ src/niweb/apps/noclook/views/detail.py | 11 ++++++ src/niweb/apps/noclook/views/edit.py | 23 ++++++++++++ 14 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_procedure.html create mode 100644 src/niweb/apps/noclook/templates/noclook/detail/procedure_detail.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_procedure.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/uses_a_group.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index a4b4cc55d..e65e8b9fe 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -798,8 +798,10 @@ class EditOrganizationForm(NewOrganizationForm): def __init__(self, *args, **kwargs): 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') relationship_parent_of = relationship_field('organization', True) + relationship_uses_a = relationship_field('procedure', True) class NewContactForm(forms.Form): @@ -847,3 +849,12 @@ class NewRoleForm(forms.Form): class EditRoleForm(NewRoleForm): pass + + +class NewProcedureForm(forms.Form): + name = forms.CharField() + description = description_field('procedure') + + +class EditProcedureForm(NewProcedureForm): + pass diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index d5bb7558e..4d4260cb8 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -885,7 +885,7 @@ def set_parent_of(user, node, child_org_id): """ :param user: Django user :param node: norduniclient model - :param responsible_for_id: unique id + :param child_org_id: unique id :return: norduniclient model, boolean """ result = node.set_child(child_org_id) @@ -896,14 +896,29 @@ def set_parent_of(user, node, child_org_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_works_for(user, node, child_org_id): +def set_uses_a(user, node, procedure_id): """ :param user: Django user :param node: norduniclient model - :param child_org_id: unique id + :param procedure_id: unique id :return: norduniclient model, boolean """ - result = node.add_organization(child_org_id) + 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): + """ + :param user: Django user + :param node: norduniclient model + :param organization_id: unique id + :return: norduniclient model, boolean + """ + result = node.add_organization(organization_id) relationship_id = result.get('Works_for')[0].get('relationship_id') relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) created = result.get('Works_for')[0].get('created') @@ -911,14 +926,14 @@ def set_works_for(user, node, child_org_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_member_of(user, node, child_org_id): +def set_member_of(user, node, group_id): """ :param user: Django user :param node: norduniclient model - :param child_org_id: unique id + :param group_id: unique id :return: norduniclient model, boolean """ - result = node.add_group(child_org_id) + 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') @@ -926,14 +941,14 @@ def set_member_of(user, node, child_org_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_is(user, node, child_org_id): +def set_is(user, node, role_id): """ :param user: Django user :param node: norduniclient model - :param child_org_id: unique id + :param role_id: unique id :return: norduniclient model, boolean """ - result = node.add_role(child_org_id) + result = node.add_role(role_id) relationship_id = result.get('Is')[0].get('relationship_id') relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) created = result.get('Is')[0].get('created') diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_contact.html b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html index 5fe7b5951..c08f8bfbb 100644 --- a/src/niweb/apps/noclook/templates/noclook/create/create_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block content %} -

Create new organization

+

Create new contact

{% if form.errors %}

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

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 index 4a5c38512..618aa1652 100644 --- a/src/niweb/apps/noclook/templates/noclook/create/create_role.html +++ b/src/niweb/apps/noclook/templates/noclook/create/create_role.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block content %} -

Create new organization

+

Create new role

{% if form.errors %}

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

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/edit/edit_organization.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html index eab1df6ee..69701fa19 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html @@ -12,6 +12,9 @@ organizationCategories = [ ['organization', 'Organizations'] ]; + procedureCategories = [ + ['procedure', 'Procedures'] + ]; } ); @@ -31,4 +34,5 @@ {{ form.type | 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/includes/parent_of_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/parent_of_group.html index c1e1c80f2..be9dde55b 100644 --- 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 @@ -1,9 +1,9 @@ {% load noclook_tags %} {% load crispy_forms_tags %} -{% blockvar responsible_title %} +{% blockvar organization_title %} {{ node_handle.node_type }} Parent Organization (optional) {% endblockvar %} -{% accordion responsible_title 'responsible-edit' '#edit-accordion' %} +{% accordion organization_title 'organization-edit' '#edit-accordion' %} {% if relations.Parent_of %} {% load noclook_tags %}

Remove organization

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..efc0ba719 --- /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 relations.Uses_a %} + {% load noclook_tags %} +

Remove procedure

+ {% for item in 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/urls.py b/src/niweb/apps/noclook/urls.py index b7302f12d..43626c5a7 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -121,6 +121,7 @@ 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/views/create.py b/src/niweb/apps/noclook/views/create.py index e5bf353a8..50401933c 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -35,6 +35,7 @@ ("optical-node", "Optical Node"), ("port", "Port"), ("provider", "Provider"), + ("procedure", "Procedure"), ("rack", "Rack"), ("role", "Role"), ("site", "Site"), @@ -593,6 +594,7 @@ def new_contact(request, **kwargs): form = forms.NewContactForm() return render(request, 'noclook/create/create_contact.html', {'form': form}) + @staff_member_required def new_role(request, **kwargs): if request.POST: @@ -609,6 +611,23 @@ def new_role(request, **kwargs): form = forms.NewRoleForm() return render(request, 'noclook/create/create_role.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}) + NEW_FUNC = { 'cable': new_cable, 'cable_csv': new_cable_csv, @@ -625,6 +644,7 @@ def new_role(request, **kwargs): 'optical-path': new_optical_path, 'port': new_port, 'provider': new_provider, + 'procedure': new_procedure, 'rack': new_rack, 'role': new_role, 'service': new_service, diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 8f236c9e1..98110c864 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -650,3 +650,14 @@ def role_detail(request, handle_id): location_path = node.get_location_path() return render(request, 'noclook/detail/role_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}) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 95852d91c..3c094314f 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -943,6 +943,9 @@ def edit_organization(request, handle_id): if form.cleaned_data['relationship_parent_of']: responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) helpers.set_parent_of(request.user, organization, responsible_nh.handle_id) + if form.cleaned_data['relationship_uses_a']: + responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_uses_a']) + helpers.set_uses_a(request.user, organization, responsible_nh.handle_id) if 'saveanddone' in request.POST: return redirect(nh.get_absolute_url()) else: @@ -1000,6 +1003,25 @@ def edit_role(request, handle_id): return render(request, 'noclook/edit/edit_role.html', {'node_handle': nh, 'form': form, 'node': role}) + +@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}) + EDIT_FUNC = { 'cable': edit_cable, 'customer': edit_customer, @@ -1019,6 +1041,7 @@ def edit_role(request, handle_id): 'peering-partner': edit_peering_partner, 'port': edit_port, 'provider': edit_provider, + 'procedure': edit_procedure, 'rack': edit_rack, 'router': edit_router, 'role': edit_role, From c86f9806d26f8af54df4285a273ad8f5fa3a286b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 18 Mar 2019 18:24:59 +0100 Subject: [PATCH 014/520] Test added for new forms --- src/niweb/apps/noclook/forms/common.py | 2 +- .../apps/noclook/tests/test_createforms.py | 63 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index e65e8b9fe..1f44abfaf 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -813,7 +813,7 @@ def __init__(self, *args, **kwargs): last_name = forms.CharField() contact_type = forms.CharField(required=False) # possible ChoiceField mobile = forms.CharField(required=False) - mailing_country = forms.ChoiceField(widget=forms.widgets.Select) + mailing_country = forms.ChoiceField(widget=forms.widgets.Select, required=False) phone = forms.CharField(required=False) salutation = forms.CharField(required=False) email = forms.CharField(required=False) diff --git a/src/niweb/apps/noclook/tests/test_createforms.py b/src/niweb/apps/noclook/tests/test_createforms.py index 0200070cc..088c8beb7 100644 --- a/src/niweb/apps/noclook/tests/test_createforms.py +++ b/src/niweb/apps/noclook/tests/test_createforms.py @@ -257,6 +257,68 @@ def test_NewOpticalMultiplexSectionForm_full(self): self.assertDictContainsSubset(data, nh.get_node().data) self.assertEqual(len(nh.get_node().relationships), 1) + def test_NewOrganizationForm_full(self): + node_type = 'Organization' + data = { + 'name': 'test organization', + 'description': 'SE', + 'phone': '08-49 400 000', + 'website': 'www.stdh.se', + 'customer_id': 'STDH', + 'type': 'University, College', + } + 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='test organization') + data['website'] = 'www.stdh.se' + self.assertDictContainsSubset(data, nh.get_node().data) + + def test_NewContactForm_full(self): + node_type = 'Contact' + country_code = forms.country_codes()[0] + data = { + 'first_name': 'Stefan', + 'last_name': 'Listrom', + 'contact_type': 'Person', + 'mobile': '+46733023915', + 'phone': '+46733023915', + 'salutation': 'Mr', + 'email': 'steli@sunet.se', + } + 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='Stefan Listrom') + data['phone'] = '+46733023915' + self.assertDictContainsSubset(data, nh.get_node().data) + + def test_NewRoleForm_full(self): + node_type = 'Role' + data = { + 'name': 'IT Manager', + } + 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='IT Manager') + data['name'] = 'IT Manager' + 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) + class NordunetNewForms(FormTestCase): @@ -398,4 +460,3 @@ def test_NewOpticalLinkForm_full(self): self.assertDictContainsSubset(data, nh.get_node().data) self.assertEqual(len(nh.get_node().relationships), 1) self.assertEqual(nh.get_node().data['name'], 'SERVICE-000001') - From abbded7669a85686918fdbacce644c093cffb98a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 20 Mar 2019 09:32:29 +0100 Subject: [PATCH 015/520] Some bugfixes and new forms for contact Group --- src/niweb/apps/noclook/forms/common.py | 12 +++++ src/niweb/apps/noclook/helpers.py | 15 ++++++ .../noclook/create/create_group.html | 22 +++++++++ .../noclook/detail/group_detail.html | 8 +++ .../templates/noclook/edit/edit_group.html | 27 ++++++++++ .../edit/includes/member_of_group.html | 6 +-- .../edit/includes/of_member_group.html | 27 ++++++++++ .../noclook/edit/includes/uses_a_group.html | 4 +- src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/create.py | 18 +++++++ src/niweb/apps/noclook/views/detail.py | 11 +++++ src/niweb/apps/noclook/views/edit.py | 49 ++++++++++++++----- 12 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_group.html create mode 100644 src/niweb/apps/noclook/templates/noclook/detail/group_detail.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_group.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/of_member_group.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 1f44abfaf..c4c2b9332 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -858,3 +858,15 @@ class NewProcedureForm(forms.Form): class EditProcedureForm(NewProcedureForm): pass + + +class NewGroupForm(forms.Form): + name = forms.CharField() + + +class EditGroupForm(NewProcedureForm): + 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) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 4d4260cb8..0dd8fbf57 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -955,3 +955,18 @@ def set_is(user, node, role_id): 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 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..d05ae62a0 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/create/create_group.html @@ -0,0 +1,22 @@ +{% 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 }} +
+ + Cancel +
+
+{% 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/edit/edit_group.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html new file mode 100644 index 000000000..2ff3a5d9a --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html @@ -0,0 +1,27 @@ +{% 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}} +
+ +
+ {% include "noclook/edit/includes/of_member_group.html" %} +{% 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 index c4174ea16..db2044353 100644 --- 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 @@ -4,10 +4,10 @@ {{ node_handle.node_type }} group (optional) {% endblockvar %} {% accordion memberof_title 'memberof-edit' '#edit-accordion' %} - {% if relations.Parent_of %} + {% if relations.Member_of %} {% load noclook_tags %} -

Remove organization

- {% for item in relations.Parent_of %} +

Remove group

+ {% for item in relations.Member_of %}
{% noclook_get_type item.node.handle_id as node_type %} 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..a764bc9f6 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/includes/of_member_group.html @@ -0,0 +1,27 @@ +{% 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

+ {% 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/uses_a_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/uses_a_group.html index efc0ba719..06f5fd26c 100644 --- 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 @@ -4,10 +4,10 @@ {{ node_handle.node_type }} Linked Procedure (optional) {% endblockvar %} {% accordion procedure_title 'procedure-edit' '#edit-accordion' %} - {% if relations.Uses_a %} + {% if out_relations.Uses_a %} {% load noclook_tags %}

Remove procedure

- {% for item in relations.Uses_a %} + {% for item in out_relations.Uses_a %}
{% noclook_get_type item.node.handle_id as node_type %} diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index 43626c5a7..e8fc0f960 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -102,6 +102,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), diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 50401933c..60d403d88 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -628,11 +628,29 @@ def new_procedure(request, **kwargs): 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}) + 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, diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 98110c864..7281d1d8f 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -661,3 +661,14 @@ def procedure_detail(request, handle_id): 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}) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 3c094314f..1d441980f 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -934,6 +934,7 @@ 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(): @@ -941,11 +942,11 @@ def edit_organization(request, handle_id): helpers.form_update_node(request.user, organization.handle_id, form) # Set child organizations if form.cleaned_data['relationship_parent_of']: - responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) - helpers.set_parent_of(request.user, organization, responsible_nh.handle_id) + 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']: - responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_uses_a']) - helpers.set_uses_a(request.user, organization, responsible_nh.handle_id) + 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: @@ -953,7 +954,7 @@ def edit_organization(request, handle_id): else: form = forms.EditOrganizationForm(organization.data) return render(request, 'noclook/edit/edit_organization.html', - {'node_handle': nh, 'form': form, 'relations': relations, 'node': organization}) + {'node_handle': nh, 'form': form, 'relations': relations, 'out_relations': out_relations, 'node': organization}) @staff_member_required def edit_contact(request, handle_id): @@ -965,16 +966,16 @@ def edit_contact(request, handle_id): if form.is_valid(): # Generic node update helpers.form_update_node(request.user, contact.handle_id, form) - # Set works for organization + # Set relationships if form.cleaned_data['relationship_works_for']: - responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_works_for']) - helpers.set_works_for(request.user, contact, responsible_nh.handle_id) + organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_works_for']) + helpers.set_works_for(request.user, contact, organization_nh.handle_id) if form.cleaned_data['relationship_member_of']: - responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_member_of']) - helpers.set_member_of(request.user, contact, responsible_nh.handle_id) + group_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_member_of']) + helpers.set_member_of(request.user, contact, group_nh.handle_id) if form.cleaned_data['relationship_is']: - responsible_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_is']) - helpers.set_is(request.user, contact, responsible_nh.handle_id) + role_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_is']) + helpers.set_is(request.user, contact, role_nh.handle_id) if 'saveanddone' in request.POST: return redirect(nh.get_absolute_url()) else: @@ -1022,6 +1023,29 @@ def edit_procedure(request, handle_id): 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) + return render(request, 'noclook/edit/edit_group.html', + {'node_handle': nh, 'form': form, 'node': group, 'relations': relations}) + EDIT_FUNC = { 'cable': edit_cable, 'customer': edit_customer, @@ -1030,6 +1054,7 @@ def edit_procedure(request, handle_id): '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, From 09359e1e3cfdc77a95da9d9ed909212679467213 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 20 Mar 2019 12:25:11 +0100 Subject: [PATCH 016/520] Added filter function for contacts in group --- .../edit/includes/of_member_group.html | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) 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 index a764bc9f6..4db7afb17 100644 --- 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 @@ -7,8 +7,46 @@ {% if relations.Member_of %} {% load noclook_tags %}

Remove contact

+
+
+ + +
+
{% for item in relations.Member_of %} -
+ +
{% noclook_get_type item.node.handle_id as node_type %} {{ node_type }} {{ item.node.data.name }} From e11593d7d1f42b5d3db5167b95b90a13fefc67e9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 20 Mar 2019 15:39:43 +0100 Subject: [PATCH 017/520] New test for group form --- src/niweb/apps/noclook/tests/test_createforms.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/tests/test_createforms.py b/src/niweb/apps/noclook/tests/test_createforms.py index 088c8beb7..9df849183 100644 --- a/src/niweb/apps/noclook/tests/test_createforms.py +++ b/src/niweb/apps/noclook/tests/test_createforms.py @@ -305,7 +305,6 @@ def test_NewRoleForm_full(self): data['name'] = 'IT Manager' self.assertDictContainsSubset(data, nh.get_node().data) - def test_NewProcedureForm_full(self): node_type = 'Procedure' data = { @@ -319,6 +318,19 @@ def test_NewProcedureForm_full(self): 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): From cd98f89bd7a5063724458170036255991c287e52 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 25 Mar 2019 13:23:09 +0100 Subject: [PATCH 018/520] Elements hidden in the menu and changes on Organization list --- src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/list.py | 27 +++++++++++++++++++++++++++ src/niweb/niweb/templates/base.html | 9 ++------- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index e8fc0f960..a72a34d53 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -85,6 +85,7 @@ 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'^router/$', _list.list_routers), url(r'^rack/$', _list.list_racks), url(r'^odf/$', _list.list_odfs), diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index 0a8d66752..5b6acf5bf 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -558,3 +558,30 @@ def list_sites(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Sites', 'urls': urls}) + +def _organization_table(org): + organization_link = { + 'url': u'/organization/{}/'.format(org.get('handle_id')), + 'name': u'{}'.format(org.get('name', '')) + } + name = org.get('customer_id') + row = TableRow(organization_link, name) + return row + +@login_required +def list_organizations(request): + q = """ + MATCH (org:Organization) + RETURN org + ORDER BY org.name + """ + + org_list = nc.query_to_list(nc.graphdb.manager, q) + urls = get_node_urls(org_list) + + table = Table('Name', 'ID') + table.rows = [_organization_table(item['org']) for item in org_list] + table.no_badges=True + + return render(request, 'noclook/list/list_generic.html', + {'table': table, 'name': 'Organizations', 'urls': urls}) diff --git a/src/niweb/niweb/templates/base.html b/src/niweb/niweb/templates/base.html index f8c0df006..7f1cf20ed 100644 --- a/src/niweb/niweb/templates/base.html +++ b/src/niweb/niweb/templates/base.html @@ -142,20 +142,15 @@
  • {% type_menu %} + {% comment %}
  • -
  • Host reports
  • -
  • Unique IDs
  • -
  • - -
  • Site map
  • -
  • Optical node map
  • + {% endcomment %} {% if user.is_authenticated %}
  • {% if user.is_staff %}
  • Create new
  • -
  • Reserve IDs
  • Users
  • {% endif %}
  • Log out
  • From fba8c56f171e3093a4d15e5d353d09da67b335d8 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 25 Mar 2019 17:16:13 +0100 Subject: [PATCH 019/520] Different aproach taken to hide elements from the menu. --- .../noclook/dynamic_preferences_registry.py | 8 ++++ .../apps/noclook/templatetags/noclook_tags.py | 46 +++++++++++++++---- src/niweb/niweb/templates/admin_menu.html | 3 ++ src/niweb/niweb/templates/base.html | 7 ++- src/niweb/niweb/templates/maps_menu.html | 6 +++ src/niweb/niweb/templates/report_menu.html | 6 +++ 6 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 src/niweb/niweb/templates/admin_menu.html create mode 100644 src/niweb/niweb/templates/maps_menu.html create mode 100644 src/niweb/niweb/templates/report_menu.html diff --git a/src/niweb/apps/noclook/dynamic_preferences_registry.py b/src/niweb/apps/noclook/dynamic_preferences_registry.py index 47abcb72f..9b27f8566 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 = 'sri' + help_text = 'sri|ni' + + @global_preferences_registry.register class PageFlashMessage(StringPreference): section = announcements diff --git a/src/niweb/apps/noclook/templatetags/noclook_tags.py b/src/niweb/apps/noclook/templatetags/noclook_tags.py index 4096258a6..82c3be7b2 100644 --- a/src/niweb/apps/noclook/templatetags/noclook_tags.py +++ b/src/niweb/apps/noclook/templatetags/noclook_tags.py @@ -24,6 +24,34 @@ def type_menu(): return {'types': types} +def mode_menu(): + """ + 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'] + + if menu_mode == 'ni': + return { 'val': True } + else: + return { 'val': False } + + +@register.inclusion_tag('report_menu.html') +def report_menu(): + return mode_menu() + + +@register.inclusion_tag('maps_menu.html') +def maps_menu(): + return mode_menu() + + +@register.inclusion_tag('type_menu.html') +def admin_menu(): + return mode_menu() + + @register.simple_tag(takes_context=True) def noclook_node_to_url(context,handle_id): """G @@ -37,7 +65,7 @@ def noclook_node_to_url(context,handle_id): return "/nodes/%s" % handle_id #else: # - #try: + #try: # return get_node_url(handle_id) #except ObjectDoesNotExist: # return '' @@ -86,7 +114,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} @@ -219,12 +247,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: @@ -234,7 +262,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 @@ -251,7 +279,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/niweb/templates/admin_menu.html b/src/niweb/niweb/templates/admin_menu.html new file mode 100644 index 000000000..41328fc6e --- /dev/null +++ b/src/niweb/niweb/templates/admin_menu.html @@ -0,0 +1,3 @@ +{% if val %} +
  • Reserve IDs
  • +{% endif %} diff --git a/src/niweb/niweb/templates/base.html b/src/niweb/niweb/templates/base.html index 7f1cf20ed..1cd6338e5 100644 --- a/src/niweb/niweb/templates/base.html +++ b/src/niweb/niweb/templates/base.html @@ -142,15 +142,14 @@
  • {% type_menu %} - {% comment %} -
  • - - {% endcomment %} + {% report_menu %} + {% maps_menu %} {% if user.is_authenticated %}
  • {% if user.is_staff %}
  • Create new
  • + {% admin_menu %}
  • Users
  • {% endif %}
  • Log out
  • diff --git a/src/niweb/niweb/templates/maps_menu.html b/src/niweb/niweb/templates/maps_menu.html new file mode 100644 index 000000000..ec0ead2a1 --- /dev/null +++ b/src/niweb/niweb/templates/maps_menu.html @@ -0,0 +1,6 @@ +{% if val %} +
  • + +
  • Site map
  • +
  • Optical node map
  • +{% endif %} diff --git a/src/niweb/niweb/templates/report_menu.html b/src/niweb/niweb/templates/report_menu.html new file mode 100644 index 000000000..7a4f52d17 --- /dev/null +++ b/src/niweb/niweb/templates/report_menu.html @@ -0,0 +1,6 @@ +{% if val %} +
  • + +
  • Host reports
  • +
  • Unique IDs
  • +{% endif %} From 1537ac7482eaa81efabed36db33b077dadff2dcf Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 25 Mar 2019 18:12:39 +0100 Subject: [PATCH 020/520] Menu reorganization redone in a more flexible fashion. --- .../noclook/dynamic_preferences_registry.py | 2 +- .../apps/noclook/templatetags/noclook_tags.py | 23 +++---------------- src/niweb/niweb/templates/admin_menu.html | 3 --- src/niweb/niweb/templates/base.html | 16 ++++++++++--- src/niweb/niweb/templates/maps_menu.html | 6 ----- src/niweb/niweb/templates/report_menu.html | 6 ----- 6 files changed, 17 insertions(+), 39 deletions(-) delete mode 100644 src/niweb/niweb/templates/admin_menu.html delete mode 100644 src/niweb/niweb/templates/maps_menu.html delete mode 100644 src/niweb/niweb/templates/report_menu.html diff --git a/src/niweb/apps/noclook/dynamic_preferences_registry.py b/src/niweb/apps/noclook/dynamic_preferences_registry.py index 9b27f8566..310f64526 100644 --- a/src/niweb/apps/noclook/dynamic_preferences_registry.py +++ b/src/niweb/apps/noclook/dynamic_preferences_registry.py @@ -30,7 +30,7 @@ class MoreInfoLink(StringPreference): class NOCLookMenuMode(StringPreference): section = general name = 'menu_mode' - default = 'sri' + default = 'ni' help_text = 'sri|ni' diff --git a/src/niweb/apps/noclook/templatetags/noclook_tags.py b/src/niweb/apps/noclook/templatetags/noclook_tags.py index 82c3be7b2..edc18a642 100644 --- a/src/niweb/apps/noclook/templatetags/noclook_tags.py +++ b/src/niweb/apps/noclook/templatetags/noclook_tags.py @@ -24,32 +24,15 @@ def type_menu(): return {'types': types} -def mode_menu(): +@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'] - if menu_mode == 'ni': - return { 'val': True } - else: - return { 'val': False } - - -@register.inclusion_tag('report_menu.html') -def report_menu(): - return mode_menu() - - -@register.inclusion_tag('maps_menu.html') -def maps_menu(): - return mode_menu() - - -@register.inclusion_tag('type_menu.html') -def admin_menu(): - return mode_menu() + { 'menu_mode': menu_mode } @register.simple_tag(takes_context=True) diff --git a/src/niweb/niweb/templates/admin_menu.html b/src/niweb/niweb/templates/admin_menu.html deleted file mode 100644 index 41328fc6e..000000000 --- a/src/niweb/niweb/templates/admin_menu.html +++ /dev/null @@ -1,3 +0,0 @@ -{% if val %} -
  • Reserve IDs
  • -{% endif %} diff --git a/src/niweb/niweb/templates/base.html b/src/niweb/niweb/templates/base.html index 1cd6338e5..c91461b04 100644 --- a/src/niweb/niweb/templates/base.html +++ b/src/niweb/niweb/templates/base.html @@ -142,14 +142,24 @@
  • {% type_menu %} - {% report_menu %} - {% maps_menu %} + {% ifequal menu_mode 'ni' %} +
  • + +
  • Host reports
  • +
  • Unique IDs
  • +
  • + +
  • Site map
  • +
  • Optical node map
  • + {% endifequal %} {% if user.is_authenticated %}
  • {% if user.is_staff %}
  • Create new
  • - {% admin_menu %} + {% ifequal menu_mode 'ni' %} +
  • Reserve IDs
  • + {% endifequal %}
  • Users
  • {% endif %}
  • Log out
  • diff --git a/src/niweb/niweb/templates/maps_menu.html b/src/niweb/niweb/templates/maps_menu.html deleted file mode 100644 index ec0ead2a1..000000000 --- a/src/niweb/niweb/templates/maps_menu.html +++ /dev/null @@ -1,6 +0,0 @@ -{% if val %} -
  • - -
  • Site map
  • -
  • Optical node map
  • -{% endif %} diff --git a/src/niweb/niweb/templates/report_menu.html b/src/niweb/niweb/templates/report_menu.html deleted file mode 100644 index 7a4f52d17..000000000 --- a/src/niweb/niweb/templates/report_menu.html +++ /dev/null @@ -1,6 +0,0 @@ -{% if val %} -
  • - -
  • Host reports
  • -
  • Unique IDs
  • -{% endif %} From 05501d391239e33b2b77ca08ebae661d804fe422 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 26 Mar 2019 12:47:20 +0100 Subject: [PATCH 021/520] Fixed encoding issue on contact form. --- src/niweb/apps/noclook/forms/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index c4c2b9332..07b78bf68 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -824,8 +824,8 @@ def clean(self): """ 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") + first_name = cleaned_data.get("first_name").encode('utf-8') + last_name = cleaned_data.get("last_name").encode('utf-8') cleaned_data['name'] = '{} {}'.format(first_name, last_name) return cleaned_data From 078221d5b0afa787cc374eb296c4177139fa7563 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 26 Mar 2019 13:36:02 +0100 Subject: [PATCH 022/520] Python 2/3 compatibility code and bugfix in contacts --- src/niweb/apps/noclook/forms/common.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 07b78bf68..93ab54331 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -817,6 +817,7 @@ def __init__(self, *args, **kwargs): phone = forms.CharField(required=False) salutation = forms.CharField(required=False) email = forms.CharField(required=False) + name = forms.CharField(required=False, widget=forms.widgets.HiddenInput) def clean(self): """ @@ -824,8 +825,13 @@ def clean(self): """ 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").encode('utf-8') - last_name = cleaned_data.get("last_name").encode('utf-8') + 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') + cleaned_data['name'] = '{} {}'.format(first_name, last_name) return cleaned_data From 6f41b3fca2e5dfa291ccd7d0c56ee7be610d809d Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 27 Mar 2019 17:49:58 +0100 Subject: [PATCH 023/520] Added a new type of file in the csv import management command. --- .../noclook/management/commands/csvimport.py | 59 +++++++++++++++++++ .../tests/management/test_csvimport.py | 52 +++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index ec6872dee..947685d43 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -24,6 +24,8 @@ def add_arguments(self, parser): 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('-d', "--delimiter", nargs='?', default=';', help='Delimiter to use use. Default ";".') @@ -56,6 +58,7 @@ def handle(self, *args, **options): csv_organizations = None csv_contacts = None + csv_secroles = None self.user = get_user() # IMPORT ORGANIZATIONS @@ -82,6 +85,18 @@ def handle(self, *args, **options): 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: @@ -204,7 +219,51 @@ def handle(self, *args, **options): 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 = NodeHandle.objects.get_or_create( + node_name = node['role'], + node_type = role_type, + node_meta_type = logical_meta_type, + creator = self.user, + modifier = self.user, + )[0] + + # we're adding the relations straight since if the relation + # already exists doesn't alter the result + contact.get_node().add_role(role.handle_id) + contact.get_node().add_organization(organization.handle_id) + + csv_secroles.close() + + def count_lines(self, file): + ''' + Counts lines in a file + ''' num_lines = 0 try: num_lines = sum(1 for line in file) diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index 5add84d7a..e9fce54e1 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -6,7 +6,7 @@ from norduniclient.exceptions import UniqueNodeError, NodeNotFound import norduniclient.models as ncmodels -from apps.noclook.models import NodeHandle, User +from apps.noclook.models import NodeHandle, NodeType, User from ..neo4j_base import NeoTestCase @@ -32,6 +32,14 @@ class CsvImportTest(NeoTestCase): "Rev";"Kiri";"Janosevic";;"Physical Therapy Assistant";"Person";;;;;"China";"568-690-1854";"118-569-1303";;"kjanosevic4@umich.edu";;;"Youspan" """ + 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 @@ -40,6 +48,9 @@ def setUp(self): # write contacts csv file to disk self.contacts_file = self.write_string_to_disk(self.contacts_str) + # write contacts csv file to disk + self.secroles_file = self.write_string_to_disk(self.secroles_str) + # create noclook user User.objects.get_or_create(username="noclook")[0] @@ -51,6 +62,9 @@ def tearDown(self): # 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( @@ -63,6 +77,7 @@ def test_organizations_import(self): 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() @@ -83,12 +98,14 @@ def test_contacts_import(self): self.assertIsNotNone(qs) contact1 = qs.first() self.assertIsNotNone(contact1) + self.assertIsInstance(contact1.get_node(), ncmodels.ContactModel) # check if it has a role assigned qs = NodeHandle.objects.filter(node_name='Computer Systems Analyst III') self.assertIsNotNone(qs) role1 = qs.first() self.assertIsNotNone(role1) + self.assertIsInstance(role1.get_node(), ncmodels.RoleModel) relations = role1.get_node().get_relations() relation = relations.get('Is', None) @@ -100,12 +117,45 @@ def test_contacts_import(self): self.assertIsNotNone(qs) organization1 = qs.first() self.assertIsNotNone(organization1) + self.assertIsInstance(organization1.get_node(), ncmodels.OrganizationModel) relations = organization1.get_node().get_relations() relation = relations.get('Works_for', None) self.assertIsNotNone(relation) self.assertIsInstance(relations['Works_for'][0]['node'], ncmodels.RelationModel) + 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 + qs = NodeHandle.objects.filter(node_name='Övrig incidentkontakt') + self.assertIsNotNone(qs) + role1 = qs.first() + self.assertIsNotNone(role1) + + relations = contact1.get_node().get_outgoing_relations() + self.assertEquals(relations['Works_for'][0]['node'], organization1.get_node()) + self.assertIsInstance(relations['Works_for'][0]['node'], ncmodels.OrganizationModel) + self.assertEquals(relations['Is'][0]['node'], role1.get_node()) + self.assertIsInstance(relations['Is'][0]['node'], ncmodels.RoleModel) + def write_string_to_disk(self, string): # get random file tf = tempfile.NamedTemporaryFile() From 1ce5d471cbabeca2bbcf9920347df47bbb7ebf41 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 1 Apr 2019 11:33:02 +0200 Subject: [PATCH 024/520] Added list for contacts with organization column as requested. --- src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/list.py | 42 ++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index a72a34d53..a3ec7d94c 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -86,6 +86,7 @@ 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'^router/$', _list.list_routers), url(r'^rack/$', _list.list_racks), url(r'^odf/$', _list.list_odfs), diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index 5b6acf5bf..39502770c 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -84,7 +84,7 @@ def _type_table(wrapped_node): @login_required def list_by_type(request, slug): node_type = get_object_or_404(NodeType, slug=slug) - q = """ + q = """ MATCH (node:%(nodetype)s) RETURN node ORDER BY node.name @@ -311,7 +311,7 @@ def list_optical_links(request): # TODO: returns [None,None] and [node, None] # tried to use [:Has *0-1] path matching but that gave "duplicate paths" q = """ - MATCH (link:Optical_Link) + MATCH (link:Optical_Link) OPTIONAL MATCH (link)-[:Depends_on]->(node) OPTIONAL MATCH p=(node)<-[:Has]-(parent) RETURN link as link, collect([node, parent]) as dependencies @@ -382,9 +382,9 @@ def list_optical_nodes(request): def _optical_path_table(path): row = TableRow( - path, - path.get('framing'), - path.get('capacity'), + path, + path.get('framing'), + path.get('capacity'), path.get('description'), ", ".join(path.get('enrs',[]))) _set_operational_state(row, path) @@ -463,7 +463,7 @@ def _router_table(router): row = TableRow(router, router.get('model'), router.get('version'), router.get('operational_state')) _set_expired(row, router) return row - + @login_required def list_routers(request): @@ -585,3 +585,33 @@ def list_organizations(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Organizations', 'urls': urls}) + +def _contact_table(con, org): + contact_link = { + 'url': u'/contact/{}/'.format(con.get('handle_id')), + 'name': u'{}'.format(con.get('name', '')) + } + name_org = '' + if org: + name_org = org.get('name', '') + + row = TableRow(contact_link, name_org) + return row + +@login_required +def list_contacts(request): + q = """ + OPTIONAL MATCH (con:Contact)-[:Works_for]->(org:Organization) + RETURN con, org + ORDER BY con.name; + """ + + con_list = nc.query_to_list(nc.graphdb.manager, q) + 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}) From 749b33ed249ccf59fac9d308a22d8197275bd253 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Mon, 1 Apr 2019 13:00:14 +0200 Subject: [PATCH 025/520] Use pypi.sunet.se if package available there --- requirements/common.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/common.txt b/requirements/common.txt index 7ba8ab3c6..674f2f150 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,3 +1,4 @@ +-i https://pypi.sunet.se/simple Django<1.12 django-activity-stream<0.7 django-jsonfield From 9ba88935eb9e7a1554503074d2ccae374401038d Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 1 Apr 2019 13:30:03 +0200 Subject: [PATCH 026/520] List corrected --- src/niweb/apps/noclook/views/list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index 39502770c..feb99e951 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -601,9 +601,9 @@ def _contact_table(con, org): @login_required def list_contacts(request): q = """ - OPTIONAL MATCH (con:Contact)-[:Works_for]->(org:Organization) + MATCH (con:Contact) + OPTIONAL MATCH (con)-[:Works_for]->(org:Organization) RETURN con, org - ORDER BY con.name; """ con_list = nc.query_to_list(nc.graphdb.manager, q) From f00cad7b759bb33fc15e0b48b477dca46f4c4c55 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 1 Apr 2019 16:10:03 +0200 Subject: [PATCH 027/520] Changes in create/edit contact form --- src/niweb/apps/noclook/forms/common.py | 23 +++++++++++++++---- .../noclook/create/create_contact.html | 18 ++++++++++----- .../templates/noclook/edit/edit_contact.html | 18 ++++++++------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 93ab54331..c2a157b5c 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -791,7 +791,20 @@ class NewOrganizationForm(forms.Form): phone = forms.CharField(required=False) website = forms.CharField(required=False) customer_id = forms.CharField(required=False) - type = forms.CharField(required=False) # possible ChoiceField + type = forms.ChoiceField(widget=forms.widgets.Select, required=False) + + def __init__(self, *args, **kwargs): + super(NewOrganizationForm, self).__init__(*args, **kwargs) + self.fields['contact_type'].choices = [ + ('university_college', 'University, College'), + ('museum', 'Museum, Institution'), + ('research', 'Research facility'), + ('university_coldep', 'University, College dep.'), + ('student_net', 'Student network'), + ('partner', 'Partner'), + ('provider', 'Service provider'), + ('supplier', 'Supplier'), + ] class EditOrganizationForm(NewOrganizationForm): @@ -807,17 +820,19 @@ def __init__(self, *args, **kwargs): class NewContactForm(forms.Form): def __init__(self, *args, **kwargs): super(NewContactForm, self).__init__(*args, **kwargs) - self.fields['mailing_country'].choices = country_codes() + self.fields['contact_type'].choices = [('person', 'Person'), ('group', 'Group')] first_name = forms.CharField() last_name = forms.CharField() - contact_type = forms.CharField(required=False) # possible ChoiceField + contact_type = forms.ChoiceField(widget=forms.widgets.Select, required=False) mobile = forms.CharField(required=False) - mailing_country = forms.ChoiceField(widget=forms.widgets.Select, required=False) phone = forms.CharField(required=False) salutation = forms.CharField(required=False) email = forms.CharField(required=False) + other_email = forms.CharField(required=False) name = forms.CharField(required=False, widget=forms.widgets.HiddenInput) + title = forms.CharField(required=False) + PGP_fingerprint = forms.CharField(required=False) def clean(self): """ diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_contact.html b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html index c08f8bfbb..d18a8f804 100644 --- a/src/niweb/apps/noclook/templates/noclook/create/create_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html @@ -18,16 +18,16 @@

    Main information

    {{ form.last_name }}
    - - {{ form.contact_type }} -
    - - {{ form.mailing_country }} -

    Additional info (optional)

    + + {{ form.title }} +
    {{ form.salutation }}
    + + {{ form.contact_type }} +
    {{ form.phone }}
    @@ -37,6 +37,12 @@

    Additional info (optional)

    {{ form.email }}
    + + {{ form.other_email }} +
    + + {{ form.PGP_fingerprint }} +
    Cancel diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html index 486e2ee77..fcf7debfc 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -25,18 +25,20 @@ {% block content %} {{ block.super }}
    - {{ form.first_name | as_crispy_field}} - {{ form.last_name | as_crispy_field}} + {{ form.first_name | as_crispy_field }} + {{ form.last_name | as_crispy_field }}
    {% accordion 'Additional info (optional)' 'additional-edit' '#edit-accordion' %} - {{ form.contact_type | as_crispy_field}} - {{ form.mobile | as_crispy_field}} - {{ form.mailing_country | as_crispy_field}} - {{ form.phone | as_crispy_field}} - {{ form.salutation | as_crispy_field}} - {{ form.email | as_crispy_field}} + {{ form.title | as_crispy_field }} + {{ form.salutation | as_crispy_field }} + {{ form.contact_type | as_crispy_field }} + {{ form.phone | as_crispy_field }} + {{ form.mobile | as_crispy_field }} + {{ form.email | as_crispy_field }} + {{ form.other_email | 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" %} From 3210f5269433d7e4585599fe57c51b12d32a91e9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 2 Apr 2019 17:40:25 +0200 Subject: [PATCH 028/520] Create/Edit Organizations: Added some fields and contacts section. --- src/niweb/apps/noclook/forms/common.py | 82 ++++++++++++++++- src/niweb/apps/noclook/helpers.py | 87 +++++++++++++++++++ .../noclook/create/create_organization.html | 22 +++++ .../noclook/edit/edit_organization.html | 21 +++++ src/niweb/apps/noclook/views/create.py | 20 ++++- src/niweb/apps/noclook/views/edit.py | 18 +++- 6 files changed, 247 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index c2a157b5c..c7d441149 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -5,6 +5,7 @@ from django.db import IntegrityError import json import csv +from apps.noclook import helpers from apps.noclook.models import NodeHandle, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown from .. import unique_ids import norduniclient as nc @@ -48,6 +49,22 @@ 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 + """ + choices = [('', '')] + q = """ + MATCH (c:Contact)-[:Works_for]->(o:Organization) + WHERE o.handle_id = {organization_id} + RETURN c.handle_id as handle_id, c.name as name + """.format(organization_id=organization_id) + + l = nc.query_to_list(nc.graphdb.manager, q) + choices.extend([tuple([item['handle_id'], item['name']]) for item in l]) + return choices + class IPAddrField(forms.CharField): def __init__(self, *args, **kwargs): if 'widget' not in kwargs: @@ -792,10 +809,18 @@ class NewOrganizationForm(forms.Form): website = forms.CharField(required=False) customer_id = forms.CharField(required=False) type = forms.ChoiceField(widget=forms.widgets.Select, required=False) + additional_info = forms.CharField(widget=forms.widgets.Textarea, required=False, label="Additional info for incident Mgmt") + # these fields will be replaced by selects in the edit form + abuse_contact = forms.CharField(required=False) + primary_contact = forms.CharField(required=False) # Primary contact at incidents + secondary_contact = forms.CharField(required=False) # Secondary contact at incidents + it_technical_contact = forms.CharField(required=False) # IT-technical + it_security_contact = forms.CharField(required=False) # IT-security + it_manager_contact = forms.CharField(required=False) # IT-manager def __init__(self, *args, **kwargs): super(NewOrganizationForm, self).__init__(*args, **kwargs) - self.fields['contact_type'].choices = [ + self.fields['type'].choices = [ ('university_college', 'University, College'), ('museum', 'Museum, Institution'), ('research', 'Research facility'), @@ -806,15 +831,70 @@ def __init__(self, *args, **kwargs): ('supplier', 'Supplier'), ] +org_contact_fields = [ + ('abuse_contact', 'Abuse'), + ('primary_contact', 'Primary contact at incidents'), + ('secondary_contact', 'Secondary contact at incidents'), + ('it_technical_contact', 'IT-technical'), + ('it_security_contact', 'IT-security'), + ('it_manager_contact', 'IT-manager'), +] 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]: + for field in org_contact_fields: + possible_contact = helpers.get_contact_for_orgrole(args[0]['handle_id'], field[1]) + if possible_contact: + field_name = field[0].decode('utf8') if six.PY2 else field[0] + args[0][field_name] = possible_contact.handle_id + 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 + if 'handle_id' in args[0]: + organization_id = args[0]['handle_id'] + contact_choices = get_contacts_for_organization(organization_id) + 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 + + def clean(self): + """ + Sets name from first and second name + """ + cleaned_data = super(EditOrganizationForm, self).clean() + contact_fields = [field[0] for field in org_contact_fields] + + for field in contact_fields: + 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 field in self._errors: + del self._errors[field] + relationship_parent_of = relationship_field('organization', True) relationship_uses_a = relationship_field('procedure', True) + abuse_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Abuse") + primary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Primary contact at incidents") # Primary contact at incidents + secondary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Secondary contact at incidents") # Secondary contact at incidents + it_technical_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-technical") # IT-technical + it_security_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-security") # IT-security + it_manager_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-manager") # IT-manager class NewContactForm(forms.Form): diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 0dd8fbf57..5b833b00d 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -14,6 +14,7 @@ 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 @@ -970,3 +971,89 @@ def set_of_member(user, node, contact_id): if created: activitylog.create_relationship(user, relationship) return relationship, created + +def link_contact_role_for_organization(user, node, contact_handle_id, role): + """ + :param user: Django user + :param node: norduniclient model + :param contact_handle_id: contact's handle_id + :return: contact, role: Only the role is created if it don't exists + """ + role_type = NodeType.objects.filter(type='Role').first() + + if six.PY2: + role = role.encode('utf-8') + + contact = NodeHandle.objects.get(handle_id=contact_handle_id) + + role = NodeHandle.objects.get_or_create( + node_name = role, + node_type = role_type, + node_meta_type = 'Logical', + creator = user, + modifier = user, + )[0] + + contact.get_node().add_role(role.handle_id) + contact.get_node().add_organization(node.handle_id) + + return contact, role + +def create_contact_role_for_organization(user, node, contact_name, role): + """ + :param user: Django user + :param node: norduniclient model + :param contact_name: full name of the contact + :return: contact, role: New objects if they're not present in the db + """ + contact_type = NodeType.objects.filter(type='Contact').first() + role_type = NodeType.objects.filter(type='Role').first() + + if six.PY2: + contact_name = contact_name.encode('utf-8') + role = role.encode('utf-8') + + first_name, last_name = contact_name.split(' ') + + contact = NodeHandle.objects.get_or_create( + node_name = contact_name, + node_type = contact_type, + node_meta_type = 'Relation', + creator = user, + modifier = user, + )[0] + + contact.get_node().add_property('first_name', first_name) + contact.get_node().add_property('last_name', last_name) + + role = NodeHandle.objects.get_or_create( + node_name = role, + node_type = role_type, + node_meta_type = 'Logical', + creator = user, + modifier = user, + )[0] + + contact.get_node().add_role(role.handle_id) + contact.get_node().add_organization(node.handle_id) + + return contact, role + +def get_contact_for_orgrole(organization_id, role_name): + """ + :param organization_id: Organization's handle_id + :param role_name: Role name + """ + contact_type = NodeType.objects.filter(type='Contact').first() + q = """ + MATCH (r:Role)<-[:Is]-(c:Contact)-[:Works_for]->(o:Organization) + WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" + RETURN c.handle_id AS handle_id + """.format(organization_id=organization_id, role_name=role_name) + d = nc.query_to_dict(nc.graphdb.manager, q) + + if 'handle_id' in d and d['handle_id']: + contact_handle_id = d['handle_id'] + contact = NodeHandle.objects.get(handle_id=contact_handle_id) + + return contact diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_organization.html b/src/niweb/apps/noclook/templates/noclook/create/create_organization.html index ebc4ea40f..815e12bfe 100644 --- a/src/niweb/apps/noclook/templates/noclook/create/create_organization.html +++ b/src/niweb/apps/noclook/templates/noclook/create/create_organization.html @@ -30,6 +30,28 @@

    Additional info (optional)


    {{ form.type }} +
    + + {{ form.additional_info }} +

    Contacts (optional)

    + + {{ form.abuse_contact }} +
    + + {{ form.primary_contact }} +
    + + {{ form.secondary_contact }} +
    + + {{ form.it_technical_contact }} +
    + + {{ form.it_security_contact }} +
    + + {{ form.it_manager_contact }} +
    Cancel diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html index 69701fa19..2d67d43ef 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html @@ -4,6 +4,8 @@ {% block js %} {{ block.super }} + + @@ -32,6 +44,15 @@ {{ form.website | as_crispy_field}} {{ form.customer_id | as_crispy_field}} {{ form.type | as_crispy_field}} + {{ form.additional_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" %} diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 60d403d88..654e0539b 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -571,7 +571,25 @@ def new_organization(request, **kwargs): except UniqueNodeError: form.add_error('name', 'An Organization with that name already exists.') return render(request, 'noclook/create/create_organization.html', {'form': form}) - helpers.form_update_node(request.user, nh.handle_id, form) + + # use property keys to avoid inserting contacts as a string property of the node + property_keys = [ + 'name', 'description', 'phone', 'website', 'customer_id', 'type', 'additional_info', + ] + helpers.form_update_node(request.user, nh.handle_id, form, property_keys) + + contact_fields = [ + ('abuse_contact', 'Abuse'), + ('primary_contact', 'Primary contact at incidents'), + ('secondary_contact', 'Secondary contact at incidents'), + ('it_technical_contact', 'IT-technical'), + ('it_security_contact', 'IT-security'), + ('it_manager_contact', 'IT-manager'), + ] + for field in contact_fields: + contact_name = form.cleaned_data[field[0]] + helpers.create_contact_role_for_organization(request.user, nh, contact_name, field[1]) + return redirect(nh.get_absolute_url()) else: form = forms.NewOrganizationForm() diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 1d441980f..9cb5b5cb4 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -10,6 +10,7 @@ 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 @@ -939,7 +940,22 @@ def edit_organization(request, handle_id): form = forms.EditOrganizationForm(request.POST) if form.is_valid(): # Generic node update - helpers.form_update_node(request.user, organization.handle_id, form) + # use property keys to avoid inserting contacts as a string property of the node + property_keys = [ + 'name', 'description', 'phone', 'website', 'customer_id', 'type', 'additional_info', + ] + helpers.form_update_node(request.user, organization.handle_id, form, property_keys) + # Set contacts + contact_fields = forms.org_contact_fields + for field in contact_fields: + if field[0] in form.cleaned_data: + contact_data = form.cleaned_data[field[0]] + if contact_data: + if isinstance(contact_data, six.string_types): + helpers.create_contact_role_for_organization(request.user, nh, contact_data, field[1]) + else: + helpers.link_contact_role_for_organization(request.user, nh, contact_data, field[1]) + # Set child organizations if form.cleaned_data['relationship_parent_of']: organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) From b0ca7b307cfcf24842492ec89a41376ff391be3e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 3 Apr 2019 12:29:15 +0200 Subject: [PATCH 029/520] The only options left on the create menu are: Contact, Group, Organization and Role --- src/niweb/apps/noclook/views/create.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 654e0539b..a8efab009 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -17,33 +17,12 @@ from apps.noclook import unique_ids from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible - TYPES = [ - ("customer", "Customer"), - ("cable", "Cable"), ("contact", "Contact"), - ("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"), - ("procedure", "Procedure"), - ("rack", "Rack"), ("role", "Role"), - ("site", "Site"), - ("site-owner", "Site Owner"), ("organization", "Organization"), + ("group", "Group"), ] -if helpers.app_enabled("apps.scan"): - TYPES.append(("/scan/queue", "Host scan")) # Create functions From 07c01090ea06134edfbaa005c5aff57a0b7c678a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 3 Apr 2019 14:08:49 +0200 Subject: [PATCH 030/520] Before setting a new contact with the specified role for an organization, any previous contact gets its role deleted. --- src/niweb/apps/noclook/helpers.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 5b833b00d..3a97d9e68 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -984,6 +984,14 @@ def link_contact_role_for_organization(user, node, contact_handle_id, role): if six.PY2: role = role.encode('utf-8') + # delete previous relationship first + q = """ + MATCH (r:Role)<-[l:Is]-(c:Contact)-[:Works_for]->(o:Organization) + WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" + DELETE l RETURN c + """.format(organization_id=node.handle_id, role_name=role) + d = nc.query_to_dict(nc.graphdb.manager, q) + contact = NodeHandle.objects.get(handle_id=contact_handle_id) role = NodeHandle.objects.get_or_create( @@ -1013,6 +1021,14 @@ def create_contact_role_for_organization(user, node, contact_name, role): contact_name = contact_name.encode('utf-8') role = role.encode('utf-8') + # delete previous relationship first + q = """ + MATCH (r:Role)<-[l:Is]-(c:Contact)-[:Works_for]->(o:Organization) + WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" + DELETE l RETURN c + """.format(organization_id=node.handle_id, role_name=role) + d = nc.query_to_dict(nc.graphdb.manager, q) + first_name, last_name = contact_name.split(' ') contact = NodeHandle.objects.get_or_create( From 81c6a86daa3fa47af0ae63f3cdced31a98bc3995 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 5 Apr 2019 09:53:12 +0200 Subject: [PATCH 031/520] Quick bugfix --- src/niweb/apps/noclook/views/create.py | 3 ++- src/niweb/apps/noclook/views/edit.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index a8efab009..62772c89f 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -567,7 +567,8 @@ def new_organization(request, **kwargs): ] for field in contact_fields: contact_name = form.cleaned_data[field[0]] - helpers.create_contact_role_for_organization(request.user, nh, contact_name, field[1]) + if contact_name: + helpers.create_contact_role_for_organization(request.user, nh, contact_name, field[1]) return redirect(nh.get_absolute_url()) else: diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 9cb5b5cb4..dc4737ea0 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -952,7 +952,8 @@ def edit_organization(request, handle_id): contact_data = form.cleaned_data[field[0]] if contact_data: if isinstance(contact_data, six.string_types): - helpers.create_contact_role_for_organization(request.user, nh, contact_data, field[1]) + if contact_data: + helpers.create_contact_role_for_organization(request.user, nh, contact_data, field[1]) else: helpers.link_contact_role_for_organization(request.user, nh, contact_data, field[1]) From 0fc9a51a238073eddd5fe3f7b94cbcc7f4881fe7 Mon Sep 17 00:00:00 2001 From: bereware Date: Fri, 5 Apr 2019 10:24:25 +0200 Subject: [PATCH 032/520] move organizations types from the sql db --- src/niweb/apps/noclook/forms/common.py | 11 +---------- .../apps/noclook/migrations/common_dropdowns.csv | 8 ++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 363a3b044..ee06f37bb 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -824,16 +824,7 @@ class NewOrganizationForm(forms.Form): def __init__(self, *args, **kwargs): super(NewOrganizationForm, self).__init__(*args, **kwargs) - self.fields['type'].choices = [ - ('university_college', 'University, College'), - ('museum', 'Museum, Institution'), - ('research', 'Research facility'), - ('university_coldep', 'University, College dep.'), - ('student_net', 'Student network'), - ('partner', 'Partner'), - ('provider', 'Service provider'), - ('supplier', 'Supplier'), - ] + self.fields['type'].choices = Dropdown.get('organization_types').as_choices() org_contact_fields = [ ('abuse_contact', 'Abuse'), diff --git a/src/niweb/apps/noclook/migrations/common_dropdowns.csv b/src/niweb/apps/noclook/migrations/common_dropdowns.csv index a161bb0e7..345fc066e 100644 --- a/src/niweb/apps/noclook/migrations/common_dropdowns.csv +++ b/src/niweb/apps/noclook/migrations/common_dropdowns.csv @@ -44,3 +44,11 @@ countries,NO,Norway countries,SE,Sweden countries,UK,United Kingdom countries,US,USA +organization_types,university_college,"University, College" +organization_types,museum,"Museum, Institution" +organization_types,research,Research facility +organization_types,university_coldep,"University, College dep" +organization_types,student_net,Student network +organization_types,partner,Partner +organization_types,provider,Service provider +organization_types,supplier,Supplier From 2cbeb7b77e2fe6efb0d7479fc9462caebd742200 Mon Sep 17 00:00:00 2001 From: bereware Date: Fri, 5 Apr 2019 10:24:58 +0200 Subject: [PATCH 033/520] remove line unused (get_contact_for_orgrole) --- src/niweb/apps/noclook/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 3a97d9e68..3bd16bdfa 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -1060,7 +1060,6 @@ def get_contact_for_orgrole(organization_id, role_name): :param organization_id: Organization's handle_id :param role_name: Role name """ - contact_type = NodeType.objects.filter(type='Contact').first() q = """ MATCH (r:Role)<-[:Is]-(c:Contact)-[:Works_for]->(o:Organization) WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" From 53c5dcb2dbbbad32d2d89ae37e8f950c92a4a4d9 Mon Sep 17 00:00:00 2001 From: bereware Date: Fri, 5 Apr 2019 10:57:37 +0200 Subject: [PATCH 034/520] contact_fields added from the sql db --- src/niweb/apps/noclook/forms/common.py | 12 ++---------- .../apps/noclook/migrations/common_dropdowns.csv | 6 ++++++ src/niweb/apps/noclook/views/create.py | 9 +-------- src/niweb/apps/noclook/views/edit.py | 2 +- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index ee06f37bb..935d92543 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -826,14 +826,6 @@ def __init__(self, *args, **kwargs): super(NewOrganizationForm, self).__init__(*args, **kwargs) self.fields['type'].choices = Dropdown.get('organization_types').as_choices() -org_contact_fields = [ - ('abuse_contact', 'Abuse'), - ('primary_contact', 'Primary contact at incidents'), - ('secondary_contact', 'Secondary contact at incidents'), - ('it_technical_contact', 'IT-technical'), - ('it_security_contact', 'IT-security'), - ('it_manager_contact', 'IT-manager'), -] class EditOrganizationForm(NewOrganizationForm): def __init__(self, *args, **kwargs): @@ -841,7 +833,7 @@ def __init__(self, *args, **kwargs): initial = {} if 'initial' not in kwargs else kwargs['initial'] if 'handle_id' in args[0]: - for field in org_contact_fields: + for field in Dropdown.get('organization_contact_types').as_choices(empty=False): possible_contact = helpers.get_contact_for_orgrole(args[0]['handle_id'], field[1]) if possible_contact: field_name = field[0].decode('utf8') if six.PY2 else field[0] @@ -867,7 +859,7 @@ def clean(self): Sets name from first and second name """ cleaned_data = super(EditOrganizationForm, self).clean() - contact_fields = [field[0] for field in org_contact_fields] + contact_fields = Dropdown.get('organization_contact_types').as_values() for field in contact_fields: if field in self.data: diff --git a/src/niweb/apps/noclook/migrations/common_dropdowns.csv b/src/niweb/apps/noclook/migrations/common_dropdowns.csv index 345fc066e..5aadda370 100644 --- a/src/niweb/apps/noclook/migrations/common_dropdowns.csv +++ b/src/niweb/apps/noclook/migrations/common_dropdowns.csv @@ -52,3 +52,9 @@ organization_types,student_net,Student network organization_types,partner,Partner organization_types,provider,Service provider organization_types,supplier,Supplier +organization_contact_types,abuse_contact,Abuse +organization_contact_types,primary_contact,Primary contact at incidents +organization_contact_types,secondary_contact,Secondary contact at incidents +organization_contact_types,it_technical_contact,IT-technical +organization_contact_types,it_security_contact,IT-security +organization_contact_types,it_manager_contact,IT-manager diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 62772c89f..c6f249c6a 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -557,14 +557,7 @@ def new_organization(request, **kwargs): ] helpers.form_update_node(request.user, nh.handle_id, form, property_keys) - contact_fields = [ - ('abuse_contact', 'Abuse'), - ('primary_contact', 'Primary contact at incidents'), - ('secondary_contact', 'Secondary contact at incidents'), - ('it_technical_contact', 'IT-technical'), - ('it_security_contact', 'IT-security'), - ('it_manager_contact', 'IT-manager'), - ] + contact_fields = Dropdown.get('organization_contact_types').as_choices(empty=False) for field in contact_fields: contact_name = form.cleaned_data[field[0]] if contact_name: diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 1169dbf5b..43752e016 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -968,7 +968,7 @@ def edit_organization(request, handle_id): ] helpers.form_update_node(request.user, organization.handle_id, form, property_keys) # Set contacts - contact_fields = forms.org_contact_fields + contact_fields = Dropdown.get('organization_contact_types').as_choices(empty=False) for field in contact_fields: if field[0] in form.cleaned_data: contact_data = form.cleaned_data[field[0]] From deace743cc73f06aaabd4a9b5689401aca4ebd64 Mon Sep 17 00:00:00 2001 From: bereware Date: Fri, 5 Apr 2019 11:48:44 +0200 Subject: [PATCH 035/520] Log relationships if they have been created - (create_contact_role_for_organization) --- src/niweb/apps/noclook/helpers.py | 63 ++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 3bd16bdfa..8e5315e16 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -1007,6 +1007,7 @@ def link_contact_role_for_organization(user, node, contact_handle_id, role): return contact, role + def create_contact_role_for_organization(user, node, contact_name, role): """ :param user: Django user @@ -1014,12 +1015,12 @@ def create_contact_role_for_organization(user, node, contact_name, role): :param contact_name: full name of the contact :return: contact, role: New objects if they're not present in the db """ - contact_type = NodeType.objects.filter(type='Contact').first() - role_type = NodeType.objects.filter(type='Role').first() + contact_type = NodeType.objects.get(type='Contact') + role_type = NodeType.objects.get(type='Role') if six.PY2: contact_name = contact_name.encode('utf-8') - role = role.encode('utf-8') + role = role.encode('utf-8') # delete previous relationship first q = """ @@ -1031,30 +1032,50 @@ def create_contact_role_for_organization(user, node, contact_name, role): first_name, last_name = contact_name.split(' ') - contact = NodeHandle.objects.get_or_create( - node_name = contact_name, - node_type = contact_type, - node_meta_type = 'Relation', - creator = user, - modifier = user, - )[0] + contact, created_contact = NodeHandle.objects.get_or_create( + node_name=contact_name, + node_type=contact_type, + node_meta_type='Relation', + creator=user, + modifier=user, + ) - contact.get_node().add_property('first_name', first_name) - contact.get_node().add_property('last_name', last_name) + if created_contact: + contact.get_node().add_property('first_name', first_name) + contact.get_node().add_property('last_name', last_name) - role = NodeHandle.objects.get_or_create( - node_name = role, - node_type = role_type, - node_meta_type = 'Logical', - creator = user, - modifier = user, - )[0] + role, created_role = NodeHandle.objects.get_or_create( + node_name=role, + node_type=role_type, + node_meta_type='Logical', + creator=user, + modifier=user, + ) - contact.get_node().add_role(role.handle_id) - contact.get_node().add_organization(node.handle_id) + result_role = contact.get_node().add_role(role.handle_id) + result_organization = contact.get_node().add_organization(node.handle_id) + + # Get relationship for contact-role + relationship_role_id = result_role.get('Is')[0].get('relationship_id') + relationship_role = nc.get_relationship_model(nc.graphdb.manager, relationship_role_id) + + # Get relationship for contact-organization + relationship_organization_id = result_organization.get('Works_for')[0].get('relationship_id') + relationship_organization = nc.get_relationship_model(nc.graphdb.manager, relationship_organization_id) + + created_relationship_role = result_role.get('Is')[0].get('created') + created_relationship_organization = result_organization.get('Works_for')[0].get('created') + + # Log relationships if they have been created + if created_relationship_role: + activitylog.create_relationship(user, relationship_role) + + if created_relationship_organization: + activitylog.create_relationship(user, relationship_organization) return contact, role + def get_contact_for_orgrole(organization_id, role_name): """ :param organization_id: Organization's handle_id From 78ed31eb2b5e369c5f9dbbbc4d944449f6567d9e Mon Sep 17 00:00:00 2001 From: bereware Date: Fri, 5 Apr 2019 13:41:53 +0200 Subject: [PATCH 036/520] Log node if they have been created - (create_contact_role_for_organization) --- src/niweb/apps/noclook/helpers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 8e5315e16..8931b10d9 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -1041,6 +1041,7 @@ def create_contact_role_for_organization(user, node, contact_name, role): ) if created_contact: + activitylog.create_node(user, contact) contact.get_node().add_property('first_name', first_name) contact.get_node().add_property('last_name', last_name) @@ -1052,6 +1053,9 @@ def create_contact_role_for_organization(user, node, contact_name, role): modifier=user, ) + if created_role: + activitylog.create_node(user, role) + result_role = contact.get_node().add_role(role.handle_id) result_organization = contact.get_node().add_organization(node.handle_id) From fd8107bac7c896be86f2796afa44fd3046317931 Mon Sep 17 00:00:00 2001 From: bereware Date: Mon, 8 Apr 2019 10:51:40 +0200 Subject: [PATCH 037/520] update queries migrated to the nordurni client --- src/niweb/apps/noclook/forms/common.py | 13 ++---- src/niweb/apps/noclook/helpers.py | 56 ++++++++++++++++---------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 935d92543..e989b576e 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -49,21 +49,16 @@ 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 """ - choices = [('', '')] - q = """ - MATCH (c:Contact)-[:Works_for]->(o:Organization) - WHERE o.handle_id = {organization_id} - RETURN c.handle_id as handle_id, c.name as name - """.format(organization_id=organization_id) + organization = NodeHandle.objects.get(handle_id=organization_id) + + return organization.get_node().get_contacts() - l = nc.query_to_list(nc.graphdb.manager, q) - choices.extend([tuple([item['handle_id'], item['name']]) for item in l]) - return choices class IPAddrField(forms.CharField): def __init__(self, *args, **kwargs): diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 8931b10d9..384f10dc6 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -972,6 +972,7 @@ def set_of_member(user, node, contact_id): activitylog.create_relationship(user, relationship) return relationship, created + def link_contact_role_for_organization(user, node, contact_handle_id, role): """ :param user: Django user @@ -982,28 +983,44 @@ def link_contact_role_for_organization(user, node, contact_handle_id, role): role_type = NodeType.objects.filter(type='Role').first() if six.PY2: - role = role.encode('utf-8') + role = role.encode('utf-8') # delete previous relationship first - q = """ - MATCH (r:Role)<-[l:Is]-(c:Contact)-[:Works_for]->(o:Organization) - WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" - DELETE l RETURN c - """.format(organization_id=node.handle_id, role_name=role) - d = nc.query_to_dict(nc.graphdb.manager, q) + node.remove_role_from_contacts(role) contact = NodeHandle.objects.get(handle_id=contact_handle_id) - role = NodeHandle.objects.get_or_create( - node_name = role, - node_type = role_type, - node_meta_type = 'Logical', - creator = user, - modifier = user, - )[0] + role, created = NodeHandle.objects.get_or_create( + node_name=role, + node_type=role_type, + node_meta_type='Logical', + creator=user, + modifier=user, + ) + + if created: + activitylog.create_node(user, contact) - contact.get_node().add_role(role.handle_id) - contact.get_node().add_organization(node.handle_id) + result_role = contact.get_node().add_role(role.handle_id) + result_organization = contact.get_node().add_organization(node.handle_id) + + # Get relationship for contact-role + relationship_role_id = result_role.get('Is')[0].get('relationship_id') + relationship_role = nc.get_relationship_model(nc.graphdb.manager, relationship_role_id) + + # Get relationship for contact-organization + relationship_organization_id = result_organization.get('Works_for')[0].get('relationship_id') + relationship_organization = nc.get_relationship_model(nc.graphdb.manager, relationship_organization_id) + + created_relationship_role = result_role.get('Is')[0].get('created') + created_relationship_organization = result_organization.get('Works_for')[0].get('created') + + # Log relationships if they have been created + if created_relationship_role: + activitylog.create_relationship(user, relationship_role) + + if created_relationship_organization: + activitylog.create_relationship(user, relationship_organization) return contact, role @@ -1023,12 +1040,7 @@ def create_contact_role_for_organization(user, node, contact_name, role): role = role.encode('utf-8') # delete previous relationship first - q = """ - MATCH (r:Role)<-[l:Is]-(c:Contact)-[:Works_for]->(o:Organization) - WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" - DELETE l RETURN c - """.format(organization_id=node.handle_id, role_name=role) - d = nc.query_to_dict(nc.graphdb.manager, q) + node.remove_role_from_contacts(role) first_name, last_name = contact_name.split(' ') From b745878168e75b38c8741c71c4e6a842c18207f7 Mon Sep 17 00:00:00 2001 From: bereware Date: Mon, 8 Apr 2019 11:15:43 +0200 Subject: [PATCH 038/520] extract tuple generator method (_get_tuples_for_iterator) --- src/niweb/apps/noclook/forms/common.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index e989b576e..c1a9c9c07 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -56,8 +56,18 @@ def get_contacts_for_organization(organization_id): works for a certain organization """ organization = NodeHandle.objects.get(handle_id=organization_id) + contacts = organization.get_node().get_contacts() - return 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): From ce79e402aa817a3dff3d522840cf4db9157efade Mon Sep 17 00:00:00 2001 From: bereware Date: Tue, 9 Apr 2019 12:25:26 +0200 Subject: [PATCH 039/520] test for helpers method - get_contact_for_orgrole, create_contact_role_for_organization, link_contact_role_for_organization --- src/niweb/apps/noclook/tests/test_helpers.py | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/niweb/apps/noclook/tests/test_helpers.py b/src/niweb/apps/noclook/tests/test_helpers.py index c0dd2dc93..3cbf6c2cd 100644 --- a/src/niweb/apps/noclook/tests/test_helpers.py +++ b/src/niweb/apps/noclook/tests/test_helpers.py @@ -7,6 +7,21 @@ class Neo4jHelpersTest(NeoTestCase): + def setUp(self): + super(Neo4jHelpersTest, self).setUp() + + organization = self.create_node('organization1', 'organization', meta='Logical') + self.organization_node = organization.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() + + role = self.create_node('role1', 'role', meta='Logical') + self.role_node = role.get_node() + def test_delete_node_utf8(self): nh = self.create_node(u'æøå-ftw', 'site') node = nh.get_node() @@ -31,3 +46,45 @@ def test_create_unique_node_handle_case_insensitive(self): 'AwesomeNess', 'host', 'Physical') + + def test_link_contact_role_for_organization(self): + data = { + 'role_name': 'IT-manager' + } + + 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, + data.get('role_name') + ) + + self.assertEqual(self.contact_node.relationships.get('Is')[0].get('node'), role) + self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) + + def test_create_contact_role_for_organization(self): + data = { + 'contact_name': 'FirstName LastName', + 'role_name': 'IT-manager' + } + + self.assertEqual(len(self.organization_node.relationships), 0) + + contact, role = helpers.create_contact_role_for_organization(self.user, self.organization_node, + data.get('contact_name'), + data.get('role_name') + ) + + self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) + self.assertEqual(contact.get_node().data.get('name'), data.get('contact_name')) + self.assertEqual(role.get_node().data.get('name'), data.get('role_name')) + + def test_get_contact_for_orgrole(self): + self.contact_node.add_role(self.role_node.handle_id) + + self.assertEqual(len(self.organization_node.relationships), 0) + + self.contact_node.add_organization(self.organization_node.handle_id) + contact = helpers.get_contact_for_orgrole(self.organization_node.handle_id, self.role_node.data.get('name')) + + self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) From e3012267dbb4714d017e2e152facc53eab6d9969 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 10 Apr 2019 13:14:48 +0200 Subject: [PATCH 040/520] Changes in dockerfile and tests to migrate the dev enviroment to python3 --- Dockerfile | 8 ++++---- src/niweb/apps/noclook/tests/management/test_csvimport.py | 2 +- src/niweb/apps/noclook/tests/test_createforms.py | 7 ++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index b060f24c1..d1953274e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM alpine:latest +FROM python:alpine LABEL authors="Markus Krogh " -RUN apk add --no-cache ca-certificates python2 py2-pip libpq +RUN apk add --no-cache ca-certificates python3 libpq RUN pip install --upgrade pip RUN mkdir /app @@ -36,8 +36,8 @@ WORKDIR /app ADD src /app ADD requirements /app/requirements -RUN apk add --no-cache --virtual build-dependencies postgresql-dev musl-dev gcc python2-dev && \ - pip install -r requirements/dev.txt && pip install -r requirements/py2.txt && \ +RUN apk add --no-cache --virtual build-dependencies postgresql-dev musl-dev gcc python3-dev && \ + pip install -r requirements/dev.txt && \ apk del build-dependencies ADD docker/alpine-start.sh /start.sh diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index e9fce54e1..68e7d06a5 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -158,7 +158,7 @@ def test_secroles_import(self): def write_string_to_disk(self, string): # get random file - tf = tempfile.NamedTemporaryFile() + tf = tempfile.NamedTemporaryFile(mode='w+') # write text tf.write(string) tf.flush() diff --git a/src/niweb/apps/noclook/tests/test_createforms.py b/src/niweb/apps/noclook/tests/test_createforms.py index 9df849183..47e944972 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 @@ -276,7 +277,11 @@ def test_NewOrganizationForm_full(self): def test_NewContactForm_full(self): node_type = 'Contact' - country_code = forms.country_codes()[0] + 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', From d8916b0b62498a97e191c9affe272fed4cd4f1a8 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 10 Apr 2019 18:08:02 +0200 Subject: [PATCH 041/520] First steps for a GraphQL api --- requirements/common.txt | 1 + src/niweb/apps/noclook/schema.py | 36 ++++++++++++++++++++++++++++++ src/niweb/niweb/schema.py | 10 +++++++++ src/niweb/niweb/settings/common.py | 7 +++++- src/niweb/niweb/urls.py | 5 +++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/niweb/apps/noclook/schema.py create mode 100644 src/niweb/niweb/schema.py diff --git a/requirements/common.txt b/requirements/common.txt index 674f2f150..076218cd0 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -14,3 +14,4 @@ django-dotenv<1.5 django-dynamic-preferences<1.4 django-attachments<2.0 configparser +graphene-django>=2.0 diff --git a/src/niweb/apps/noclook/schema.py b/src/niweb/apps/noclook/schema.py new file mode 100644 index 000000000..767083b5c --- /dev/null +++ b/src/niweb/apps/noclook/schema.py @@ -0,0 +1,36 @@ +import graphene +from graphene_django import DjangoObjectType + +from .models import * + +class NodeTypeType(DjangoObjectType): + class Meta: + model = NodeType + +class NodeHandleType(DjangoObjectType): + class Meta: + model = NodeHandle + +class RoleType(NodeHandleType): + name = graphene.String(required=True) + + def resolve_name(self, info, **kwargs): + return self.get_node().data['name'] + + class Meta: + model = NodeHandle + +class Query(graphene.ObjectType): + nodetypes = graphene.List(NodeTypeType) + nodehandles = graphene.List(NodeHandleType) + roles = graphene.List(RoleType) + + def resolve_nodetypes(self, info, **kwargs): + return NodeType.objects.all() + + def resolve_nodehandles(self, info, **kwargs): + return NodeHandle.objects.all() + + def resolve_roles(self, info, **kwargs): + role_type = NodeType.objects.get(type="Role") + return NodeHandle.objects.filter(node_type=role_type) diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py new file mode 100644 index 000000000..76ce2fba1 --- /dev/null +++ b/src/niweb/niweb/schema.py @@ -0,0 +1,10 @@ +import graphene +import apps.noclook.schema as ncschema + +class Query(ncschema.Query, graphene.ObjectType): + pass + +schema = graphene.Schema( + query=Query, + auto_camelcase=False, + ) diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index 005c57c87..fef295074 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -156,7 +156,7 @@ normpath(join(DJANGO_ROOT, 'templates')), ], 'APP_DIRS': True, - 'OPTIONS': { + 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.debug', @@ -232,6 +232,7 @@ 'crispy_forms', 'dynamic_preferences', 'attachments', + 'graphene_django', ) LOCAL_APPS = ( @@ -253,6 +254,10 @@ 'USE_JSONFIELD': True, 'GFK_FETCH_DEPTH': 1, } + +GRAPHENE = { + 'SCHEMA': 'niweb.schema.schema', +} ########## END APP CONFIGURATION diff --git a/src/niweb/niweb/urls.py b/src/niweb/niweb/urls.py index 761491645..c940d277b 100644 --- a/src/niweb/niweb/urls.py +++ b/src/niweb/niweb/urls.py @@ -3,6 +3,8 @@ from tastypie.api import Api import apps.noclook.api.resources as niapi from django.contrib.auth import views as auth_views +from django.views.decorators.csrf import csrf_exempt +from graphene_django.views import GraphQLView # Uncomment the next two lines to enable the admin: from django.contrib import admin @@ -78,6 +80,9 @@ def if_installed(appname, *args, **kwargs): # Tastypie URLs url(r'^api/', include(v1_api.urls)), + # GraphQL endpoint + url(r'^graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))), + # Django Generic Comments url(r'^comments/', include('django_comments.urls')), From 459d76b31ce57e7c67c7ae68dc4c0b4c6d83d8b4 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 11 Apr 2019 15:05:19 +0200 Subject: [PATCH 042/520] Added .jenkins.yaml --- .jenkins.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .jenkins.yaml diff --git a/.jenkins.yaml b/.jenkins.yaml new file mode 100644 index 000000000..a4f7d0bb1 --- /dev/null +++ b/.jenkins.yaml @@ -0,0 +1,7 @@ +# TODO: Run tests before triggering downstream +builders: + - script +downstream: + - docker-ni +script: + - echo "Bogus script to trigger downstream" From 4a4d1ce3ff17c4440cd6f8725ed75a34e02f6a84 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 12 Apr 2019 14:22:15 +0200 Subject: [PATCH 043/520] First version of the metaclass for NI/SRI nodes --- src/niweb/apps/noclook/schema.py | 104 +++++++++++++++++++++++++------ src/niweb/niweb/schema.py | 4 ++ 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/niweb/apps/noclook/schema.py b/src/niweb/apps/noclook/schema.py index 767083b5c..d4db194c0 100644 --- a/src/niweb/apps/noclook/schema.py +++ b/src/niweb/apps/noclook/schema.py @@ -1,36 +1,100 @@ import graphene from graphene_django import DjangoObjectType +from graphene_django.types import DjangoObjectTypeOptions from .models import * -class NodeTypeType(DjangoObjectType): - class Meta: - model = NodeType +def get_srifield_resolver(field_name, field_type): + def srifield_resolver(self, info, **kwargs): + return self.get_node().data.get(field_name) + + if isinstance(field_type, graphene.String) or isinstance(field_type, graphene.Int): + return srifield_resolver + else: + return srifield_resolver + +class NIObjectType(DjangoObjectType): + @classmethod + def __init_subclass_with_meta__( + cls, + sri_fields=None, + **options, + ): + if sri_fields: + for sri_field, sri_dict in sri_fields.items(): + field_kwargs = sri_dict.get('kwargs', {}) + field_type = sri_dict['type'] + + # adding the field + setattr(cls, sri_field, field_type(**field_kwargs)) + + # adding the resolver + setattr(cls, 'resolve_{}'.format(sri_field), \ + get_srifield_resolver(sri_field, field_type)) + + super(NIObjectType, cls).__init_subclass_with_meta__( + model=NodeHandle, + **options + ) -class NodeHandleType(DjangoObjectType): class Meta: model = NodeHandle -class RoleType(NodeHandleType): - name = graphene.String(required=True) +class RoleType(NIObjectType): + class Meta: + sri_fields = { + 'name': { 'type': graphene.String, 'kwargs': { 'required': True } }, + } - def resolve_name(self, info, **kwargs): - return self.get_node().data['name'] +class ContactType(NIObjectType): + is_roles = graphene.List(RoleType) + + def resolve_is_roles(self, info, **kwargs): + relations = self.get_node().get_outgoing_relations() + roles = relations.get('Is') + + # this may be the worst way to do it, but it's just for a PoC + handle_id_list = [] + for role in roles: + role = role['node'] + role_id = role.data.get('handle_id') + handle_id_list.append(role_id) + + ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) + + return ret class Meta: - model = NodeHandle + sri_fields = { + 'name': { 'type': graphene.String, 'kwargs': { 'required': True } }, + 'first_name': { 'type': graphene.String, 'kwargs': { 'required': True } }, + 'last_name': { 'type': graphene.String, 'kwargs': { 'required': True } }, + 'title': { 'type': graphene.String }, + 'salutation': { 'type': graphene.String }, + 'contact_type': { 'type': graphene.String }, # enum + 'phone': { 'type': graphene.String }, + 'mobile': { 'type': graphene.String }, + 'email': { 'type': graphene.String }, + 'other_email': { 'type': graphene.String }, + 'PGP_fingerprint': { 'type': graphene.String }, + } class Query(graphene.ObjectType): - nodetypes = graphene.List(NodeTypeType) - nodehandles = graphene.List(NodeHandleType) - roles = graphene.List(RoleType) - - def resolve_nodetypes(self, info, **kwargs): - return NodeType.objects.all() + roles = graphene.List(RoleType, limit=graphene.Int()) + contacts = graphene.List(ContactType, limit=graphene.Int()) - def resolve_nodehandles(self, info, **kwargs): - return NodeHandle.objects.all() + def resolve_roles(self, info, **args): + limit = args.get('limit', False) + type = NodeType.objects.get(type="Role") # TODO too raw + if limit: + return NodeHandle.objects.filter(node_type=type)[:10] + else: + return NodeHandle.objects.filter(node_type=type) - def resolve_roles(self, info, **kwargs): - role_type = NodeType.objects.get(type="Role") - return NodeHandle.objects.filter(node_type=role_type) + def resolve_contacts(self, info, **args): + limit = args.get('limit', False) + type = NodeType.objects.get(type="Contact") # TODO too raw + if limit: + return NodeHandle.objects.filter(node_type=type)[:10] + else: + return NodeHandle.objects.filter(node_type=type) diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py index 76ce2fba1..db5e2b0f9 100644 --- a/src/niweb/niweb/schema.py +++ b/src/niweb/niweb/schema.py @@ -7,4 +7,8 @@ class Query(ncschema.Query, graphene.ObjectType): schema = graphene.Schema( query=Query, auto_camelcase=False, + types=[ + ncschema.RoleType, + ncschema.ContactType, + ] ) From 519b6b98129caf1adddc3f7e844ced5bbdd0e0b8 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Apr 2019 11:05:14 +0200 Subject: [PATCH 044/520] First prototype of model like NIObjectType --- src/niweb/apps/noclook/schema.py | 130 +++++++++++++++++++------------ 1 file changed, 82 insertions(+), 48 deletions(-) diff --git a/src/niweb/apps/noclook/schema.py b/src/niweb/apps/noclook/schema.py index d4db194c0..0d428963b 100644 --- a/src/niweb/apps/noclook/schema.py +++ b/src/niweb/apps/noclook/schema.py @@ -1,18 +1,47 @@ import graphene +import re +from collections import OrderedDict from graphene_django import DjangoObjectType from graphene_django.types import DjangoObjectTypeOptions from .models import * -def get_srifield_resolver(field_name, field_type): +def get_srifield_resolver(field_name, field_type, rel_name='', rel_method=None): def srifield_resolver(self, info, **kwargs): return self.get_node().data.get(field_name) + def resolve_is_roles(self, info, **kwargs): + neo4jnode = self.get_node() + relations = neo4jnode.getattr(rel_method)() + roles = relations.get(rel_name) + + # this may be the worst way to do it, but it's just for a PoC + handle_id_list = [] + for role in roles: + role = role['node'] + role_id = role.data.get('handle_id') + handle_id_list.append(role_id) + + ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) + + return ret + if isinstance(field_type, graphene.String) or isinstance(field_type, graphene.Int): return srifield_resolver + elif isinstance(field_type, graphene.List): + return resolve_is_roles else: return srifield_resolver +class NIObjectField(): + def __init__(self, field_type=graphene.String, type_args=None, + type_kwargs=None, rel_name=None, rel_method=None, **kwargs): + self.field_type = field_type + self.type_args = type_args + self.type_kwargs = type_kwargs + self.rel_name = rel_name + self.rel_method = rel_method + class NIObjectType(DjangoObjectType): @classmethod def __init_subclass_with_meta__( @@ -20,20 +49,47 @@ def __init_subclass_with_meta__( sri_fields=None, **options, ): - if sri_fields: - for sri_field, sri_dict in sri_fields.items(): - field_kwargs = sri_dict.get('kwargs', {}) - field_type = sri_dict['type'] - - # adding the field - setattr(cls, sri_field, field_type(**field_kwargs)) - - # adding the resolver - setattr(cls, 'resolve_{}'.format(sri_field), \ - get_srifield_resolver(sri_field, field_type)) + fields_names = '' + allfields = cls.__dict__ + graphfields = OrderedDict() + for name, field in allfields.items(): + pattern = re.compile("^__.*__$") + if pattern.match(name):# or callable(field): + continue + graphfields[name] = field + + for name, field in graphfields.items(): + fields_names = fields_names + ' ' + '({} / {})'.format(name, field.__dict__) + field_fields = field.__dict__ + + field_type = field_fields.get('field_type') + 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') + + # adding the field + if type_kwargs: + field_value = field_type(**type_kwargs) + elif type_args: + field_value = field_type(*type_args) + else: + field_value = field_type(**{}) + + setattr(cls, name, field_value) + + # adding the resolver + setattr(cls, 'resolve_{}'.format(name), \ + get_srifield_resolver( + name, + field_type, + rel_name, + rel_method, + ) + ) super(NIObjectType, cls).__init_subclass_with_meta__( - model=NodeHandle, + model=NIObjectType._meta.model, **options ) @@ -41,43 +97,21 @@ class Meta: model = NodeHandle class RoleType(NIObjectType): - class Meta: - sri_fields = { - 'name': { 'type': graphene.String, 'kwargs': { 'required': True } }, - } + name = NIObjectField(type_kwargs={ 'required': True }) class ContactType(NIObjectType): - is_roles = graphene.List(RoleType) - - def resolve_is_roles(self, info, **kwargs): - relations = self.get_node().get_outgoing_relations() - roles = relations.get('Is') - - # this may be the worst way to do it, but it's just for a PoC - handle_id_list = [] - for role in roles: - role = role['node'] - role_id = role.data.get('handle_id') - handle_id_list.append(role_id) - - ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) - - return ret - - class Meta: - sri_fields = { - 'name': { 'type': graphene.String, 'kwargs': { 'required': True } }, - 'first_name': { 'type': graphene.String, 'kwargs': { 'required': True } }, - 'last_name': { 'type': graphene.String, 'kwargs': { 'required': True } }, - 'title': { 'type': graphene.String }, - 'salutation': { 'type': graphene.String }, - 'contact_type': { 'type': graphene.String }, # enum - 'phone': { 'type': graphene.String }, - 'mobile': { 'type': graphene.String }, - 'email': { 'type': graphene.String }, - 'other_email': { 'type': graphene.String }, - 'PGP_fingerprint': { 'type': graphene.String }, - } + name = NIObjectField(type_kwargs={ 'required': True }) + first_name = NIObjectField(type_kwargs={ 'required': True }) + last_name = NIObjectField(type_kwargs={ 'required': True }) + title = NIObjectField() + salutation = NIObjectField() + contact_type = NIObjectField() + phone = NIObjectField() + mobile = NIObjectField() + email = NIObjectField() + other_email = NIObjectField() + PGP_fingerprint = NIObjectField() + is_roles = NIObjectField(field_type=graphene.List, type_args=(RoleType,), rel_name='Is', rel_method='get_outgoing_relations') class Query(graphene.ObjectType): roles = graphene.List(RoleType, limit=graphene.Int()) From 4ba63790f9f03314f97acfc4f757e628d1bc3c0a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Apr 2019 11:29:18 +0200 Subject: [PATCH 045/520] functional list resolver --- src/niweb/apps/noclook/schema.py | 63 +++++++++++++++++++------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/src/niweb/apps/noclook/schema.py b/src/niweb/apps/noclook/schema.py index 0d428963b..6d953aada 100644 --- a/src/niweb/apps/noclook/schema.py +++ b/src/niweb/apps/noclook/schema.py @@ -7,12 +7,12 @@ from .models import * def get_srifield_resolver(field_name, field_type, rel_name='', rel_method=None): - def srifield_resolver(self, info, **kwargs): + def resolve_node_string(self, info, **kwargs): return self.get_node().data.get(field_name) - def resolve_is_roles(self, info, **kwargs): + def resolve_relationship_list(self, info, **kwargs): neo4jnode = self.get_node() - relations = neo4jnode.getattr(rel_method)() + relations = getattr(neo4jnode, rel_method)() roles = relations.get(rel_name) # this may be the worst way to do it, but it's just for a PoC @@ -27,20 +27,25 @@ def resolve_is_roles(self, info, **kwargs): return ret if isinstance(field_type, graphene.String) or isinstance(field_type, graphene.Int): - return srifield_resolver + return resolve_node_string elif isinstance(field_type, graphene.List): - return resolve_is_roles + return resolve_relationship_list else: - return srifield_resolver + raise Exception(field_type.__class__) + return resolve_node_string class NIObjectField(): - def __init__(self, field_type=graphene.String, type_args=None, - type_kwargs=None, rel_name=None, rel_method=None, **kwargs): - self.field_type = field_type - self.type_args = type_args - self.type_kwargs = type_kwargs - self.rel_name = rel_name - self.rel_method = rel_method + def __init__(self, field_type=graphene.String, manual_resolver=False, + type_args=None, type_kwargs=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.type_kwargs = type_kwargs + self.rel_name = rel_name + self.rel_method = rel_method + self.not_null_list = not_null_list class NIObjectType(DjangoObjectType): @classmethod @@ -62,31 +67,37 @@ def __init_subclass_with_meta__( fields_names = fields_names + ' ' + '({} / {})'.format(name, field.__dict__) field_fields = field.__dict__ - field_type = field_fields.get('field_type') - 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') + 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 - setattr(cls, 'resolve_{}'.format(name), \ - get_srifield_resolver( - name, - field_type, - rel_name, - rel_method, - ) - ) + if not manual_resolver: + setattr(cls, 'resolve_{}'.format(name), \ + get_srifield_resolver( + name, + field_value, + rel_name, + rel_method, + ) + ) super(NIObjectType, cls).__init_subclass_with_meta__( model=NIObjectType._meta.model, From d4b358f8f88ebc2d5e7749b9e92f890e61dced8f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Apr 2019 11:51:10 +0200 Subject: [PATCH 046/520] Reorganization of code and test module (empty for the moment) added --- src/niweb/apps/noclook/schema/__init__.py | 9 ++++ .../noclook/{schema.py => schema/core.py} | 43 ++----------------- src/niweb/apps/noclook/schema/query.py | 22 ++++++++++ src/niweb/apps/noclook/schema/types.py | 19 ++++++++ .../apps/noclook/tests/schema/test_schema.py | 2 + 5 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 src/niweb/apps/noclook/schema/__init__.py rename src/niweb/apps/noclook/{schema.py => schema/core.py} (71%) create mode 100644 src/niweb/apps/noclook/schema/query.py create mode 100644 src/niweb/apps/noclook/schema/types.py create mode 100644 src/niweb/apps/noclook/tests/schema/test_schema.py diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py new file mode 100644 index 000000000..37bd5b2c5 --- /dev/null +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +import graphene + +from ..models import * +from .core import * +from .types import * +from .query import * diff --git a/src/niweb/apps/noclook/schema.py b/src/niweb/apps/noclook/schema/core.py similarity index 71% rename from src/niweb/apps/noclook/schema.py rename to src/niweb/apps/noclook/schema/core.py index 6d953aada..f63f2654d 100644 --- a/src/niweb/apps/noclook/schema.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -1,11 +1,11 @@ -import graphene +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + import re from collections import OrderedDict from graphene_django import DjangoObjectType from graphene_django.types import DjangoObjectTypeOptions -from .models import * - def get_srifield_resolver(field_name, field_type, rel_name='', rel_method=None): def resolve_node_string(self, info, **kwargs): return self.get_node().data.get(field_name) @@ -106,40 +106,3 @@ def __init_subclass_with_meta__( class Meta: model = NodeHandle - -class RoleType(NIObjectType): - name = NIObjectField(type_kwargs={ 'required': True }) - -class ContactType(NIObjectType): - name = NIObjectField(type_kwargs={ 'required': True }) - first_name = NIObjectField(type_kwargs={ 'required': True }) - last_name = NIObjectField(type_kwargs={ 'required': True }) - title = NIObjectField() - salutation = NIObjectField() - contact_type = NIObjectField() - phone = NIObjectField() - mobile = NIObjectField() - email = NIObjectField() - other_email = NIObjectField() - PGP_fingerprint = NIObjectField() - is_roles = NIObjectField(field_type=graphene.List, type_args=(RoleType,), rel_name='Is', rel_method='get_outgoing_relations') - -class Query(graphene.ObjectType): - roles = graphene.List(RoleType, limit=graphene.Int()) - contacts = graphene.List(ContactType, limit=graphene.Int()) - - def resolve_roles(self, info, **args): - limit = args.get('limit', False) - type = NodeType.objects.get(type="Role") # TODO too raw - if limit: - return NodeHandle.objects.filter(node_type=type)[:10] - else: - return NodeHandle.objects.filter(node_type=type) - - def resolve_contacts(self, info, **args): - limit = args.get('limit', False) - type = NodeType.objects.get(type="Contact") # TODO too raw - if limit: - return NodeHandle.objects.filter(node_type=type)[:10] - else: - return NodeHandle.objects.filter(node_type=type) diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py new file mode 100644 index 000000000..5a6b7c8d8 --- /dev/null +++ b/src/niweb/apps/noclook/schema/query.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +class Query(graphene.ObjectType): + roles = graphene.List(RoleType, limit=graphene.Int()) + contacts = graphene.List(ContactType, limit=graphene.Int()) + + def resolve_roles(self, info, **args): + limit = args.get('limit', False) + type = NodeType.objects.get(type="Role") # TODO too raw + if limit: + return NodeHandle.objects.filter(node_type=type)[:10] + else: + return NodeHandle.objects.filter(node_type=type) + + def resolve_contacts(self, info, **args): + limit = args.get('limit', False) + type = NodeType.objects.get(type="Contact") # TODO too raw + if limit: + return NodeHandle.objects.filter(node_type=type)[:10] + else: + return NodeHandle.objects.filter(node_type=type) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py new file mode 100644 index 000000000..16997f206 --- /dev/null +++ b/src/niweb/apps/noclook/schema/types.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +class RoleType(NIObjectType): + name = NIObjectField(type_kwargs={ 'required': True }) + +class ContactType(NIObjectType): + name = NIObjectField(type_kwargs={ 'required': True }) + first_name = NIObjectField(type_kwargs={ 'required': True }) + last_name = NIObjectField(type_kwargs={ 'required': True }) + title = NIObjectField() + salutation = NIObjectField() + contact_type = NIObjectField() + phone = NIObjectField() + mobile = NIObjectField() + email = NIObjectField() + other_email = NIObjectField() + PGP_fingerprint = NIObjectField() + is_roles = NIObjectField(field_type=graphene.List, type_args=(RoleType,), rel_name='Is', rel_method='get_outgoing_relations') 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..29e5cbcc8 --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' From c9ab7f9c9b68122fd222538e40692d180c5f289b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Apr 2019 13:14:38 +0200 Subject: [PATCH 047/520] Adding the option of a manual resolve of the field --- src/niweb/apps/noclook/schema/__init__.py | 3 --- src/niweb/apps/noclook/schema/core.py | 17 +++++++++++++---- src/niweb/apps/noclook/schema/query.py | 3 +++ src/niweb/apps/noclook/schema/types.py | 22 ++++++++++++++++++++++ src/niweb/niweb/schema.py | 8 ++++---- 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index 37bd5b2c5..b62073f93 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' -import graphene - -from ..models import * from .core import * from .types import * from .query import * diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index f63f2654d..9e296a0a6 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +import graphene import re from collections import OrderedDict from graphene_django import DjangoObjectType from graphene_django.types import DjangoObjectTypeOptions -def get_srifield_resolver(field_name, field_type, rel_name='', rel_method=None): +from ..models import * + +def get_srifield_resolver(field_name, field_type, rel_name=None, rel_method=None): def resolve_node_string(self, info, **kwargs): return self.get_node().data.get(field_name) @@ -23,7 +26,7 @@ def resolve_relationship_list(self, info, **kwargs): handle_id_list.append(role_id) ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) - + return ret if isinstance(field_type, graphene.String) or isinstance(field_type, graphene.Int): @@ -31,7 +34,6 @@ def resolve_relationship_list(self, info, **kwargs): elif isinstance(field_type, graphene.List): return resolve_relationship_list else: - raise Exception(field_type.__class__) return resolve_node_string class NIObjectField(): @@ -59,7 +61,7 @@ def __init_subclass_with_meta__( graphfields = OrderedDict() for name, field in allfields.items(): pattern = re.compile("^__.*__$") - if pattern.match(name):# or callable(field): + if pattern.match(name) or callable(field): continue graphfields[name] = field @@ -98,6 +100,13 @@ def __init_subclass_with_meta__( 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) + ) super(NIObjectType, cls).__init_subclass_with_meta__( model=NIObjectType._meta.model, diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 5a6b7c8d8..ba618919d 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +import graphene +from .types import * + class Query(graphene.ObjectType): roles = graphene.List(RoleType, limit=graphene.Int()) contacts = graphene.List(ContactType, limit=graphene.Int()) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 16997f206..befc7489c 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -1,6 +1,27 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +from .core import * +from ..models import * + +def resolve_roles_list(self, info, **kwargs): + rel_method = 'get_outgoing_relations' + rel_name = 'Is' + neo4jnode = self.get_node() + relations = getattr(neo4jnode, rel_method)() + roles = relations.get(rel_name) + + # this may be the worst way to do it, but it's just for a PoC + handle_id_list = [] + for role in roles: + role = role['node'] + role_id = role.data.get('handle_id') + handle_id_list.append(role_id) + + ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) + + return ret + class RoleType(NIObjectType): name = NIObjectField(type_kwargs={ 'required': True }) @@ -17,3 +38,4 @@ class ContactType(NIObjectType): other_email = NIObjectField() PGP_fingerprint = NIObjectField() is_roles = NIObjectField(field_type=graphene.List, type_args=(RoleType,), rel_name='Is', rel_method='get_outgoing_relations') + #is_roles2 = NIObjectField(field_type=graphene.List, type_args=(RoleType,), manual_resolver=resolve_roles_list) diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py index db5e2b0f9..ba5bab334 100644 --- a/src/niweb/niweb/schema.py +++ b/src/niweb/niweb/schema.py @@ -1,14 +1,14 @@ import graphene -import apps.noclook.schema as ncschema +import apps.noclook.schema as nocschema -class Query(ncschema.Query, graphene.ObjectType): +class Query(nocschema.Query, graphene.ObjectType): pass schema = graphene.Schema( query=Query, auto_camelcase=False, types=[ - ncschema.RoleType, - ncschema.ContactType, + nocschema.RoleType, + nocschema.ContactType, ] ) From a61f6aff6d65a68dfc94c136303dc83dadcbcafb Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Apr 2019 13:45:11 +0200 Subject: [PATCH 048/520] Added Groups type, bugfixes and external resolver example --- src/niweb/apps/noclook/schema/core.py | 19 +++++++++++++------ src/niweb/apps/noclook/schema/query.py | 9 +++++++++ src/niweb/apps/noclook/schema/types.py | 22 ++++++++++++---------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 9e296a0a6..08d38b90a 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -16,17 +16,18 @@ def resolve_node_string(self, info, **kwargs): def resolve_relationship_list(self, info, **kwargs): neo4jnode = self.get_node() relations = getattr(neo4jnode, rel_method)() - roles = relations.get(rel_name) + nodes = relations.get(rel_name) # this may be the worst way to do it, but it's just for a PoC handle_id_list = [] - for role in roles: - role = role['node'] - role_id = role.data.get('handle_id') - handle_id_list.append(role_id) + 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) - + return ret if isinstance(field_type, graphene.String) or isinstance(field_type, graphene.Int): @@ -36,6 +37,7 @@ def resolve_relationship_list(self, info, **kwargs): else: return resolve_node_string +# define new and different types: String, List class NIObjectField(): def __init__(self, field_type=graphene.String, manual_resolver=False, type_args=None, type_kwargs=None, rel_name=None, @@ -59,12 +61,15 @@ def __init_subclass_with_meta__( fields_names = '' allfields = cls.__dict__ graphfields = OrderedDict() + + # getting all not magic attributes for name, field in allfields.items(): pattern = re.compile("^__.*__$") if pattern.match(name) or callable(field): continue graphfields[name] = field + # run over the fields defined and adding graphene fields and resolvers for name, field in graphfields.items(): fields_names = fields_names + ' ' + '({} / {})'.format(name, field.__dict__) field_fields = field.__dict__ @@ -108,6 +113,8 @@ def __init_subclass_with_meta__( .format(name) ) + # add data field and resolver + super(NIObjectType, cls).__init_subclass_with_meta__( model=NIObjectType._meta.model, **options diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index ba618919d..490a68eaf 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -6,6 +6,7 @@ class Query(graphene.ObjectType): roles = graphene.List(RoleType, limit=graphene.Int()) + groups = graphene.List(GroupType, limit=graphene.Int()) contacts = graphene.List(ContactType, limit=graphene.Int()) def resolve_roles(self, info, **args): @@ -16,6 +17,14 @@ def resolve_roles(self, info, **args): else: return NodeHandle.objects.filter(node_type=type) + def resolve_groups(self, info, **args): + limit = args.get('limit', False) + type = NodeType.objects.get(type="Group") # TODO too raw + if limit: + return NodeHandle.objects.filter(node_type=type)[:10] + else: + return NodeHandle.objects.filter(node_type=type) + def resolve_contacts(self, info, **args): limit = args.get('limit', False) type = NodeType.objects.get(type="Contact") # TODO too raw diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index befc7489c..26f3e0194 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -5,18 +5,17 @@ from ..models import * def resolve_roles_list(self, info, **kwargs): - rel_method = 'get_outgoing_relations' - rel_name = 'Is' neo4jnode = self.get_node() - relations = getattr(neo4jnode, rel_method)() - roles = relations.get(rel_name) + relations = neo4jnode.get_outgoing_relations() + roles = relations.get('Is') # this may be the worst way to do it, but it's just for a PoC handle_id_list = [] - for role in roles: - role = role['node'] - role_id = role.data.get('handle_id') - handle_id_list.append(role_id) + if roles: + for role in roles: + role = role['node'] + role_id = role.data.get('handle_id') + handle_id_list.append(role_id) ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) @@ -25,6 +24,9 @@ def resolve_roles_list(self, info, **kwargs): class RoleType(NIObjectType): name = NIObjectField(type_kwargs={ 'required': True }) +class GroupType(NIObjectType): + name = NIObjectField(type_kwargs={ 'required': True }) + class ContactType(NIObjectType): name = NIObjectField(type_kwargs={ 'required': True }) first_name = NIObjectField(type_kwargs={ 'required': True }) @@ -37,5 +39,5 @@ class ContactType(NIObjectType): email = NIObjectField() other_email = NIObjectField() PGP_fingerprint = NIObjectField() - is_roles = NIObjectField(field_type=graphene.List, type_args=(RoleType,), rel_name='Is', rel_method='get_outgoing_relations') - #is_roles2 = NIObjectField(field_type=graphene.List, type_args=(RoleType,), manual_resolver=resolve_roles_list) + is_roles = NIObjectField(field_type=graphene.List, type_args=(RoleType,), manual_resolver=resolve_roles_list) + member_of_groups = NIObjectField(field_type=graphene.List, type_args=(GroupType,), rel_name='Member_of', rel_method='get_outgoing_relations') From 29b9bb8324309df6380c72f4827b72b32b8af2bc Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Apr 2019 16:59:44 +0200 Subject: [PATCH 049/520] Added first test --- .../apps/noclook/tests/schema/__init__.py | 46 ++++++++++++++++++ .../apps/noclook/tests/schema/test_schema.py | 47 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/niweb/apps/noclook/tests/schema/__init__.py 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..27583d0a3 --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from ..neo4j_base import NeoTestCase + +class Neo4jGraphQLTest(NeoTestCase): + def setUp(self): + super(Neo4jGraphQLTest, self).setUp() + + # create nodes + organization1 = self.create_node('organization1', 'organization', meta='Logical') + organization2 = self.create_node('organization2', 'organization', meta='Logical') + contact1 = self.create_node('contact1', 'contact', meta='Relation') + contact2 = self.create_node('contact2', 'contact', meta='Relation') + role1 = self.create_node('role1', 'role', meta='Logical') + role2 = self.create_node('role2', 'role', meta='Logical') + group1 = self.create_node('group1', 'group', meta='Logical') + group2 = self.create_node('group2', 'group', meta='Logical') + + # add some data + contact1_data = { + 'first_name': 'Jane', + 'last_name': 'Doe', + 'name': 'Jane Doe', + } + + for key, value in contact1_data.items(): + 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(): + contact2.get_node().add_property(key, value) + + # create relationships + contact1.get_node().add_role(role1.handle_id) + contact1.get_node().add_group(group1.handle_id) + contact1.get_node().add_organization(organization1.handle_id) + + contact2.get_node().add_role(role2.handle_id) + contact2.get_node().add_group(group2.handle_id) + contact2.get_node().add_organization(organization2.handle_id) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 29e5cbcc8..464467a38 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -1,2 +1,49 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' + +from collections import OrderedDict +from . import Neo4jGraphQLTest +from niweb.schema import schema + +class QueryTest(Neo4jGraphQLTest): + def test_get_contacts(self): + query = ''' + query getLastTenContacts { + contacts(limit: 10) { + handle_id + name + first_name + last_name + is_roles { + name + } + member_of_groups { + name + } + } + } + ''' + + expected = { + 'contacts': [ + OrderedDict([ + ('handle_id', '4'), + ('name', 'John Smith'), + ('first_name', 'John'), + ('last_name', 'Smith'), + ('is_roles', [OrderedDict([('name', 'role2')])]), + ('member_of_groups', [OrderedDict([('name', 'group2')])]), + ]), + OrderedDict([ + ('handle_id', '3'), + ('name', 'Jane Doe'), + ('first_name', 'Jane'), + ('last_name', 'Doe'), + ('is_roles', [OrderedDict([('name', 'role1')])]), + ('member_of_groups', [OrderedDict([('name', 'group1')])]), + ]), + ] + } + result = schema.execute(query) + assert not result.errors + assert result.data == expected From 939afff2cba3b755a63cd7dec039e1383f28f846 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 16 Apr 2019 12:12:26 +0200 Subject: [PATCH 050/520] More code refactoring for a more flexible type system and schema loading --- src/niweb/apps/noclook/schema/__init__.py | 10 ++ src/niweb/apps/noclook/schema/core.py | 109 +++++++++++++++------- src/niweb/apps/noclook/schema/types.py | 30 +++--- src/niweb/niweb/schema.py | 11 +-- 4 files changed, 104 insertions(+), 56 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index b62073f93..20a4045cc 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -4,3 +4,13 @@ from .core import * from .types import * from .query import * + +NOCSCHEMA_TYPES = [ + RoleType, + GroupType, + ContactType, +] + +NOCSCHEMA_QUERIES = [ + Query, +] diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 08d38b90a..556d0ccf7 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -9,48 +9,84 @@ from ..models import * -def get_srifield_resolver(field_name, field_type, rel_name=None, rel_method=None): - def resolve_node_string(self, info, **kwargs): - return self.get_node().data.get(field_name) +class DictEntryType(graphene.ObjectType): + key = graphene.String(required=True) + value = graphene.String(required=True) - def resolve_relationship_list(self, info, **kwargs): - neo4jnode = self.get_node() - relations = getattr(neo4jnode, rel_method)() - nodes = relations.get(rel_name) +def resolve_nidata(self, info, **kwargs): + ret = [] - # this may be the worst way to do it, but it's just for a PoC - 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) + alldata = self.get_node().data + for key, value in alldata.items(): + ret.append(DictEntryType(key=key, value=value)) - ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) + return ret - return ret +class NIBasicField(): + 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_string(self, info, **kwargs): + return self.get_node().data.get(field_name) - if isinstance(field_type, graphene.String) or isinstance(field_type, graphene.Int): - return resolve_node_string - elif isinstance(field_type, graphene.List): - return resolve_relationship_list - else: return resolve_node_string -# define new and different types: String, List -class NIObjectField(): - def __init__(self, field_type=graphene.String, manual_resolver=False, - type_args=None, type_kwargs=None, rel_name=None, - rel_method=None, not_null_list=False, **kwargs): +class NIStringField(NIBasicField): + pass + +class NIIntField(NIBasicField): + 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 NIListField(NIBasicField): + 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.type_kwargs = type_kwargs 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(self, info, **kwargs): + neo4jnode = self.get_node() + relations = getattr(neo4jnode, rel_method)() + nodes = relations.get(rel_name) + + # this may be the worst way to do it, but it's just for a PoC + 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) + + return ret + + return resolve_relationship_list + class NIObjectType(DjangoObjectType): @classmethod def __init_subclass_with_meta__( @@ -62,10 +98,11 @@ def __init_subclass_with_meta__( allfields = cls.__dict__ graphfields = OrderedDict() - # getting all not magic attributes + # getting all not magic attributes, also filter non NI fields for name, field in allfields.items(): pattern = re.compile("^__.*__$") - if pattern.match(name) or callable(field): + is_nibasicfield = issubclass(field.__class__, NIBasicField) + if pattern.match(name) or callable(field) or not is_nibasicfield: continue graphfields[name] = field @@ -98,13 +135,13 @@ def __init_subclass_with_meta__( # adding the resolver if not manual_resolver: setattr(cls, 'resolve_{}'.format(name), \ - get_srifield_resolver( - name, - field_value, - rel_name, - rel_method, - ) + 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: @@ -114,6 +151,8 @@ def __init_subclass_with_meta__( ) # add data field and resolver + setattr(cls, 'nidata', graphene.List(DictEntryType)) + setattr(cls, 'resolve_nidata', resolve_nidata) super(NIObjectType, cls).__init_subclass_with_meta__( model=NIObjectType._meta.model, diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 26f3e0194..0dff6daa0 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -22,22 +22,22 @@ def resolve_roles_list(self, info, **kwargs): return ret class RoleType(NIObjectType): - name = NIObjectField(type_kwargs={ 'required': True }) + name = NIStringField(type_kwargs={ 'required': True }) class GroupType(NIObjectType): - name = NIObjectField(type_kwargs={ 'required': True }) + name = NIStringField(type_kwargs={ 'required': True }) class ContactType(NIObjectType): - name = NIObjectField(type_kwargs={ 'required': True }) - first_name = NIObjectField(type_kwargs={ 'required': True }) - last_name = NIObjectField(type_kwargs={ 'required': True }) - title = NIObjectField() - salutation = NIObjectField() - contact_type = NIObjectField() - phone = NIObjectField() - mobile = NIObjectField() - email = NIObjectField() - other_email = NIObjectField() - PGP_fingerprint = NIObjectField() - is_roles = NIObjectField(field_type=graphene.List, type_args=(RoleType,), manual_resolver=resolve_roles_list) - member_of_groups = NIObjectField(field_type=graphene.List, type_args=(GroupType,), rel_name='Member_of', rel_method='get_outgoing_relations') + 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 = NIStringField() + phone = NIStringField() + mobile = NIStringField() + email = NIStringField() + other_email = NIStringField() + PGP_fingerprint = NIStringField() + is_roles = NIListField(type_args=(RoleType,), manual_resolver=resolve_roles_list) + member_of_groups = NIListField(type_args=(GroupType,), rel_name='Member_of', rel_method='get_outgoing_relations') diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py index ba5bab334..b1b3b34a2 100644 --- a/src/niweb/niweb/schema.py +++ b/src/niweb/niweb/schema.py @@ -1,14 +1,13 @@ import graphene -import apps.noclook.schema as nocschema +from apps.noclook.schema import NOCSCHEMA_QUERIES, NOCSCHEMA_TYPES -class Query(nocschema.Query, graphene.ObjectType): +class Query(*NOCSCHEMA_QUERIES, graphene.ObjectType): pass +ALL_TYPES = NOCSCHEMA_TYPES + schema = graphene.Schema( query=Query, auto_camelcase=False, - types=[ - nocschema.RoleType, - nocschema.ContactType, - ] + types=ALL_TYPES ) From 8d3fa0bc1aed05153d0b630245689bab0e6f0077 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 23 Apr 2019 10:41:00 +0200 Subject: [PATCH 051/520] WIP: Docstrings, relay adaptations and mutations wip --- src/niweb/apps/noclook/schema/__init__.py | 5 + src/niweb/apps/noclook/schema/core.py | 56 ++++++- src/niweb/apps/noclook/schema/mutations.py | 147 ++++++++++++++++++ src/niweb/apps/noclook/schema/query.py | 1 + src/niweb/apps/noclook/schema/types.py | 4 + .../apps/noclook/tests/schema/test_core.py | 0 .../noclook/tests/schema/test_mutations.py | 0 src/niweb/niweb/schema.py | 2 +- 8 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/niweb/apps/noclook/schema/mutations.py create mode 100644 src/niweb/apps/noclook/tests/schema/test_core.py create mode 100644 src/niweb/apps/noclook/tests/schema/test_mutations.py diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index 20a4045cc..00d9b50cc 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -4,6 +4,7 @@ from .core import * from .types import * from .query import * +from .mutations import * NOCSCHEMA_TYPES = [ RoleType, @@ -14,3 +15,7 @@ NOCSCHEMA_QUERIES = [ Query, ] + +NOCSCHEMA_MUTATIONS = [ + +] diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 556d0ccf7..a1e998aca 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -4,16 +4,49 @@ import graphene import re from collections import OrderedDict +from graphene import relay from graphene_django import DjangoObjectType from graphene_django.types import DjangoObjectTypeOptions from ..models import * +class NIRelayNode(relay.Node): + ''' + from https://docs.graphene-python.org/en/latest/relay/nodes/ + This node may implement the id policies in the graph database + ''' + class Meta: + name = 'NIRelayNode' + + @staticmethod + def to_global_id(type, id): + return '{}:{}'.format(type, id) + + @staticmethod + def get_node_from_global_id(info, global_id, only_type=None): + type, id = global_id.split(':') + if only_type: + # We assure that the node type that we want to retrieve + # is the same that was indicated in the field type + assert type == only_type._meta.name, 'Received not compatible node.' + + if type == 'User': + return get_user(id) + elif type == 'Photo': + return get_photo(id) + class DictEntryType(graphene.ObjectType): + ''' + This type represents an key value pair in a dictionary for the data + dict of the norduniclient nodes + ''' key = graphene.String(required=True) value = graphene.String(required=True) def resolve_nidata(self, info, **kwargs): + ''' + Resolvers norduniclient nodes data dictionary + ''' ret = [] alldata = self.get_node().data @@ -23,6 +56,9 @@ def resolve_nidata(self, info, **kwargs): return ret class NIBasicField(): + ''' + Super class of the type fields + ''' def __init__(self, field_type=graphene.String, manual_resolver=False, type_kwargs=None, **kwargs): @@ -44,15 +80,24 @@ def resolve_node_string(self, info, **kwargs): return resolve_node_string 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 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): @@ -88,6 +133,12 @@ def resolve_relationship_list(self, info, **kwargs): return resolve_relationship_list 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. + ''' @classmethod def __init_subclass_with_meta__( cls, @@ -154,10 +205,13 @@ def __init_subclass_with_meta__( setattr(cls, 'nidata', graphene.List(DictEntryType)) setattr(cls, 'resolve_nidata', resolve_nidata) + options['model'] = NIObjectType._meta.model + options['interfaces'] = NIObjectType._meta.interfaces + super(NIObjectType, cls).__init_subclass_with_meta__( - model=NIObjectType._meta.model, **options ) class Meta: model = NodeHandle + interfaces = (NIRelayNode, ) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py new file mode 100644 index 000000000..08784a526 --- /dev/null +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +import graphene + +from django import forms +from graphene import relay +from apps.noclook.forms import * +from pprint import pprint + +# what we actually need here: +# A relay.ClientIDMutation subclass (NIMutation) that wraps a django form inside +# it should use the form to generate the input fields of the mutation +# the mutate_and_get_payload method should contain the create/edit django view +# code. It also may be a good idea to implement the most of the boilerplate of +# these views in this class. +# it could be a good idea to encapsulate the create, update and delete mutations +# into a single class + +class MockupNIMutation(): + pass + # get_create_mutation() + # get_update_mutation() + # get_delete_mutation() + + @classmethod + def edit_mutate_and_get_payload(cls, root, info, **input): + pass + # in the template methods of the superclass is where the boilerplate code + # should be called, like checking form.is_valid() to trigger all the clean + # validation. + # Also it should get the errors from the form to be added to the output + +def empty_mutate_and_get_payload(cls): + pass + +def create_mutate_and_get_payload(cls): + pass + +def edit_mutate_and_get_payload(cls): + pass + +def delete_mutate_and_get_payload(cls): + pass + +def form_to_graphene_field(form_field): + graphene_field = None + + # get attributes + graph_kwargs = {} + + for attr_name, attr_value in form_field.__dict__.items(): + if attr_name == 'required': + graph_kwargs['required'] = attr_value + + # compare types + 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 = graphene.String(**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.NullBooleanField): + 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) + + return graphene_field + +class AbstractNIMutation(relay.ClientIDMutation): + @classmethod + def __init_subclass_with_meta__( + cls, **options + ): + # read form + django_form = getattr(cls, 'django_form', None) + + # 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 name, field in class_field.items(): + # convert form field into mutation input field + graphene_field = form_to_graphene_field(field) + + if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): + if field not in django_form.Meta.exclude: + inner_fields[name] = graphene_field + else: + inner_fields[name] = graphene_field + + # add Input attribute to class + inner_class = type('Input', (object,), inner_fields) + setattr(cls, 'Input', inner_class) + + # build and set mutate_and_get_payload + setattr(cls, 'mutate_and_get_payload', classmethod(empty_mutate_and_get_payload)) + + foo = options.get('abstract') + + super(AbstractNIMutation, cls).__init_subclass_with_meta__( + **options + ) + + class Meta: + abstract = True + +'''class CreateNIMutation(AbstractNIMutation): + pass + +class UpdateNIMutation(AbstractNIMutation): + pass + +class DeleteNIMutation(AbstractNIMutation): + pass''' + +class CreateNIRoleMutation(AbstractNIMutation): + django_form = NewRoleForm + + class Meta: + abstract = False + +class NIMutationFactory(): + def __init_subclass__(cls, default_name, **kwargs): + pass + # check defined form attributes + +"""class RoleMutation(NIMutationFactory): + # only form | create_form and edit_form should be defined at the same time + form = 'NewRoleForm' # we'll get the input fields from the form, single form for Create/Edit + create_form = 'NewRoleForm' # use different forms to create + edit_form = 'EditRoleForm' # or to edit, both or none should be defined + + @classmethod + def create_mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None): + ''' + this method would override be the mutate_and_get_payload for the get_create_mutation + ''' + pass""" diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 490a68eaf..8aab46858 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -5,6 +5,7 @@ from .types import * class Query(graphene.ObjectType): + node = NIRelayNode.Field() roles = graphene.List(RoleType, limit=graphene.Int()) groups = graphene.List(GroupType, limit=graphene.Int()) contacts = graphene.List(ContactType, limit=graphene.Int()) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 0dff6daa0..62bd2ec14 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -5,6 +5,10 @@ from ..models import * def resolve_roles_list(self, info, **kwargs): + """ + This method is only present here to illustrate how a manual resolver + could be used + """ neo4jnode = self.get_node() relations = neo4jnode.get_outgoing_relations() roles = relations.get('Is') diff --git a/src/niweb/apps/noclook/tests/schema/test_core.py b/src/niweb/apps/noclook/tests/schema/test_core.py new file mode 100644 index 000000000..e69de29bb 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..e69de29bb diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py index b1b3b34a2..7aa72e17a 100644 --- a/src/niweb/niweb/schema.py +++ b/src/niweb/niweb/schema.py @@ -1,5 +1,5 @@ import graphene -from apps.noclook.schema import NOCSCHEMA_QUERIES, NOCSCHEMA_TYPES +from apps.noclook.schema import NOCSCHEMA_QUERIES, NOCSCHEMA_TYPES, NIRelayNode class Query(*NOCSCHEMA_QUERIES, graphene.ObjectType): pass From 2a29982a73b7e22a9b977497d2f97463f5dd96c0 Mon Sep 17 00:00:00 2001 From: bereware Date: Thu, 25 Apr 2019 08:56:12 +0200 Subject: [PATCH 052/520] show debugtoolbar and developer tools --- requirements/dev.txt | 2 ++ src/niweb/niweb/settings/dev.py | 12 ++++++++---- src/niweb/niweb/urls.py | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index b980d7262..2c2f24eab 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,3 +2,5 @@ ipython django-debug-toolbar django-nose<1.5 +django-extensions +ipdb diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 2dac6dad5..79f474b00 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -70,18 +70,22 @@ # See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation INSTALLED_APPS += ( 'debug_toolbar', - 'django_nose' + 'django_nose', + 'django_extensions' ) # See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation INTERNAL_IPS = ('127.0.0.1',) # See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation -MIDDLEWARE_CLASSES += ( +MIDDLEWARE_CLASSES = ( 'debug_toolbar.middleware.DebugToolbarMiddleware', -) +) + MIDDLEWARE_CLASSES -DEBUG_TOOLBAR_CONFIG = {} +DEBUG_TOOLBAR_CONFIG = { + 'INTERCEPT_REDIRECTS': False, + 'SHOW_TOOLBAR_CALLBACK': lambda x: True, +} ########## END TOOLBAR CONFIGURATION ########## SECRET CONFIGURATION diff --git a/src/niweb/niweb/urls.py b/src/niweb/niweb/urls.py index 761491645..ea3c6cd29 100644 --- a/src/niweb/niweb/urls.py +++ b/src/niweb/niweb/urls.py @@ -74,6 +74,12 @@ def if_installed(appname, *args, **kwargs): url(r'^saml2/', include('djangosaml2.urls')), ] +if settings.DEBUG: + import debug_toolbar + urlpatterns = [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns + urlpatterns += [ # Tastypie URLs url(r'^api/', include(v1_api.urls)), From 8d56acdf8627a654217133989265631be9e7a20c Mon Sep 17 00:00:00 2001 From: bereware Date: Thu, 25 Apr 2019 08:59:17 +0200 Subject: [PATCH 053/520] fix - give to the method the model of the organization --- src/niweb/apps/noclook/views/edit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 43752e016..2b15a0015 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -975,9 +975,9 @@ def edit_organization(request, handle_id): if contact_data: if isinstance(contact_data, six.string_types): if contact_data: - helpers.create_contact_role_for_organization(request.user, nh, contact_data, field[1]) + helpers.create_contact_role_for_organization(request.user, organization, contact_data, field[1]) else: - helpers.link_contact_role_for_organization(request.user, nh, contact_data, field[1]) + helpers.link_contact_role_for_organization(request.user, organization, contact_data, field[1]) # Set child organizations if form.cleaned_data['relationship_parent_of']: From 451545fdca6c9977f5f00095c0dbaacf45bc444b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 26 Apr 2019 10:46:31 +0200 Subject: [PATCH 054/520] WIP: Still implementing the mutation factory --- src/niweb/apps/noclook/schema/__init__.py | 4 +- src/niweb/apps/noclook/schema/mutations.py | 381 +++++++++++++++------ src/niweb/apps/noclook/schema/query.py | 2 +- src/niweb/niweb/schema.py | 13 +- 4 files changed, 298 insertions(+), 102 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index 00d9b50cc..dd9a20cdb 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -13,9 +13,9 @@ ] NOCSCHEMA_QUERIES = [ - Query, + NOCRootQuery, ] NOCSCHEMA_MUTATIONS = [ - + NOCRootMutation, ] diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 08784a526..67f5ba2ad 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -1,86 +1,27 @@ # -*- coding: utf-8 -*- +__author__ = 'ffuentes' import graphene from django import forms +from django.test import RequestFactory from graphene import relay +from graphql import GraphQLError +from apps.noclook import helpers from apps.noclook.forms import * from pprint import pprint -# what we actually need here: -# A relay.ClientIDMutation subclass (NIMutation) that wraps a django form inside -# it should use the form to generate the input fields of the mutation -# the mutate_and_get_payload method should contain the create/edit django view -# code. It also may be a good idea to implement the most of the boilerplate of -# these views in this class. -# it could be a good idea to encapsulate the create, update and delete mutations -# into a single class - -class MockupNIMutation(): - pass - # get_create_mutation() - # get_update_mutation() - # get_delete_mutation() - - @classmethod - def edit_mutate_and_get_payload(cls, root, info, **input): - pass - # in the template methods of the superclass is where the boilerplate code - # should be called, like checking form.is_valid() to trigger all the clean - # validation. - # Also it should get the errors from the form to be added to the output - -def empty_mutate_and_get_payload(cls): - pass - -def create_mutate_and_get_payload(cls): - pass - -def edit_mutate_and_get_payload(cls): - pass - -def delete_mutate_and_get_payload(cls): - pass - -def form_to_graphene_field(form_field): - graphene_field = None - - # get attributes - graph_kwargs = {} - - for attr_name, attr_value in form_field.__dict__.items(): - if attr_name == 'required': - graph_kwargs['required'] = attr_value - - # compare types - 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 = graphene.String(**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.NullBooleanField): - 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) - - return graphene_field - +# the payload fields should be defined as it isn't something we can guess class AbstractNIMutation(relay.ClientIDMutation): @classmethod def __init_subclass_with_meta__( - cls, **options + 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 django_form = getattr(cls, 'django_form', None) + name = getattr(cls, 'mutation_name', None) # build fields into Input inner_fields = {} @@ -89,59 +30,307 @@ def __init_subclass_with_meta__( if class_field_name == 'declared_fields' or class_field_name == 'base_fields': for name, field in class_field.items(): # convert form field into mutation input field - graphene_field = form_to_graphene_field(field) + graphene_field = cls.form_to_graphene_field(field) - if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): - if field not in django_form.Meta.exclude: + if graphene_field: + if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): + if field not in django_form.Meta.exclude: + inner_fields[name] = graphene_field + else: inner_fields[name] = graphene_field - else: - inner_fields[name] = graphene_field + else: + if cls.__name__ in 'Delete': + raise Exception(cls.__name__) + # this would set a handle_id for the input param + inner_fields['handle_id'] = forms.IntegerField(required=True) # add Input attribute to class inner_class = type('Input', (object,), inner_fields) setattr(cls, 'Input', inner_class) - # build and set mutate_and_get_payload - setattr(cls, 'mutate_and_get_payload', classmethod(empty_mutate_and_get_payload)) - - foo = options.get('abstract') + cls.set_mutate_and_get_payload() super(AbstractNIMutation, cls).__init_subclass_with_meta__( - **options + output, input_fields, arguments, name, **options ) + @classmethod + def form_to_graphene_field(cls, form_field): + '''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 = graphene.String(**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.NullBooleanField): + 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) + # fields left out to implement + # IPAddrField (CharField) + # JSONField (CharField) + # NodeChoiceField (ModelChoiceField) + # DatePickerField (DateField) + # description_field (CharField) + # relationship_field (ChoiceField / IntegerField) + else: + return None + + return graphene_field + + # to be implemented by the subclass + @classmethod + def get_payload_parameters(cls, *args, **kwargs): + '''This method should be implemented in the concerning subclasses + ''' + raise Exception('The class {} doesn\'t implemet the get_payload_parameters method'.format(cls)) + + @classmethod + def set_mutate_and_get_payload(cls): + '''This method should be implemented in the concerning subclasses + ''' + raise Exception('The class {} doesn\'t implemet the set_mutate_and_get_payload method'.format(cls.__name__)) + class Meta: abstract = True -'''class CreateNIMutation(AbstractNIMutation): - pass +class CreateNIMutation(AbstractNIMutation): + node_type = None + node_meta_type = None + request_path = None + + @classmethod + def set_mutate_and_get_payload(cls): + # get operation and check for method overrides + mutate_function = cls.__dict__.get('create_mutate_and_get_payload') + + # set mutate_and_get_payload + setattr(cls, 'mutate_and_get_payload', classmethod(mutate_function)) + + @classmethod + def get_payload_parameters(cls): + for attr_name, attr_value in cls.__dict__.items(): + if attr_name != 'Input': + pass + + @classmethod + def create_mutate_and_get_payload(cls, root, info, **input): + # get input values + input_class = getattr(cls, 'Input', None) + input_params = {} + if input_class: + for attr_name, attr_field in input_class.__dict__.items(): + print('Attribute {} value {}'.format(attr_name, attr_field)) + attr_value = input.get(attr_name) + input_params[attr_name] = attr_value + + plparams = cls.get_payload_parameters() + + # get node_type and node_metatype + # get request_path and request_data + node_type = getattr(cls, node_type) + node_meta_type = getattr(cls, node_meta_type) + request_path = getattr(cls, request_path) + + ret = cls.do_create( + node_type=node_type, node_metatype=node_meta_type, + request_path=request_path, request_data=input_params + ) + + return cls(**plparams) + + @classmethod + def do_create(cls, *args, **kwargs): + # get form + form_class = getattr(cls, 'django_form', None) + node_type = kwargs.get('node_type') + node_metatype = kwargs.get('node_metatype') + input_params = kwargs.get('input_params') + + request_factory = RequestFactory() + request_path = kwargs.get('request_path', '/') + request_data = kwargs.get('request_data', {}) + request = request_factory.post(request_path, data=request_data) + + ## code from role creation + form = form_class(request.POST) + if form.is_valid(): + try: + nh = helpers.form_to_unique_node_handle(request, form, + node_type, node_metatype) + except UniqueNodeError: + raise GraphQLError( + 'A {} with that name already exists.'.format(node_type) + ) + helpers.form_update_node(request.user, nh.handle_id, form) + #return redirect(nh.get_absolute_url()) + else: + # get the errors and return them + raise GraphQLError('Form errors: {}'.format(vars(form.errors))) + + class Meta: + abstract = False class UpdateNIMutation(AbstractNIMutation): - pass + @classmethod + def set_mutate_and_get_payload(cls): + # get operation and check for method overrides + mutate_function = cls.__dict__.get('edit_mutate_and_get_payload') + + # set mutate_and_get_payload + setattr(cls, 'mutate_and_get_payload', classmethod(mutate_function)) + + @classmethod + def edit_mutate_and_get_payload(cls, root, info, **input): + pass + + # to be implemented by the subclass + @classmethod + def do_edit(cls, *args, **kwargs): + raise Exception('The class {} doesn\'t implemet the \ + do_edit method'.format(cls)) + + class Meta: + abstract = False class DeleteNIMutation(AbstractNIMutation): - pass''' + @classmethod + def set_mutate_and_get_payload(cls): + # get operation and check for method overrides + mutate_function = cls.__dict__.get('delete_mutate_and_get_payload') + + # set mutate_and_get_payload + setattr(cls, 'mutate_and_get_payload', classmethod(mutate_function)) + + @classmethod + def delete_mutate_and_get_payload(cls, root, info, **input): + pass -class CreateNIRoleMutation(AbstractNIMutation): - django_form = NewRoleForm + # to be implemented by the subclass + @classmethod + def do_delete(cls, *args, **kwargs): + raise Exception('The class {} doesn\'t implemet the \ + do_delete method'.format(cls)) class Meta: abstract = False class NIMutationFactory(): - def __init_subclass__(cls, default_name, **kwargs): - pass + ''' + This class could have the methods create|update|delete_mutate_and_get_payload + implemented to override the default functionality, but it must implement + do_create|do_update|do_delete so these methods could be added to the generated + classes that would be part of the schema + ''' + + node_type = None + node_meta_type = None + request_path = None + + def __init_subclass__(cls, **kwargs): + cls._create_mutation = None + cls._update_mutation = None + cls._delete_mutation = None + # check defined form attributes + form = getattr(cls, 'form', None) + create_form = getattr(cls, 'create_form', None) + update_form = getattr(cls, 'update_form', None) -"""class RoleMutation(NIMutationFactory): - # only form | create_form and edit_form should be defined at the same time - form = 'NewRoleForm' # we'll get the input fields from the form, single form for Create/Edit - create_form = 'NewRoleForm' # use different forms to create - edit_form = 'EditRoleForm' # or to edit, both or none should be defined + 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 + + node_type = getattr(cls, 'node_type', None) + node_meta_type = getattr(cls, 'node_meta_type', None) + request_path = getattr(cls, 'request_path', None) + class_name = 'CreateNI{}Mutation'.format(node_type.capitalize()) + + attr_dict = { + 'django_form': create_form, + 'mutation_name': class_name, + 'node_type': node_type, + 'node_meta_type': node_meta_type, + 'request_path': request_path, + } + + cls._create_mutation = type( + class_name, + (CreateNIMutation,), + attr_dict, + ) + + class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) + attr_dict['django_form'] = update_form + attr_dict['mutation_name'] = class_name + + cls._update_mutation = type( + class_name, + (UpdateNIMutation,), + attr_dict, + ) + + class_name = 'DeleteNI{}Mutation'.format(node_type.capitalize()) + del attr_dict['django_form'] + attr_dict['mutation_name'] = class_name + + cls._delete_mutation = type( + class_name, + (DeleteNIMutation,), + attr_dict, + ) @classmethod - def create_mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None): - ''' - this method would override be the mutate_and_get_payload for the get_create_mutation - ''' - pass""" + 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 + +class NIRoleMutationFactory(NIMutationFactory): + node_type = 'role' + node_meta_type = 'Logical' + request_path = '/' + + create_form = NewRoleForm + update_form = EditRoleForm + +class NOCRootMutation(graphene.ObjectType): + create_role = NIRoleMutationFactory.get_create_mutation().Field() + #update_role = NIRoleMutationFactory.get_update_mutation().Field() + #delete_role = NIRoleMutationFactory.get_delete_mutation().Field() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 8aab46858..9bc85adb4 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -4,7 +4,7 @@ import graphene from .types import * -class Query(graphene.ObjectType): +class NOCRootQuery(graphene.ObjectType): node = NIRelayNode.Field() roles = graphene.List(RoleType, limit=graphene.Int()) groups = graphene.List(GroupType, limit=graphene.Int()) diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py index 7aa72e17a..e80af4e05 100644 --- a/src/niweb/niweb/schema.py +++ b/src/niweb/niweb/schema.py @@ -1,13 +1,20 @@ import graphene -from apps.noclook.schema import NOCSCHEMA_QUERIES, NOCSCHEMA_TYPES, NIRelayNode +from apps.noclook.schema import NOCSCHEMA_QUERIES, NOCSCHEMA_MUTATIONS,\ + NOCSCHEMA_TYPES, NIRelayNode -class Query(*NOCSCHEMA_QUERIES, graphene.ObjectType): +ALL_TYPES = NOCSCHEMA_TYPES # + OTHER_APP_TYPES +ALL_QUERIES = NOCSCHEMA_QUERIES +ALL_MUTATIONS = NOCSCHEMA_MUTATIONS + +class Query(*ALL_QUERIES, graphene.ObjectType): pass -ALL_TYPES = NOCSCHEMA_TYPES +class Mutation(*ALL_MUTATIONS, graphene.ObjectType): + pass schema = graphene.Schema( query=Query, + mutation=Mutation, auto_camelcase=False, types=ALL_TYPES ) From 25d56ff5984eb17bbe0f5fe683b828af59f14af1 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 29 Apr 2019 17:39:35 +0200 Subject: [PATCH 055/520] Mutation factory wip: First version of CreateMutation --- src/niweb/apps/noclook/schema/core.py | 4 + src/niweb/apps/noclook/schema/mutations.py | 204 +++++++++--------- src/niweb/apps/noclook/schema/types.py | 3 + .../noclook/tests/schema/test_mutations.py | 22 ++ .../apps/noclook/tests/schema/test_schema.py | 5 +- 5 files changed, 135 insertions(+), 103 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index a1e998aca..e34302559 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -3,6 +3,7 @@ import graphene import re +from apps.nerds.lib.consumer_util import get_user from collections import OrderedDict from graphene import relay from graphene_django import DjangoObjectType @@ -215,3 +216,6 @@ def __init_subclass_with_meta__( class Meta: model = NodeHandle interfaces = (NIRelayNode, ) + +def get_logger_user(): + return get_user() diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 67f5ba2ad..e39e5584e 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -3,16 +3,20 @@ import graphene +from apps.noclook import helpers +from apps.noclook.forms import * from django import forms from django.test import RequestFactory from graphene import relay from graphql import GraphQLError -from apps.noclook import helpers -from apps.noclook.forms import * +from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible from pprint import pprint -# the payload fields should be defined as it isn't something we can guess +from .types import * + class AbstractNIMutation(relay.ClientIDMutation): + nodehandle = graphene.Field(NodeHandleType, required=True) # the type should be replaced + @classmethod def __init_subclass_with_meta__( cls, output=None, input_fields=None, arguments=None, name=None, **options @@ -20,38 +24,37 @@ def __init_subclass_with_meta__( ''' In this method we'll build an input nested object using the form ''' # read form - django_form = getattr(cls, 'django_form', None) - name = getattr(cls, 'mutation_name', None) + ni_metaclass = getattr(cls, 'NIMetaClass') + django_form = getattr(ni_metaclass, 'django_form', None) + mutation_name = getattr(ni_metaclass, 'mutation_name', cls.__name__) # 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 name, field in class_field.items(): + 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: if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): if field not in django_form.Meta.exclude: - inner_fields[name] = graphene_field + inner_fields[field_name] = graphene_field else: - inner_fields[name] = graphene_field + inner_fields[field_name] = graphene_field else: - if cls.__name__ in 'Delete': - raise Exception(cls.__name__) # this would set a handle_id for the input param + '''assert 'Delete' in cls.__name__, \ + '{} is not a Delete Mutation '.format(cls.__name__)''' inner_fields['handle_id'] = forms.IntegerField(required=True) # add Input attribute to class inner_class = type('Input', (object,), inner_fields) setattr(cls, 'Input', inner_class) - cls.set_mutate_and_get_payload() - super(AbstractNIMutation, cls).__init_subclass_with_meta__( - output, input_fields, arguments, name, **options + output, inner_fields, arguments, name=mutation_name, **options ) @classmethod @@ -91,7 +94,8 @@ def form_to_graphene_field(cls, form_field): graphene_field = graphene.String(**graph_kwargs) else: graphene_field = graphene.String(**graph_kwargs) - # fields left out to implement + + ### fields to be implement: ### # IPAddrField (CharField) # JSONField (CharField) # NodeChoiceField (ModelChoiceField) @@ -103,110 +107,89 @@ def form_to_graphene_field(cls, form_field): return graphene_field - # to be implemented by the subclass - @classmethod - def get_payload_parameters(cls, *args, **kwargs): - '''This method should be implemented in the concerning subclasses - ''' - raise Exception('The class {} doesn\'t implemet the get_payload_parameters method'.format(cls)) - @classmethod - def set_mutate_and_get_payload(cls): - '''This method should be implemented in the concerning subclasses - ''' - raise Exception('The class {} doesn\'t implemet the set_mutate_and_get_payload method'.format(cls.__name__)) + def get_type(cls): + ni_metaclass = getattr(cls, 'NIMetaClass') + return getattr(ni_metaclass, 'typeclass') class Meta: abstract = True class CreateNIMutation(AbstractNIMutation): - node_type = None - node_meta_type = None - request_path = None + class NIMetaClass: + node_type = None + node_meta_type = None + request_path = None @classmethod - def set_mutate_and_get_payload(cls): - # get operation and check for method overrides - mutate_function = cls.__dict__.get('create_mutate_and_get_payload') - - # set mutate_and_get_payload - setattr(cls, 'mutate_and_get_payload', classmethod(mutate_function)) - - @classmethod - def get_payload_parameters(cls): - for attr_name, attr_value in cls.__dict__.items(): - if attr_name != 'Input': - pass - - @classmethod - def create_mutate_and_get_payload(cls, root, info, **input): + def mutate_and_get_payload(cls, root, info, **input): # get input values input_class = getattr(cls, 'Input', None) input_params = {} if input_class: for attr_name, attr_field in input_class.__dict__.items(): - print('Attribute {} value {}'.format(attr_name, attr_field)) attr_value = input.get(attr_name) input_params[attr_name] = attr_value - plparams = cls.get_payload_parameters() - # get node_type and node_metatype # get request_path and request_data - node_type = getattr(cls, node_type) - node_meta_type = getattr(cls, node_meta_type) - request_path = getattr(cls, request_path) + ni_metaclass = getattr(cls, 'NIMetaClass') + form_class = getattr(ni_metaclass, 'django_form', None) + node_type = getattr(ni_metaclass, 'node_type') + node_meta_type = getattr(ni_metaclass, 'node_meta_type') + request_path = getattr(ni_metaclass, 'request_path', '/') - ret = cls.do_create( - node_type=node_type, node_metatype=node_meta_type, - request_path=request_path, request_data=input_params - ) + request_factory = RequestFactory() + request = request_factory.post(request_path, data=input_params) + request.user = get_logger_user() - return cls(**plparams) + ret = cls.do_create(request, form_class=form_class, node_type=node_type, + node_meta_type=node_meta_type) - @classmethod - def do_create(cls, *args, **kwargs): - # get form - form_class = getattr(cls, 'django_form', None) - node_type = kwargs.get('node_type') - node_metatype = kwargs.get('node_metatype') - input_params = kwargs.get('input_params') + return cls(nodehandle=ret) - request_factory = RequestFactory() - request_path = kwargs.get('request_path', '/') - request_data = kwargs.get('request_data', {}) - request = request_factory.post(request_path, data=request_data) + @classmethod + def do_create(cls, request, **kwargs): + form_class = kwargs.get('form_class') + node_type = kwargs.get('node_type') + node_meta_type = kwargs.get('node_meta_type') ## code from role creation form = form_class(request.POST) if form.is_valid(): try: nh = helpers.form_to_unique_node_handle(request, form, - node_type, node_metatype) + node_type, node_meta_type) except UniqueNodeError: raise GraphQLError( 'A {} with that name already exists.'.format(node_type) ) helpers.form_update_node(request.user, nh.handle_id, form) - #return redirect(nh.get_absolute_url()) + return nh else: # get the errors and return them - raise GraphQLError('Form errors: {}'.format(vars(form.errors))) + raise GraphQLError('Form errors: {}'.format(form)) + +class CreateRoleNIMutation(CreateNIMutation): + nodehandle = graphene.Field(RoleType, required=True) + + class NIMetaClass: + node_type = 'role' + node_meta_type = 'Logical' + request_path = '/' + django_form = NewRoleForm class Meta: abstract = False class UpdateNIMutation(AbstractNIMutation): - @classmethod - def set_mutate_and_get_payload(cls): - # get operation and check for method overrides - mutate_function = cls.__dict__.get('edit_mutate_and_get_payload') - - # set mutate_and_get_payload - setattr(cls, 'mutate_and_get_payload', classmethod(mutate_function)) + class NIMetaClass: + node_type = None + node_meta_type = None + request_path = None @classmethod - def edit_mutate_and_get_payload(cls, root, info, **input): + def mutate_and_get_payload(cls, root, info, **input): pass # to be implemented by the subclass @@ -219,16 +202,13 @@ class Meta: abstract = False class DeleteNIMutation(AbstractNIMutation): - @classmethod - def set_mutate_and_get_payload(cls): - # get operation and check for method overrides - mutate_function = cls.__dict__.get('delete_mutate_and_get_payload') - - # set mutate_and_get_payload - setattr(cls, 'mutate_and_get_payload', classmethod(mutate_function)) + class NIMetaClass: + node_type = None + node_meta_type = None + request_path = None @classmethod - def delete_mutate_and_get_payload(cls, root, info, **input): + def mutate_and_get_payload(cls, root, info, **input): pass # to be implemented by the subclass @@ -253,15 +233,24 @@ class NIMutationFactory(): request_path = None 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 - form = getattr(cls, 'form', None) - create_form = getattr(cls, 'create_form', None) - update_form = getattr(cls, 'update_form', None) - + 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) + node_type = getattr(ni_metaclass, 'node_type', None) + node_meta_type = getattr(ni_metaclass, 'node_meta_type', None) + request_path = getattr(ni_metaclass, 'request_path', None) + nodetype = getattr(ni_metaclass, 'nodetype', NodeHandleType) + + # 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 {}'\ @@ -271,11 +260,8 @@ def __init_subclass__(cls, **kwargs): create_form = form update_form = form - node_type = getattr(cls, 'node_type', None) - node_meta_type = getattr(cls, 'node_meta_type', None) - request_path = getattr(cls, 'request_path', None) + # create mutations class_name = 'CreateNI{}Mutation'.format(node_type.capitalize()) - attr_dict = { 'django_form': create_form, 'mutation_name': class_name, @@ -284,30 +270,43 @@ def __init_subclass__(cls, **kwargs): 'request_path': request_path, } + create_metaclass = type(metaclass_name, (object,), attr_dict) + cls._create_mutation = type( class_name, (CreateNIMutation,), - attr_dict, + { + nh_field: graphene.Field(nodetype, required=True), + metaclass_name: create_metaclass, + }, ) class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) attr_dict['django_form'] = update_form attr_dict['mutation_name'] = class_name + update_metaclass = type(metaclass_name, (object,), attr_dict) cls._update_mutation = type( class_name, (UpdateNIMutation,), - attr_dict, + { + nh_field: graphene.Field(nodetype, required=True), + metaclass_name: update_metaclass, + }, ) class_name = 'DeleteNI{}Mutation'.format(node_type.capitalize()) del attr_dict['django_form'] attr_dict['mutation_name'] = class_name + delete_metaclass = type(metaclass_name, (object,), attr_dict) cls._delete_mutation = type( class_name, (DeleteNIMutation,), - attr_dict, + { + nh_field: graphene.Field(nodetype, required=True), + metaclass_name: delete_metaclass, + }, ) @classmethod @@ -323,14 +322,17 @@ def get_delete_mutation(cls, *args, **kwargs): return cls._delete_mutation class NIRoleMutationFactory(NIMutationFactory): - node_type = 'role' - node_meta_type = 'Logical' - request_path = '/' - create_form = NewRoleForm update_form = EditRoleForm + class NIMetaClass: + node_type = 'role' + node_meta_type = 'Logical' + request_path = '/' + form = NewRoleForm + nodetype = RoleType + class NOCRootMutation(graphene.ObjectType): create_role = NIRoleMutationFactory.get_create_mutation().Field() - #update_role = NIRoleMutationFactory.get_update_mutation().Field() - #delete_role = NIRoleMutationFactory.get_delete_mutation().Field() + update_role = NIRoleMutationFactory.get_update_mutation().Field() + delete_role = NIRoleMutationFactory.get_delete_mutation().Field() diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 62bd2ec14..ff4d960c8 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -25,6 +25,9 @@ def resolve_roles_list(self, info, **kwargs): return ret +class NodeHandleType(NIObjectType): + pass + class RoleType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index e69de29bb..ccc23c3f1 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from . import Neo4jGraphQLTest +from niweb.schema import schema + +class QueryTest(Neo4jGraphQLTest): + def test_get_contacts(self): + query = ''' + mutation create_test_role { + create_role(input: {name: "New test role"}){ + nodehandle { + id + name + } + clientMutationId + } + } + ''' + + result = schema.execute(query) + assert not result.errors diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 464467a38..363058f29 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -27,7 +27,7 @@ def test_get_contacts(self): expected = { 'contacts': [ OrderedDict([ - ('handle_id', '4'), + ('handle_id', '13'), ('name', 'John Smith'), ('first_name', 'John'), ('last_name', 'Smith'), @@ -35,7 +35,7 @@ def test_get_contacts(self): ('member_of_groups', [OrderedDict([('name', 'group2')])]), ]), OrderedDict([ - ('handle_id', '3'), + ('handle_id', '12'), ('name', 'Jane Doe'), ('first_name', 'Jane'), ('last_name', 'Doe'), @@ -45,5 +45,6 @@ def test_get_contacts(self): ] } result = schema.execute(query) + assert not result.errors assert result.data == expected From c4682bd8437aadeed7cdbe484abea3c39a9f6921 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 30 Apr 2019 13:00:45 +0200 Subject: [PATCH 056/520] Update mutation and parametrization of the class creation in the factory --- src/niweb/apps/noclook/schema/mutations.py | 162 ++++++++++-------- .../apps/noclook/tests/schema/__init__.py | 12 ++ .../noclook/tests/schema/test_mutations.py | 67 +++++++- 3 files changed, 171 insertions(+), 70 deletions(-) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index e39e5584e..c0426a31f 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -24,9 +24,10 @@ def __init_subclass_with_meta__( ''' In this method we'll build an input nested object using the form ''' # read form - ni_metaclass = getattr(cls, 'NIMetaClass') - django_form = getattr(ni_metaclass, 'django_form', None) + ni_metaclass = getattr(cls, 'NIMetaClass') + django_form = getattr(ni_metaclass, 'django_form', None) mutation_name = getattr(ni_metaclass, 'mutation_name', cls.__name__) + is_create = getattr(ni_metaclass, 'is_create', False) # build fields into Input inner_fields = {} @@ -43,11 +44,10 @@ def __init_subclass_with_meta__( inner_fields[field_name] = graphene_field else: inner_fields[field_name] = graphene_field - else: - # this would set a handle_id for the input param - '''assert 'Delete' in cls.__name__, \ - '{} is not a Delete Mutation '.format(cls.__name__)''' - inner_fields['handle_id'] = forms.IntegerField(required=True) + + # add handle_id + if not is_create: + inner_fields['handle_id'] = graphene.Int(required=True) # add Input attribute to class inner_class = type('Input', (object,), inner_fields) @@ -112,17 +112,21 @@ def get_type(cls): ni_metaclass = getattr(cls, 'NIMetaClass') return getattr(ni_metaclass, 'typeclass') - class Meta: - abstract = True - -class CreateNIMutation(AbstractNIMutation): - class NIMetaClass: - node_type = None - node_meta_type = None - request_path = None - @classmethod - def mutate_and_get_payload(cls, root, info, **input): + def from_input_to_request(cls, **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) + node_type = getattr(ni_metaclass, 'node_type') + node_meta_type = getattr(ni_metaclass, 'node_meta_type') + request_path = getattr(ni_metaclass, 'request_path', '/') + is_create = getattr(ni_metaclass, 'is_create', False) + # get input values input_class = getattr(cls, 'Input', None) input_params = {} @@ -131,25 +135,40 @@ def mutate_and_get_payload(cls, root, info, **input): attr_value = input.get(attr_name) input_params[attr_name] = attr_value - # get node_type and node_metatype - # get request_path and request_data - ni_metaclass = getattr(cls, 'NIMetaClass') - form_class = getattr(ni_metaclass, 'django_form', None) - node_type = getattr(ni_metaclass, 'node_type') - node_meta_type = getattr(ni_metaclass, 'node_meta_type') - request_path = getattr(ni_metaclass, 'request_path', '/') + if not is_create: + input_params['handle_id'] = input.get('handle_id') + # forge request request_factory = RequestFactory() request = request_factory.post(request_path, data=input_params) request.user = get_logger_user() - ret = cls.do_create(request, form_class=form_class, node_type=node_type, - node_meta_type=node_meta_type) + 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): + reqinput = cls.from_input_to_request(**input) + ret = cls.do_request(reqinput[0], **reqinput[1]) return cls(nodehandle=ret) + class Meta: + abstract = True + +class CreateNIMutation(AbstractNIMutation): + ''' + This class is used by the Mutation factory but it could be used as the + superclass of a manualy coded class in case it's needed. + ''' + class NIMetaClass: + node_type = None + node_meta_type = None + request_path = None + is_create = True + @classmethod - def do_create(cls, request, **kwargs): + def do_request(cls, request, **kwargs): form_class = kwargs.get('form_class') node_type = kwargs.get('node_type') node_meta_type = kwargs.get('node_meta_type') @@ -170,18 +189,6 @@ def do_create(cls, request, **kwargs): # get the errors and return them raise GraphQLError('Form errors: {}'.format(form)) -class CreateRoleNIMutation(CreateNIMutation): - nodehandle = graphene.Field(RoleType, required=True) - - class NIMetaClass: - node_type = 'role' - node_meta_type = 'Logical' - request_path = '/' - django_form = NewRoleForm - - class Meta: - abstract = False - class UpdateNIMutation(AbstractNIMutation): class NIMetaClass: node_type = None @@ -189,17 +196,22 @@ class NIMetaClass: request_path = None @classmethod - def mutate_and_get_payload(cls, root, info, **input): - pass - - # to be implemented by the subclass - @classmethod - def do_edit(cls, *args, **kwargs): - raise Exception('The class {} doesn\'t implemet the \ - do_edit method'.format(cls)) - - class Meta: - abstract = False + def do_request(cls, request, **kwargs): + form_class = kwargs.get('form_class') + node_type = kwargs.get('node_type') + node_meta_type = kwargs.get('node_meta_type') + handle_id = request.POST.get('handle_id') + + nh, nodehandler = helpers.get_nh_node(handle_id) + if request.POST: + form = form_class(request.POST) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, nodehandler.handle_id, form) + return nh + else: + # get the errors and return them + raise GraphQLError('Form errors: {}'.format(form)) class DeleteNIMutation(AbstractNIMutation): class NIMetaClass: @@ -208,30 +220,25 @@ class NIMetaClass: request_path = None @classmethod - def mutate_and_get_payload(cls, root, info, **input): + def do_request(cls, request, **kwargs): pass - # to be implemented by the subclass - @classmethod - def do_delete(cls, *args, **kwargs): - raise Exception('The class {} doesn\'t implemet the \ - do_delete method'.format(cls)) - - class Meta: - abstract = False - class NIMutationFactory(): ''' - This class could have the methods create|update|delete_mutate_and_get_payload - implemented to override the default functionality, but it must implement - do_create|do_update|do_delete so these methods could be added to the generated - classes that would be part of the schema + 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' @@ -268,13 +275,14 @@ def __init_subclass__(cls, **kwargs): 'node_type': node_type, 'node_meta_type': node_meta_type, 'request_path': request_path, + 'is_create': True, } create_metaclass = type(metaclass_name, (object,), attr_dict) cls._create_mutation = type( class_name, - (CreateNIMutation,), + (cls.create_mutation_class,), { nh_field: graphene.Field(nodetype, required=True), metaclass_name: create_metaclass, @@ -282,13 +290,14 @@ def __init_subclass__(cls, **kwargs): ) class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) - attr_dict['django_form'] = update_form + attr_dict['django_form'] = update_form attr_dict['mutation_name'] = class_name + attr_dict['is_create'] = False update_metaclass = type(metaclass_name, (object,), attr_dict) cls._update_mutation = type( class_name, - (UpdateNIMutation,), + (cls.update_mutation_class,), { nh_field: graphene.Field(nodetype, required=True), metaclass_name: update_metaclass, @@ -302,7 +311,7 @@ def __init_subclass__(cls, **kwargs): cls._delete_mutation = type( class_name, - (DeleteNIMutation,), + (cls.delete_mutation_class,), { nh_field: graphene.Field(nodetype, required=True), metaclass_name: delete_metaclass, @@ -332,6 +341,23 @@ class NIMetaClass: form = NewRoleForm nodetype = RoleType + class Meta: + abstract = False + +class CreateRoleNIMutation(CreateNIMutation): + '''This class is not used but left out as documentation in the case that as + finer grain of control is needed''' + nodehandle = graphene.Field(RoleType, required=True) + + class NIMetaClass: + node_type = 'role' + node_meta_type = 'Logical' + request_path = '/' + django_form = NewRoleForm + + class Meta: + abstract = False + class NOCRootMutation(graphene.ObjectType): create_role = NIRoleMutationFactory.get_create_mutation().Field() update_role = NIRoleMutationFactory.get_update_mutation().Field() diff --git a/src/niweb/apps/noclook/tests/schema/__init__.py b/src/niweb/apps/noclook/tests/schema/__init__.py index 27583d0a3..857b319df 100644 --- a/src/niweb/apps/noclook/tests/schema/__init__.py +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +from django.db import connection + +from apps.noclook.models import NodeHandle from ..neo4j_base import NeoTestCase class Neo4jGraphQLTest(NeoTestCase): @@ -44,3 +47,12 @@ def setUp(self): contact2.get_node().add_role(role2.handle_id) contact2.get_node().add_group(group2.handle_id) contact2.get_node().add_organization(organization2.handle_id) + + 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_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index ccc23c3f1..6f40f9fd4 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +from collections import OrderedDict from . import Neo4jGraphQLTest from niweb.schema import schema class QueryTest(Neo4jGraphQLTest): - def test_get_contacts(self): + def test_role(self): + ## create ## query = ''' mutation create_test_role { create_role(input: {name: "New test role"}){ nodehandle { - id + handle_id name } clientMutationId @@ -18,5 +20,66 @@ def test_get_contacts(self): } ''' + expected = { + 'create_role': [ + OrderedDict([ + ('nodehandle', + OrderedDict([ + ('handle_id', '9'), + ('name', 'New test role'), + ])), + ('clientMutationId', None), + ]), + ] + } + + expected = OrderedDict([ + ('create_role', + OrderedDict([ + ('nodehandle', + OrderedDict([ + ('handle_id', '9'), + ('name', 'New test role') + ])), + ('clientMutationId', None) + ]) + ) + ]) + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + ## update ## + role_handle_id = int(result.data['create_role']['nodehandle']['handle_id']) + query = """ + mutation update_test_role { + update_role(input: {handle_id: 9, name: "A test role"}){ + nodehandle { + id + name + } + clientMutationId + } + } + """ + + result = schema.execute(query) + assert not result.errors + + """## delete ## + query = ''' + mutation create_test_role { + delete_role(input: {handle_id: {}}){ + nodehandle + clientMutationId + } + } + '''.format(role_handle_id) + + result = schema.execute(query) + import pprint + raise Exception(pprint.pprint(result.data)) + assert not result.errors""" From 18e68672d91bfd25b4d19e8943a8f188f389fb71 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 30 Apr 2019 13:21:25 +0200 Subject: [PATCH 057/520] Delete mutation part and test completion --- src/niweb/apps/noclook/schema/mutations.py | 10 +++- .../noclook/tests/schema/test_mutations.py | 54 ++++++++++--------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index c0426a31f..2feec4001 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -214,6 +214,8 @@ def do_request(cls, request, **kwargs): raise GraphQLError('Form errors: {}'.format(form)) class DeleteNIMutation(AbstractNIMutation): + nodehandle = graphene.Boolean(required=True) + class NIMetaClass: node_type = None node_meta_type = None @@ -221,7 +223,12 @@ class NIMetaClass: @classmethod def do_request(cls, request, **kwargs): - pass + handle_id = request.POST.get('handle_id') + + nh, node = helpers.get_nh_node(handle_id) + helpers.delete_node(request.user, node.handle_id) + + return True class NIMutationFactory(): ''' @@ -313,7 +320,6 @@ def __init_subclass__(cls, **kwargs): class_name, (cls.delete_mutation_class,), { - nh_field: graphene.Field(nodetype, required=True), metaclass_name: delete_metaclass, }, ) diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index 6f40f9fd4..64530cc7e 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -20,19 +20,6 @@ def test_role(self): } ''' - expected = { - 'create_role': [ - OrderedDict([ - ('nodehandle', - OrderedDict([ - ('handle_id', '9'), - ('name', 'New test role'), - ])), - ('clientMutationId', None), - ]), - ] - } - expected = OrderedDict([ ('create_role', OrderedDict([ @@ -46,7 +33,6 @@ def test_role(self): ) ]) - result = schema.execute(query) assert not result.errors @@ -58,7 +44,7 @@ def test_role(self): mutation update_test_role { update_role(input: {handle_id: 9, name: "A test role"}){ nodehandle { - id + handle_id name } clientMutationId @@ -66,20 +52,40 @@ def test_role(self): } """ + expected = OrderedDict([ + ('update_role', + OrderedDict([ + ('nodehandle', + OrderedDict([ + ('handle_id', '9'), + ('name', 'A test role') + ])), + ('clientMutationId', None) + ]) + ) + ]) + result = schema.execute(query) assert not result.errors + assert result.data == expected - """## delete ## - query = ''' - mutation create_test_role { - delete_role(input: {handle_id: {}}){ + ## delete ## + query = """ + mutation delete_test_role { + delete_role(input: {handle_id: 9}){ nodehandle - clientMutationId } } - '''.format(role_handle_id) + """ + + expected = OrderedDict([ + ('delete_role', + OrderedDict([ + ('nodehandle', True), + ]) + ) + ]) result = schema.execute(query) - import pprint - raise Exception(pprint.pprint(result.data)) - assert not result.errors""" + assert not result.errors + assert result.data == expected From c5b9581412de8e07e4abb40024e7b70a69a91160 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 11 Apr 2019 14:07:09 +0200 Subject: [PATCH 058/520] Merge pull request #7 from PaKZer0/master SRI feedback tweaks (cherry picked from commit 319ef7011d770b3acfce13f95917525baec7157a) From 099e7109a23e39be01c55cb4873fae2b6c9338b9 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 11 Apr 2019 15:05:19 +0200 Subject: [PATCH 059/520] Added .jenkins.yaml (cherry picked from commit 459d76b31ce57e7c67c7ae68dc4c0b4c6d83d8b4) --- .jenkins.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .jenkins.yaml diff --git a/.jenkins.yaml b/.jenkins.yaml new file mode 100644 index 000000000..a4f7d0bb1 --- /dev/null +++ b/.jenkins.yaml @@ -0,0 +1,7 @@ +# TODO: Run tests before triggering downstream +builders: + - script +downstream: + - docker-ni +script: + - echo "Bogus script to trigger downstream" From 3c2a2633a6bfb58c652ee4e7a2078a4ce0158302 Mon Sep 17 00:00:00 2001 From: bereware Date: Thu, 25 Apr 2019 08:56:12 +0200 Subject: [PATCH 060/520] show debugtoolbar and developer tools (cherry picked from commit 2a29982a73b7e22a9b977497d2f97463f5dd96c0) --- requirements/dev.txt | 2 ++ src/niweb/niweb/settings/dev.py | 12 ++++++++---- src/niweb/niweb/urls.py | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index b980d7262..2c2f24eab 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,3 +2,5 @@ ipython django-debug-toolbar django-nose<1.5 +django-extensions +ipdb diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 2dac6dad5..79f474b00 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -70,18 +70,22 @@ # See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation INSTALLED_APPS += ( 'debug_toolbar', - 'django_nose' + 'django_nose', + 'django_extensions' ) # See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation INTERNAL_IPS = ('127.0.0.1',) # See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation -MIDDLEWARE_CLASSES += ( +MIDDLEWARE_CLASSES = ( 'debug_toolbar.middleware.DebugToolbarMiddleware', -) +) + MIDDLEWARE_CLASSES -DEBUG_TOOLBAR_CONFIG = {} +DEBUG_TOOLBAR_CONFIG = { + 'INTERCEPT_REDIRECTS': False, + 'SHOW_TOOLBAR_CALLBACK': lambda x: True, +} ########## END TOOLBAR CONFIGURATION ########## SECRET CONFIGURATION diff --git a/src/niweb/niweb/urls.py b/src/niweb/niweb/urls.py index 761491645..ea3c6cd29 100644 --- a/src/niweb/niweb/urls.py +++ b/src/niweb/niweb/urls.py @@ -74,6 +74,12 @@ def if_installed(appname, *args, **kwargs): url(r'^saml2/', include('djangosaml2.urls')), ] +if settings.DEBUG: + import debug_toolbar + urlpatterns = [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns + urlpatterns += [ # Tastypie URLs url(r'^api/', include(v1_api.urls)), From 4adc463740c21f203bf1ed33dd34a8aeaea2019f Mon Sep 17 00:00:00 2001 From: bereware Date: Thu, 25 Apr 2019 08:59:17 +0200 Subject: [PATCH 061/520] fix - give to the method the model of the organization (cherry picked from commit 8d56acdf8627a654217133989265631be9e7a20c) --- src/niweb/apps/noclook/views/edit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 43752e016..2b15a0015 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -975,9 +975,9 @@ def edit_organization(request, handle_id): if contact_data: if isinstance(contact_data, six.string_types): if contact_data: - helpers.create_contact_role_for_organization(request.user, nh, contact_data, field[1]) + helpers.create_contact_role_for_organization(request.user, organization, contact_data, field[1]) else: - helpers.link_contact_role_for_organization(request.user, nh, contact_data, field[1]) + helpers.link_contact_role_for_organization(request.user, organization, contact_data, field[1]) # Set child organizations if form.cleaned_data['relationship_parent_of']: From dbb76a44a085b8d1b2a14436884c6f1d085f18d2 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 25 Apr 2019 10:35:46 +0200 Subject: [PATCH 062/520] Merge pull request #8 from SUNET/anavarro Fix error when edit a organization and add developer tools (cherry picked from commit ec4e4a306c51966d5d72d12e5d812a45ae9ee195) From a5d1bbd29432628be4b20132a95534ae867547b9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 3 May 2019 10:04:15 +0200 Subject: [PATCH 063/520] Reconstruction of the code --- src/niweb/apps/noclook/schema/core.py | 330 ++++++++++++++++++++- src/niweb/apps/noclook/schema/mutations.py | 329 +------------------- src/niweb/apps/noclook/schema/types.py | 3 - 3 files changed, 329 insertions(+), 333 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index e34302559..adae6856c 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -4,12 +4,16 @@ import graphene import re from apps.nerds.lib.consumer_util import get_user +from apps.noclook import helpers +from apps.noclook.models import NodeType, NodeHandle from collections import OrderedDict +from django import forms +from django.test import RequestFactory from graphene import relay from graphene_django import DjangoObjectType from graphene_django.types import DjangoObjectTypeOptions - -from ..models import * +from graphql import GraphQLError +from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible class NIRelayNode(relay.Node): ''' @@ -217,5 +221,327 @@ class Meta: model = NodeHandle interfaces = (NIRelayNode, ) +class AbstractNIMutation(relay.ClientIDMutation): + nodehandle = graphene.Field(NIObjectType, required=True) # the type should be replaced + + @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') + django_form = getattr(ni_metaclass, 'django_form', None) + mutation_name = getattr(ni_metaclass, 'mutation_name', cls.__name__) + is_create = getattr(ni_metaclass, 'is_create', False) + + # 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: + if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): + if field not in django_form.Meta.exclude: + inner_fields[field_name] = graphene_field + else: + inner_fields[field_name] = graphene_field + + # add handle_id + if not is_create: + inner_fields['handle_id'] = graphene.Int(required=True) + + # add Input attribute to class + inner_class = type('Input', (object,), inner_fields) + setattr(cls, 'Input', inner_class) + + super(AbstractNIMutation, cls).__init_subclass_with_meta__( + output, inner_fields, arguments, name=mutation_name, **options + ) + + @classmethod + def form_to_graphene_field(cls, form_field): + '''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 = graphene.String(**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.NullBooleanField): + 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) + + ### 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, **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) + node_type = getattr(ni_metaclass, 'node_type') + node_meta_type = getattr(ni_metaclass, 'node_meta_type') + request_path = getattr(ni_metaclass, 'request_path', '/') + is_create = getattr(ni_metaclass, 'is_create', False) + + # get input values + 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) + input_params[attr_name] = attr_value + + if not is_create: + input_params['handle_id'] = input.get('handle_id') + + # forge request + request_factory = RequestFactory() + request = request_factory.post(request_path, data=input_params) + request.user = get_logger_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): + reqinput = cls.from_input_to_request(**input) + ret = cls.do_request(reqinput[0], **reqinput[1]) + + return cls(nodehandle=ret) + + class Meta: + abstract = True + +class CreateNIMutation(AbstractNIMutation): + ''' + This class is used by the Mutation factory but it could be used as the + superclass of a manualy coded class in case it's needed. + ''' + class NIMetaClass: + node_type = None + node_meta_type = None + request_path = None + is_create = True + + @classmethod + def do_request(cls, request, **kwargs): + form_class = kwargs.get('form_class') + node_type = kwargs.get('node_type') + node_meta_type = kwargs.get('node_meta_type') + + ## code from role creation + form = form_class(request.POST) + if form.is_valid(): + try: + nh = helpers.form_to_unique_node_handle(request, form, + node_type, node_meta_type) + except UniqueNodeError: + raise GraphQLError( + 'A {} with that name already exists.'.format(node_type) + ) + helpers.form_update_node(request.user, nh.handle_id, form) + return nh + else: + # get the errors and return them + raise GraphQLError('Form errors: {}'.format(form)) + +class UpdateNIMutation(AbstractNIMutation): + class NIMetaClass: + node_type = None + node_meta_type = None + request_path = None + + @classmethod + def do_request(cls, request, **kwargs): + form_class = kwargs.get('form_class') + node_type = kwargs.get('node_type') + node_meta_type = kwargs.get('node_meta_type') + handle_id = request.POST.get('handle_id') + + nh, nodehandler = helpers.get_nh_node(handle_id) + if request.POST: + form = form_class(request.POST) + if form.is_valid(): + # Generic node update + helpers.form_update_node(request.user, nodehandler.handle_id, form) + return nh + else: + # get the errors and return them + raise GraphQLError('Form errors: {}'.format(form)) + +class DeleteNIMutation(AbstractNIMutation): + nodehandle = graphene.Boolean(required=True) + + class NIMetaClass: + node_type = None + node_meta_type = None + request_path = None + + @classmethod + def do_request(cls, request, **kwargs): + handle_id = request.POST.get('handle_id') + + nh, node = helpers.get_nh_node(handle_id) + helpers.delete_node(request.user, node.handle_id) + + return True + +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) + node_type = getattr(ni_metaclass, 'node_type', None) + node_meta_type = getattr(ni_metaclass, 'node_meta_type', None) + request_path = getattr(ni_metaclass, 'request_path', None) + nodetype = getattr(ni_metaclass, 'nodetype', NIObjectType) + + # 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 = 'CreateNI{}Mutation'.format(node_type.capitalize()) + attr_dict = { + 'django_form': create_form, + 'mutation_name': class_name, + 'node_type': node_type, + 'node_meta_type': node_meta_type, + 'request_path': request_path, + 'is_create': True, + } + + create_metaclass = type(metaclass_name, (object,), attr_dict) + + cls._create_mutation = type( + class_name, + (cls.create_mutation_class,), + { + nh_field: graphene.Field(nodetype, required=True), + metaclass_name: create_metaclass, + }, + ) + + class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) + attr_dict['django_form'] = update_form + attr_dict['mutation_name'] = class_name + attr_dict['is_create'] = False + update_metaclass = type(metaclass_name, (object,), attr_dict) + + cls._update_mutation = type( + class_name, + (cls.update_mutation_class,), + { + nh_field: graphene.Field(nodetype, required=True), + metaclass_name: update_metaclass, + }, + ) + + class_name = 'DeleteNI{}Mutation'.format(node_type.capitalize()) + del attr_dict['django_form'] + attr_dict['mutation_name'] = class_name + delete_metaclass = type(metaclass_name, (object,), attr_dict) + + cls._delete_mutation = type( + class_name, + (cls.delete_mutation_class,), + { + metaclass_name: delete_metaclass, + }, + ) + + @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 + def get_logger_user(): return get_user() diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 2feec4001..9363d7395 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -5,337 +5,10 @@ from apps.noclook import helpers from apps.noclook.forms import * -from django import forms -from django.test import RequestFactory -from graphene import relay -from graphql import GraphQLError -from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible -from pprint import pprint +from .core import NIMutationFactory, CreateNIMutation from .types import * -class AbstractNIMutation(relay.ClientIDMutation): - nodehandle = graphene.Field(NodeHandleType, required=True) # the type should be replaced - - @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') - django_form = getattr(ni_metaclass, 'django_form', None) - mutation_name = getattr(ni_metaclass, 'mutation_name', cls.__name__) - is_create = getattr(ni_metaclass, 'is_create', False) - - # 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: - if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): - if field not in django_form.Meta.exclude: - inner_fields[field_name] = graphene_field - else: - inner_fields[field_name] = graphene_field - - # add handle_id - if not is_create: - inner_fields['handle_id'] = graphene.Int(required=True) - - # add Input attribute to class - inner_class = type('Input', (object,), inner_fields) - setattr(cls, 'Input', inner_class) - - super(AbstractNIMutation, cls).__init_subclass_with_meta__( - output, inner_fields, arguments, name=mutation_name, **options - ) - - @classmethod - def form_to_graphene_field(cls, form_field): - '''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 = graphene.String(**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.NullBooleanField): - 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) - - ### 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, **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) - node_type = getattr(ni_metaclass, 'node_type') - node_meta_type = getattr(ni_metaclass, 'node_meta_type') - request_path = getattr(ni_metaclass, 'request_path', '/') - is_create = getattr(ni_metaclass, 'is_create', False) - - # get input values - 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) - input_params[attr_name] = attr_value - - if not is_create: - input_params['handle_id'] = input.get('handle_id') - - # forge request - request_factory = RequestFactory() - request = request_factory.post(request_path, data=input_params) - request.user = get_logger_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): - reqinput = cls.from_input_to_request(**input) - ret = cls.do_request(reqinput[0], **reqinput[1]) - - return cls(nodehandle=ret) - - class Meta: - abstract = True - -class CreateNIMutation(AbstractNIMutation): - ''' - This class is used by the Mutation factory but it could be used as the - superclass of a manualy coded class in case it's needed. - ''' - class NIMetaClass: - node_type = None - node_meta_type = None - request_path = None - is_create = True - - @classmethod - def do_request(cls, request, **kwargs): - form_class = kwargs.get('form_class') - node_type = kwargs.get('node_type') - node_meta_type = kwargs.get('node_meta_type') - - ## code from role creation - form = form_class(request.POST) - if form.is_valid(): - try: - nh = helpers.form_to_unique_node_handle(request, form, - node_type, node_meta_type) - except UniqueNodeError: - raise GraphQLError( - 'A {} with that name already exists.'.format(node_type) - ) - helpers.form_update_node(request.user, nh.handle_id, form) - return nh - else: - # get the errors and return them - raise GraphQLError('Form errors: {}'.format(form)) - -class UpdateNIMutation(AbstractNIMutation): - class NIMetaClass: - node_type = None - node_meta_type = None - request_path = None - - @classmethod - def do_request(cls, request, **kwargs): - form_class = kwargs.get('form_class') - node_type = kwargs.get('node_type') - node_meta_type = kwargs.get('node_meta_type') - handle_id = request.POST.get('handle_id') - - nh, nodehandler = helpers.get_nh_node(handle_id) - if request.POST: - form = form_class(request.POST) - if form.is_valid(): - # Generic node update - helpers.form_update_node(request.user, nodehandler.handle_id, form) - return nh - else: - # get the errors and return them - raise GraphQLError('Form errors: {}'.format(form)) - -class DeleteNIMutation(AbstractNIMutation): - nodehandle = graphene.Boolean(required=True) - - class NIMetaClass: - node_type = None - node_meta_type = None - request_path = None - - @classmethod - def do_request(cls, request, **kwargs): - handle_id = request.POST.get('handle_id') - - nh, node = helpers.get_nh_node(handle_id) - helpers.delete_node(request.user, node.handle_id) - - return True - -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) - node_type = getattr(ni_metaclass, 'node_type', None) - node_meta_type = getattr(ni_metaclass, 'node_meta_type', None) - request_path = getattr(ni_metaclass, 'request_path', None) - nodetype = getattr(ni_metaclass, 'nodetype', NodeHandleType) - - # 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 = 'CreateNI{}Mutation'.format(node_type.capitalize()) - attr_dict = { - 'django_form': create_form, - 'mutation_name': class_name, - 'node_type': node_type, - 'node_meta_type': node_meta_type, - 'request_path': request_path, - 'is_create': True, - } - - create_metaclass = type(metaclass_name, (object,), attr_dict) - - cls._create_mutation = type( - class_name, - (cls.create_mutation_class,), - { - nh_field: graphene.Field(nodetype, required=True), - metaclass_name: create_metaclass, - }, - ) - - class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) - attr_dict['django_form'] = update_form - attr_dict['mutation_name'] = class_name - attr_dict['is_create'] = False - update_metaclass = type(metaclass_name, (object,), attr_dict) - - cls._update_mutation = type( - class_name, - (cls.update_mutation_class,), - { - nh_field: graphene.Field(nodetype, required=True), - metaclass_name: update_metaclass, - }, - ) - - class_name = 'DeleteNI{}Mutation'.format(node_type.capitalize()) - del attr_dict['django_form'] - attr_dict['mutation_name'] = class_name - delete_metaclass = type(metaclass_name, (object,), attr_dict) - - cls._delete_mutation = type( - class_name, - (cls.delete_mutation_class,), - { - metaclass_name: delete_metaclass, - }, - ) - - @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 - class NIRoleMutationFactory(NIMutationFactory): create_form = NewRoleForm update_form = EditRoleForm diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index ff4d960c8..62bd2ec14 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -25,9 +25,6 @@ def resolve_roles_list(self, info, **kwargs): return ret -class NodeHandleType(NIObjectType): - pass - class RoleType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) From 0d72b8b92a17018575f93f3b9eafef31123c826c Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 3 May 2019 12:47:43 +0200 Subject: [PATCH 064/520] Created superclass for automatic method generation for lists and lookups --- src/niweb/apps/noclook/schema/__init__.py | 1 + src/niweb/apps/noclook/schema/core.py | 96 +++++++++++++++++-- src/niweb/apps/noclook/schema/query.py | 33 +------ src/niweb/apps/noclook/schema/types.py | 17 ++++ .../apps/noclook/tests/schema/__init__.py | 79 ++++++++------- .../apps/noclook/tests/schema/test_schema.py | 13 +++ 6 files changed, 168 insertions(+), 71 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index dd9a20cdb..44a170dc1 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -10,6 +10,7 @@ RoleType, GroupType, ContactType, + NodeHandleType, ] NOCSCHEMA_QUERIES = [ diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index adae6856c..29a1ea6db 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -15,6 +15,8 @@ from graphql import GraphQLError from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible +from ..models import NodeType, NodeHandle + class NIRelayNode(relay.Node): ''' from https://docs.graphene-python.org/en/latest/relay/nodes/ @@ -35,10 +37,10 @@ def get_node_from_global_id(info, global_id, only_type=None): # is the same that was indicated in the field type assert type == only_type._meta.name, 'Received not compatible node.' - if type == 'User': - return get_user(id) - elif type == 'Photo': - return get_photo(id) + if type == 'DropdownType': + return Dropdown.objects.get(pk=id) + else: + return NodeHandle.objects.get(handle_id=id) class DictEntryType(graphene.ObjectType): ''' @@ -221,8 +223,11 @@ class Meta: model = NodeHandle interfaces = (NIRelayNode, ) +class NodeHandleType(NIObjectType): + pass + class AbstractNIMutation(relay.ClientIDMutation): - nodehandle = graphene.Field(NIObjectType, required=True) # the type should be replaced + nodehandle = graphene.Field(NodeHandleType, required=True) # the type should be replaced @classmethod def __init_subclass_with_meta__( @@ -469,7 +474,7 @@ def __init_subclass__(cls, **kwargs): node_type = getattr(ni_metaclass, 'node_type', None) node_meta_type = getattr(ni_metaclass, 'node_meta_type', None) request_path = getattr(ni_metaclass, 'request_path', None) - nodetype = getattr(ni_metaclass, 'nodetype', NIObjectType) + nodetype = getattr(ni_metaclass, 'nodetype', NodeHandleType) # specify and set create and update forms assert form and not create_form and not update_form or\ @@ -543,5 +548,84 @@ def get_update_mutation(cls, *args, **kwargs): def get_delete_mutation(cls, *args, **kwargs): return cls._delete_mutation +# TODO: this should evolve to a method with pagination +def get_list_resolver(nodetype): + def generic_list_resolver(self, info, **args): + limit = args.get('limit', False) + node_type = NodeType.objects.get(type=nodetype) + + if limit: + return NodeHandle.objects.filter(node_type=node_type)[:10] + else: + return NodeHandle.objects.filter(node_type=node_type) + + return generic_list_resolver + +def get_byid_resolver(nodetype): + def generic_byid_resolver(self, info, **args): + handle_id = args.get('handle_id') + node_type = NodeType.objects.get(type=nodetype) + + ret = None + if handle_id: + 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(nodetype, handle_id)) + + return ret + +class NOCAutoQuery(graphene.ObjectType): + node = NIRelayNode.Field() + + getNodeById = graphene.Field(NodeHandleType, handle_id=graphene.Int()) + + def resolve_getNodeById(self, info, **args): + handle_id = args.get('handle_id') + + if handle_id: + return NodeHandle.objects.get(handle_id=handle_id) + else: + raise GraphQLError('A handle_id must be provided') + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + _nimeta = getattr(cls, 'NIMeta') + graphql_types = getattr(_nimeta, 'graphql_types') + + assert graphql_types, \ + 'A tuple with the types should be set in the Meta class of {}'.format(cls.__name__) + + # add list with pagination resolver + # add by id resolver + for graphql_type in graphql_types: + _nimetatype = getattr(graphql_type, 'NIMetaType') + + ni_type = getattr(_nimetatype, 'ni_type') + assert ni_type, '{} has not set its ni_type attribute'.format(cls.__name__) + ni_metatype = getattr(_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() + type_name = node_type.type + type_slug = node_type.slug + + # construct field and resolver for list + field_name = '{}s'.format(type_slug) + resolver_name = 'resolve_{}'.format(field_name) + + setattr(cls, field_name, graphene.List(graphql_type, limit=graphene.Int())) + setattr(cls, resolver_name, get_list_resolver(type_name)) + + # construct 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, handle_id=graphene.Int())) + setattr(cls, resolver_name, get_byid_resolver(type_name)) + def get_logger_user(): return get_user() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 9bc85adb4..66daa17a9 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -2,34 +2,11 @@ __author__ = 'ffuentes' import graphene +from graphql import GraphQLError from .types import * -class NOCRootQuery(graphene.ObjectType): - node = NIRelayNode.Field() - roles = graphene.List(RoleType, limit=graphene.Int()) - groups = graphene.List(GroupType, limit=graphene.Int()) - contacts = graphene.List(ContactType, limit=graphene.Int()) +class NOCRootQuery(NOCAutoQuery): + pass - def resolve_roles(self, info, **args): - limit = args.get('limit', False) - type = NodeType.objects.get(type="Role") # TODO too raw - if limit: - return NodeHandle.objects.filter(node_type=type)[:10] - else: - return NodeHandle.objects.filter(node_type=type) - - def resolve_groups(self, info, **args): - limit = args.get('limit', False) - type = NodeType.objects.get(type="Group") # TODO too raw - if limit: - return NodeHandle.objects.filter(node_type=type)[:10] - else: - return NodeHandle.objects.filter(node_type=type) - - def resolve_contacts(self, info, **args): - limit = args.get('limit', False) - type = NodeType.objects.get(type="Contact") # TODO too raw - if limit: - return NodeHandle.objects.filter(node_type=type)[:10] - else: - return NodeHandle.objects.filter(node_type=type) + class NIMeta: + graphql_types = [ RoleType, GroupType, ContactType ] diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 62bd2ec14..584b8d7c2 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -25,12 +25,25 @@ def resolve_roles_list(self, info, **kwargs): return ret +class DropdownType(DjangoObjectType): + class Meta: + model = NodeHandle + interfaces = (NIRelayNode, ) + class RoleType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) + class NIMetaType: + ni_type = 'Role' + ni_metatype = 'logical' + class GroupType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) + class NIMetaType: + ni_type = 'Group' + ni_metatype = 'logical' + class ContactType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) first_name = NIStringField(type_kwargs={ 'required': True }) @@ -45,3 +58,7 @@ class ContactType(NIObjectType): PGP_fingerprint = NIStringField() is_roles = NIListField(type_args=(RoleType,), manual_resolver=resolve_roles_list) member_of_groups = NIListField(type_args=(GroupType,), rel_name='Member_of', rel_method='get_outgoing_relations') + + class NIMetaType: + ni_type = 'Contact' + ni_metatype = 'relation' diff --git a/src/niweb/apps/noclook/tests/schema/__init__.py b/src/niweb/apps/noclook/tests/schema/__init__.py index 857b319df..0823239da 100644 --- a/src/niweb/apps/noclook/tests/schema/__init__.py +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -7,46 +7,51 @@ from ..neo4j_base import NeoTestCase class Neo4jGraphQLTest(NeoTestCase): + initialized = False + def setUp(self): super(Neo4jGraphQLTest, self).setUp() - # create nodes - organization1 = self.create_node('organization1', 'organization', meta='Logical') - organization2 = self.create_node('organization2', 'organization', meta='Logical') - contact1 = self.create_node('contact1', 'contact', meta='Relation') - contact2 = self.create_node('contact2', 'contact', meta='Relation') - role1 = self.create_node('role1', 'role', meta='Logical') - role2 = self.create_node('role2', 'role', meta='Logical') - group1 = self.create_node('group1', 'group', meta='Logical') - group2 = self.create_node('group2', 'group', meta='Logical') - - # add some data - contact1_data = { - 'first_name': 'Jane', - 'last_name': 'Doe', - 'name': 'Jane Doe', - } - - for key, value in contact1_data.items(): - 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(): - contact2.get_node().add_property(key, value) - - # create relationships - contact1.get_node().add_role(role1.handle_id) - contact1.get_node().add_group(group1.handle_id) - contact1.get_node().add_organization(organization1.handle_id) - - contact2.get_node().add_role(role2.handle_id) - contact2.get_node().add_group(group2.handle_id) - contact2.get_node().add_organization(organization2.handle_id) + if not self.initialized: + # create nodes + organization1 = self.create_node('organization1', 'organization', meta='Logical') + organization2 = self.create_node('organization2', 'organization', meta='Logical') + contact1 = self.create_node('contact1', 'contact', meta='Relation') + contact2 = self.create_node('contact2', 'contact', meta='Relation') + role1 = self.create_node('role1', 'role', meta='Logical') + role2 = self.create_node('role2', 'role', meta='Logical') + group1 = self.create_node('group1', 'group', meta='Logical') + group2 = self.create_node('group2', 'group', meta='Logical') + + # add some data + contact1_data = { + 'first_name': 'Jane', + 'last_name': 'Doe', + 'name': 'Jane Doe', + } + + for key, value in contact1_data.items(): + 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(): + contact2.get_node().add_property(key, value) + + # create relationships + contact1.get_node().add_role(role1.handle_id) + contact1.get_node().add_group(group1.handle_id) + contact1.get_node().add_organization(organization1.handle_id) + + contact2.get_node().add_role(role2.handle_id) + contact2.get_node().add_group(group2.handle_id) + contact2.get_node().add_organization(organization2.handle_id) + + self.initialized = True def tearDown(self): super(Neo4jGraphQLTest, self).tearDown() diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 363058f29..1ea17e5c0 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -48,3 +48,16 @@ def test_get_contacts(self): assert not result.errors assert result.data == expected + + def test_getnodebyhandle_id(self): + query = ''' + query { + getNodeById(handle_id: 13){ + handle_id + } + } + ''' + + result = schema.execute(query) + + #assert not result.errors From ae387c1381cbe0a7b6187a85e1e2fabc2e8032b6 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 3 May 2019 14:21:38 +0200 Subject: [PATCH 065/520] The type/metatype of a node now is only set on the type definition --- src/niweb/apps/noclook/schema/core.py | 47 +++++++++++++--------- src/niweb/apps/noclook/schema/mutations.py | 7 +--- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 29a1ea6db..42e69f84e 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -334,11 +334,14 @@ def from_input_to_request(cls, **input): # get ni metaclass data ni_metaclass = getattr(cls, 'NIMetaClass') form_class = getattr(ni_metaclass, 'django_form', None) - node_type = getattr(ni_metaclass, 'node_type') - node_meta_type = getattr(ni_metaclass, 'node_meta_type') request_path = getattr(ni_metaclass, 'request_path', '/') is_create = getattr(ni_metaclass, 'is_create', False) + 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 input_class = getattr(cls, 'Input', None) input_params = {} @@ -374,16 +377,18 @@ class CreateNIMutation(AbstractNIMutation): superclass of a manualy coded class in case it's needed. ''' class NIMetaClass: - node_type = None - node_meta_type = None request_path = None is_create = True + graphql_type = None @classmethod def do_request(cls, request, **kwargs): form_class = kwargs.get('form_class') - node_type = kwargs.get('node_type') - node_meta_type = kwargs.get('node_meta_type') + 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() ## code from role creation form = form_class(request.POST) @@ -403,15 +408,17 @@ def do_request(cls, request, **kwargs): class UpdateNIMutation(AbstractNIMutation): class NIMetaClass: - node_type = None - node_meta_type = None request_path = None + graphql_type = None @classmethod def do_request(cls, request, **kwargs): form_class = kwargs.get('form_class') - node_type = kwargs.get('node_type') - node_meta_type = kwargs.get('node_meta_type') + 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() handle_id = request.POST.get('handle_id') nh, nodehandler = helpers.get_nh_node(handle_id) @@ -429,9 +436,8 @@ class DeleteNIMutation(AbstractNIMutation): nodehandle = graphene.Boolean(required=True) class NIMetaClass: - node_type = None - node_meta_type = None request_path = None + graphql_type = None @classmethod def do_request(cls, request, **kwargs): @@ -471,10 +477,12 @@ def __init_subclass__(cls, **kwargs): form = getattr(ni_metaclass, 'form', None) create_form = getattr(ni_metaclass, 'create_form', None) update_form = getattr(ni_metaclass, 'update_form', None) - node_type = getattr(ni_metaclass, 'node_type', None) - node_meta_type = getattr(ni_metaclass, 'node_meta_type', None) request_path = getattr(ni_metaclass, 'request_path', None) - nodetype = getattr(ni_metaclass, 'nodetype', NodeHandleType) + graphql_type = getattr(ni_metaclass, 'graphql_type', NodeHandleType) + + # 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\ @@ -491,10 +499,9 @@ def __init_subclass__(cls, **kwargs): attr_dict = { 'django_form': create_form, 'mutation_name': class_name, - 'node_type': node_type, - 'node_meta_type': node_meta_type, 'request_path': request_path, 'is_create': True, + 'graphql_type': graphql_type, } create_metaclass = type(metaclass_name, (object,), attr_dict) @@ -503,7 +510,7 @@ def __init_subclass__(cls, **kwargs): class_name, (cls.create_mutation_class,), { - nh_field: graphene.Field(nodetype, required=True), + nh_field: graphene.Field(graphql_type, required=True), metaclass_name: create_metaclass, }, ) @@ -518,7 +525,7 @@ def __init_subclass__(cls, **kwargs): class_name, (cls.update_mutation_class,), { - nh_field: graphene.Field(nodetype, required=True), + nh_field: graphene.Field(graphql_type, required=True), metaclass_name: update_metaclass, }, ) @@ -589,7 +596,7 @@ def resolve_getNodeById(self, info, **args): return NodeHandle.objects.get(handle_id=handle_id) else: raise GraphQLError('A handle_id must be provided') - + def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 9363d7395..87e8d58b6 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -14,11 +14,9 @@ class NIRoleMutationFactory(NIMutationFactory): update_form = EditRoleForm class NIMetaClass: - node_type = 'role' - node_meta_type = 'Logical' request_path = '/' form = NewRoleForm - nodetype = RoleType + graphql_type = RoleType class Meta: abstract = False @@ -29,10 +27,9 @@ class CreateRoleNIMutation(CreateNIMutation): nodehandle = graphene.Field(RoleType, required=True) class NIMetaClass: - node_type = 'role' - node_meta_type = 'Logical' request_path = '/' django_form = NewRoleForm + graphql_type = RoleType class Meta: abstract = False From 1dc0ad2abb45bc1cf8e904ad01adabb01aa084e0 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 6 May 2019 16:57:48 +0200 Subject: [PATCH 066/520] Added group and contacts mutation factories and dropdown methods --- src/niweb/apps/noclook/forms/common.py | 2 +- src/niweb/apps/noclook/schema/core.py | 4 +-- src/niweb/apps/noclook/schema/mutations.py | 40 ++++++++++++++++++---- src/niweb/apps/noclook/schema/query.py | 12 ++++++- src/niweb/apps/noclook/schema/types.py | 7 +++- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index c1a9c9c07..217f529ac 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -896,7 +896,7 @@ def __init__(self, *args, **kwargs): first_name = forms.CharField() last_name = forms.CharField() - contact_type = forms.ChoiceField(widget=forms.widgets.Select, required=False) + contact_type = forms.ChoiceField(widget=forms.widgets.Select) mobile = forms.CharField(required=False) phone = forms.CharField(required=False) salutation = forms.CharField(required=False) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 42e69f84e..4f7e760e6 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -37,7 +37,7 @@ def get_node_from_global_id(info, global_id, only_type=None): # is the same that was indicated in the field type assert type == only_type._meta.name, 'Received not compatible node.' - if type == 'DropdownType': + if type == 'DropdownType' or 'RoleType': # TODO too raw return Dropdown.objects.get(pk=id) else: return NodeHandle.objects.get(handle_id=id) @@ -404,7 +404,7 @@ def do_request(cls, request, **kwargs): return nh else: # get the errors and return them - raise GraphQLError('Form errors: {}'.format(form)) + raise GraphQLError('Form errors: {}'.format(form._errors)) class UpdateNIMutation(AbstractNIMutation): class NIMetaClass: diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 87e8d58b6..692ef4f05 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -10,17 +10,35 @@ from .types import * class NIRoleMutationFactory(NIMutationFactory): - create_form = NewRoleForm - update_form = EditRoleForm - class NIMetaClass: + create_form = NewRoleForm + update_form = EditRoleForm request_path = '/' - form = NewRoleForm graphql_type = RoleType class Meta: abstract = False +class NIGroupMutationFactory(NIMutationFactory): + class NIMetaClass: + create_form = NewGroupForm + update_form = EditGroupForm + request_path = '/' + graphql_type = GroupType + + class Meta: + abstract = False + +class NIContactMutationFactory(NIMutationFactory): + class NIMetaClass: + create_form = NewContactForm + update_form = EditContactForm + request_path = '/' + graphql_type = ContactType + + class Meta: + abstract = False + class CreateRoleNIMutation(CreateNIMutation): '''This class is not used but left out as documentation in the case that as finer grain of control is needed''' @@ -35,6 +53,14 @@ class Meta: abstract = False class NOCRootMutation(graphene.ObjectType): - create_role = NIRoleMutationFactory.get_create_mutation().Field() - update_role = NIRoleMutationFactory.get_update_mutation().Field() - delete_role = NIRoleMutationFactory.get_delete_mutation().Field() + create_role = NIRoleMutationFactory.get_create_mutation().Field() + update_role = NIRoleMutationFactory.get_update_mutation().Field() + delete_role = NIRoleMutationFactory.get_delete_mutation().Field() + + create_group = NIGroupMutationFactory.get_create_mutation().Field() + update_group = NIGroupMutationFactory.get_update_mutation().Field() + delete_group = NIGroupMutationFactory.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() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 66daa17a9..5e13cb60d 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -3,10 +3,20 @@ import graphene from graphql import GraphQLError +from ..models import Dropdown from .types import * class NOCRootQuery(NOCAutoQuery): - pass + getChoicesForDropdown = graphene.List(ChoiceType, name=graphene.String(required=True)) + + def resolve_getChoicesForDropdown(self, info, **kwargs): + name = kwargs.get('name') + ddqs = Dropdown.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)) class NIMeta: graphql_types = [ RoleType, GroupType, ContactType ] diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 584b8d7c2..083b48434 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -27,7 +27,12 @@ def resolve_roles_list(self, info, **kwargs): class DropdownType(DjangoObjectType): class Meta: - model = NodeHandle + model = Dropdown + interfaces = (NIRelayNode, ) + +class ChoiceType(DjangoObjectType): + class Meta: + model = Choice interfaces = (NIRelayNode, ) class RoleType(NIObjectType): From 0a209672614c8f6a9a7a5de5208ffcfd14986719 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 7 May 2019 12:17:28 +0200 Subject: [PATCH 067/520] Testing view field and connection for lists from relay. --- src/niweb/apps/noclook/schema/query.py | 12 ++++++++++++ src/niweb/apps/noclook/schema/types.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 5e13cb60d..e8c5cc984 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -5,9 +5,21 @@ from graphql import GraphQLError from ..models import Dropdown from .types import * +from .core import get_logger_user class NOCRootQuery(NOCAutoQuery): + viewer = graphene.Field(UserType) getChoicesForDropdown = graphene.List(ChoiceType, name=graphene.String(required=True)) + rolesconn = graphene.relay.ConnectionField(RoleConnection) + + def resolve_rolesconn(self, info, **kwargs): + node_type = NodeType.objects.filter(type='Role').first() + return NodeHandle.objects.filter(node_type=node_type) + + # viewer field for relay + def resolve_viewer(self, info, **kwargs): + user = get_logger_user() + return user def resolve_getChoicesForDropdown(self, info, **kwargs): name = kwargs.get('name') diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 083b48434..1ca07db46 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +from django.contrib.auth.models import User +from graphene import relay from .core import * from ..models import * @@ -25,6 +27,11 @@ def resolve_roles_list(self, info, **kwargs): return ret +class UserType(DjangoObjectType): + class Meta: + model = User + interfaces = (NIRelayNode, ) + class DropdownType(DjangoObjectType): class Meta: model = Dropdown @@ -42,6 +49,11 @@ class NIMetaType: ni_type = 'Role' ni_metatype = 'logical' +## connection test: this should be removed and autoimplemented in core ## +class RoleConnection(relay.Connection): + class Meta: + node = RoleType + class GroupType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) From 21e64cc59171f99e6904e3f864a6f5e39f7adfa9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 7 May 2019 14:14:54 +0200 Subject: [PATCH 068/520] Previous element list replaced with automatic relay connection --- src/niweb/apps/noclook/schema/core.py | 32 ++++++++++++++++---------- src/niweb/apps/noclook/schema/query.py | 5 ---- src/niweb/apps/noclook/schema/types.py | 5 ---- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 4f7e760e6..2c1be36c7 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -555,16 +555,14 @@ def get_update_mutation(cls, *args, **kwargs): def get_delete_mutation(cls, *args, **kwargs): return cls._delete_mutation -# TODO: this should evolve to a method with pagination -def get_list_resolver(nodetype): +def get_connection_resolver(nodetype): def generic_list_resolver(self, info, **args): - limit = args.get('limit', False) node_type = NodeType.objects.get(type=nodetype) + ret = NodeHandle.objects.filter(node_type=node_type) + if not ret: + ret = [] - if limit: - return NodeHandle.objects.filter(node_type=node_type)[:10] - else: - return NodeHandle.objects.filter(node_type=node_type) + return ret return generic_list_resolver @@ -584,6 +582,8 @@ def generic_byid_resolver(self, info, **args): return ret + return generic_byid_resolver + class NOCAutoQuery(graphene.ObjectType): node = NIRelayNode.Field() @@ -595,7 +595,7 @@ def resolve_getNodeById(self, info, **args): if handle_id: return NodeHandle.objects.get(handle_id=handle_id) else: - raise GraphQLError('A handle_id must be provided') + raise GraphQLError('A valid handle_id must be provided') def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -609,6 +609,7 @@ def __init_subclass__(cls, **kwargs): # add list with pagination resolver # add by id resolver for graphql_type in graphql_types: + # extract values _nimetatype = getattr(graphql_type, 'NIMetaType') ni_type = getattr(_nimetatype, 'ni_type') @@ -620,14 +621,21 @@ def __init_subclass__(cls, **kwargs): type_name = node_type.type type_slug = node_type.slug - # construct field and resolver for list + # build connection class field_name = '{}s'.format(type_slug) resolver_name = 'resolve_{}'.format(field_name) - setattr(cls, field_name, graphene.List(graphql_type, limit=graphene.Int())) - setattr(cls, resolver_name, get_list_resolver(type_name)) + connection_meta = type('Meta', (object, ), dict(node=graphql_type)) + connection_class = type( + '{}Connection'.format(type_name), + (graphene.relay.Connection,), + dict(Meta=connection_meta) + ) + + setattr(cls, field_name, graphene.relay.ConnectionField(connection_class)) + setattr(cls, resolver_name, get_connection_resolver(type_name)) - # construct field and resolver byid + # build field and resolver byid field_name = 'get{}ById'.format(type_name) resolver_name = 'resolve_{}'.format(field_name) diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index e8c5cc984..68dcfd57c 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -10,11 +10,6 @@ class NOCRootQuery(NOCAutoQuery): viewer = graphene.Field(UserType) getChoicesForDropdown = graphene.List(ChoiceType, name=graphene.String(required=True)) - rolesconn = graphene.relay.ConnectionField(RoleConnection) - - def resolve_rolesconn(self, info, **kwargs): - node_type = NodeType.objects.filter(type='Role').first() - return NodeHandle.objects.filter(node_type=node_type) # viewer field for relay def resolve_viewer(self, info, **kwargs): diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 1ca07db46..3a134a7c7 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -49,11 +49,6 @@ class NIMetaType: ni_type = 'Role' ni_metatype = 'logical' -## connection test: this should be removed and autoimplemented in core ## -class RoleConnection(relay.Connection): - class Meta: - node = RoleType - class GroupType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) From 87895986fb487c7eedb21f802dd108b6e412e4f7 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 13 May 2019 11:41:43 +0200 Subject: [PATCH 069/520] Viewer removed --- src/niweb/apps/noclook/schema/query.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 68dcfd57c..063738233 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -8,14 +8,8 @@ from .core import get_logger_user class NOCRootQuery(NOCAutoQuery): - viewer = graphene.Field(UserType) getChoicesForDropdown = graphene.List(ChoiceType, name=graphene.String(required=True)) - # viewer field for relay - def resolve_viewer(self, info, **kwargs): - user = get_logger_user() - return user - def resolve_getChoicesForDropdown(self, info, **kwargs): name = kwargs.get('name') ddqs = Dropdown.get(name) From 81c33fe30488a617660784a76d4079191af040d9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 13 May 2019 16:12:09 +0200 Subject: [PATCH 070/520] Removed dummy user function. Now the authentication relies in django --- src/niweb/apps/noclook/schema/core.py | 79 +++++++++++++++++++------- src/niweb/apps/noclook/schema/query.py | 1 - 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 2c1be36c7..99d68670d 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -3,11 +3,11 @@ import graphene import re -from apps.nerds.lib.consumer_util import get_user from apps.noclook import helpers from apps.noclook.models import NodeType, NodeHandle from collections import OrderedDict from django import forms +from django.db.models import Q from django.test import RequestFactory from graphene import relay from graphene_django import DjangoObjectType @@ -219,6 +219,15 @@ def __init_subclass_with_meta__( **options ) + @classmethod + def get_queryset(cls, queryset, info): + if info.context.user.is_anonymous: + return queryset.none() + else: + return queryset.filter( + Q(creator=info.context.user) | Q(modifier=info.context.user) + ) + class Meta: model = NodeHandle interfaces = (NIRelayNode, ) @@ -325,7 +334,7 @@ def get_type(cls): return getattr(ni_metaclass, 'typeclass') @classmethod - def from_input_to_request(cls, **input): + 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 @@ -356,14 +365,14 @@ def from_input_to_request(cls, **input): # forge request request_factory = RequestFactory() request = request_factory.post(request_path, data=input_params) - request.user = get_logger_user() + 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): - reqinput = cls.from_input_to_request(**input) + reqinput = cls.from_input_to_request(info.context.user, **input) ret = cls.do_request(reqinput[0], **reqinput[1]) return cls(nodehandle=ret) @@ -555,14 +564,29 @@ def get_update_mutation(cls, *args, **kwargs): def get_delete_mutation(cls, *args, **kwargs): return cls._delete_mutation +class GraphQLAuthException(Exception): + def __init__(self, message=None): + message = 'You need to be authenticated{}'.format( + ': {}'.format(message) if message else '' + ) + super().__init__(message) + def get_connection_resolver(nodetype): def generic_list_resolver(self, info, **args): node_type = NodeType.objects.get(type=nodetype) - ret = NodeHandle.objects.filter(node_type=node_type) - if not ret: - ret = [] - return ret + if info.context.user.is_authenticated(): + ret = NodeHandle.objects.filter(node_type=node_type) + ret.filter( + Q(creator=info.context.user) | Q(modifier=info.context.user) + ) + + if not ret: + ret = [] + + return ret + else: + raise GraphQLAuthException() return generic_list_resolver @@ -572,30 +596,44 @@ def generic_byid_resolver(self, info, **args): node_type = NodeType.objects.get(type=nodetype) ret = None - if handle_id: - 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(nodetype, handle_id)) + if info.context.user.is_authenticated(): + if handle_id: + ret = NodeHandle.objects.filter(node_type=node_type).get(handle_id=handle_id) + else: + raise GraphQLError('A handle_id must be provided') - return ret + if not ret: + raise GraphQLError("There isn't any {} with handle_id {}".format(nodetype, handle_id)) + + return ret + else: + raise GraphQLAuthException() return generic_byid_resolver class NOCAutoQuery(graphene.ObjectType): node = NIRelayNode.Field() - getNodeById = graphene.Field(NodeHandleType, handle_id=graphene.Int()) def resolve_getNodeById(self, info, **args): handle_id = args.get('handle_id') - if handle_id: - return NodeHandle.objects.get(handle_id=handle_id) + ret = None + + if info.context.user.is_authenticated(): + if handle_id: + ret = NodeHandle.objects.get(handle_id=handle_id) + else: + raise GraphQLError('A valid handle_id must be provided') + + if not ret: + raise GraphQLError("There isn't any {} with handle_id {}".format(nodetype, handle_id)) + + return ret else: - raise GraphQLError('A valid handle_id must be provided') + raise GraphQLAuthException() + def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -641,6 +679,3 @@ def __init_subclass__(cls, **kwargs): setattr(cls, field_name, graphene.Field(graphql_type, handle_id=graphene.Int())) setattr(cls, resolver_name, get_byid_resolver(type_name)) - -def get_logger_user(): - return get_user() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 063738233..5e13cb60d 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -5,7 +5,6 @@ from graphql import GraphQLError from ..models import Dropdown from .types import * -from .core import get_logger_user class NOCRootQuery(NOCAutoQuery): getChoicesForDropdown = graphene.List(ChoiceType, name=graphene.String(required=True)) From 866181ca8a1fa7b505df64f94b84a3955528200c Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 13 May 2019 16:31:53 +0200 Subject: [PATCH 071/520] Added CORS policy with django-cors-headers --- requirements/common.txt | 1 + src/niweb/niweb/settings/common.py | 2 ++ src/niweb/niweb/settings/dev.py | 1 + src/niweb/niweb/settings/prod.py | 1 + 4 files changed, 5 insertions(+) diff --git a/requirements/common.txt b/requirements/common.txt index 076218cd0..611256dc6 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -15,3 +15,4 @@ django-dynamic-preferences<1.4 django-attachments<2.0 configparser graphene-django>=2.0 +django-cors-headers diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index fef295074..6c3871bb2 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -184,6 +184,7 @@ ########## MIDDLEWARE CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#middleware-classes MIDDLEWARE_CLASSES = ( + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -233,6 +234,7 @@ 'dynamic_preferences', 'attachments', 'graphene_django', + 'corsheaders', ) LOCAL_APPS = ( diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 79f474b00..3d2ff1b95 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -97,3 +97,4 @@ ########## END TESTING GOOGLE_MAPS_API_KEY = environ.get('GOOGLE_MAPS_API_KEY', 'no-apikey') +CORS_ORIGIN_ALLOW_ALL = True diff --git a/src/niweb/niweb/settings/prod.py b/src/niweb/niweb/settings/prod.py index 95356129e..d3ba84b6f 100644 --- a/src/niweb/niweb/settings/prod.py +++ b/src/niweb/niweb/settings/prod.py @@ -116,3 +116,4 @@ ########## END SECRET CONFIGURATION GOOGLE_MAPS_API_KEY = environ.get('GOOGLE_MAPS_API_KEY', 'no-apikey') +CORS_ORIGIN_ALLOW_ALL = False From cbafb1a3b8be69f04bdc399fb920bd4249635c91 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 14 May 2019 11:45:55 +0200 Subject: [PATCH 072/520] Added authentication exception on mutate --- src/niweb/apps/noclook/schema/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 99d68670d..8fbff6c4e 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -372,6 +372,9 @@ def from_input_to_request(cls, user, **input): @classmethod def mutate_and_get_payload(cls, root, info, **input): + if not info.context or not info.context.user.is_authenticated(): + raise GraphQLAuthException() + reqinput = cls.from_input_to_request(info.context.user, **input) ret = cls.do_request(reqinput[0], **reqinput[1]) From 18f100bb1f916bbca72c81ef7542636a274a7ac5 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 15 May 2019 09:25:06 +0200 Subject: [PATCH 073/520] Some minor tweaks --- src/niweb/apps/noclook/schema/core.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 8fbff6c4e..b823b44ba 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -372,7 +372,7 @@ def from_input_to_request(cls, user, **input): @classmethod def mutate_and_get_payload(cls, root, info, **input): - if not info.context or not info.context.user.is_authenticated(): + if not info.context or not info.context.user.is_authenticated: raise GraphQLAuthException() reqinput = cls.from_input_to_request(info.context.user, **input) @@ -569,7 +569,7 @@ def get_delete_mutation(cls, *args, **kwargs): class GraphQLAuthException(Exception): def __init__(self, message=None): - message = 'You need to be authenticated{}'.format( + message = 'You must be logged in the system: {}'.format( ': {}'.format(message) if message else '' ) super().__init__(message) @@ -578,11 +578,11 @@ def get_connection_resolver(nodetype): def generic_list_resolver(self, info, **args): node_type = NodeType.objects.get(type=nodetype) - if info.context.user.is_authenticated(): + if info.context and info.context.user.is_authenticated: ret = NodeHandle.objects.filter(node_type=node_type) - ret.filter( + """ret.filter( Q(creator=info.context.user) | Q(modifier=info.context.user) - ) + )""" if not ret: ret = [] @@ -600,7 +600,7 @@ def generic_byid_resolver(self, info, **args): ret = None - if info.context.user.is_authenticated(): + if info.context and info.context.user.is_authenticated: if handle_id: ret = NodeHandle.objects.filter(node_type=node_type).get(handle_id=handle_id) else: @@ -624,7 +624,7 @@ def resolve_getNodeById(self, info, **args): ret = None - if info.context.user.is_authenticated(): + if info.context and info.context.user.is_authenticated: if handle_id: ret = NodeHandle.objects.get(handle_id=handle_id) else: From 27408b38ab49ec4f9ecdc22ede882948f81f1442 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 15 May 2019 10:23:43 +0200 Subject: [PATCH 074/520] Added login required to graphql endpoint --- src/niweb/apps/noclook/schema/__init__.py | 8 ++++++++ src/niweb/niweb/settings/dev.py | 2 +- src/niweb/niweb/urls.py | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index 44a170dc1..148124377 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -1,6 +1,10 @@ # -*- 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 * @@ -20,3 +24,7 @@ NOCSCHEMA_MUTATIONS = [ NOCRootMutation, ] + +@method_decorator(login_required, name='dispatch') +class AuthGraphQLView(GraphQLView): + pass diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 3d2ff1b95..8eaff76e5 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -84,7 +84,7 @@ DEBUG_TOOLBAR_CONFIG = { 'INTERCEPT_REDIRECTS': False, - 'SHOW_TOOLBAR_CALLBACK': lambda x: True, + 'SHOW_TOOLBAR_CALLBACK': lambda x: False, } ########## END TOOLBAR CONFIGURATION diff --git a/src/niweb/niweb/urls.py b/src/niweb/niweb/urls.py index 58916ebf0..4f86f444e 100644 --- a/src/niweb/niweb/urls.py +++ b/src/niweb/niweb/urls.py @@ -4,7 +4,7 @@ import apps.noclook.api.resources as niapi from django.contrib.auth import views as auth_views from django.views.decorators.csrf import csrf_exempt -from graphene_django.views import GraphQLView +from apps.noclook.schema import AuthGraphQLView # Uncomment the next two lines to enable the admin: from django.contrib import admin @@ -87,7 +87,7 @@ def if_installed(appname, *args, **kwargs): url(r'^api/', include(v1_api.urls)), # GraphQL endpoint - url(r'^graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))), + url(r'^graphql/', csrf_exempt(AuthGraphQLView.as_view(graphiql=True))), # Django Generic Comments url(r'^comments/', include('django_comments.urls')), From eed29054a916b3f8fcd1748cee3a486cc0d4e458 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 15 May 2019 12:07:38 +0200 Subject: [PATCH 075/520] Tests fixed. The context object must be forged and passed to the query. --- .../apps/noclook/tests/schema/__init__.py | 5 ++ .../noclook/tests/schema/test_mutations.py | 8 +-- .../apps/noclook/tests/schema/test_schema.py | 67 ++++++++++--------- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/niweb/apps/noclook/tests/schema/__init__.py b/src/niweb/apps/noclook/tests/schema/__init__.py index 0823239da..4f36487ff 100644 --- a/src/niweb/apps/noclook/tests/schema/__init__.py +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -6,11 +6,16 @@ from apps.noclook.models import NodeHandle from ..neo4j_base import NeoTestCase +class TestContext(): + def __init__(self, user, *ignore): + self.user = user + class Neo4jGraphQLTest(NeoTestCase): initialized = False def setUp(self): super(Neo4jGraphQLTest, self).setUp() + self.context = TestContext(self.user) if not self.initialized: # create nodes diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index 64530cc7e..bea6516d4 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -33,9 +33,9 @@ def test_role(self): ) ]) - result = schema.execute(query) + result = schema.execute(query, context=self.context) - assert not result.errors + assert not result.errors, result.errors assert result.data == expected ## update ## @@ -65,7 +65,7 @@ def test_role(self): ) ]) - result = schema.execute(query) + result = schema.execute(query, context=self.context) assert not result.errors assert result.data == expected @@ -86,6 +86,6 @@ def test_role(self): ) ]) - result = schema.execute(query) + result = schema.execute(query, context=self.context) assert not result.errors assert result.data == expected diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 1ea17e5c0..f8925d24b 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -9,44 +9,45 @@ class QueryTest(Neo4jGraphQLTest): def test_get_contacts(self): query = ''' query getLastTenContacts { - contacts(limit: 10) { - handle_id - name - first_name - last_name - is_roles { - name - } - member_of_groups { - name + contacts(first: 10) { + edges { + node { + handle_id + name + first_name + last_name + is_roles { + name + } + member_of_groups { + name + } + } } } } ''' - expected = { - 'contacts': [ - OrderedDict([ - ('handle_id', '13'), - ('name', 'John Smith'), - ('first_name', 'John'), - ('last_name', 'Smith'), - ('is_roles', [OrderedDict([('name', 'role2')])]), - ('member_of_groups', [OrderedDict([('name', 'group2')])]), - ]), - OrderedDict([ - ('handle_id', '12'), - ('name', 'Jane Doe'), - ('first_name', 'Jane'), - ('last_name', 'Doe'), - ('is_roles', [OrderedDict([('name', 'role1')])]), - ('member_of_groups', [OrderedDict([('name', 'group1')])]), - ]), - ] - } - result = schema.execute(query) + expected = OrderedDict([('contacts', OrderedDict([('edges', + [OrderedDict([('node',OrderedDict([ + ('handle_id', '13'), + ('name', 'John Smith'), + ('first_name', 'John'), + ('last_name', 'Smith'), + ('is_roles', [OrderedDict([('name', 'role2')])]), + ('member_of_groups', [OrderedDict([('name', 'group2')])]), + ]))]), OrderedDict([('node', OrderedDict([ + ('handle_id', '12'), + ('name', 'Jane Doe'), + ('first_name', 'Jane'), + ('last_name', 'Doe'), + ('is_roles', [OrderedDict([('name', 'role1')])]), + ('member_of_groups', [OrderedDict([('name', 'group1')])]), + ]))])])]))]) + + result = schema.execute(query, context=self.context) - assert not result.errors + assert not result.errors, result.errors assert result.data == expected def test_getnodebyhandle_id(self): @@ -58,6 +59,6 @@ def test_getnodebyhandle_id(self): } ''' - result = schema.execute(query) + result = schema.execute(query, context=self.context) #assert not result.errors From d361773cac13366d2ee984c28a1c1c8328bc8747 Mon Sep 17 00:00:00 2001 From: bereware Date: Wed, 15 May 2019 13:55:19 +0200 Subject: [PATCH 076/520] nginx config - add Allowed_hosts to dev and session cookie for all subdomains --- src/niweb/niweb/settings/common.py | 2 +- src/niweb/niweb/settings/dev.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index 6c3871bb2..ce5d3831d 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -55,7 +55,7 @@ ########## ALLOWED HOSTS CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '').split() +ALLOWED_HOSTS = "*" ########## END ALLOWED HOST CONFIGURATION ########## MANAGER CONFIGURATION diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 8eaff76e5..74bcc29a2 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -36,6 +36,9 @@ ########## END DEBUG CONFIGURATION +SESSION_COOKIE_DOMAIN = '.localni.info' +LOGIN_REDICRECT_URL = 'http://react.localni.info' + ########## EMAIL CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' From 16f8890fe1b79a595325bba6088d8c2d504e5209 Mon Sep 17 00:00:00 2001 From: bereware Date: Thu, 16 May 2019 15:00:59 +0200 Subject: [PATCH 077/520] endpoint graphene --- src/niweb/apps/noclook/schema/__init__.py | 6 ++--- src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/other.py | 7 +++++- src/niweb/niweb/settings/dev.py | 10 +++++++-- src/niweb/niweb/urls.py | 27 ++++++++++++++++++++++- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index 148124377..619eda20d 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' -from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.utils.decorators import method_decorator from graphene_django.views import GraphQLView @@ -25,6 +25,6 @@ NOCRootMutation, ] -@method_decorator(login_required, name='dispatch') -class AuthGraphQLView(GraphQLView): + +class AuthGraphQLView(LoginRequiredMixin, GraphQLView): pass diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index a3ec7d94c..61fca50d6 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -4,6 +4,7 @@ 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 diff --git a/src/niweb/apps/noclook/views/other.py b/src/niweb/apps/noclook/views/other.py index 716869f41..b59494549 100644 --- a/src/niweb/apps/noclook/views/other.py +++ b/src/niweb/apps/noclook/views/other.py @@ -4,9 +4,10 @@ # -*- 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 re import escape as re_escape import json @@ -16,6 +17,10 @@ import norduniclient as nc +def csrf(request): + return JsonResponse({'csrfToken': get_token(request)}) + + def index(request): return render(request, 'noclook/index.html', {}) diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 74bcc29a2..8e5fbd137 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -36,8 +36,14 @@ ########## END DEBUG CONFIGURATION -SESSION_COOKIE_DOMAIN = '.localni.info' -LOGIN_REDICRECT_URL = 'http://react.localni.info' +# SESSION_COOKIE_DOMAIN = 'localni.info' +LOGIN_REDICRECT_URL = 'react.localni.info' +SESSION_COOKIE_HTTPONLY = False + +CORS_ALLOW_CREDENTIALS = True +# CORS_ORIGIN_WHITELIST = ('react.localni.info',) +# +# CSRF_TRUSTED_ORIGINS = ('react.localni.info',) ########## EMAIL CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend diff --git a/src/niweb/niweb/urls.py b/src/niweb/niweb/urls.py index 4f86f444e..2c50e0605 100644 --- a/src/niweb/niweb/urls.py +++ b/src/niweb/niweb/urls.py @@ -65,9 +65,34 @@ def if_installed(appname, *args, **kwargs): url(r'^admin/', admin.site.urls), ] + +class SuccessURLAllowedHostsMixin(object): + success_url_allowed_hosts = set() + + def get_success_url_allowed_hosts(self): + allowed_hosts = {self.request.get_host()} + allowed_hosts.update(self.success_url_allowed_hosts) + return allowed_hosts + + +class CustomLoginView(SuccessURLAllowedHostsMixin, auth_views.LoginView): + + def get_success_url(self): + from django.shortcuts import resolve_url + url = self.get_redirect_url() + return url or resolve_url(settings.LOGIN_REDIRECT_URL) + + def form_valid(self, form): + from django.http import HttpResponseRedirect + from django.contrib.auth import login as auth_login + """Security check complete. Log the user in.""" + auth_login(self.request, form.get_user()) + return HttpResponseRedirect("http://localhost:3000/") + + if not settings.DJANGO_LOGIN_DISABLED: urlpatterns += [ - url(r'^accounts/login/$', auth_views.LoginView.as_view(), name='django_login'), + url(r'^accounts/login/$', CustomLoginView.as_view(), name='django_login'), ] # Federated login From fb8c44015e87bb6d1305b8b5696fac4c122c89d7 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 21 May 2019 09:08:09 +0200 Subject: [PATCH 078/520] Added db dropdown for hardcoded combo. --- src/niweb/apps/noclook/forms/common.py | 2 +- src/niweb/apps/noclook/migrations/common_dropdowns.csv | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index c1a9c9c07..e63eefc07 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -892,7 +892,7 @@ def clean(self): class NewContactForm(forms.Form): def __init__(self, *args, **kwargs): super(NewContactForm, self).__init__(*args, **kwargs) - self.fields['contact_type'].choices = [('person', 'Person'), ('group', 'Group')] + self.fields['contact_type'].choices = Dropdown.get('contact_type').as_choices() first_name = forms.CharField() last_name = forms.CharField() diff --git a/src/niweb/apps/noclook/migrations/common_dropdowns.csv b/src/niweb/apps/noclook/migrations/common_dropdowns.csv index 5aadda370..362d53232 100644 --- a/src/niweb/apps/noclook/migrations/common_dropdowns.csv +++ b/src/niweb/apps/noclook/migrations/common_dropdowns.csv @@ -58,3 +58,5 @@ organization_contact_types,secondary_contact,Secondary contact at incidents organization_contact_types,it_technical_contact,IT-technical organization_contact_types,it_security_contact,IT-security organization_contact_types,it_manager_contact,IT-manager +contact_type,person,Person +contact_type,group,Group From d951ea2365a7061693be7b4c656d901362026196 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 21 May 2019 10:49:48 +0200 Subject: [PATCH 079/520] Added filter param and filter builder --- src/niweb/apps/noclook/schema/core.py | 35 +++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index b823b44ba..21829147c 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -86,6 +86,9 @@ def resolve_node_string(self, info, **kwargs): return resolve_node_string + def get_field_type(self): + return self.field_type + class NIStringField(NIBasicField): ''' String type @@ -650,7 +653,7 @@ def __init_subclass__(cls, **kwargs): # add list with pagination resolver # add by id resolver for graphql_type in graphql_types: - # extract values + ## extract values _nimetatype = getattr(graphql_type, 'NIMetaType') ni_type = getattr(_nimetatype, 'ni_type') @@ -662,23 +665,45 @@ def __init_subclass__(cls, **kwargs): type_name = node_type.type type_slug = node_type.slug - # build connection class + # add connection attribute field_name = '{}s'.format(type_slug) resolver_name = 'resolve_{}'.format(field_name) + connection_input = cls.build_filter_input(graphql_type, type_name) + connection_meta = type('Meta', (object, ), dict(node=graphql_type)) connection_class = type( '{}Connection'.format(type_name), (graphene.relay.Connection,), + #(connection_type,), dict(Meta=connection_meta) ) - - setattr(cls, field_name, graphene.relay.ConnectionField(connection_class)) + + setattr(cls, field_name, graphene.relay.ConnectionField(connection_class, filter=graphene.Argument(connection_input))) setattr(cls, resolver_name, get_connection_resolver(type_name)) - # build field and resolver byid + ## 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, handle_id=graphene.Int())) setattr(cls, resolver_name, get_byid_resolver(type_name)) + + @classmethod + def build_filter_input(cls, graphql_type, type_name): + ## Maybe the input class should be declared in the types + + # build filter input class + filter_attrib = {} + #raise Exception(graphql_type.__dict__) + for name, field in graphql_type.__dict__.items(): + if field and not isinstance(field, str) and field.__module__ == 'graphene.types.scalars': + # adding filter attributes + filter_attrib['{}'.format(name)] = field + filter_attrib['{}_not'.format(name)] = field + filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(type(field))) + filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(type(field))) + + filter_input = type('{}Filter'.format(type_name), (graphene.InputObjectType, ), filter_attrib) + + return filter_input From 068cf898eaf88983b0c41c715ae59bb60be39fa5 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 21 May 2019 12:31:45 +0200 Subject: [PATCH 080/520] Removed Node subclass from NIObjectType --- src/niweb/apps/noclook/schema/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 21829147c..d8da14ec7 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -233,7 +233,7 @@ def get_queryset(cls, queryset, info): class Meta: model = NodeHandle - interfaces = (NIRelayNode, ) + interfaces = (relay.Node, ) class NodeHandleType(NIObjectType): pass @@ -678,7 +678,7 @@ def __init_subclass__(cls, **kwargs): #(connection_type,), dict(Meta=connection_meta) ) - + setattr(cls, field_name, graphene.relay.ConnectionField(connection_class, filter=graphene.Argument(connection_input))) setattr(cls, resolver_name, get_connection_resolver(type_name)) From b5328e61c663b6eaa7844a7881784d39e13b885e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 21 May 2019 13:04:13 +0200 Subject: [PATCH 081/520] NIRelayNode discarded --- src/niweb/apps/noclook/schema/core.py | 38 ++++++++------------------ src/niweb/apps/noclook/schema/types.py | 3 -- src/niweb/niweb/schema.py | 2 +- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index d8da14ec7..7bf803e38 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -17,31 +17,6 @@ from ..models import NodeType, NodeHandle -class NIRelayNode(relay.Node): - ''' - from https://docs.graphene-python.org/en/latest/relay/nodes/ - This node may implement the id policies in the graph database - ''' - class Meta: - name = 'NIRelayNode' - - @staticmethod - def to_global_id(type, id): - return '{}:{}'.format(type, id) - - @staticmethod - def get_node_from_global_id(info, global_id, only_type=None): - type, id = global_id.split(':') - if only_type: - # We assure that the node type that we want to retrieve - # is the same that was indicated in the field type - assert type == only_type._meta.name, 'Received not compatible node.' - - if type == 'DropdownType' or 'RoleType': # TODO too raw - return Dropdown.objects.get(pk=id) - else: - return NodeHandle.objects.get(handle_id=id) - class DictEntryType(graphene.ObjectType): ''' This type represents an key value pair in a dictionary for the data @@ -619,7 +594,7 @@ def generic_byid_resolver(self, info, **args): return generic_byid_resolver class NOCAutoQuery(graphene.ObjectType): - node = NIRelayNode.Field() + node = relay.Node.Field() getNodeById = graphene.Field(NodeHandleType, handle_id=graphene.Int()) def resolve_getNodeById(self, info, **args): @@ -703,6 +678,17 @@ def build_filter_input(cls, graphql_type, type_name): filter_attrib['{}_not'.format(name)] = field filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(type(field))) filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(type(field))) + filter_attrib['{}_lt'.format(name)] = field + filter_attrib['{}_lte'.format(name)] = field + filter_attrib['{}_gt'.format(name)] = field + filter_attrib['{}_gte'.format(name)] = field + filter_attrib['{}_contains'.format(name)] = field + filter_attrib['{}_not_contains'.format(name)] = field + filter_attrib['{}_starts_with'.format(name)] = field + filter_attrib['{}_not_starts_with'.format(name)] = field + filter_attrib['{}_ends_with'.format(name)] = field + filter_attrib['{}_not_ends_with'.format(name)] = field + filter_attrib['{}'.format(name)] = field filter_input = type('{}Filter'.format(type_name), (graphene.InputObjectType, ), filter_attrib) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 3a134a7c7..7766862d8 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -30,17 +30,14 @@ def resolve_roles_list(self, info, **kwargs): class UserType(DjangoObjectType): class Meta: model = User - interfaces = (NIRelayNode, ) class DropdownType(DjangoObjectType): class Meta: model = Dropdown - interfaces = (NIRelayNode, ) class ChoiceType(DjangoObjectType): class Meta: model = Choice - interfaces = (NIRelayNode, ) class RoleType(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) diff --git a/src/niweb/niweb/schema.py b/src/niweb/niweb/schema.py index e80af4e05..24ef47179 100644 --- a/src/niweb/niweb/schema.py +++ b/src/niweb/niweb/schema.py @@ -1,6 +1,6 @@ import graphene from apps.noclook.schema import NOCSCHEMA_QUERIES, NOCSCHEMA_MUTATIONS,\ - NOCSCHEMA_TYPES, NIRelayNode + NOCSCHEMA_TYPES ALL_TYPES = NOCSCHEMA_TYPES # + OTHER_APP_TYPES ALL_QUERIES = NOCSCHEMA_QUERIES From b7afd171e5ae38be3dccd9dd1de2e3a00d7bae82 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 21 May 2019 14:00:18 +0200 Subject: [PATCH 082/520] Make the connections naming consistent --- src/niweb/apps/noclook/schema/__init__.py | 3 +++ src/niweb/apps/noclook/schema/core.py | 24 ++++++++++++----------- src/niweb/apps/noclook/schema/types.py | 1 + 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index 148124377..de27a54da 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -11,6 +11,9 @@ from .mutations import * NOCSCHEMA_TYPES = [ + UserType, + DropdownType, + ChoiceType, RoleType, GroupType, ContactType, diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 7bf803e38..39b310ac7 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -644,11 +644,10 @@ def __init_subclass__(cls, **kwargs): field_name = '{}s'.format(type_slug) resolver_name = 'resolve_{}'.format(field_name) - connection_input = cls.build_filter_input(graphql_type, type_name) - + connection_input = cls.build_filter_and_order(graphql_type, type_name) connection_meta = type('Meta', (object, ), dict(node=graphql_type)) connection_class = type( - '{}Connection'.format(type_name), + '{}Connection'.format(graphql_type.__name__), (graphene.relay.Connection,), #(connection_type,), dict(Meta=connection_meta) @@ -665,7 +664,7 @@ def __init_subclass__(cls, **kwargs): setattr(cls, resolver_name, get_byid_resolver(type_name)) @classmethod - def build_filter_input(cls, graphql_type, type_name): + def build_filter_and_order(cls, graphql_type, type_name): ## Maybe the input class should be declared in the types # build filter input class @@ -682,13 +681,16 @@ def build_filter_input(cls, graphql_type, type_name): filter_attrib['{}_lte'.format(name)] = field filter_attrib['{}_gt'.format(name)] = field filter_attrib['{}_gte'.format(name)] = field - filter_attrib['{}_contains'.format(name)] = field - filter_attrib['{}_not_contains'.format(name)] = field - filter_attrib['{}_starts_with'.format(name)] = field - filter_attrib['{}_not_starts_with'.format(name)] = field - filter_attrib['{}_ends_with'.format(name)] = field - filter_attrib['{}_not_ends_with'.format(name)] = field - filter_attrib['{}'.format(name)] = field + + if isinstance(field, graphene.String): + filter_attrib['{}_contains'.format(name)] = field + filter_attrib['{}_not_contains'.format(name)] = field + filter_attrib['{}_starts_with'.format(name)] = field + filter_attrib['{}_not_starts_with'.format(name)] = field + filter_attrib['{}_ends_with'.format(name)] = field + filter_attrib['{}_not_ends_with'.format(name)] = field + + # adding order attributes filter_input = type('{}Filter'.format(type_name), (graphene.InputObjectType, ), filter_attrib) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 7766862d8..3afde261f 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -30,6 +30,7 @@ def resolve_roles_list(self, info, **kwargs): class UserType(DjangoObjectType): class Meta: model = User + exclude_fields = ['creator', 'modifier'] class DropdownType(DjangoObjectType): class Meta: From 438cc26a04dc161cb7f3f39aae513b94a575c97a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 21 May 2019 16:50:31 +0200 Subject: [PATCH 083/520] Removed nodehandle attribute from mutation payload and added return type --- src/niweb/apps/noclook/schema/core.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 39b310ac7..527b4c50a 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -214,8 +214,6 @@ class NodeHandleType(NIObjectType): pass class AbstractNIMutation(relay.ClientIDMutation): - nodehandle = graphene.Field(NodeHandleType, required=True) # the type should be replaced - @classmethod def __init_subclass_with_meta__( cls, output=None, input_fields=None, arguments=None, name=None, **options @@ -224,6 +222,7 @@ def __init_subclass_with_meta__( ''' # 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', cls.__name__) is_create = getattr(ni_metaclass, 'is_create', False) @@ -252,6 +251,10 @@ def __init_subclass_with_meta__( inner_class = type('Input', (object,), inner_fields) setattr(cls, 'Input', inner_class) + # add return type + if graphql_type: + setattr(cls, graphql_type.__name__.lower(), graphene.Field(graphql_type)) + super(AbstractNIMutation, cls).__init_subclass_with_meta__( output, inner_fields, arguments, name=mutation_name, **options ) @@ -356,7 +359,19 @@ def mutate_and_get_payload(cls, root, info, **input): reqinput = cls.from_input_to_request(info.context.user, **input) ret = cls.do_request(reqinput[0], **reqinput[1]) - return cls(nodehandle=ret) + graphql_type = cls.get_graphql_type() + init_params = {} + if graphql_type: + init_params[graphql_type.__name__.lower()] = 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 class Meta: abstract = True @@ -423,8 +438,6 @@ def do_request(cls, request, **kwargs): raise GraphQLError('Form errors: {}'.format(form)) class DeleteNIMutation(AbstractNIMutation): - nodehandle = graphene.Boolean(required=True) - class NIMetaClass: request_path = None graphql_type = None @@ -436,7 +449,7 @@ def do_request(cls, request, **kwargs): nh, node = helpers.get_nh_node(handle_id) helpers.delete_node(request.user, node.handle_id) - return True + return None class NIMutationFactory(): ''' @@ -500,7 +513,6 @@ def __init_subclass__(cls, **kwargs): class_name, (cls.create_mutation_class,), { - nh_field: graphene.Field(graphql_type, required=True), metaclass_name: create_metaclass, }, ) @@ -515,7 +527,6 @@ def __init_subclass__(cls, **kwargs): class_name, (cls.update_mutation_class,), { - nh_field: graphene.Field(graphql_type, required=True), metaclass_name: update_metaclass, }, ) From 8976e97f54e0205552edaea15009bd7381e735e0 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 21 May 2019 17:29:52 +0200 Subject: [PATCH 084/520] Types simplification --- src/niweb/apps/noclook/schema/__init__.py | 14 +++++++------- src/niweb/apps/noclook/schema/core.py | 7 ++++--- src/niweb/apps/noclook/schema/mutations.py | 10 +++++----- src/niweb/apps/noclook/schema/query.py | 4 ++-- src/niweb/apps/noclook/schema/types.py | 16 ++++++++-------- .../apps/noclook/tests/schema/test_mutations.py | 16 +++++++++------- 6 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index de27a54da..f36603fac 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -11,13 +11,13 @@ from .mutations import * NOCSCHEMA_TYPES = [ - UserType, - DropdownType, - ChoiceType, - RoleType, - GroupType, - ContactType, - NodeHandleType, + User, + Dropdown, + Choice, + Role, + Group, + Contact, + NodeHandler, ] NOCSCHEMA_QUERIES = [ diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 527b4c50a..f2bb1b95c 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -210,7 +210,7 @@ class Meta: model = NodeHandle interfaces = (relay.Node, ) -class NodeHandleType(NIObjectType): +class NodeHandler(NIObjectType): pass class AbstractNIMutation(relay.ClientIDMutation): @@ -481,7 +481,7 @@ def __init_subclass__(cls, **kwargs): 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', NodeHandleType) + graphql_type = getattr(ni_metaclass, 'graphql_type', NodeHandler) # we'll retrieve these values NI type/metatype from the GraphQLType nimetatype = getattr(graphql_type, 'NIMetaType') @@ -606,7 +606,7 @@ def generic_byid_resolver(self, info, **args): class NOCAutoQuery(graphene.ObjectType): node = relay.Node.Field() - getNodeById = graphene.Field(NodeHandleType, handle_id=graphene.Int()) + getNodeById = graphene.Field(NodeHandler, handle_id=graphene.Int()) def resolve_getNodeById(self, info, **args): handle_id = args.get('handle_id') @@ -704,5 +704,6 @@ def build_filter_and_order(cls, graphql_type, type_name): # adding order attributes filter_input = type('{}Filter'.format(type_name), (graphene.InputObjectType, ), filter_attrib) + #Episode = graphene.Enum('Episode', [('NEWHOPE', 4), ('EMPIRE', 5), ('JEDI', 6)]) return filter_input diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 692ef4f05..41b2a0dc5 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -14,7 +14,7 @@ class NIMetaClass: create_form = NewRoleForm update_form = EditRoleForm request_path = '/' - graphql_type = RoleType + graphql_type = Role class Meta: abstract = False @@ -24,7 +24,7 @@ class NIMetaClass: create_form = NewGroupForm update_form = EditGroupForm request_path = '/' - graphql_type = GroupType + graphql_type = Group class Meta: abstract = False @@ -34,7 +34,7 @@ class NIMetaClass: create_form = NewContactForm update_form = EditContactForm request_path = '/' - graphql_type = ContactType + graphql_type = Contact class Meta: abstract = False @@ -42,12 +42,12 @@ class Meta: class CreateRoleNIMutation(CreateNIMutation): '''This class is not used but left out as documentation in the case that as finer grain of control is needed''' - nodehandle = graphene.Field(RoleType, required=True) + role = graphene.Field(Role, required=True) class NIMetaClass: request_path = '/' django_form = NewRoleForm - graphql_type = RoleType + graphql_type = Role class Meta: abstract = False diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 5e13cb60d..c83cb508b 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -7,7 +7,7 @@ from .types import * class NOCRootQuery(NOCAutoQuery): - getChoicesForDropdown = graphene.List(ChoiceType, name=graphene.String(required=True)) + getChoicesForDropdown = graphene.List(Choice, name=graphene.String(required=True)) def resolve_getChoicesForDropdown(self, info, **kwargs): name = kwargs.get('name') @@ -19,4 +19,4 @@ def resolve_getChoicesForDropdown(self, info, **kwargs): raise Exception(u'Could not find dropdown with name \'{}\'. Please create it using /admin/'.format(name)) class NIMeta: - graphql_types = [ RoleType, GroupType, ContactType ] + graphql_types = [ Role, Group, Contact ] diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 3afde261f..5d7141102 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -27,34 +27,34 @@ def resolve_roles_list(self, info, **kwargs): return ret -class UserType(DjangoObjectType): +class User(DjangoObjectType): class Meta: model = User exclude_fields = ['creator', 'modifier'] -class DropdownType(DjangoObjectType): +class Dropdown(DjangoObjectType): class Meta: model = Dropdown -class ChoiceType(DjangoObjectType): +class Choice(DjangoObjectType): class Meta: model = Choice -class RoleType(NIObjectType): +class Role(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) class NIMetaType: ni_type = 'Role' ni_metatype = 'logical' -class GroupType(NIObjectType): +class Group(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) class NIMetaType: ni_type = 'Group' ni_metatype = 'logical' -class ContactType(NIObjectType): +class Contact(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) first_name = NIStringField(type_kwargs={ 'required': True }) last_name = NIStringField(type_kwargs={ 'required': True }) @@ -66,8 +66,8 @@ class ContactType(NIObjectType): email = NIStringField() other_email = NIStringField() PGP_fingerprint = NIStringField() - is_roles = NIListField(type_args=(RoleType,), manual_resolver=resolve_roles_list) - member_of_groups = NIListField(type_args=(GroupType,), rel_name='Member_of', rel_method='get_outgoing_relations') + is_roles = NIListField(type_args=(Role,), manual_resolver=resolve_roles_list) + member_of_groups = NIListField(type_args=(Group,), rel_name='Member_of', rel_method='get_outgoing_relations') class NIMetaType: ni_type = 'Contact' diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index bea6516d4..eadfbd444 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -11,7 +11,7 @@ def test_role(self): query = ''' mutation create_test_role { create_role(input: {name: "New test role"}){ - nodehandle { + role { handle_id name } @@ -23,7 +23,7 @@ def test_role(self): expected = OrderedDict([ ('create_role', OrderedDict([ - ('nodehandle', + ('role', OrderedDict([ ('handle_id', '9'), ('name', 'New test role') @@ -39,11 +39,11 @@ def test_role(self): assert result.data == expected ## update ## - role_handle_id = int(result.data['create_role']['nodehandle']['handle_id']) + role_handle_id = int(result.data['create_role']['role']['handle_id']) query = """ mutation update_test_role { update_role(input: {handle_id: 9, name: "A test role"}){ - nodehandle { + role { handle_id name } @@ -55,7 +55,7 @@ def test_role(self): expected = OrderedDict([ ('update_role', OrderedDict([ - ('nodehandle', + ('role', OrderedDict([ ('handle_id', '9'), ('name', 'A test role') @@ -73,7 +73,9 @@ def test_role(self): query = """ mutation delete_test_role { delete_role(input: {handle_id: 9}){ - nodehandle + role{ + handle_id + } } } """ @@ -81,7 +83,7 @@ def test_role(self): expected = OrderedDict([ ('delete_role', OrderedDict([ - ('nodehandle', True), + ('role', None), ]) ) ]) From 38bd4f501c9c1c9ff97d12b420c815c8ad6b077f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 22 May 2019 09:01:42 +0200 Subject: [PATCH 085/520] Import bugfix --- src/niweb/apps/noclook/schema/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index c83cb508b..78634cb1d 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -3,7 +3,7 @@ import graphene from graphql import GraphQLError -from ..models import Dropdown +from ..models import Dropdown as DropdownModel from .types import * class NOCRootQuery(NOCAutoQuery): @@ -11,7 +11,7 @@ class NOCRootQuery(NOCAutoQuery): def resolve_getChoicesForDropdown(self, info, **kwargs): name = kwargs.get('name') - ddqs = Dropdown.get(name) + ddqs = DropdownModel.get(name) if not isinstance(ddqs, DummyDropdown): return ddqs.choice_set.order_by('name') From bafd33bc7065c6483ad1311079e70013ecb1a525 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 22 May 2019 11:46:36 +0200 Subject: [PATCH 086/520] Added order argument and improved tests --- src/niweb/apps/noclook/schema/core.py | 18 +++++--- .../apps/noclook/tests/schema/__init__.py | 10 ++++- .../apps/noclook/tests/schema/test_schema.py | 44 +++++++++++++++++-- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index f2bb1b95c..ce4986243 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -655,7 +655,7 @@ def __init_subclass__(cls, **kwargs): field_name = '{}s'.format(type_slug) resolver_name = 'resolve_{}'.format(field_name) - connection_input = cls.build_filter_and_order(graphql_type, type_name) + connection_input, connection_order = cls.build_filter_and_order(graphql_type, type_name) connection_meta = type('Meta', (object, ), dict(node=graphql_type)) connection_class = type( '{}Connection'.format(graphql_type.__name__), @@ -664,7 +664,11 @@ def __init_subclass__(cls, **kwargs): dict(Meta=connection_meta) ) - setattr(cls, field_name, graphene.relay.ConnectionField(connection_class, filter=graphene.Argument(connection_input))) + setattr(cls, field_name, graphene.relay.ConnectionField( + connection_class, + filter=graphene.Argument(connection_input), + orderBy=graphene.Argument(connection_order), + )) setattr(cls, resolver_name, get_connection_resolver(type_name)) ## build field and resolver byid @@ -678,8 +682,10 @@ def __init_subclass__(cls, **kwargs): def build_filter_and_order(cls, graphql_type, type_name): ## Maybe the input class should be declared in the types - # build filter input class + # build filter input class and order enum filter_attrib = {} + enum_options = [] + #raise Exception(graphql_type.__dict__) for name, field in graphql_type.__dict__.items(): if field and not isinstance(field, str) and field.__module__ == 'graphene.types.scalars': @@ -702,8 +708,10 @@ def build_filter_and_order(cls, graphql_type, type_name): filter_attrib['{}_not_ends_with'.format(name)] = field # adding order attributes + enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) + enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) filter_input = type('{}Filter'.format(type_name), (graphene.InputObjectType, ), filter_attrib) - #Episode = graphene.Enum('Episode', [('NEWHOPE', 4), ('EMPIRE', 5), ('JEDI', 6)]) + orderBy = graphene.Enum('{}OrderBy'.format(type_name), enum_options) - return filter_input + return filter_input, orderBy diff --git a/src/niweb/apps/noclook/tests/schema/__init__.py b/src/niweb/apps/noclook/tests/schema/__init__.py index 4f36487ff..8652337c4 100644 --- a/src/niweb/apps/noclook/tests/schema/__init__.py +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -3,7 +3,7 @@ from django.db import connection -from apps.noclook.models import NodeHandle +from apps.noclook.models import NodeHandle, Dropdown, Choice from ..neo4j_base import NeoTestCase class TestContext(): @@ -56,6 +56,14 @@ def setUp(self): contact2.get_node().add_group(group2.handle_id) contact2.get_node().add_organization(organization2.handle_id) + # 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() + self.initialized = True def tearDown(self): diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index f8925d24b..f859a112e 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -30,14 +30,14 @@ def test_get_contacts(self): expected = OrderedDict([('contacts', OrderedDict([('edges', [OrderedDict([('node',OrderedDict([ - ('handle_id', '13'), + ('handle_id', '21'), ('name', 'John Smith'), ('first_name', 'John'), ('last_name', 'Smith'), ('is_roles', [OrderedDict([('name', 'role2')])]), ('member_of_groups', [OrderedDict([('name', 'group2')])]), ]))]), OrderedDict([('node', OrderedDict([ - ('handle_id', '12'), + ('handle_id', '20'), ('name', 'Jane Doe'), ('first_name', 'Jane'), ('last_name', 'Doe'), @@ -50,15 +50,51 @@ def test_get_contacts(self): assert not result.errors, result.errors assert result.data == expected + query = ''' + query { + getNodeById(handle_id: 20){ + handle_id + } + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors + def test_getnodebyhandle_id(self): query = ''' query { - getNodeById(handle_id: 13){ + getNodeById(handle_id: 1){ handle_id } } ''' result = schema.execute(query, context=self.context) + #assert not result.errors, result.errors - #assert not result.errors + def test_dropdown(self): + query = ''' + query{ + getChoicesForDropdown(name:"contact_type"){ + id + dropdown{ + id + name + choice_set{ + id + dropdown { + id + } + name + value + } + } + name + value + } + } + ''' + + result = schema.execute(query, context=self.context) + assert not result.errors, result.errors From 9f132162ee2f50e5101055d37e6052639968d04e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 22 May 2019 12:45:46 +0200 Subject: [PATCH 087/520] Added nested AND and OR filters --- src/niweb/apps/noclook/schema/core.py | 37 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index ce4986243..d6c1caf3c 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -565,6 +565,7 @@ def __init__(self, message=None): def get_connection_resolver(nodetype): def generic_list_resolver(self, info, **args): + raise Exception(args) node_type = NodeType.objects.get(type=nodetype) if info.context and info.context.user.is_authenticated: @@ -686,32 +687,38 @@ def build_filter_and_order(cls, graphql_type, type_name): filter_attrib = {} enum_options = [] - #raise Exception(graphql_type.__dict__) for name, field in graphql_type.__dict__.items(): if field and not isinstance(field, str) and field.__module__ == 'graphene.types.scalars': + input_field = type(field)() + # adding filter attributes - filter_attrib['{}'.format(name)] = field - filter_attrib['{}_not'.format(name)] = field - filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(type(field))) - filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(type(field))) - filter_attrib['{}_lt'.format(name)] = field - filter_attrib['{}_lte'.format(name)] = field - filter_attrib['{}_gt'.format(name)] = field - filter_attrib['{}_gte'.format(name)] = field + filter_attrib['{}'.format(name)] = input_field + filter_attrib['{}_not'.format(name)] = input_field + filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) + filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) + filter_attrib['{}_lt'.format(name)] = input_field + filter_attrib['{}_lte'.format(name)] = input_field + filter_attrib['{}_gt'.format(name)] = input_field + filter_attrib['{}_gte'.format(name)] = input_field if isinstance(field, graphene.String): - filter_attrib['{}_contains'.format(name)] = field - filter_attrib['{}_not_contains'.format(name)] = field - filter_attrib['{}_starts_with'.format(name)] = field - filter_attrib['{}_not_starts_with'.format(name)] = field - filter_attrib['{}_ends_with'.format(name)] = field - filter_attrib['{}_not_ends_with'.format(name)] = field + filter_attrib['{}_contains'.format(name)] = input_field + filter_attrib['{}_not_contains'.format(name)] = input_field + filter_attrib['{}_starts_with'.format(name)] = input_field + filter_attrib['{}_not_starts_with'.format(name)] = input_field + filter_attrib['{}_ends_with'.format(name)] = input_field + filter_attrib['{}_not_ends_with'.format(name)] = input_field # adding order attributes enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) + simple_filter_input = type('{}NestedFilter'.format(type_name), (graphene.InputObjectType, ), 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(type_name), (graphene.InputObjectType, ), filter_attrib) + orderBy = graphene.Enum('{}OrderBy'.format(type_name), enum_options) return filter_input, orderBy From 95735a942a6f8553f393dc676de36b8a12fc1744 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 22 May 2019 16:42:02 +0200 Subject: [PATCH 088/520] Improved list resolver, wip for filter and ordering --- src/niweb/apps/noclook/schema/core.py | 73 ++++++++++++++++++--------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index d6c1caf3c..e62c74da3 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -2,7 +2,9 @@ __author__ = 'ffuentes' import graphene +import norduniclient as nc import re + from apps.noclook import helpers from apps.noclook.models import NodeType, NodeHandle from collections import OrderedDict @@ -565,14 +567,24 @@ def __init__(self, message=None): def get_connection_resolver(nodetype): def generic_list_resolver(self, info, **args): - raise Exception(args) - node_type = NodeType.objects.get(type=nodetype) + filter = args.get('filter', None) + orderBy = args.get('orderBy', None) if info.context and info.context.user.is_authenticated: - ret = NodeHandle.objects.filter(node_type=node_type) - """ret.filter( - Q(creator=info.context.user) | Q(modifier=info.context.user) - )""" + # filtering will take a different approach + nodes = None + if filter: + pass + else: + nodes = nc.get_nodes_by_type(nc.graphdb.manager, nodetype) + + if not nodes: + ret = [] + else: + handle_ids = [ node['handle_id'] for node in nodes ] + ret = NodeHandle.objects.filter( + handle_id__in=handle_ids + ) if not ret: ret = [] @@ -681,10 +693,8 @@ def __init_subclass__(cls, **kwargs): @classmethod def build_filter_and_order(cls, graphql_type, type_name): - ## Maybe the input class should be declared in the types - # build filter input class and order enum - filter_attrib = {} + simple_filter_attrib = {} enum_options = [] for name, field in graphql_type.__dict__.items(): @@ -692,28 +702,43 @@ def build_filter_and_order(cls, graphql_type, type_name): input_field = type(field)() # adding filter attributes - filter_attrib['{}'.format(name)] = input_field - filter_attrib['{}_not'.format(name)] = input_field - filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) - filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) - filter_attrib['{}_lt'.format(name)] = input_field - filter_attrib['{}_lte'.format(name)] = input_field - filter_attrib['{}_gt'.format(name)] = input_field - filter_attrib['{}_gte'.format(name)] = input_field + simple_filter_attrib['{}'.format(name)] = input_field + simple_filter_attrib['{}_not'.format(name)] = input_field + simple_filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) + simple_filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) + simple_filter_attrib['{}_lt'.format(name)] = input_field + simple_filter_attrib['{}_lte'.format(name)] = input_field + simple_filter_attrib['{}_gt'.format(name)] = input_field + simple_filter_attrib['{}_gte'.format(name)] = input_field if isinstance(field, graphene.String): - filter_attrib['{}_contains'.format(name)] = input_field - filter_attrib['{}_not_contains'.format(name)] = input_field - filter_attrib['{}_starts_with'.format(name)] = input_field - filter_attrib['{}_not_starts_with'.format(name)] = input_field - filter_attrib['{}_ends_with'.format(name)] = input_field - filter_attrib['{}_not_ends_with'.format(name)] = input_field + simple_filter_attrib['{}_contains'.format(name)] = input_field + simple_filter_attrib['{}_not_contains'.format(name)] = input_field + simple_filter_attrib['{}_starts_with'.format(name)] = input_field + simple_filter_attrib['{}_not_starts_with'.format(name)] = input_field + simple_filter_attrib['{}_ends_with'.format(name)] = input_field + simple_filter_attrib['{}_not_ends_with'.format(name)] = input_field # adding order attributes enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) - simple_filter_input = type('{}NestedFilter'.format(type_name), (graphene.InputObjectType, ), filter_attrib) + # add handle_id + name = 'handle_id' + simple_filter_attrib['{}'.format(name)] = graphene.Int() + simple_filter_attrib['{}_not'.format(name)] = graphene.Int() + simple_filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(graphene.Int)) + simple_filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(graphene.Int)) + simple_filter_attrib['{}_lt'.format(name)] = graphene.Int() + simple_filter_attrib['{}_lte'.format(name)] = graphene.Int() + simple_filter_attrib['{}_gt'.format(name)] = graphene.Int() + simple_filter_attrib['{}_gte'.format(name)] = graphene.Int() + enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) + enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) + + simple_filter_input = type('{}NestedFilter'.format(type_name), (graphene.InputObjectType, ), simple_filter_attrib) + + filter_attrib = {} filter_attrib['AND'] = graphene.List(graphene.NonNull(simple_filter_input)) filter_attrib['OR'] = graphene.List(graphene.NonNull(simple_filter_input)) From 65e0d2fa66793fe134aa5b3641f4b985a27d6000 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 22 May 2019 17:37:07 +0200 Subject: [PATCH 089/520] Simple ordering implemented --- src/niweb/apps/noclook/schema/core.py | 20 +++++++--- .../apps/noclook/tests/schema/test_schema.py | 40 +++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index e62c74da3..711606f39 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -578,13 +578,21 @@ def generic_list_resolver(self, info, **args): else: nodes = nc.get_nodes_by_type(nc.graphdb.manager, nodetype) - if not nodes: - ret = [] - else: + if nodes: + # ordering + nodes = list(nodes) + if orderBy: + m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) + prop = m[1] + order = m[2] + reverse = True if order == 'DESC' else False + nodes.sort(key=lambda x: x.get(prop, ''), reverse=reverse) + + # get the QuerySet handle_ids = [ node['handle_id'] for node in nodes ] - ret = NodeHandle.objects.filter( - handle_id__in=handle_ids - ) + ret = [ NodeHandle.objects.get(handle_id=handle_id) for handle_id in handle_ids ] + else: + ret = [] if not ret: ret = [] diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index f859a112e..d1377feb0 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -28,22 +28,30 @@ def test_get_contacts(self): } ''' - expected = OrderedDict([('contacts', OrderedDict([('edges', - [OrderedDict([('node',OrderedDict([ - ('handle_id', '21'), - ('name', 'John Smith'), - ('first_name', 'John'), - ('last_name', 'Smith'), - ('is_roles', [OrderedDict([('name', 'role2')])]), - ('member_of_groups', [OrderedDict([('name', 'group2')])]), - ]))]), OrderedDict([('node', OrderedDict([ - ('handle_id', '20'), - ('name', 'Jane Doe'), - ('first_name', 'Jane'), - ('last_name', 'Doe'), - ('is_roles', [OrderedDict([('name', 'role1')])]), - ('member_of_groups', [OrderedDict([('name', 'group1')])]), - ]))])])]))]) + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('handle_id', '20'), + ('name', 'Jane Doe'), + ('first_name', 'Jane'), + ('last_name', 'Doe'), + ('is_roles', + [OrderedDict([('name', + 'role1')])]), + ('member_of_groups', + [OrderedDict([('name', + 'group1')])])]))]), + OrderedDict([('node', + OrderedDict([('handle_id', '21'), + ('name', 'John Smith'), + ('first_name', 'John'), + ('last_name', 'Smith'), + ('is_roles', + [OrderedDict([('name', + 'role2')])]), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])])]))])])]))]) result = schema.execute(query, context=self.context) From 0592f4767ffea7ada71994fe2e3494a5d60ce384 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 23 May 2019 13:10:23 +0200 Subject: [PATCH 090/520] Add protofiltering --- src/niweb/apps/noclook/schema/core.py | 39 +++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 711606f39..3502947fc 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -574,13 +574,15 @@ def generic_list_resolver(self, info, **args): # filtering will take a different approach nodes = None if filter: - pass + q = build_filter_query(filter, nodetype) + nodes = nc.query_to_list(nc.graphdb.manager, q) + nodes = [ node['n'].properties for node in nodes] else: nodes = nc.get_nodes_by_type(nc.graphdb.manager, nodetype) + nodes = list(nodes) if nodes: # ordering - nodes = list(nodes) if orderBy: m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) prop = m[1] @@ -588,9 +590,13 @@ def generic_list_resolver(self, info, **args): reverse = True if order == 'DESC' else False nodes.sort(key=lambda x: x.get(prop, ''), reverse=reverse) - # get the QuerySet - handle_ids = [ node['handle_id'] for node in nodes ] - ret = [ NodeHandle.objects.get(handle_id=handle_id) for handle_id in handle_ids ] + # get the QuerySet + handle_ids = [ node['handle_id'] for node in nodes ] + ret = [ NodeHandle.objects.get(handle_id=handle_id) for handle_id in handle_ids ] + else: + handle_ids = [ node['handle_id'] for node in nodes ] + node_type = NodeType.objects.get(type=nodetype) + ret = NodeHandle.objects.filter(node_type=node_type) else: ret = [] @@ -603,6 +609,29 @@ def generic_list_resolver(self, info, **args): return generic_list_resolver +def build_filter_query(filter, nodetype): + build_query = '' + + # build AND block + and_query = '' + and_filters = filter.get('AND') + for and_filter in and_filters: + pass + + # build OR block + or_query = '' + or_filters = filter.get('OR') + for or_filter in or_filters: + pass + + q = """ + MATCH (n:{label}) + {build_query} + RETURN distinct n + """.format(label=nodetype, build_query=build_query) + + return q + def get_byid_resolver(nodetype): def generic_byid_resolver(self, info, **args): handle_id = args.get('handle_id') From a8d7ad6c5f9493cea8cb2eefd4a655b64187edbe Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 27 May 2019 11:36:56 +0200 Subject: [PATCH 091/520] Created relation type in schema core --- src/niweb/apps/noclook/schema/core.py | 41 +++++++++++++++++++------- src/niweb/apps/noclook/schema/query.py | 14 +++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 3502947fc..fb9adb8a4 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -129,7 +129,6 @@ class NIObjectType(DjangoObjectType): @classmethod def __init_subclass_with_meta__( cls, - sri_fields=None, **options, ): fields_names = '' @@ -199,15 +198,6 @@ def __init_subclass_with_meta__( **options ) - @classmethod - def get_queryset(cls, queryset, info): - if info.context.user.is_anonymous: - return queryset.none() - else: - return queryset.filter( - Q(creator=info.context.user) | Q(modifier=info.context.user) - ) - class Meta: model = NodeHandle interfaces = (relay.Node, ) @@ -215,6 +205,37 @@ class Meta: class NodeHandler(NIObjectType): pass +class NIRelationType(graphene.ObjectType): + @classmethod + def __init_subclass_with_meta__( + cls, + **options, + ): + super(NIObjectType, 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(NIObjectType, required=True) + end = graphene.Field(NIObjectType, required=True) + data = graphene.List(DictEntryType) + + 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(): + ret.append(DictEntryType(key=key, value=value)) + + return ret + + class Meta: + interfaces = (relay.Node, ) + class AbstractNIMutation(relay.ClientIDMutation): @classmethod def __init_subclass_with_meta__( diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 78634cb1d..4b09f7e3f 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -2,12 +2,15 @@ __author__ = 'ffuentes' import graphene +import norduniclient as nc + from graphql import GraphQLError from ..models import Dropdown as DropdownModel from .types import * class NOCRootQuery(NOCAutoQuery): getChoicesForDropdown = graphene.List(Choice, name=graphene.String(required=True)) + getRelationFromId = graphene.Field(NIRelationType, relation_id=graphene.Int(required=True)) def resolve_getChoicesForDropdown(self, info, **kwargs): name = kwargs.get('name') @@ -18,5 +21,16 @@ def resolve_getChoicesForDropdown(self, info, **kwargs): else: raise Exception(u'Could not find dropdown with name \'{}\'. Please create it using /admin/'.format(name)) + def resolve_getRelationFromId(self, info, **kwargs): + relation_id = kwargs.get('relation_id') + rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) + rel.relation_id = rel.id + rel.id = None + + #raise Exception(rel) + #raise Exception(vars(rel)) + + return rel + class NIMeta: graphql_types = [ Role, Group, Contact ] From 8883e9644b568f659b5630d870fab88d8ddc483f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 28 May 2019 11:45:09 +0200 Subject: [PATCH 092/520] Added relation dictionary to the nodes. --- src/niweb/apps/noclook/schema/core.py | 156 +++++++++++++++++++------ src/niweb/apps/noclook/schema/query.py | 7 +- 2 files changed, 125 insertions(+), 38 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index fb9adb8a4..3a6d75946 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -39,6 +39,38 @@ def resolve_nidata(self, info, **kwargs): return ret +""" +class NIRelationType(graphene.ObjectType): + @classmethod + def __init_subclass_with_meta__( + cls, + **options, + ): + super(NIObjectType, 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(graphene.Int, required=True) + end = graphene.Field(graphene.Int, required=True) + data = graphene.List(DictEntryType) + + 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(): + ret.append(DictEntryType(key=key, value=value)) + + return ret + + class Meta: + interfaces = (relay.Node, )""" + class NIBasicField(): ''' Super class of the type fields @@ -58,10 +90,10 @@ def get_resolver(self, **kwargs): field_name, self.__class__ ) ) - def resolve_node_string(self, info, **kwargs): + def resolve_node_value(self, info, **kwargs): return self.get_node().data.get(field_name) - return resolve_node_string + return resolve_node_value def get_field_type(self): return self.field_type @@ -80,6 +112,27 @@ 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 NIRelationField(NIBasicField): + def __init__(self, field_type=NIRelationType, manual_resolver=False, + type_kwargs=None, **kwargs): + super(NIRelationField, self).__init__(field_type, manual_resolver, + type_kwargs, **kwargs) + + def get_resolver(self, **kwargs): + 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_value(self, info, **kwargs): + return self.get_node().data.get(field_name) + + return resolve_node_value''' class NIListField(NIBasicField): ''' @@ -119,6 +172,52 @@ def resolve_relationship_list(self, info, **kwargs): return resolve_relationship_list +class NIRelationType(graphene.ObjectType): + @classmethod + def __init_subclass_with_meta__( + cls, + **options, + ): + super(NIObjectType, 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(graphene.Int, required=True) + end = graphene.Field(graphene.Int, required=True) + data = graphene.List(DictEntryType) + + def resolve_relation_id(self, info, **kwargs): + self.relation_id = self.id + self.id = None + + 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(): + ret.append(DictEntryType(key=key, value=value)) + + return ret + + class Meta: + interfaces = (relay.Node, ) + +class DictRelationType(graphene.ObjectType): + ''' + This type represents an key value pair in a dictionary for the data + dict of the norduniclient nodes + ''' + name = graphene.String(required=True) + relation = graphene.Field(NIRelationType, required=True) + + class NIObjectType(DjangoObjectType): ''' This class expands graphene_django object type adding the defined fields in @@ -126,6 +225,7 @@ class NIObjectType(DjangoObjectType): adds a resolver for each field, a nidata field is also added to hold the values of the node data dict. ''' + @classmethod def __init_subclass_with_meta__( cls, @@ -187,10 +287,6 @@ def __init_subclass_with_meta__( .format(name) ) - # add data field and resolver - setattr(cls, 'nidata', graphene.List(DictEntryType)) - setattr(cls, 'resolve_nidata', resolve_nidata) - options['model'] = NIObjectType._meta.model options['interfaces'] = NIObjectType._meta.interfaces @@ -198,44 +294,38 @@ def __init_subclass_with_meta__( **options ) - class Meta: - model = NodeHandle - interfaces = (relay.Node, ) + nidata = graphene.List(DictEntryType, resolver=resolve_nidata) -class NodeHandler(NIObjectType): - pass + incoming = graphene.List(DictRelationType) + outgoing = graphene.List(DictRelationType) -class NIRelationType(graphene.ObjectType): - @classmethod - def __init_subclass_with_meta__( - cls, - **options, - ): - super(NIObjectType, cls).__init_subclass_with_meta__( - **options - ) + def resolve_incoming(self, info, **kwargs): + incoming_rels = self.get_node().incoming + ret = [] + for rel_name, rel in incoming_rels.items(): + relation_id = rel[0]['relationship_id'] + rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) + ret.append(DictRelationType(name=rel_name, relation=rel)) - relation_id = graphene.Int(required=True) - type = graphene.String(required=True) # this may be set to an Enum - start = graphene.Field(NIObjectType, required=True) - end = graphene.Field(NIObjectType, required=True) - data = graphene.List(DictEntryType) + return ret - def resolve_nidata(self, info, **kwargs): - ''' - Is just the same than old resolve_nidata, but it doesn't resolve the node - ''' + def resolve_outgoing(self, info, **kwargs): + outgoing_rels = self.get_node().outgoing ret = [] - - alldata = self.data - for key, value in alldata.items(): - ret.append(DictEntryType(key=key, value=value)) + for rel_name, rel in outgoing_rels.items(): + relation_id = rel[0]['relationship_id'] + rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) + ret.append(DictRelationType(name=rel_name, relation=rel)) return ret class Meta: + model = NodeHandle interfaces = (relay.Node, ) +class NodeHandler(NIObjectType): + name = NIStringField(type_kwargs={ 'required': True }) + class AbstractNIMutation(relay.ClientIDMutation): @classmethod def __init_subclass_with_meta__( diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 4b09f7e3f..deef62eba 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -10,7 +10,7 @@ class NOCRootQuery(NOCAutoQuery): getChoicesForDropdown = graphene.List(Choice, name=graphene.String(required=True)) - getRelationFromId = graphene.Field(NIRelationType, relation_id=graphene.Int(required=True)) + getRelationById = graphene.Field(NIRelationType, relation_id=graphene.Int(required=True)) def resolve_getChoicesForDropdown(self, info, **kwargs): name = kwargs.get('name') @@ -21,15 +21,12 @@ def resolve_getChoicesForDropdown(self, info, **kwargs): else: raise Exception(u'Could not find dropdown with name \'{}\'. Please create it using /admin/'.format(name)) - def resolve_getRelationFromId(self, info, **kwargs): + def resolve_getRelationById(self, info, **kwargs): relation_id = kwargs.get('relation_id') rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) rel.relation_id = rel.id rel.id = None - #raise Exception(rel) - #raise Exception(vars(rel)) - return rel class NIMeta: From 95a9faf81382b3cd763d0174838836830d722c1e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 28 May 2019 17:59:58 +0200 Subject: [PATCH 093/520] Added NIRelationField for the types --- src/niweb/apps/noclook/schema/core.py | 88 +++++++------------ src/niweb/apps/noclook/schema/types.py | 2 + .../apps/noclook/tests/schema/test_schema.py | 27 +++--- 3 files changed, 49 insertions(+), 68 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 3a6d75946..f4b888f13 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -39,38 +39,6 @@ def resolve_nidata(self, info, **kwargs): return ret -""" -class NIRelationType(graphene.ObjectType): - @classmethod - def __init_subclass_with_meta__( - cls, - **options, - ): - super(NIObjectType, 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(graphene.Int, required=True) - end = graphene.Field(graphene.Int, required=True) - data = graphene.List(DictEntryType) - - 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(): - ret.append(DictEntryType(key=key, value=value)) - - return ret - - class Meta: - interfaces = (relay.Node, )""" - class NIBasicField(): ''' Super class of the type fields @@ -112,27 +80,6 @@ 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 NIRelationField(NIBasicField): - def __init__(self, field_type=NIRelationType, manual_resolver=False, - type_kwargs=None, **kwargs): - super(NIRelationField, self).__init__(field_type, manual_resolver, - type_kwargs, **kwargs) - - def get_resolver(self, **kwargs): - 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_value(self, info, **kwargs): - return self.get_node().data.get(field_name) - - return resolve_node_value''' class NIListField(NIBasicField): ''' @@ -217,7 +164,6 @@ class DictRelationType(graphene.ObjectType): name = graphene.String(required=True) relation = graphene.Field(NIRelationType, required=True) - class NIObjectType(DjangoObjectType): ''' This class expands graphene_django object type adding the defined fields in @@ -231,7 +177,6 @@ def __init_subclass_with_meta__( cls, **options, ): - fields_names = '' allfields = cls.__dict__ graphfields = OrderedDict() @@ -245,7 +190,6 @@ def __init_subclass_with_meta__( # run over the fields defined and adding graphene fields and resolvers for name, field in graphfields.items(): - fields_names = fields_names + ' ' + '({} / {})'.format(name, field.__dict__) field_fields = field.__dict__ field_type = field_fields.get('field_type') @@ -323,6 +267,38 @@ class Meta: model = NodeHandle interfaces = (relay.Node, ) +class NIRelationField(NIBasicField): + 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): + 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[rel_name] + + for reldict in reldicts: + relbundle = nc.get_relationship_bundle(nc.graphdb.manager, relationship_id=reldict['relationship_id']) + relation = nc.models.BaseRelationshipModel(nc.graphdb.manager) + relation.load(relbundle) + ret.append(relation) + + return ret + + return resolve_node_relation + class NodeHandler(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 5d7141102..1894ff7b3 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -69,6 +69,8 @@ class Contact(NIObjectType): is_roles = NIListField(type_args=(Role,), manual_resolver=resolve_roles_list) member_of_groups = NIListField(type_args=(Group,), rel_name='Member_of', rel_method='get_outgoing_relations') + works_for = NIRelationField(rel_name='Works_for') + class NIMetaType: ni_type = 'Contact' ni_metatype = 'relation' diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index d1377feb0..b5a5dcced 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -30,17 +30,7 @@ def test_get_contacts(self): expected = OrderedDict([('contacts', OrderedDict([('edges', - [OrderedDict([('node', - OrderedDict([('handle_id', '20'), - ('name', 'Jane Doe'), - ('first_name', 'Jane'), - ('last_name', 'Doe'), - ('is_roles', - [OrderedDict([('name', - 'role1')])]), - ('member_of_groups', - [OrderedDict([('name', - 'group1')])])]))]), + [ OrderedDict([('node', OrderedDict([('handle_id', '21'), ('name', 'John Smith'), @@ -51,7 +41,20 @@ def test_get_contacts(self): 'role2')])]), ('member_of_groups', [OrderedDict([('name', - 'group2')])])]))])])]))]) + 'group2')])])]))]), + OrderedDict([('node', + OrderedDict([('handle_id', '20'), + ('name', 'Jane Doe'), + ('first_name', 'Jane'), + ('last_name', 'Doe'), + ('is_roles', + [OrderedDict([('name', + 'role1')])]), + ('member_of_groups', + [OrderedDict([('name', + 'group1')])])]))]), + ])]))]) + result = schema.execute(query, context=self.context) From d3f0df6221fbe0058e75cc4bb562d8a65f595312 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 29 May 2019 17:17:07 +0200 Subject: [PATCH 094/520] Connection filter is built more dynamically --- src/niweb/apps/noclook/schema/core.py | 88 +++++++++-------- .../apps/noclook/tests/schema/__init__.py | 96 +++++++++---------- .../apps/noclook/tests/schema/test_core.py | 0 .../noclook/tests/schema/test_mutations.py | 4 +- .../apps/noclook/tests/schema/test_schema.py | 8 +- 5 files changed, 99 insertions(+), 97 deletions(-) delete mode 100644 src/niweb/apps/noclook/tests/schema/test_core.py diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index f4b888f13..65a4dfd73 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -696,20 +696,38 @@ def generic_list_resolver(self, info, **args): return generic_list_resolver +filter_array = { + '': { 'wrapper_field': None, 'only_strings': False }, + 'not': { 'wrapper_field': None, 'only_strings': False }, + 'in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, + 'not_in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, + 'lt': { 'wrapper_field': None, 'only_strings': False }, + 'lte': { 'wrapper_field': None, 'only_strings': False }, + 'gt': { 'wrapper_field': None, 'only_strings': False }, + 'gte': { 'wrapper_field': None, 'only_strings': False }, + + 'contains': { 'wrapper_field': None, 'only_strings': True }, + 'not_contains': { 'wrapper_field': None, 'only_strings': True }, + 'starts_with': { 'wrapper_field': None, 'only_strings': True }, + 'not_starts_with': { 'wrapper_field': None, 'only_strings': True }, + 'ends_with': { 'wrapper_field': None, 'only_strings': True }, + 'not_ends_with': { 'wrapper_field': None, 'only_strings': True }, +} + def build_filter_query(filter, nodetype): build_query = '' # build AND block and_query = '' - and_filters = filter.get('AND') + and_filters = filter.get('AND', []) for and_filter in and_filters: - pass + print(and_filter) # build OR block or_query = '' - or_filters = filter.get('OR') + or_filters = filter.get('OR', []) for or_filter in or_filters: - pass + print(or_filter) q = """ MATCH (n:{label}) @@ -820,45 +838,35 @@ def build_filter_and_order(cls, graphql_type, type_name): # build filter input class and order enum simple_filter_attrib = {} enum_options = [] + input_fields = {} for name, field in graphql_type.__dict__.items(): if field and not isinstance(field, str) and field.__module__ == 'graphene.types.scalars': - input_field = type(field)() - - # adding filter attributes - simple_filter_attrib['{}'.format(name)] = input_field - simple_filter_attrib['{}_not'.format(name)] = input_field - simple_filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) - simple_filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(type(input_field))) - simple_filter_attrib['{}_lt'.format(name)] = input_field - simple_filter_attrib['{}_lte'.format(name)] = input_field - simple_filter_attrib['{}_gt'.format(name)] = input_field - simple_filter_attrib['{}_gte'.format(name)] = input_field - - if isinstance(field, graphene.String): - simple_filter_attrib['{}_contains'.format(name)] = input_field - simple_filter_attrib['{}_not_contains'.format(name)] = input_field - simple_filter_attrib['{}_starts_with'.format(name)] = input_field - simple_filter_attrib['{}_not_starts_with'.format(name)] = input_field - simple_filter_attrib['{}_ends_with'.format(name)] = input_field - simple_filter_attrib['{}_not_ends_with'.format(name)] = input_field - - # adding order attributes - enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) - enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) - - # add handle_id - name = 'handle_id' - simple_filter_attrib['{}'.format(name)] = graphene.Int() - simple_filter_attrib['{}_not'.format(name)] = graphene.Int() - simple_filter_attrib['{}_in'.format(name)] = graphene.List(graphene.NonNull(graphene.Int)) - simple_filter_attrib['{}_not_in'.format(name)] = graphene.List(graphene.NonNull(graphene.Int)) - simple_filter_attrib['{}_lt'.format(name)] = graphene.Int() - simple_filter_attrib['{}_lte'.format(name)] = graphene.Int() - simple_filter_attrib['{}_gt'.format(name)] = graphene.Int() - simple_filter_attrib['{}_gte'.format(name)] = graphene.Int() - enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) - enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) + input_field = type(field) + input_fields[name] = input_field + + input_fields['handler_id'] = graphene.Int + + for name, input_field in input_fields.items(): + # adding order attributes + enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) + enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) + + # adding filter attributes + for suffix, suffix_attr in filter_array.items(): + fmt_filter_field = '{}_{}'.format(name, suffix) + if suffix == '': + fmt_filter_field = '{}'.format(name) + + if not suffix_attr['only_strings'] or isinstance(input_field(), graphene.String): + if 'wrapper_field' not in suffix_attr or not suffix_attr['wrapper_field']: + simple_filter_attrib[fmt_filter_field] = input_field() + else: + the_field = input_field + for wrapper_field in suffix_attr['wrapper_field']: + the_field = wrapper_field(the_field) + + simple_filter_attrib[fmt_filter_field] = the_field simple_filter_input = type('{}NestedFilter'.format(type_name), (graphene.InputObjectType, ), simple_filter_attrib) diff --git a/src/niweb/apps/noclook/tests/schema/__init__.py b/src/niweb/apps/noclook/tests/schema/__init__.py index 8652337c4..f2edc4239 100644 --- a/src/niweb/apps/noclook/tests/schema/__init__.py +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -11,60 +11,54 @@ def __init__(self, user, *ignore): self.user = user class Neo4jGraphQLTest(NeoTestCase): - initialized = False - def setUp(self): super(Neo4jGraphQLTest, self).setUp() self.context = TestContext(self.user) - - if not self.initialized: - # create nodes - organization1 = self.create_node('organization1', 'organization', meta='Logical') - organization2 = self.create_node('organization2', 'organization', meta='Logical') - contact1 = self.create_node('contact1', 'contact', meta='Relation') - contact2 = self.create_node('contact2', 'contact', meta='Relation') - role1 = self.create_node('role1', 'role', meta='Logical') - role2 = self.create_node('role2', 'role', meta='Logical') - group1 = self.create_node('group1', 'group', meta='Logical') - group2 = self.create_node('group2', 'group', meta='Logical') - - # add some data - contact1_data = { - 'first_name': 'Jane', - 'last_name': 'Doe', - 'name': 'Jane Doe', - } - - for key, value in contact1_data.items(): - 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(): - contact2.get_node().add_property(key, value) - - # create relationships - contact1.get_node().add_role(role1.handle_id) - contact1.get_node().add_group(group1.handle_id) - contact1.get_node().add_organization(organization1.handle_id) - - contact2.get_node().add_role(role2.handle_id) - contact2.get_node().add_group(group2.handle_id) - contact2.get_node().add_organization(organization2.handle_id) - - # 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() - - self.initialized = True + # create nodes + organization1 = self.create_node('organization1', 'organization', meta='Logical') + organization2 = self.create_node('organization2', 'organization', meta='Logical') + contact1 = self.create_node('contact1', 'contact', meta='Relation') + contact2 = self.create_node('contact2', 'contact', meta='Relation') + role1 = self.create_node('role1', 'role', meta='Logical') + role2 = self.create_node('role2', 'role', meta='Logical') + group1 = self.create_node('group1', 'group', meta='Logical') + group2 = self.create_node('group2', 'group', meta='Logical') + + # add some data + contact1_data = { + 'first_name': 'Jane', + 'last_name': 'Doe', + 'name': 'Jane Doe', + } + + for key, value in contact1_data.items(): + 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(): + contact2.get_node().add_property(key, value) + + # create relationships + contact1.get_node().add_role(role1.handle_id) + contact1.get_node().add_group(group1.handle_id) + contact1.get_node().add_organization(organization1.handle_id) + + contact2.get_node().add_role(role2.handle_id) + contact2.get_node().add_group(group2.handle_id) + contact2.get_node().add_organization(organization2.handle_id) + + # 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() diff --git a/src/niweb/apps/noclook/tests/schema/test_core.py b/src/niweb/apps/noclook/tests/schema/test_core.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index eadfbd444..b9717e954 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -25,7 +25,7 @@ def test_role(self): OrderedDict([ ('role', OrderedDict([ - ('handle_id', '9'), + ('handle_id', '17'), ('name', 'New test role') ])), ('clientMutationId', None) @@ -34,7 +34,7 @@ def test_role(self): ]) result = schema.execute(query, context=self.context) - + assert not result.errors, result.errors assert result.data == expected diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index b5a5dcced..6542c4751 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -9,7 +9,7 @@ class QueryTest(Neo4jGraphQLTest): def test_get_contacts(self): query = ''' query getLastTenContacts { - contacts(first: 10) { + contacts(first: 2) { edges { node { handle_id @@ -32,7 +32,7 @@ def test_get_contacts(self): OrderedDict([('edges', [ OrderedDict([('node', - OrderedDict([('handle_id', '21'), + OrderedDict([('handle_id', '29'), ('name', 'John Smith'), ('first_name', 'John'), ('last_name', 'Smith'), @@ -43,7 +43,7 @@ def test_get_contacts(self): [OrderedDict([('name', 'group2')])])]))]), OrderedDict([('node', - OrderedDict([('handle_id', '20'), + OrderedDict([('handle_id', '28'), ('name', 'Jane Doe'), ('first_name', 'Jane'), ('last_name', 'Doe'), @@ -63,7 +63,7 @@ def test_get_contacts(self): query = ''' query { - getNodeById(handle_id: 20){ + getNodeById(handle_id: 28){ handle_id } } From 1657c2588825165611295a91caaf842d865741c7 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 30 May 2019 11:12:06 +0200 Subject: [PATCH 095/520] Slight documentation and filter/order generation moved to the type --- src/niweb/apps/noclook/schema/core.py | 168 +++++++++++++++---------- src/niweb/apps/noclook/schema/types.py | 15 +++ 2 files changed, 115 insertions(+), 68 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 65a4dfd73..3f1bca5e6 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -19,6 +19,24 @@ from ..models import NodeType, NodeHandle +filter_array = { + '': { 'wrapper_field': None, 'only_strings': False }, + 'not': { 'wrapper_field': None, 'only_strings': False }, + 'in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, + 'not_in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, + 'lt': { 'wrapper_field': None, 'only_strings': False }, + 'lte': { 'wrapper_field': None, 'only_strings': False }, + 'gt': { 'wrapper_field': None, 'only_strings': False }, + 'gte': { 'wrapper_field': None, 'only_strings': False }, + + 'contains': { 'wrapper_field': None, 'only_strings': True }, + 'not_contains': { 'wrapper_field': None, 'only_strings': True }, + 'starts_with': { 'wrapper_field': None, 'only_strings': True }, + 'not_starts_with': { 'wrapper_field': None, 'only_strings': True }, + 'ends_with': { 'wrapper_field': None, 'only_strings': True }, + 'not_ends_with': { 'wrapper_field': None, 'only_strings': True }, +} + class DictEntryType(graphene.ObjectType): ''' This type represents an key value pair in a dictionary for the data @@ -120,6 +138,9 @@ def resolve_relationship_list(self, info, **kwargs): return resolve_relationship_list class NIRelationType(graphene.ObjectType): + ''' + This class represents a relationship and its properties + ''' @classmethod def __init_subclass_with_meta__( cls, @@ -158,8 +179,8 @@ class Meta: class DictRelationType(graphene.ObjectType): ''' - This type represents an key value pair in a dictionary for the data - dict of the norduniclient nodes + 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) @@ -244,6 +265,9 @@ def __init_subclass_with_meta__( outgoing = graphene.List(DictRelationType) def resolve_incoming(self, info, **kwargs): + ''' + Resolver for incoming relationships for the node + ''' incoming_rels = self.get_node().incoming ret = [] for rel_name, rel in incoming_rels.items(): @@ -254,6 +278,9 @@ def resolve_incoming(self, info, **kwargs): 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 in outgoing_rels.items(): @@ -263,11 +290,81 @@ def resolve_outgoing(self, info, **kwargs): return ret + @classmethod + def get_ni_type(cls): + ni_metatype = getattr(cls, 'NIMetaType') + return getattr(ni_metatype, 'ni_type') + + @classmethod + def get_filter_input_fields(cls): + ''' + Method used by build_filter_and_order + ''' + input_fields = {} + + for name, field in cls.__dict__.items(): + if field and not isinstance(field, str) and field.__module__ == 'graphene.types.scalars': + input_field = type(field) + input_fields[name] = input_field + + input_fields['handler_id'] = graphene.Int + + 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 + ''' + type_name = cls.get_ni_type() + + # build filter input class and order enum + simple_filter_attrib = {} + enum_options = [] + input_fields = cls.get_filter_input_fields() + + for name, input_field in input_fields.items(): + # adding order attributes + enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) + enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) + + # adding filter attributes + for suffix, suffix_attr in filter_array.items(): + fmt_filter_field = '{}_{}'.format(name, suffix) + if suffix == '': + fmt_filter_field = '{}'.format(name) + + if not suffix_attr['only_strings'] or isinstance(input_field(), graphene.String): + if 'wrapper_field' not in suffix_attr or not suffix_attr['wrapper_field']: + simple_filter_attrib[fmt_filter_field] = input_field() + else: + the_field = input_field + for wrapper_field in suffix_attr['wrapper_field']: + the_field = wrapper_field(the_field) + + simple_filter_attrib[fmt_filter_field] = the_field + + simple_filter_input = type('{}NestedFilter'.format(type_name), (graphene.InputObjectType, ), simple_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(type_name), (graphene.InputObjectType, ), filter_attrib) + + orderBy = graphene.Enum('{}OrderBy'.format(type_name), enum_options) + + return filter_input, orderBy + class Meta: model = NodeHandle interfaces = (relay.Node, ) 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 @@ -696,24 +793,6 @@ def generic_list_resolver(self, info, **args): return generic_list_resolver -filter_array = { - '': { 'wrapper_field': None, 'only_strings': False }, - 'not': { 'wrapper_field': None, 'only_strings': False }, - 'in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, - 'not_in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, - 'lt': { 'wrapper_field': None, 'only_strings': False }, - 'lte': { 'wrapper_field': None, 'only_strings': False }, - 'gt': { 'wrapper_field': None, 'only_strings': False }, - 'gte': { 'wrapper_field': None, 'only_strings': False }, - - 'contains': { 'wrapper_field': None, 'only_strings': True }, - 'not_contains': { 'wrapper_field': None, 'only_strings': True }, - 'starts_with': { 'wrapper_field': None, 'only_strings': True }, - 'not_starts_with': { 'wrapper_field': None, 'only_strings': True }, - 'ends_with': { 'wrapper_field': None, 'only_strings': True }, - 'not_ends_with': { 'wrapper_field': None, 'only_strings': True }, -} - def build_filter_query(filter, nodetype): build_query = '' @@ -810,7 +889,7 @@ def __init_subclass__(cls, **kwargs): field_name = '{}s'.format(type_slug) resolver_name = 'resolve_{}'.format(field_name) - connection_input, connection_order = cls.build_filter_and_order(graphql_type, type_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__), @@ -832,50 +911,3 @@ def __init_subclass__(cls, **kwargs): setattr(cls, field_name, graphene.Field(graphql_type, handle_id=graphene.Int())) setattr(cls, resolver_name, get_byid_resolver(type_name)) - - @classmethod - def build_filter_and_order(cls, graphql_type, type_name): - # build filter input class and order enum - simple_filter_attrib = {} - enum_options = [] - input_fields = {} - - for name, field in graphql_type.__dict__.items(): - if field and not isinstance(field, str) and field.__module__ == 'graphene.types.scalars': - input_field = type(field) - input_fields[name] = input_field - - input_fields['handler_id'] = graphene.Int - - for name, input_field in input_fields.items(): - # adding order attributes - enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) - enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) - - # adding filter attributes - for suffix, suffix_attr in filter_array.items(): - fmt_filter_field = '{}_{}'.format(name, suffix) - if suffix == '': - fmt_filter_field = '{}'.format(name) - - if not suffix_attr['only_strings'] or isinstance(input_field(), graphene.String): - if 'wrapper_field' not in suffix_attr or not suffix_attr['wrapper_field']: - simple_filter_attrib[fmt_filter_field] = input_field() - else: - the_field = input_field - for wrapper_field in suffix_attr['wrapper_field']: - the_field = wrapper_field(the_field) - - simple_filter_attrib[fmt_filter_field] = the_field - - simple_filter_input = type('{}NestedFilter'.format(type_name), (graphene.InputObjectType, ), simple_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(type_name), (graphene.InputObjectType, ), filter_attrib) - - orderBy = graphene.Enum('{}OrderBy'.format(type_name), enum_options) - - return filter_input, orderBy diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 1894ff7b3..167bb803a 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -28,15 +28,24 @@ def resolve_roles_list(self, info, **kwargs): return ret class User(DjangoObjectType): + ''' + The django user type + ''' class Meta: model = User exclude_fields = ['creator', 'modifier'] class Dropdown(DjangoObjectType): + ''' + This class represents a dropdown to use in forms + ''' class Meta: model = Dropdown class Choice(DjangoObjectType): + ''' + This class is used for the choices available in a dropdown + ''' class Meta: model = Choice @@ -48,6 +57,9 @@ class NIMetaType: ni_metatype = 'logical' class Group(NIObjectType): + ''' + The group type is used to group contacts + ''' name = NIStringField(type_kwargs={ 'required': True }) class NIMetaType: @@ -55,6 +67,9 @@ class NIMetaType: ni_metatype = 'logical' 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 }) From 8a7731a6195b0e982a53afff98d09b49bd5ef063 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 30 May 2019 12:17:34 +0200 Subject: [PATCH 096/520] Method ordering and simplification --- src/niweb/apps/noclook/schema/core.py | 222 ++++++++++++++------------ 1 file changed, 120 insertions(+), 102 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 3f1bca5e6..1fcba46e0 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -291,9 +291,15 @@ def resolve_outgoing(self, info, **kwargs): return ret @classmethod - def get_ni_type(cls): + def get_from_nimetatype(cls, attr): ni_metatype = getattr(cls, 'NIMetaType') - return getattr(ni_metatype, 'ni_type') + 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_filter_input_fields(cls): @@ -317,7 +323,7 @@ def build_filter_and_order(cls): This method generates a Filter and Order object from the class itself to be used in filtering connections ''' - type_name = cls.get_ni_type() + ni_type = cls.get_from_nimetatype('ni_type') # build filter input class and order enum simple_filter_attrib = {} @@ -345,18 +351,114 @@ def build_filter_and_order(cls): simple_filter_attrib[fmt_filter_field] = the_field - simple_filter_input = type('{}NestedFilter'.format(type_name), (graphene.InputObjectType, ), simple_filter_attrib) + simple_filter_input = type('{}NestedFilter'.format(ni_type), (graphene.InputObjectType, ), simple_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(type_name), (graphene.InputObjectType, ), filter_attrib) + filter_input = type('{}Filter'.format(ni_type), (graphene.InputObjectType, ), filter_attrib) - orderBy = graphene.Enum('{}OrderBy'.format(type_name), enum_options) + orderBy = graphene.Enum('{}OrderBy'.format(ni_type), enum_options) return filter_input, orderBy + @classmethod + def get_byid_resolver(cls): + type_name = cls.get_type_name() + + def generic_byid_resolver(self, info, **args): + handle_id = args.get('handle_id') + node_type = NodeType.objects.get(type=type_name) + + ret = None + + if info.context and info.context.user.is_authenticated: + if handle_id: + 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_connection_resolver(cls): + type_name = cls.get_type_name() + + def generic_list_resolver(self, info, **args): + filter = args.get('filter', None) + orderBy = args.get('orderBy', None) + + if info.context and info.context.user.is_authenticated: + # filtering will take a different approach + nodes = None + if filter: + q = cls.build_filter_query(filter, type_name) + nodes = nc.query_to_list(nc.graphdb.manager, q) + nodes = [ node['n'].properties for node in nodes] + else: + nodes = nc.get_nodes_by_type(nc.graphdb.manager, type_name) + nodes = list(nodes) + + if nodes: + # ordering + if orderBy: + m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) + prop = m[1] + order = m[2] + reverse = True if order == 'DESC' else False + nodes.sort(key=lambda x: x.get(prop, ''), reverse=reverse) + + # get the QuerySet + handle_ids = [ node['handle_id'] for node in nodes ] + ret = [ NodeHandle.objects.get(handle_id=handle_id) for handle_id in handle_ids ] + else: + handle_ids = [ node['handle_id'] for node in nodes ] + node_type = NodeType.objects.get(type=type_name) + ret = NodeHandle.objects.filter(node_type=node_type) + else: + ret = [] + + if not ret: + ret = [] + + return ret + else: + raise GraphQLAuthException() + + return generic_list_resolver + + @classmethod + def build_filter_query(cls, filter, nodetype): + build_query = '' + + # build AND block + and_query = '' + and_filters = filter.get('AND', []) + for and_filter in and_filters: + print(and_filter) + + # build OR block + or_query = '' + or_filters = filter.get('OR', []) + for or_filter in or_filters: + print(or_filter) + + q = """ + MATCH (n:{label}) + {build_query} + RETURN distinct n + """.format(label=nodetype, build_query=build_query) + + return q + class Meta: model = NodeHandle interfaces = (relay.Node, ) @@ -743,102 +845,20 @@ def get_delete_mutation(cls, *args, **kwargs): return cls._delete_mutation 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) -def get_connection_resolver(nodetype): - def generic_list_resolver(self, info, **args): - filter = args.get('filter', None) - orderBy = args.get('orderBy', None) - - if info.context and info.context.user.is_authenticated: - # filtering will take a different approach - nodes = None - if filter: - q = build_filter_query(filter, nodetype) - nodes = nc.query_to_list(nc.graphdb.manager, q) - nodes = [ node['n'].properties for node in nodes] - else: - nodes = nc.get_nodes_by_type(nc.graphdb.manager, nodetype) - nodes = list(nodes) - - if nodes: - # ordering - if orderBy: - m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) - prop = m[1] - order = m[2] - reverse = True if order == 'DESC' else False - nodes.sort(key=lambda x: x.get(prop, ''), reverse=reverse) - - # get the QuerySet - handle_ids = [ node['handle_id'] for node in nodes ] - ret = [ NodeHandle.objects.get(handle_id=handle_id) for handle_id in handle_ids ] - else: - handle_ids = [ node['handle_id'] for node in nodes ] - node_type = NodeType.objects.get(type=nodetype) - ret = NodeHandle.objects.filter(node_type=node_type) - else: - ret = [] - - if not ret: - ret = [] - - return ret - else: - raise GraphQLAuthException() - - return generic_list_resolver - -def build_filter_query(filter, nodetype): - build_query = '' - - # build AND block - and_query = '' - and_filters = filter.get('AND', []) - for and_filter in and_filters: - print(and_filter) - - # build OR block - or_query = '' - or_filters = filter.get('OR', []) - for or_filter in or_filters: - print(or_filter) - - q = """ - MATCH (n:{label}) - {build_query} - RETURN distinct n - """.format(label=nodetype, build_query=build_query) - - return q - -def get_byid_resolver(nodetype): - def generic_byid_resolver(self, info, **args): - handle_id = args.get('handle_id') - node_type = NodeType.objects.get(type=nodetype) - - ret = None - - if info.context and info.context.user.is_authenticated: - if handle_id: - 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(nodetype, handle_id)) - - return ret - else: - raise GraphQLAuthException() - - return generic_byid_resolver - 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. + ''' node = relay.Node.Field() getNodeById = graphene.Field(NodeHandler, handle_id=graphene.Int()) @@ -854,7 +874,7 @@ def resolve_getNodeById(self, info, **args): raise GraphQLError('A valid handle_id must be provided') if not ret: - raise GraphQLError("There isn't any {} with handle_id {}".format(nodetype, handle_id)) + raise GraphQLError("There isn't any {} with handle_id {}"\.format(nodetype, handle_id)) return ret else: @@ -874,11 +894,9 @@ def __init_subclass__(cls, **kwargs): # add by id resolver for graphql_type in graphql_types: ## extract values - _nimetatype = getattr(graphql_type, 'NIMetaType') - - ni_type = getattr(_nimetatype, 'ni_type') + ni_type = graphql_type.get_from_nimetatype('ni_type') assert ni_type, '{} has not set its ni_type attribute'.format(cls.__name__) - ni_metatype = getattr(_nimetatype, 'ni_metatype') + 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() @@ -903,11 +921,11 @@ def __init_subclass__(cls, **kwargs): filter=graphene.Argument(connection_input), orderBy=graphene.Argument(connection_order), )) - setattr(cls, resolver_name, get_connection_resolver(type_name)) + 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, handle_id=graphene.Int())) - setattr(cls, resolver_name, get_byid_resolver(type_name)) + setattr(cls, resolver_name, graphql_type.get_byid_resolver()) From d7134eec1440521ef288bd5cbdb71bc5b8cafc50 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 30 May 2019 14:01:11 +0200 Subject: [PATCH 097/520] WIP: filter in use --- src/niweb/apps/noclook/schema/core.py | 188 +++++++++++++++++++++----- 1 file changed, 152 insertions(+), 36 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 1fcba46e0..8e68ba570 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -19,22 +19,88 @@ from ..models import NodeType, NodeHandle +def build_match_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} = {value}""".format(field=field, value=value) + + return ret + +def build_not_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} <> {value}""".format(field=field, value=value) + + return ret + +def build_in_predicate(field, value, type): # a list predicate builder + ret = None + return ret + +def build_not_in_predicate(field, value, type): # a list predicate builder + ret = None + return ret + +def build_lt_predicate(field, value, type): + ret = None + return ret + +def build_lte_predicate(field, value, type): + ret = None + return ret + +def build_gt_predicate(field, value, type): + ret = None + return ret + +def build_gte_predicate(field, value, type): + ret = None + return ret + +def build_contains_predicate(field, value, type): + ret = None + return ret + +def build_not_contains_predicate(field, value, type): + ret = None + return ret + +def build_starts_with_predicate(field, value, type): + ret = None + return ret + +def build_not_starts_with_predicate(field, value, type): + ret = None + return ret + +def build_ends_with_predicate(field, value, type): + ret = None + return ret + +def build_not_ends_with_predicate(field, value, type): + ret = None + return ret + filter_array = { - '': { 'wrapper_field': None, 'only_strings': False }, - 'not': { 'wrapper_field': None, 'only_strings': False }, - 'in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, - 'not_in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False }, - 'lt': { 'wrapper_field': None, 'only_strings': False }, - 'lte': { 'wrapper_field': None, 'only_strings': False }, - 'gt': { 'wrapper_field': None, 'only_strings': False }, - 'gte': { 'wrapper_field': None, 'only_strings': False }, - - 'contains': { 'wrapper_field': None, 'only_strings': True }, - 'not_contains': { 'wrapper_field': None, 'only_strings': True }, - 'starts_with': { 'wrapper_field': None, 'only_strings': True }, - 'not_starts_with': { 'wrapper_field': None, 'only_strings': True }, - 'ends_with': { 'wrapper_field': None, 'only_strings': True }, - 'not_ends_with': { 'wrapper_field': None, 'only_strings': True }, + '': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_match_predicate }, + 'not': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_not_predicate }, + 'in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False, 'qpredicate': build_in_predicate }, + 'not_in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False, 'qpredicate': build_not_in_predicate }, + 'lt': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_lt_predicate }, + 'lte': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_lte_predicate }, + 'gt': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_gt_predicate }, + 'gte': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_gte_predicate }, + + 'contains': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_contains_predicate }, + 'not_contains': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_contains_predicate }, + 'starts_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_starts_with_predicate }, + 'not_starts_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_starts_with_predicate }, + 'ends_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_ends_with_predicate }, + 'not_ends_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_ends_with_predicate }, } class DictEntryType(graphene.ObjectType): @@ -193,6 +259,8 @@ class NIObjectType(DjangoObjectType): values of the node data dict. ''' + filter_names = None + @classmethod def __init_subclass_with_meta__( cls, @@ -309,7 +377,7 @@ def get_filter_input_fields(cls): input_fields = {} for name, field in cls.__dict__.items(): - if field and not isinstance(field, str) and field.__module__ == 'graphene.types.scalars': + if field and not isinstance(field, str) and getattr(field, '__module__', None) == 'graphene.types.scalars': input_field = type(field) input_fields[name] = input_field @@ -326,32 +394,44 @@ def build_filter_and_order(cls): ni_type = cls.get_from_nimetatype('ni_type') # build filter input class and order enum - simple_filter_attrib = {} + filter_attrib = {} + cls.filter_names = {} enum_options = [] input_fields = cls.get_filter_input_fields() - for name, input_field in input_fields.items(): + for field_name, input_field in input_fields.items(): # adding order attributes - enum_options.append(['{}_ASC'.format(name), '{}_ASC'.format(name)]) - enum_options.append(['{}_DESC'.format(name), '{}_DESC'.format(name)]) + enum_options.append(['{}_ASC'.format(field_name), '{}_ASC'.format(field_name)]) + enum_options.append(['{}_DESC'.format(field_name), '{}_DESC'.format(field_name)]) # adding filter attributes for suffix, suffix_attr in filter_array.items(): - fmt_filter_field = '{}_{}'.format(name, suffix) - if suffix == '': - fmt_filter_field = '{}'.format(name) + if not suffix == '': + suffix = '_{}'.format(suffix) + + fmt_filter_field = '{}{}'.format(field_name, suffix) if not suffix_attr['only_strings'] or isinstance(input_field(), graphene.String): if 'wrapper_field' not in suffix_attr or not suffix_attr['wrapper_field']: - simple_filter_attrib[fmt_filter_field] = input_field() + filter_attrib[fmt_filter_field] = input_field() + cls.filter_names[fmt_filter_field] = { + 'field' : field_name, + 'suffix': suffix, + 'field_type': input_field(), + } else: the_field = input_field for wrapper_field in suffix_attr['wrapper_field']: the_field = wrapper_field(the_field) - simple_filter_attrib[fmt_filter_field] = the_field + filter_attrib[fmt_filter_field] = the_field + cls.filter_names[fmt_filter_field] = { + 'field' : field_name, + 'suffix': suffix, + 'field_type': the_field, + } - simple_filter_input = type('{}NestedFilter'.format(ni_type), (graphene.InputObjectType, ), simple_filter_attrib) + simple_filter_input = type('{}NestedFilter'.format(ni_type), (graphene.InputObjectType, ), filter_attrib) filter_attrib = {} filter_attrib['AND'] = graphene.List(graphene.NonNull(simple_filter_input)) @@ -393,6 +473,7 @@ def get_connection_resolver(cls): type_name = cls.get_type_name() def generic_list_resolver(self, info, **args): + ret = None filter = args.get('filter', None) orderBy = args.get('orderBy', None) @@ -408,6 +489,7 @@ def generic_list_resolver(self, info, **args): nodes = list(nodes) if nodes: + handle_ids = [] # ordering if orderBy: m = re.match(r"([\w|\_]*)_(ASC|DESC)", orderBy) @@ -415,16 +497,12 @@ def generic_list_resolver(self, info, **args): order = m[2] reverse = True if order == 'DESC' else False nodes.sort(key=lambda x: x.get(prop, ''), reverse=reverse) - - # get the QuerySet handle_ids = [ node['handle_id'] for node in nodes ] - ret = [ NodeHandle.objects.get(handle_id=handle_id) for handle_id in handle_ids ] else: handle_ids = [ node['handle_id'] for node in nodes ] node_type = NodeType.objects.get(type=type_name) - ret = NodeHandle.objects.filter(node_type=node_type) - else: - ret = [] + + ret = [ NodeHandle.objects.get(handle_id=handle_id) for handle_id in handle_ids ] if not ret: ret = [] @@ -440,17 +518,55 @@ def build_filter_query(cls, filter, nodetype): build_query = '' # build AND block - and_query = '' and_filters = filter.get('AND', []) + and_predicates = [] + + # iterate through the nested filters for and_filter in and_filters: - print(and_filter) + # iterate though values of a nested filter + for filter_key, filter_value in and_filter.items(): + filter_field = cls.filter_names[filter_key] + field = filter_field['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(): + if fa_suffix != '': + fa_suffix = '_{}'.format(fa_suffix) + + # get the predicate + if suffix == fa_suffix: + build_preficate_func = fa_value['qpredicate'] + predicate = build_preficate_func(field, filter_value, field_type) + if predicate: + and_predicates.append(predicate) # build OR block - or_query = '' or_filters = filter.get('OR', []) + or_predicates = [] + for or_filter in or_filters: print(or_filter) + and_query = ' AND '.join(and_predicates) + or_query = ' OR '.join(or_predicates) + + if and_query and or_query: + build_query = '{} OR {}'.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) + q = """ MATCH (n:{label}) {build_query} @@ -874,7 +990,7 @@ def resolve_getNodeById(self, info, **args): raise GraphQLError('A valid handle_id must be provided') if not ret: - raise GraphQLError("There isn't any {} with handle_id {}"\.format(nodetype, handle_id)) + raise GraphQLError("There isn't any {} with handle_id {}".format(nodetype, handle_id)) return ret else: From acd8974db53d1f40950971ba85ad64341b839257 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 30 May 2019 14:22:53 +0200 Subject: [PATCH 098/520] fixed order by handle_id --- src/niweb/apps/noclook/schema/core.py | 2 +- src/niweb/apps/noclook/tests/schema/test_schema.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 8e68ba570..fa9db261e 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -381,7 +381,7 @@ def get_filter_input_fields(cls): input_field = type(field) input_fields[name] = input_field - input_fields['handler_id'] = graphene.Int + input_fields['handle_id'] = graphene.Int return input_fields diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 6542c4751..fe241e2f8 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -9,7 +9,7 @@ class QueryTest(Neo4jGraphQLTest): def test_get_contacts(self): query = ''' query getLastTenContacts { - contacts(first: 2) { + contacts(first: 2, orderBy: handle_id_DESC) { edges { node { handle_id From 8420453c8d88c79b2d1164dd0db05feb768d61cc Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 30 May 2019 14:54:56 +0200 Subject: [PATCH 099/520] Filter implemented --- src/niweb/apps/noclook/schema/core.py | 82 ++++++++++++++++++++------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index fa9db261e..95992b22d 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -37,53 +37,93 @@ def build_not_predicate(field, value, type): return ret -def build_in_predicate(field, value, type): # a list predicate builder - ret = None +def build_in_predicate(field, values, type): # a list predicate builder + # string quoting + filter_strings = False + if isinstance(type, graphene.String): + filter_strings = True + + subpredicates = [] + for value in values: + if filter_strings: + value = "'{}'".format(value) + subpredicates.append( + """n.{field} = {value}""".format(field=field, value=value) + ) + + ret = ' OR '.join(subpredicates) return ret -def build_not_in_predicate(field, value, type): # a list predicate builder - ret = None +def build_not_in_predicate(field, values, type): # a list predicate builder + # string quoting + filter_strings = False + if isinstance(type, graphene.String): + filter_strings = True + + subpredicates = [] + for value in values: + if filter_strings: + value = "'{}'".format(value) + subpredicates.append( + """n.{field} <> {value}""".format(field=field, value=value) + ) + + ret = ' AND '.join(subpredicates) return ret def build_lt_predicate(field, value, type): - ret = None + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} < {value}""".format(field=field, value=value) + return ret def build_lte_predicate(field, value, type): - ret = None + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} <= {value}""".format(field=field, value=value) + return ret def build_gt_predicate(field, value, type): - ret = None + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} > {value}""".format(field=field, value=value) + return ret def build_gte_predicate(field, value, type): - ret = None + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} >= {value}""".format(field=field, value=value) + return ret def build_contains_predicate(field, value, type): - ret = None - return ret + return """n.{field} CONTAINS '{value}'""".format(field=field, value=value) def build_not_contains_predicate(field, value, type): - ret = None - return ret + return """NOT n.{field} CONTAINS '{value}'""".format(field=field, value=value) def build_starts_with_predicate(field, value, type): - ret = None - return ret + return """n.{field} STARTS WITH '{value}'""".format(field=field, value=value) def build_not_starts_with_predicate(field, value, type): - ret = None - return ret + return """NOT n.{field} STARTS WITH '{value}'""".format(field=field, value=value) def build_ends_with_predicate(field, value, type): - ret = None - return ret + return """n.{field} ENDS WITH '{value}'""".format(field=field, value=value) def build_not_ends_with_predicate(field, value, type): - ret = None - return ret + return """NOT n.{field} ENDS WITH '{value}'""".format(field=field, value=value) filter_array = { '': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_match_predicate }, @@ -428,7 +468,7 @@ def build_filter_and_order(cls): cls.filter_names[fmt_filter_field] = { 'field' : field_name, 'suffix': suffix, - 'field_type': the_field, + 'field_type': input_field(), } simple_filter_input = type('{}NestedFilter'.format(ni_type), (graphene.InputObjectType, ), filter_attrib) From c9d00721a845ce414ab378b8f6831643988a6727 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 31 May 2019 11:39:21 +0200 Subject: [PATCH 100/520] Implemented or filter and test suite expanded --- src/niweb/apps/noclook/schema/core.py | 20 +++++- .../apps/noclook/tests/schema/test_schema.py | 70 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 95992b22d..f15333e58 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -588,7 +588,25 @@ def build_filter_query(cls, filter, nodetype): or_predicates = [] for or_filter in or_filters: - print(or_filter) + # iterate though values of a nested filter + for filter_key, filter_value in or_filter.items(): + filter_field = cls.filter_names[filter_key] + field = filter_field['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(): + if fa_suffix != '': + fa_suffix = '_{}'.format(fa_suffix) + + # get the predicate + if suffix == fa_suffix: + build_preficate_func = fa_value['qpredicate'] + predicate = build_preficate_func(field, filter_value, field_type) + if predicate: + or_predicates.append(predicate) and_query = ' AND '.join(and_predicates) or_query = ' OR '.join(or_predicates) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index fe241e2f8..5e43f7452 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -7,6 +7,7 @@ class QueryTest(Neo4jGraphQLTest): def test_get_contacts(self): + # test contacts query = ''' query getLastTenContacts { contacts(first: 2, orderBy: handle_id_DESC) { @@ -61,6 +62,7 @@ def test_get_contacts(self): assert not result.errors, result.errors assert result.data == expected + # getNodeById query = ''' query { getNodeById(handle_id: 28){ @@ -72,6 +74,74 @@ def test_get_contacts(self): result = schema.execute(query, context=self.context) assert not result.errors, result.errors + # filter tests + query = ''' + { + groups(first: 10, filter:{ + AND:[{ + name: "group1", name_not: "group2", + name_not_in: ["group2"] + }] + }, orderBy: handle_id_ASC){ + edges{ + node{ + handle_id + name + } + } + } + } + ''' + expected = OrderedDict([('groups', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('handle_id', '32'), + ('name', + 'group1')] + ))])] + )])) + ]) + + + result = schema.execute(query, context=self.context) + + assert not result.errors, result.errors + assert result.data == expected + + query = ''' + { + groups(first: 10, filter:{ + OR:[{ + name: "group1", + name_in: ["group1", "group2"] + },{ + name: "group2", + }] + }, orderBy: handle_id_ASC){ + edges{ + node{ + handle_id + name + } + } + } + } + ''' + expected = OrderedDict([('groups', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('handle_id', '32'), + ('name', 'group1')]))]), + OrderedDict([('node', + OrderedDict([('handle_id', '33'), + ('name', + 'group2')]))])])]))]) + + result = schema.execute(query, context=self.context) + + assert not result.errors, result.errors + assert result.data == expected + def test_getnodebyhandle_id(self): query = ''' query { From d28fc883ed7f8816e81de02a1064e41637f74cf0 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 4 Jun 2019 12:06:54 +0200 Subject: [PATCH 101/520] Changes in the csvimport command to reflect the changes on the roles --- .../noclook/management/commands/csvimport.py | 44 +++++++------------ .../tests/management/test_csvimport.py | 36 +++++---------- 2 files changed, 25 insertions(+), 55 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 947685d43..8cec5afae 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -179,22 +179,6 @@ def handle(self, *args, **options): if key not in ['node_type', 'contact_role', 'name', 'account_name'] and node[key]: graph_node.add_property(key, node[key]) - # dj: role exist?: create or get - role_name = node['contact_role'] - - if role_name: - role_type = NodeType.objects.filter(type=self.new_types[4]).first() # role - new_role = NodeHandle.objects.get_or_create( - node_name = role_name, - node_type = role_type, - node_meta_type = logical_meta_type, - creator = self.user, - modifier = self.user, - )[0] - - # n4: add relation between role and contact - graph_node.add_role(new_role.pk) - # dj: organization exist?: create or get organization_name = node.get('account_name', None) @@ -209,8 +193,15 @@ def handle(self, *args, **options): modifier = self.user, )[0] - # n4: add relation between role and organization - graph_node.add_organization(new_org.pk) + # add role relatioship + role_name = node['contact_role'] + + nc.models.RoleRelationship.link_contact_organization( + new_contact.handle_id, + new_org.handle_id, + role_name + ) + # Print iterations progress if options['verbosity'] > 0: @@ -244,18 +235,13 @@ def handle(self, *args, **options): modifier = self.user, )[0] - role = NodeHandle.objects.get_or_create( - node_name = node['role'], - node_type = role_type, - node_meta_type = logical_meta_type, - creator = self.user, - modifier = self.user, - )[0] + role_name = node['role'] - # we're adding the relations straight since if the relation - # already exists doesn't alter the result - contact.get_node().add_role(role.handle_id) - contact.get_node().add_organization(organization.handle_id) + nc.models.RoleRelationship.link_contact_organization( + contact.handle_id, + organization.handle_id, + role_name + ) csv_secroles.close() diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index e9fce54e1..0601b7331 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -100,18 +100,6 @@ def test_contacts_import(self): self.assertIsNotNone(contact1) self.assertIsInstance(contact1.get_node(), ncmodels.ContactModel) - # check if it has a role assigned - qs = NodeHandle.objects.filter(node_name='Computer Systems Analyst III') - self.assertIsNotNone(qs) - role1 = qs.first() - self.assertIsNotNone(role1) - self.assertIsInstance(role1.get_node(), ncmodels.RoleModel) - - relations = role1.get_node().get_relations() - relation = relations.get('Is', None) - self.assertIsNotNone(relation) - self.assertIsInstance(relations['Is'][0]['node'], ncmodels.RelationModel) - # check if works for an organization qs = NodeHandle.objects.filter(node_name='Gabtune') self.assertIsNotNone(qs) @@ -119,10 +107,11 @@ def test_contacts_import(self): self.assertIsNotNone(organization1) self.assertIsInstance(organization1.get_node(), ncmodels.OrganizationModel) - relations = organization1.get_node().get_relations() - relation = relations.get('Works_for', None) - self.assertIsNotNone(relation) - self.assertIsInstance(relations['Works_for'][0]['node'], ncmodels.RelationModel) + # 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') def test_secroles_import(self): # call csvimport command (verbose 0) @@ -145,20 +134,15 @@ def test_secroles_import(self): self.assertIsNotNone(contact1) # check if role is created - qs = NodeHandle.objects.filter(node_name='Övrig incidentkontakt') - self.assertIsNotNone(qs) - role1 = qs.first() + role1 = ncmodels.RoleRelationship(nc.core.GraphDB.get_instance().manager) + role1.load_from_nodes(contact1.handle_id, organization1.handle_id) self.assertIsNotNone(role1) - - relations = contact1.get_node().get_outgoing_relations() - self.assertEquals(relations['Works_for'][0]['node'], organization1.get_node()) - self.assertIsInstance(relations['Works_for'][0]['node'], ncmodels.OrganizationModel) - self.assertEquals(relations['Is'][0]['node'], role1.get_node()) - self.assertIsInstance(relations['Is'][0]['node'], ncmodels.RoleModel) + self.assertEquals(role1.name, 'Övrig incidentkontakt') def write_string_to_disk(self, string): # get random file - tf = tempfile.NamedTemporaryFile() + tf = tempfile.NamedTemporaryFile(mode='w+') + # write text tf.write(string) tf.flush() From e3f55b223ea0b03c9f0b10d8db488d24d18ea10b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 5 Jun 2019 11:31:58 +0200 Subject: [PATCH 102/520] Required changes for roles. Only tests left to change --- src/niweb/apps/noclook/forms/common.py | 12 +- src/niweb/apps/noclook/helpers.py | 148 +++++++----------- .../templates/noclook/create/create_role.html | 22 --- .../templates/noclook/edit/edit_contact.html | 4 - .../templates/noclook/edit/edit_role.html | 13 -- .../noclook/edit/includes/is_group.html | 27 ---- .../edit/includes/works_for_group.html | 5 +- src/niweb/apps/noclook/urls.py | 1 - src/niweb/apps/noclook/views/create.py | 19 --- src/niweb/apps/noclook/views/detail.py | 15 +- src/niweb/apps/noclook/views/edit.py | 26 +-- 11 files changed, 62 insertions(+), 230 deletions(-) delete mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_role.html delete mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_role.html delete mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index e63eefc07..daf062b29 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -929,20 +929,10 @@ def __init__(self, *args, **kwargs): super(EditContactForm, self).__init__(*args, **kwargs) self.fields['relationship_works_for'].choices = get_node_type_tuples('Organization') self.fields['relationship_member_of'].choices = get_node_type_tuples('Group') - self.fields['relationship_is'].choices = get_node_type_tuples('Role') relationship_works_for = relationship_field('organization', True) relationship_member_of = relationship_field('group', True) - relationship_is = relationship_field('role', True) - - -class NewRoleForm(forms.Form): - name = forms.CharField() - - -class EditRoleForm(NewRoleForm): - pass - + role_name = forms.CharField(required=False) class NewProcedureForm(forms.Form): name = forms.CharField() diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 384f10dc6..1dd268d9b 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -912,17 +912,24 @@ def set_uses_a(user, node, procedure_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_works_for(user, node, organization_id): +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 """ - result = node.add_organization(organization_id) - relationship_id = result.get('Works_for')[0].get('relationship_id') - relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) - created = result.get('Works_for')[0].get('created') + from pprint import pprint + contact_id = node.handle_id + relationship = nc.models.RoleRelationship.link_contact_organization(contact_id, organization_id, role_name) + + if not relationship: + relationship = RoleRelationship() + 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 @@ -942,20 +949,6 @@ def set_member_of(user, node, group_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_is(user, node, role_id): - """ - :param user: Django user - :param node: norduniclient model - :param role_id: unique id - :return: norduniclient model, boolean - """ - result = node.add_role(role_id) - relationship_id = result.get('Is')[0].get('relationship_id') - relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) - created = result.get('Is')[0].get('created') - if created: - activitylog.create_relationship(user, relationship) - return relationship, created def set_of_member(user, node, contact_id): """ @@ -968,82 +961,61 @@ def set_of_member(user, node, 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): +def link_contact_role_for_organization(user, node, contact_handle_id, role_name): """ :param user: Django user - :param node: norduniclient model + :param node: norduniclient contact model :param contact_handle_id: contact's handle_id - :return: contact, role: Only the role is created if it don't exists + :param role_name: role name + :return: contact """ - role_type = NodeType.objects.filter(type='Role').first() - if six.PY2: role = role.encode('utf-8') - # delete previous relationship first - node.remove_role_from_contacts(role) - - contact = NodeHandle.objects.get(handle_id=contact_handle_id) - - role, created = NodeHandle.objects.get_or_create( - node_name=role, - node_type=role_type, - node_meta_type='Logical', - creator=user, - modifier=user, + relationship = nc.models.RoleRelationship.link_contact_organization( + contact_handle_id, + node.handle_id, + role_name ) - if created: - activitylog.create_node(user, contact) - - result_role = contact.get_node().add_role(role.handle_id) - result_organization = contact.get_node().add_organization(node.handle_id) - - # Get relationship for contact-role - relationship_role_id = result_role.get('Is')[0].get('relationship_id') - relationship_role = nc.get_relationship_model(nc.graphdb.manager, relationship_role_id) + if not relationship: + relationship = RoleRelationship() + relationship.load_from_nodes(contact_id, organization_id) - # Get relationship for contact-organization - relationship_organization_id = result_organization.get('Works_for')[0].get('relationship_id') - relationship_organization = nc.get_relationship_model(nc.graphdb.manager, relationship_organization_id) - - created_relationship_role = result_role.get('Is')[0].get('created') - created_relationship_organization = result_organization.get('Works_for')[0].get('created') - - # Log relationships if they have been created - if created_relationship_role: - activitylog.create_relationship(user, relationship_role) + node = node.reload() + created = node.incoming.get('Works_for')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) - if created_relationship_organization: - activitylog.create_relationship(user, relationship_organization) + contact = NodeHandle.objects.get(handle_id=contact_handle_id) - return contact, role + return contact -def create_contact_role_for_organization(user, node, contact_name, role): +def create_contact_role_for_organization(user, node, contact_name, role_name): """ :param user: Django user - :param node: norduniclient model + :param node: norduniclient organization model :param contact_name: full name of the contact :return: contact, role: New objects if they're not present in the db """ contact_type = NodeType.objects.get(type='Contact') - role_type = NodeType.objects.get(type='Role') + # convert string if necesary if six.PY2: contact_name = contact_name.encode('utf-8') - role = role.encode('utf-8') - - # delete previous relationship first - node.remove_role_from_contacts(role) + role_name = role_name.encode('utf-8') first_name, last_name = contact_name.split(' ') + # create or get contact contact, created_contact = NodeHandle.objects.get_or_create( node_name=contact_name, node_type=contact_type, @@ -1057,39 +1029,25 @@ def create_contact_role_for_organization(user, node, contact_name, role): contact.get_node().add_property('first_name', first_name) contact.get_node().add_property('last_name', last_name) - role, created_role = NodeHandle.objects.get_or_create( - node_name=role, - node_type=role_type, - node_meta_type='Logical', - creator=user, - modifier=user, + relationship = nc.models.RoleRelationship.link_contact_organization( + contact.handle_id, + node.handle_id, + role_name ) - if created_role: - activitylog.create_node(user, role) - - result_role = contact.get_node().add_role(role.handle_id) - result_organization = contact.get_node().add_organization(node.handle_id) - - # Get relationship for contact-role - relationship_role_id = result_role.get('Is')[0].get('relationship_id') - relationship_role = nc.get_relationship_model(nc.graphdb.manager, relationship_role_id) - - # Get relationship for contact-organization - relationship_organization_id = result_organization.get('Works_for')[0].get('relationship_id') - relationship_organization = nc.get_relationship_model(nc.graphdb.manager, relationship_organization_id) + if not relationship: + relationship = RoleRelationship() + relationship.load_from_nodes(contact_id, organization_id) - created_relationship_role = result_role.get('Is')[0].get('created') - created_relationship_organization = result_organization.get('Works_for')[0].get('created') + node = node.reload() - # Log relationships if they have been created - if created_relationship_role: - activitylog.create_relationship(user, relationship_role) - - if created_relationship_organization: - activitylog.create_relationship(user, relationship_organization) - - return contact, role + created = False + for relation in node.relationships.get('Works_for'): + if relation['node'].handle_id == contact.handle_id: + created = relation.get('created') + + if created: + activitylog.create_relationship(user, relationship) def get_contact_for_orgrole(organization_id, role_name): @@ -1098,8 +1056,8 @@ def get_contact_for_orgrole(organization_id, role_name): :param role_name: Role name """ q = """ - MATCH (r:Role)<-[:Is]-(c:Contact)-[:Works_for]->(o:Organization) - WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" + MATCH (c:Contact)-[:Works_for {{ name: '{role_name}'}}]->(o:Organization) + WHERE o.handle_id = {organization_id} RETURN c.handle_id AS handle_id """.format(organization_id=organization_id, role_name=role_name) d = nc.query_to_dict(nc.graphdb.manager, q) diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_role.html b/src/niweb/apps/noclook/templates/noclook/create/create_role.html deleted file mode 100644 index 618aa1652..000000000 --- a/src/niweb/apps/noclook/templates/noclook/create/create_role.html +++ /dev/null @@ -1,22 +0,0 @@ -{% 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 }} -
    - - Cancel -
    -
    -{% 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 index fcf7debfc..436c02bf4 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -15,9 +15,6 @@ groupCategories = [ ['group', 'Groups'] ]; - roleCategories = [ - ['role', 'Roles'] - ]; } ); @@ -42,5 +39,4 @@ {% endaccordion %} {% include "noclook/edit/includes/works_for_group.html" %} {% include "noclook/edit/includes/member_of_group.html" %} - {% include "noclook/edit/includes/is_group.html" %} {% 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 deleted file mode 100644 index a1097b335..000000000 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_role.html +++ /dev/null @@ -1,13 +0,0 @@ -{% 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}} -
    -{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html deleted file mode 100644 index b4d430c95..000000000 --- a/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html +++ /dev/null @@ -1,27 +0,0 @@ -{% load noclook_tags %} -{% load crispy_forms_tags %} -{% blockvar is_title %} - {{ node_handle.node_type }} role (optional) -{% endblockvar %} -{% accordion is_title 'is-edit' '#edit-accordion' %} - {% if relations.Is %} - {% load noclook_tags %} -

    Remove role

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

    Add role

    -
    - {{ form.relationship_is | 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 index 8f1fb971e..c0aceade0 100644 --- 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 @@ -23,5 +23,8 @@

    Remove organization

    Link contact to organization

    {{ form.relationship_works_for | as_crispy_field }} -
    +
    +
    + {{ form.role_name | as_crispy_field }} +
    {% endaccordion %} diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index a3ec7d94c..bd654b72e 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -114,7 +114,6 @@ url(r'^port/(?P\d+)/$', detail.port_detail), url(r'^site/(?P\d+)/$', detail.site_detail), url(r'^rack/(?P\d+)/$', detail.rack_detail), - url(r'^role/(?P\d+)/$', detail.role_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), diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index c6f249c6a..e9914911b 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -19,7 +19,6 @@ TYPES = [ ("contact", "Contact"), - ("role", "Role"), ("organization", "Organization"), ("group", "Group"), ] @@ -586,23 +585,6 @@ def new_contact(request, **kwargs): return render(request, 'noclook/create/create_contact.html', {'form': form}) -@staff_member_required -def new_role(request, **kwargs): - if request.POST: - form = forms.NewRoleForm(request.POST) - if form.is_valid(): - try: - nh = helpers.form_to_unique_node_handle(request, form, 'role', 'Logical') - except UniqueNodeError: - form.add_error('name', 'A Role with that name already exists.') - return render(request, 'noclook/create/create_role.html', {'form': form}) - helpers.form_update_node(request.user, nh.handle_id, form) - return redirect(nh.get_absolute_url()) - else: - form = forms.NewRoleForm() - return render(request, 'noclook/create/create_role.html', {'form': form}) - - @staff_member_required def new_procedure(request, **kwargs): if request.POST: @@ -655,7 +637,6 @@ def new_group(request, **kwargs): 'provider': new_provider, 'procedure': new_procedure, 'rack': new_rack, - 'role': new_role, 'service': new_service, 'site': new_site, 'site-owner': new_site_owner, diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 7281d1d8f..999c2af00 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -498,7 +498,7 @@ def router_detail(request, handle_id): hw_name = "{}-hardware.json".format(router.data.get('name', 'router')) hw_attachment = helpers.find_attachments(handle_id, hw_name).first() if hw_attachment: - try: + try: hardware_modules = [json.loads(helpers.attachment_content(hw_attachment))] except IOError as e: logger.warning('Missing hardware modules json for router %s(%s). Error was: %s', nh.node_name, nh.handle_id, e) @@ -605,7 +605,7 @@ def switch_detail(request, handle_id): hw_name = "{}-hardware.json".format(switch.data.get('name', 'switch')) hw_attachment = helpers.find_attachments(handle_id, hw_name).first() if hw_attachment: - try: + try: hardware_modules = [json.loads(helpers.attachment_content(hw_attachment))] except IOError as e: logger.warning('Missing hardware modules json for router %s(%s). Error was: %s', nh.node_name, nh.handle_id, e) @@ -641,17 +641,6 @@ def contact_detail(request, handle_id): {'node_handle': nh, 'node': node, 'location_path': location_path}) -@login_required -def role_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/role_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) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 2b15a0015..2e8a1483a 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -1008,13 +1008,11 @@ def edit_contact(request, handle_id): # Set relationships if form.cleaned_data['relationship_works_for']: organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_works_for']) - helpers.set_works_for(request.user, contact, organization_nh.handle_id) + role_name = form.cleaned_data['role_name'] + 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 form.cleaned_data['relationship_is']: - role_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_is']) - helpers.set_is(request.user, contact, role_nh.handle_id) if 'saveanddone' in request.POST: return redirect(nh.get_absolute_url()) else: @@ -1025,25 +1023,6 @@ def edit_contact(request, handle_id): {'node_handle': nh, 'form': form, 'relations': relations, 'node': contact}) -@staff_member_required -def edit_role(request, handle_id): - # Get needed data from node - nh, role = helpers.get_nh_node(handle_id) - if request.POST: - form = forms.EditRoleForm(request.POST) - if form.is_valid(): - # Generic node update - helpers.form_update_node(request.user, role.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.EditRoleForm(role.data) - return render(request, 'noclook/edit/edit_role.html', - {'node_handle': nh, 'form': form, 'node': role}) - - @staff_member_required def edit_procedure(request, handle_id): # Get needed data from node @@ -1109,7 +1088,6 @@ def edit_group(request, handle_id): 'procedure': edit_procedure, 'rack': edit_rack, 'router': edit_router, - 'role': edit_role, 'site': edit_site, 'site-owner': edit_site_owner, 'switch': edit_switch, From 395ae2037a4430ecd496043a7222fb74d0fd15f1 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 5 Jun 2019 14:17:32 +0200 Subject: [PATCH 103/520] Tests finished --- src/niweb/apps/noclook/helpers.py | 8 +++--- src/niweb/apps/noclook/tests/test_helpers.py | 26 ++++++-------------- src/niweb/apps/noclook/views/list.py | 2 +- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 1dd268d9b..2ed6acd4d 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -977,7 +977,7 @@ def link_contact_role_for_organization(user, node, contact_handle_id, role_name) :return: contact """ if six.PY2: - role = role.encode('utf-8') + role_name = role_name.encode('utf-8') relationship = nc.models.RoleRelationship.link_contact_organization( contact_handle_id, @@ -996,7 +996,7 @@ def link_contact_role_for_organization(user, node, contact_handle_id, role_name) contact = NodeHandle.objects.get(handle_id=contact_handle_id) - return contact + return contact, relationship def create_contact_role_for_organization(user, node, contact_name, role_name): @@ -1045,10 +1045,12 @@ def create_contact_role_for_organization(user, node, contact_name, role_name): for relation in node.relationships.get('Works_for'): if relation['node'].handle_id == contact.handle_id: created = relation.get('created') - + if created: activitylog.create_relationship(user, relationship) + return contact, relationship + def get_contact_for_orgrole(organization_id, role_name): """ diff --git a/src/niweb/apps/noclook/tests/test_helpers.py b/src/niweb/apps/noclook/tests/test_helpers.py index 3cbf6c2cd..31bc090c4 100644 --- a/src/niweb/apps/noclook/tests/test_helpers.py +++ b/src/niweb/apps/noclook/tests/test_helpers.py @@ -48,7 +48,7 @@ def test_create_unique_node_handle_case_insensitive(self): 'Physical') def test_link_contact_role_for_organization(self): - data = { + thedata = { 'role_name': 'IT-manager' } @@ -56,14 +56,14 @@ def test_link_contact_role_for_organization(self): contact, role = helpers.link_contact_role_for_organization(self.user, self.organization_node, self.contact_node.handle_id, - data.get('role_name') + thedata['role_name'] ) - self.assertEqual(self.contact_node.relationships.get('Is')[0].get('node'), role) + self.assertEqual(role.name, thedata['role_name']) self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) def test_create_contact_role_for_organization(self): - data = { + thedata = { 'contact_name': 'FirstName LastName', 'role_name': 'IT-manager' } @@ -71,20 +71,10 @@ def test_create_contact_role_for_organization(self): self.assertEqual(len(self.organization_node.relationships), 0) contact, role = helpers.create_contact_role_for_organization(self.user, self.organization_node, - data.get('contact_name'), - data.get('role_name') + thedata['contact_name'], + thedata['role_name'], ) self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) - self.assertEqual(contact.get_node().data.get('name'), data.get('contact_name')) - self.assertEqual(role.get_node().data.get('name'), data.get('role_name')) - - def test_get_contact_for_orgrole(self): - self.contact_node.add_role(self.role_node.handle_id) - - self.assertEqual(len(self.organization_node.relationships), 0) - - self.contact_node.add_organization(self.organization_node.handle_id) - contact = helpers.get_contact_for_orgrole(self.organization_node.handle_id, self.role_node.data.get('name')) - - self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) + self.assertEqual(contact.get_node().data.get('name'), thedata['contact_name']) + self.assertEqual(role.name, thedata['role_name']) diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index feb99e951..45a7b65cf 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -603,7 +603,7 @@ def list_contacts(request): q = """ MATCH (con:Contact) OPTIONAL MATCH (con)-[:Works_for]->(org:Organization) - RETURN con, org + RETURN DISTINCT con.handle_id, con, org """ con_list = nc.query_to_list(nc.graphdb.manager, q) From f7bc78808c99cebd21beecf641be8e1669927381 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 6 Jun 2019 11:33:15 +0200 Subject: [PATCH 104/520] Role name isn't set on the contact props & no duplicates in contact list --- src/niweb/apps/noclook/helpers.py | 2 +- src/niweb/apps/noclook/views/list.py | 30 ++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 2ed6acd4d..55ae3ef0d 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -117,7 +117,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 = [] diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index 45a7b65cf..eb7c0f9e7 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -586,16 +586,15 @@ def list_organizations(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Organizations', 'urls': urls}) -def _contact_table(con, org): +def _contact_table(con, org_name): contact_link = { 'url': u'/contact/{}/'.format(con.get('handle_id')), 'name': u'{}'.format(con.get('name', '')) } - name_org = '' - if org: - name_org = org.get('name', '') + if not org_name: + org_name = '' - row = TableRow(contact_link, name_org) + row = TableRow(contact_link, org_name) return row @login_required @@ -603,10 +602,29 @@ def list_contacts(request): q = """ MATCH (con:Contact) OPTIONAL MATCH (con)-[:Works_for]->(org:Organization) - RETURN DISTINCT con.handle_id, con, org + RETURN con.handle_id AS con_handle_id, con, 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 = row['org']['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') From 5ba2d98a276bed5575069eba0197133bf81b1631 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 7 Jun 2019 08:52:05 +0200 Subject: [PATCH 105/520] Merge branch 'rolefix' into graphqldev --- src/niweb/apps/noclook/forms/common.py | 12 +- src/niweb/apps/noclook/helpers.py | 150 +++++++----------- .../noclook/management/commands/csvimport.py | 44 ++--- .../templates/noclook/create/create_role.html | 22 --- .../templates/noclook/edit/edit_contact.html | 4 - .../templates/noclook/edit/edit_role.html | 13 -- .../noclook/edit/includes/is_group.html | 27 ---- .../edit/includes/works_for_group.html | 5 +- .../tests/management/test_csvimport.py | 34 ++-- src/niweb/apps/noclook/tests/test_helpers.py | 26 +-- src/niweb/apps/noclook/urls.py | 1 - src/niweb/apps/noclook/views/create.py | 19 --- src/niweb/apps/noclook/views/detail.py | 15 +- src/niweb/apps/noclook/views/edit.py | 26 +-- src/niweb/apps/noclook/views/list.py | 30 +++- 15 files changed, 120 insertions(+), 308 deletions(-) delete mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_role.html delete mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_role.html delete mode 100644 src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 21074ded0..7cc8f88a0 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -929,20 +929,10 @@ def __init__(self, *args, **kwargs): super(EditContactForm, self).__init__(*args, **kwargs) self.fields['relationship_works_for'].choices = get_node_type_tuples('Organization') self.fields['relationship_member_of'].choices = get_node_type_tuples('Group') - self.fields['relationship_is'].choices = get_node_type_tuples('Role') relationship_works_for = relationship_field('organization', True) relationship_member_of = relationship_field('group', True) - relationship_is = relationship_field('role', True) - - -class NewRoleForm(forms.Form): - name = forms.CharField() - - -class EditRoleForm(NewRoleForm): - pass - + role_name = forms.CharField(required=False) class NewProcedureForm(forms.Form): name = forms.CharField() diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 384f10dc6..55ae3ef0d 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -117,7 +117,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 = [] @@ -912,17 +912,24 @@ def set_uses_a(user, node, procedure_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_works_for(user, node, organization_id): +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 """ - result = node.add_organization(organization_id) - relationship_id = result.get('Works_for')[0].get('relationship_id') - relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) - created = result.get('Works_for')[0].get('created') + from pprint import pprint + contact_id = node.handle_id + relationship = nc.models.RoleRelationship.link_contact_organization(contact_id, organization_id, role_name) + + if not relationship: + relationship = RoleRelationship() + 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 @@ -942,20 +949,6 @@ def set_member_of(user, node, group_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_is(user, node, role_id): - """ - :param user: Django user - :param node: norduniclient model - :param role_id: unique id - :return: norduniclient model, boolean - """ - result = node.add_role(role_id) - relationship_id = result.get('Is')[0].get('relationship_id') - relationship = nc.get_relationship_model(nc.graphdb.manager, relationship_id) - created = result.get('Is')[0].get('created') - if created: - activitylog.create_relationship(user, relationship) - return relationship, created def set_of_member(user, node, contact_id): """ @@ -968,82 +961,61 @@ def set_of_member(user, node, 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): +def link_contact_role_for_organization(user, node, contact_handle_id, role_name): """ :param user: Django user - :param node: norduniclient model + :param node: norduniclient contact model :param contact_handle_id: contact's handle_id - :return: contact, role: Only the role is created if it don't exists + :param role_name: role name + :return: contact """ - role_type = NodeType.objects.filter(type='Role').first() - if six.PY2: - role = role.encode('utf-8') + role_name = role_name.encode('utf-8') - # delete previous relationship first - node.remove_role_from_contacts(role) - - contact = NodeHandle.objects.get(handle_id=contact_handle_id) - - role, created = NodeHandle.objects.get_or_create( - node_name=role, - node_type=role_type, - node_meta_type='Logical', - creator=user, - modifier=user, + relationship = nc.models.RoleRelationship.link_contact_organization( + contact_handle_id, + node.handle_id, + role_name ) - if created: - activitylog.create_node(user, contact) - - result_role = contact.get_node().add_role(role.handle_id) - result_organization = contact.get_node().add_organization(node.handle_id) - - # Get relationship for contact-role - relationship_role_id = result_role.get('Is')[0].get('relationship_id') - relationship_role = nc.get_relationship_model(nc.graphdb.manager, relationship_role_id) + if not relationship: + relationship = RoleRelationship() + relationship.load_from_nodes(contact_id, organization_id) - # Get relationship for contact-organization - relationship_organization_id = result_organization.get('Works_for')[0].get('relationship_id') - relationship_organization = nc.get_relationship_model(nc.graphdb.manager, relationship_organization_id) - - created_relationship_role = result_role.get('Is')[0].get('created') - created_relationship_organization = result_organization.get('Works_for')[0].get('created') - - # Log relationships if they have been created - if created_relationship_role: - activitylog.create_relationship(user, relationship_role) + node = node.reload() + created = node.incoming.get('Works_for')[0].get('created') + if created: + activitylog.create_relationship(user, relationship) - if created_relationship_organization: - activitylog.create_relationship(user, relationship_organization) + contact = NodeHandle.objects.get(handle_id=contact_handle_id) - return contact, role + return contact, relationship -def create_contact_role_for_organization(user, node, contact_name, role): +def create_contact_role_for_organization(user, node, contact_name, role_name): """ :param user: Django user - :param node: norduniclient model + :param node: norduniclient organization model :param contact_name: full name of the contact :return: contact, role: New objects if they're not present in the db """ contact_type = NodeType.objects.get(type='Contact') - role_type = NodeType.objects.get(type='Role') + # convert string if necesary if six.PY2: contact_name = contact_name.encode('utf-8') - role = role.encode('utf-8') - - # delete previous relationship first - node.remove_role_from_contacts(role) + role_name = role_name.encode('utf-8') first_name, last_name = contact_name.split(' ') + # create or get contact contact, created_contact = NodeHandle.objects.get_or_create( node_name=contact_name, node_type=contact_type, @@ -1057,39 +1029,27 @@ def create_contact_role_for_organization(user, node, contact_name, role): contact.get_node().add_property('first_name', first_name) contact.get_node().add_property('last_name', last_name) - role, created_role = NodeHandle.objects.get_or_create( - node_name=role, - node_type=role_type, - node_meta_type='Logical', - creator=user, - modifier=user, + relationship = nc.models.RoleRelationship.link_contact_organization( + contact.handle_id, + node.handle_id, + role_name ) - if created_role: - activitylog.create_node(user, role) + if not relationship: + relationship = RoleRelationship() + relationship.load_from_nodes(contact_id, organization_id) - result_role = contact.get_node().add_role(role.handle_id) - result_organization = contact.get_node().add_organization(node.handle_id) + node = node.reload() - # Get relationship for contact-role - relationship_role_id = result_role.get('Is')[0].get('relationship_id') - relationship_role = nc.get_relationship_model(nc.graphdb.manager, relationship_role_id) + created = False + for relation in node.relationships.get('Works_for'): + if relation['node'].handle_id == contact.handle_id: + created = relation.get('created') - # Get relationship for contact-organization - relationship_organization_id = result_organization.get('Works_for')[0].get('relationship_id') - relationship_organization = nc.get_relationship_model(nc.graphdb.manager, relationship_organization_id) - - created_relationship_role = result_role.get('Is')[0].get('created') - created_relationship_organization = result_organization.get('Works_for')[0].get('created') - - # Log relationships if they have been created - if created_relationship_role: - activitylog.create_relationship(user, relationship_role) - - if created_relationship_organization: - activitylog.create_relationship(user, relationship_organization) + if created: + activitylog.create_relationship(user, relationship) - return contact, role + return contact, relationship def get_contact_for_orgrole(organization_id, role_name): @@ -1098,8 +1058,8 @@ def get_contact_for_orgrole(organization_id, role_name): :param role_name: Role name """ q = """ - MATCH (r:Role)<-[:Is]-(c:Contact)-[:Works_for]->(o:Organization) - WHERE o.handle_id = {organization_id} AND r.name = "{role_name}" + MATCH (c:Contact)-[:Works_for {{ name: '{role_name}'}}]->(o:Organization) + WHERE o.handle_id = {organization_id} RETURN c.handle_id AS handle_id """.format(organization_id=organization_id, role_name=role_name) d = nc.query_to_dict(nc.graphdb.manager, q) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 947685d43..8cec5afae 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -179,22 +179,6 @@ def handle(self, *args, **options): if key not in ['node_type', 'contact_role', 'name', 'account_name'] and node[key]: graph_node.add_property(key, node[key]) - # dj: role exist?: create or get - role_name = node['contact_role'] - - if role_name: - role_type = NodeType.objects.filter(type=self.new_types[4]).first() # role - new_role = NodeHandle.objects.get_or_create( - node_name = role_name, - node_type = role_type, - node_meta_type = logical_meta_type, - creator = self.user, - modifier = self.user, - )[0] - - # n4: add relation between role and contact - graph_node.add_role(new_role.pk) - # dj: organization exist?: create or get organization_name = node.get('account_name', None) @@ -209,8 +193,15 @@ def handle(self, *args, **options): modifier = self.user, )[0] - # n4: add relation between role and organization - graph_node.add_organization(new_org.pk) + # add role relatioship + role_name = node['contact_role'] + + nc.models.RoleRelationship.link_contact_organization( + new_contact.handle_id, + new_org.handle_id, + role_name + ) + # Print iterations progress if options['verbosity'] > 0: @@ -244,18 +235,13 @@ def handle(self, *args, **options): modifier = self.user, )[0] - role = NodeHandle.objects.get_or_create( - node_name = node['role'], - node_type = role_type, - node_meta_type = logical_meta_type, - creator = self.user, - modifier = self.user, - )[0] + role_name = node['role'] - # we're adding the relations straight since if the relation - # already exists doesn't alter the result - contact.get_node().add_role(role.handle_id) - contact.get_node().add_organization(organization.handle_id) + nc.models.RoleRelationship.link_contact_organization( + contact.handle_id, + organization.handle_id, + role_name + ) csv_secroles.close() diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_role.html b/src/niweb/apps/noclook/templates/noclook/create/create_role.html deleted file mode 100644 index 618aa1652..000000000 --- a/src/niweb/apps/noclook/templates/noclook/create/create_role.html +++ /dev/null @@ -1,22 +0,0 @@ -{% 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 }} -
    - - Cancel -
    -
    -{% 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 index fcf7debfc..436c02bf4 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -15,9 +15,6 @@ groupCategories = [ ['group', 'Groups'] ]; - roleCategories = [ - ['role', 'Roles'] - ]; } ); @@ -42,5 +39,4 @@ {% endaccordion %} {% include "noclook/edit/includes/works_for_group.html" %} {% include "noclook/edit/includes/member_of_group.html" %} - {% include "noclook/edit/includes/is_group.html" %} {% 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 deleted file mode 100644 index a1097b335..000000000 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_role.html +++ /dev/null @@ -1,13 +0,0 @@ -{% 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}} -
    -{% endblock %} diff --git a/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html b/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html deleted file mode 100644 index b4d430c95..000000000 --- a/src/niweb/apps/noclook/templates/noclook/edit/includes/is_group.html +++ /dev/null @@ -1,27 +0,0 @@ -{% load noclook_tags %} -{% load crispy_forms_tags %} -{% blockvar is_title %} - {{ node_handle.node_type }} role (optional) -{% endblockvar %} -{% accordion is_title 'is-edit' '#edit-accordion' %} - {% if relations.Is %} - {% load noclook_tags %} -

    Remove role

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

    Add role

    -
    - {{ form.relationship_is | 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 index 8f1fb971e..c0aceade0 100644 --- 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 @@ -23,5 +23,8 @@

    Remove organization

    Link contact to organization

    {{ form.relationship_works_for | as_crispy_field }} -
    +
    +
    + {{ form.role_name | as_crispy_field }} +
    {% endaccordion %} diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index 68e7d06a5..0601b7331 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -100,18 +100,6 @@ def test_contacts_import(self): self.assertIsNotNone(contact1) self.assertIsInstance(contact1.get_node(), ncmodels.ContactModel) - # check if it has a role assigned - qs = NodeHandle.objects.filter(node_name='Computer Systems Analyst III') - self.assertIsNotNone(qs) - role1 = qs.first() - self.assertIsNotNone(role1) - self.assertIsInstance(role1.get_node(), ncmodels.RoleModel) - - relations = role1.get_node().get_relations() - relation = relations.get('Is', None) - self.assertIsNotNone(relation) - self.assertIsInstance(relations['Is'][0]['node'], ncmodels.RelationModel) - # check if works for an organization qs = NodeHandle.objects.filter(node_name='Gabtune') self.assertIsNotNone(qs) @@ -119,10 +107,11 @@ def test_contacts_import(self): self.assertIsNotNone(organization1) self.assertIsInstance(organization1.get_node(), ncmodels.OrganizationModel) - relations = organization1.get_node().get_relations() - relation = relations.get('Works_for', None) - self.assertIsNotNone(relation) - self.assertIsInstance(relations['Works_for'][0]['node'], ncmodels.RelationModel) + # 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') def test_secroles_import(self): # call csvimport command (verbose 0) @@ -145,20 +134,15 @@ def test_secroles_import(self): self.assertIsNotNone(contact1) # check if role is created - qs = NodeHandle.objects.filter(node_name='Övrig incidentkontakt') - self.assertIsNotNone(qs) - role1 = qs.first() + role1 = ncmodels.RoleRelationship(nc.core.GraphDB.get_instance().manager) + role1.load_from_nodes(contact1.handle_id, organization1.handle_id) self.assertIsNotNone(role1) - - relations = contact1.get_node().get_outgoing_relations() - self.assertEquals(relations['Works_for'][0]['node'], organization1.get_node()) - self.assertIsInstance(relations['Works_for'][0]['node'], ncmodels.OrganizationModel) - self.assertEquals(relations['Is'][0]['node'], role1.get_node()) - self.assertIsInstance(relations['Is'][0]['node'], ncmodels.RoleModel) + self.assertEquals(role1.name, 'Övrig incidentkontakt') def write_string_to_disk(self, string): # get random file tf = tempfile.NamedTemporaryFile(mode='w+') + # write text tf.write(string) tf.flush() diff --git a/src/niweb/apps/noclook/tests/test_helpers.py b/src/niweb/apps/noclook/tests/test_helpers.py index 3cbf6c2cd..31bc090c4 100644 --- a/src/niweb/apps/noclook/tests/test_helpers.py +++ b/src/niweb/apps/noclook/tests/test_helpers.py @@ -48,7 +48,7 @@ def test_create_unique_node_handle_case_insensitive(self): 'Physical') def test_link_contact_role_for_organization(self): - data = { + thedata = { 'role_name': 'IT-manager' } @@ -56,14 +56,14 @@ def test_link_contact_role_for_organization(self): contact, role = helpers.link_contact_role_for_organization(self.user, self.organization_node, self.contact_node.handle_id, - data.get('role_name') + thedata['role_name'] ) - self.assertEqual(self.contact_node.relationships.get('Is')[0].get('node'), role) + self.assertEqual(role.name, thedata['role_name']) self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) def test_create_contact_role_for_organization(self): - data = { + thedata = { 'contact_name': 'FirstName LastName', 'role_name': 'IT-manager' } @@ -71,20 +71,10 @@ def test_create_contact_role_for_organization(self): self.assertEqual(len(self.organization_node.relationships), 0) contact, role = helpers.create_contact_role_for_organization(self.user, self.organization_node, - data.get('contact_name'), - data.get('role_name') + thedata['contact_name'], + thedata['role_name'], ) self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) - self.assertEqual(contact.get_node().data.get('name'), data.get('contact_name')) - self.assertEqual(role.get_node().data.get('name'), data.get('role_name')) - - def test_get_contact_for_orgrole(self): - self.contact_node.add_role(self.role_node.handle_id) - - self.assertEqual(len(self.organization_node.relationships), 0) - - self.contact_node.add_organization(self.organization_node.handle_id) - contact = helpers.get_contact_for_orgrole(self.organization_node.handle_id, self.role_node.data.get('name')) - - self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) + self.assertEqual(contact.get_node().data.get('name'), thedata['contact_name']) + self.assertEqual(role.name, thedata['role_name']) diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index a3ec7d94c..bd654b72e 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -114,7 +114,6 @@ url(r'^port/(?P\d+)/$', detail.port_detail), url(r'^site/(?P\d+)/$', detail.site_detail), url(r'^rack/(?P\d+)/$', detail.rack_detail), - url(r'^role/(?P\d+)/$', detail.role_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), diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index c6f249c6a..e9914911b 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -19,7 +19,6 @@ TYPES = [ ("contact", "Contact"), - ("role", "Role"), ("organization", "Organization"), ("group", "Group"), ] @@ -586,23 +585,6 @@ def new_contact(request, **kwargs): return render(request, 'noclook/create/create_contact.html', {'form': form}) -@staff_member_required -def new_role(request, **kwargs): - if request.POST: - form = forms.NewRoleForm(request.POST) - if form.is_valid(): - try: - nh = helpers.form_to_unique_node_handle(request, form, 'role', 'Logical') - except UniqueNodeError: - form.add_error('name', 'A Role with that name already exists.') - return render(request, 'noclook/create/create_role.html', {'form': form}) - helpers.form_update_node(request.user, nh.handle_id, form) - return redirect(nh.get_absolute_url()) - else: - form = forms.NewRoleForm() - return render(request, 'noclook/create/create_role.html', {'form': form}) - - @staff_member_required def new_procedure(request, **kwargs): if request.POST: @@ -655,7 +637,6 @@ def new_group(request, **kwargs): 'provider': new_provider, 'procedure': new_procedure, 'rack': new_rack, - 'role': new_role, 'service': new_service, 'site': new_site, 'site-owner': new_site_owner, diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 7281d1d8f..999c2af00 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -498,7 +498,7 @@ def router_detail(request, handle_id): hw_name = "{}-hardware.json".format(router.data.get('name', 'router')) hw_attachment = helpers.find_attachments(handle_id, hw_name).first() if hw_attachment: - try: + try: hardware_modules = [json.loads(helpers.attachment_content(hw_attachment))] except IOError as e: logger.warning('Missing hardware modules json for router %s(%s). Error was: %s', nh.node_name, nh.handle_id, e) @@ -605,7 +605,7 @@ def switch_detail(request, handle_id): hw_name = "{}-hardware.json".format(switch.data.get('name', 'switch')) hw_attachment = helpers.find_attachments(handle_id, hw_name).first() if hw_attachment: - try: + try: hardware_modules = [json.loads(helpers.attachment_content(hw_attachment))] except IOError as e: logger.warning('Missing hardware modules json for router %s(%s). Error was: %s', nh.node_name, nh.handle_id, e) @@ -641,17 +641,6 @@ def contact_detail(request, handle_id): {'node_handle': nh, 'node': node, 'location_path': location_path}) -@login_required -def role_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/role_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) diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 2b15a0015..2e8a1483a 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -1008,13 +1008,11 @@ def edit_contact(request, handle_id): # Set relationships if form.cleaned_data['relationship_works_for']: organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_works_for']) - helpers.set_works_for(request.user, contact, organization_nh.handle_id) + role_name = form.cleaned_data['role_name'] + 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 form.cleaned_data['relationship_is']: - role_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_is']) - helpers.set_is(request.user, contact, role_nh.handle_id) if 'saveanddone' in request.POST: return redirect(nh.get_absolute_url()) else: @@ -1025,25 +1023,6 @@ def edit_contact(request, handle_id): {'node_handle': nh, 'form': form, 'relations': relations, 'node': contact}) -@staff_member_required -def edit_role(request, handle_id): - # Get needed data from node - nh, role = helpers.get_nh_node(handle_id) - if request.POST: - form = forms.EditRoleForm(request.POST) - if form.is_valid(): - # Generic node update - helpers.form_update_node(request.user, role.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.EditRoleForm(role.data) - return render(request, 'noclook/edit/edit_role.html', - {'node_handle': nh, 'form': form, 'node': role}) - - @staff_member_required def edit_procedure(request, handle_id): # Get needed data from node @@ -1109,7 +1088,6 @@ def edit_group(request, handle_id): 'procedure': edit_procedure, 'rack': edit_rack, 'router': edit_router, - 'role': edit_role, 'site': edit_site, 'site-owner': edit_site_owner, 'switch': edit_switch, diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index feb99e951..eb7c0f9e7 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -586,16 +586,15 @@ def list_organizations(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Organizations', 'urls': urls}) -def _contact_table(con, org): +def _contact_table(con, org_name): contact_link = { 'url': u'/contact/{}/'.format(con.get('handle_id')), 'name': u'{}'.format(con.get('name', '')) } - name_org = '' - if org: - name_org = org.get('name', '') + if not org_name: + org_name = '' - row = TableRow(contact_link, name_org) + row = TableRow(contact_link, org_name) return row @login_required @@ -603,10 +602,29 @@ def list_contacts(request): q = """ MATCH (con:Contact) OPTIONAL MATCH (con)-[:Works_for]->(org:Organization) - RETURN con, org + RETURN con.handle_id AS con_handle_id, con, 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 = row['org']['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') From 6d744325658ee4f944bceee1f7c7f2e6121d78e3 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 7 Jun 2019 13:30:48 +0200 Subject: [PATCH 106/520] Roles integrated into dropdown function. Key value pairs interfaced --- src/niweb/apps/noclook/schema/core.py | 21 ++++++++++- src/niweb/apps/noclook/schema/mutations.py | 27 ------------- src/niweb/apps/noclook/schema/query.py | 44 +++++++++++++++++++--- src/niweb/apps/noclook/schema/types.py | 16 ++++---- 4 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index f15333e58..dc9763f32 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -143,13 +143,18 @@ def build_not_ends_with_predicate(field, value, type): 'not_ends_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_ends_with_predicate }, } +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 ''' - key = graphene.String(required=True) - value = graphene.String(required=True) + + class Meta: + interfaces = (KeyValue, ) def resolve_nidata(self, info, **kwargs): ''' @@ -894,11 +899,19 @@ def do_request(cls, request, **kwargs): if form.is_valid(): # Generic node update helpers.form_update_node(request.user, nodehandler.handle_id, form) + + # process relations if implemented + cls.process_relations(form, nodehandler) + return nh else: # get the errors and return them raise GraphQLError('Form errors: {}'.format(form)) + @classmethod + def process_relations(cls, form, nodehandler): + pass + class DeleteNIMutation(AbstractNIMutation): class NIMetaClass: request_path = None @@ -1018,6 +1031,10 @@ def get_update_mutation(cls, *args, **kwargs): def get_delete_mutation(cls, *args, **kwargs): return cls._delete_mutation +# TODO: create a new mutation factory for relationships types +# update relationship attributes or delete the relation itself +# what about creating relationships between two related entities? + class GraphQLAuthException(Exception): ''' Simple auth exception diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 41b2a0dc5..e443c7a19 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -9,16 +9,6 @@ from .core import NIMutationFactory, CreateNIMutation from .types import * -class NIRoleMutationFactory(NIMutationFactory): - class NIMetaClass: - create_form = NewRoleForm - update_form = EditRoleForm - request_path = '/' - graphql_type = Role - - class Meta: - abstract = False - class NIGroupMutationFactory(NIMutationFactory): class NIMetaClass: create_form = NewGroupForm @@ -39,24 +29,7 @@ class NIMetaClass: class Meta: abstract = False -class CreateRoleNIMutation(CreateNIMutation): - '''This class is not used but left out as documentation in the case that as - finer grain of control is needed''' - role = graphene.Field(Role, required=True) - - class NIMetaClass: - request_path = '/' - django_form = NewRoleForm - graphql_type = Role - - class Meta: - abstract = False - class NOCRootMutation(graphene.ObjectType): - create_role = NIRoleMutationFactory.get_create_mutation().Field() - update_role = NIRoleMutationFactory.get_update_mutation().Field() - delete_role = NIRoleMutationFactory.get_delete_mutation().Field() - create_group = NIGroupMutationFactory.get_create_mutation().Field() update_group = NIGroupMutationFactory.get_update_mutation().Field() delete_group = NIGroupMutationFactory.get_delete_mutation().Field() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index deef62eba..d24cb39e8 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -8,18 +8,50 @@ from ..models import Dropdown as DropdownModel from .types import * +def get_roles_dropdown(): + ret = [] + roles = nc.models.RoleRelationship.get_all_roles() + for role in roles: + name = role.replace(' ', '_').lower() + ret.append(Role(name=name, value=role)) + + return ret + +NEO4J_DROPDOWNS = { + 'roles': get_roles_dropdown, +} + class NOCRootQuery(NOCAutoQuery): - getChoicesForDropdown = graphene.List(Choice, name=graphene.String(required=True)) + getAvailableDropdowns = graphene.List(graphene.String) + getChoicesForDropdown = graphene.List(KeyValue, name=graphene.String(required=True)) getRelationById = graphene.Field(NIRelationType, relation_id=graphene.Int(required=True)) + def resolve_getAvailableDropdowns(self, info, **kwargs): + django_dropdowns = [d.name for d in DropdownModel.objects.all()] + neo4j_dropdowns = [x for x in NEO4J_DROPDOWNS.keys()] + + return django_dropdowns + neo4j_dropdowns + def resolve_getChoicesForDropdown(self, info, **kwargs): name = kwargs.get('name') - ddqs = DropdownModel.get(name) - if not isinstance(ddqs, DummyDropdown): - return ddqs.choice_set.order_by('name') + if name in NEO4J_DROPDOWNS.keys(): + # neo4j resolver + dd_function = NEO4J_DROPDOWNS.get(name) + + if callable(dd_function): + ret = dd_function() + return ret + else: + raise Exception('Can\'t resolve {}'.format(name)) else: - raise Exception(u'Could not find dropdown with name \'{}\'. Please create it using /admin/'.format(name)) + # django dropdown resolver + 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_getRelationById(self, info, **kwargs): relation_id = kwargs.get('relation_id') @@ -30,4 +62,4 @@ def resolve_getRelationById(self, info, **kwargs): return rel class NIMeta: - graphql_types = [ Role, Group, Contact ] + graphql_types = [ Group, Contact ] diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 167bb803a..36ce2dbeb 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' +import graphene + from django.contrib.auth.models import User from graphene import relay from .core import * @@ -40,6 +42,7 @@ class Dropdown(DjangoObjectType): This class represents a dropdown to use in forms ''' class Meta: + only_fields = ('id', 'name') model = Dropdown class Choice(DjangoObjectType): @@ -47,14 +50,13 @@ class Choice(DjangoObjectType): This class is used for the choices available in a dropdown ''' class Meta: + only_fields = ('name', 'value') model = Choice + interfaces = (KeyValue, ) -class Role(NIObjectType): - name = NIStringField(type_kwargs={ 'required': True }) - - class NIMetaType: - ni_type = 'Role' - ni_metatype = 'logical' +class Role(graphene.ObjectType): + class Meta: + interfaces = (KeyValue, ) class Group(NIObjectType): ''' @@ -81,9 +83,7 @@ class Contact(NIObjectType): email = NIStringField() other_email = NIStringField() PGP_fingerprint = NIStringField() - is_roles = NIListField(type_args=(Role,), manual_resolver=resolve_roles_list) member_of_groups = NIListField(type_args=(Group,), rel_name='Member_of', rel_method='get_outgoing_relations') - works_for = NIRelationField(rel_name='Works_for') class NIMetaType: From 70212837f7b5781cb58120cc04e5278f70e93f1d Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 7 Jun 2019 14:47:15 +0200 Subject: [PATCH 107/520] Name change for clarification --- src/niweb/apps/noclook/schema/__init__.py | 2 +- src/niweb/apps/noclook/schema/query.py | 2 +- src/niweb/apps/noclook/schema/types.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index f36603fac..d5745f338 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -14,7 +14,7 @@ User, Dropdown, Choice, - Role, + Neo4jChoice, Group, Contact, NodeHandler, diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index d24cb39e8..d21f825d6 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -13,7 +13,7 @@ def get_roles_dropdown(): roles = nc.models.RoleRelationship.get_all_roles() for role in roles: name = role.replace(' ', '_').lower() - ret.append(Role(name=name, value=role)) + ret.append(Neo4jChoice(name=name, value=role)) return ret diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 36ce2dbeb..2562fb704 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -54,7 +54,7 @@ class Meta: model = Choice interfaces = (KeyValue, ) -class Role(graphene.ObjectType): +class Neo4jChoice(graphene.ObjectType): class Meta: interfaces = (KeyValue, ) From baf9a3c57195da8afee2fc1fa3d183b12ba56d12 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 10 Jun 2019 11:35:18 +0200 Subject: [PATCH 108/520] Corrections on some of the tests and methods --- src/niweb/apps/noclook/forms/common.py | 2 +- src/niweb/apps/noclook/helpers.py | 2 +- src/niweb/apps/noclook/schema/core.py | 14 +++--- .../apps/noclook/tests/schema/__init__.py | 19 +++++--- .../noclook/tests/schema/test_connections.py | 35 +++++++++++++++ .../noclook/tests/schema/test_mutations.py | 44 ++++++++++--------- .../apps/noclook/tests/schema/test_schema.py | 34 +++----------- .../apps/noclook/tests/test_createforms.py | 22 +++------- 8 files changed, 95 insertions(+), 77 deletions(-) create mode 100644 src/niweb/apps/noclook/tests/schema/test_connections.py diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 7cc8f88a0..4617bf348 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -947,7 +947,7 @@ class NewGroupForm(forms.Form): name = forms.CharField() -class EditGroupForm(NewProcedureForm): +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') diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 55ae3ef0d..b2c8da107 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -971,7 +971,7 @@ def set_of_member(user, node, contact_id): def link_contact_role_for_organization(user, node, contact_handle_id, role_name): """ :param user: Django user - :param node: norduniclient contact model + :param node: norduniclient organization model :param contact_handle_id: contact's handle_id :param role_name: role name :return: contact diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index dc9763f32..e6115e391 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -265,7 +265,7 @@ def __init_subclass_with_meta__( type = graphene.String(required=True) # this may be set to an Enum start = graphene.Field(graphene.Int, required=True) end = graphene.Field(graphene.Int, required=True) - data = graphene.List(DictEntryType) + nidata = graphene.List(DictEntryType) def resolve_relation_id(self, info, **kwargs): self.relation_id = self.id @@ -281,7 +281,8 @@ def resolve_nidata(self, info, **kwargs): alldata = self.data for key, value in alldata.items(): - ret.append(DictEntryType(key=key, value=value)) + if key and value: + ret.append(DictEntryType(name=key, value=value)) return ret @@ -805,7 +806,8 @@ def from_input_to_request(cls, user, **input): if input_class: for attr_name, attr_field in input_class.__dict__.items(): attr_value = input.get(attr_name) - input_params[attr_name] = attr_value + if attr_value: + input_params[attr_name] = attr_value if not is_create: input_params['handle_id'] = input.get('handle_id') @@ -876,7 +878,7 @@ def do_request(cls, request, **kwargs): return nh else: # get the errors and return them - raise GraphQLError('Form errors: {}'.format(form._errors)) + raise GraphQLError('Form errors: {}'.format(form.errors)) class UpdateNIMutation(AbstractNIMutation): class NIMetaClass: @@ -904,9 +906,11 @@ def do_request(cls, request, **kwargs): cls.process_relations(form, nodehandler) return nh + else: + raise GraphQLError('Form is not valid: {}'.format(form.errors)) else: # get the errors and return them - raise GraphQLError('Form errors: {}'.format(form)) + raise GraphQLError('Form errors: {}'.format(form.errors)) @classmethod def process_relations(cls, form, nodehandler): diff --git a/src/niweb/apps/noclook/tests/schema/__init__.py b/src/niweb/apps/noclook/tests/schema/__init__.py index f2edc4239..6002ca012 100644 --- a/src/niweb/apps/noclook/tests/schema/__init__.py +++ b/src/niweb/apps/noclook/tests/schema/__init__.py @@ -3,6 +3,7 @@ from django.db import connection +from apps.noclook import helpers from apps.noclook.models import NodeHandle, Dropdown, Choice from ..neo4j_base import NeoTestCase @@ -44,13 +45,21 @@ def setUp(self): contact2.get_node().add_property(key, value) # create relationships - contact1.get_node().add_role(role1.handle_id) contact1.get_node().add_group(group1.handle_id) - contact1.get_node().add_organization(organization1.handle_id) - - contact2.get_node().add_role(role2.handle_id) contact2.get_node().add_group(group2.handle_id) - contact2.get_node().add_organization(organization2.handle_id) + + helpers.link_contact_role_for_organization( + self.context.user, + organization1.get_node(), + contact1.handle_id, + 'role1' + ) + helpers.link_contact_role_for_organization( + self.context.user, + organization2.get_node(), + contact2.handle_id, + 'role2' + ) # create dummy dropdown dropdown = Dropdown.objects.get_or_create(name='contact_type')[0] 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..c5e7ecf0b --- /dev/null +++ b/src/niweb/apps/noclook/tests/schema/test_connections.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from collections import OrderedDict +from . import Neo4jGraphQLTest +from niweb.schema import schema + +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 index b9717e954..68c1541bd 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -6,12 +6,12 @@ from niweb.schema import schema class QueryTest(Neo4jGraphQLTest): - def test_role(self): + def test_group(self): ## create ## query = ''' - mutation create_test_role { - create_role(input: {name: "New test role"}){ - role { + mutation create_test_group { + create_group(input: {name: "New test group"}){ + group { handle_id name } @@ -21,12 +21,12 @@ def test_role(self): ''' expected = OrderedDict([ - ('create_role', + ('create_group', OrderedDict([ - ('role', + ('group', OrderedDict([ - ('handle_id', '17'), - ('name', 'New test role') + ('handle_id', '9'), + ('name', 'New test group') ])), ('clientMutationId', None) ]) @@ -34,16 +34,17 @@ def test_role(self): ]) result = schema.execute(query, context=self.context) - + #from pprint import pformat + #raise Exception(pformat(result.data)) + assert not result.errors, result.errors assert result.data == expected ## update ## - role_handle_id = int(result.data['create_role']['role']['handle_id']) query = """ - mutation update_test_role { - update_role(input: {handle_id: 9, name: "A test role"}){ - role { + mutation update_test_group { + update_group(input: {handle_id: 9, name: "A test group"}){ + group { handle_id name } @@ -53,12 +54,12 @@ def test_role(self): """ expected = OrderedDict([ - ('update_role', + ('update_group', OrderedDict([ - ('role', + ('group', OrderedDict([ ('handle_id', '9'), - ('name', 'A test role') + ('name', 'A test group') ])), ('clientMutationId', None) ]) @@ -66,14 +67,15 @@ def test_role(self): ]) result = schema.execute(query, context=self.context) + assert not result.errors assert result.data == expected ## delete ## query = """ - mutation delete_test_role { - delete_role(input: {handle_id: 9}){ - role{ + mutation delete_test_group { + delete_group(input: {handle_id: 9}){ + group{ handle_id } } @@ -81,9 +83,9 @@ def test_role(self): """ expected = OrderedDict([ - ('delete_role', + ('delete_group', OrderedDict([ - ('role', None), + ('group', None), ]) ) ]) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 5e43f7452..53e69245d 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -17,9 +17,6 @@ def test_get_contacts(self): name first_name last_name - is_roles { - name - } member_of_groups { name } @@ -33,24 +30,18 @@ def test_get_contacts(self): OrderedDict([('edges', [ OrderedDict([('node', - OrderedDict([('handle_id', '29'), + OrderedDict([('handle_id', '12'), ('name', 'John Smith'), ('first_name', 'John'), ('last_name', 'Smith'), - ('is_roles', - [OrderedDict([('name', - 'role2')])]), ('member_of_groups', [OrderedDict([('name', 'group2')])])]))]), OrderedDict([('node', - OrderedDict([('handle_id', '28'), + OrderedDict([('handle_id', '11'), ('name', 'Jane Doe'), ('first_name', 'Jane'), ('last_name', 'Doe'), - ('is_roles', - [OrderedDict([('name', - 'role1')])]), ('member_of_groups', [OrderedDict([('name', 'group1')])])]))]), @@ -65,7 +56,7 @@ def test_get_contacts(self): # getNodeById query = ''' query { - getNodeById(handle_id: 28){ + getNodeById(handle_id: 12){ handle_id } } @@ -95,7 +86,7 @@ def test_get_contacts(self): expected = OrderedDict([('groups', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '32'), + OrderedDict([('handle_id', '15'), ('name', 'group1')] ))])] @@ -130,10 +121,10 @@ def test_get_contacts(self): expected = OrderedDict([('groups', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '32'), + OrderedDict([('handle_id', '15'), ('name', 'group1')]))]), OrderedDict([('node', - OrderedDict([('handle_id', '33'), + OrderedDict([('handle_id', '16'), ('name', 'group2')]))])])]))]) @@ -158,19 +149,6 @@ def test_dropdown(self): query = ''' query{ getChoicesForDropdown(name:"contact_type"){ - id - dropdown{ - id - name - choice_set{ - id - dropdown { - id - } - name - value - } - } name value } diff --git a/src/niweb/apps/noclook/tests/test_createforms.py b/src/niweb/apps/noclook/tests/test_createforms.py index 47e944972..da692c944 100644 --- a/src/niweb/apps/noclook/tests/test_createforms.py +++ b/src/niweb/apps/noclook/tests/test_createforms.py @@ -266,10 +266,11 @@ def test_NewOrganizationForm_full(self): 'phone': '08-49 400 000', 'website': 'www.stdh.se', 'customer_id': 'STDH', - 'type': 'University, College', + 'type': 'university_college', } resp = self.client.post('/new/{}/'.format(slugify(node_type)), data) - self.assertEqual(resp.status_code, 302) + 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') data['website'] = 'www.stdh.se' @@ -285,31 +286,20 @@ def test_NewContactForm_full(self): data = { 'first_name': 'Stefan', 'last_name': 'Listrom', - 'contact_type': 'Person', + 'contact_type': 'person', 'mobile': '+46733023915', 'phone': '+46733023915', 'salutation': 'Mr', 'email': 'steli@sunet.se', } resp = self.client.post('/new/{}/'.format(slugify(node_type)), data) - self.assertEqual(resp.status_code, 302) + 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') data['phone'] = '+46733023915' self.assertDictContainsSubset(data, nh.get_node().data) - def test_NewRoleForm_full(self): - node_type = 'Role' - data = { - 'name': 'IT Manager', - } - 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='IT Manager') - data['name'] = 'IT Manager' - self.assertDictContainsSubset(data, nh.get_node().data) - def test_NewProcedureForm_full(self): node_type = 'Procedure' data = { From 5fdc2291df8debf89657c6e39abf835bb5da0f7c Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 11 Jun 2019 14:36:57 +0200 Subject: [PATCH 109/520] Expanded role type from NIRelationType --- src/niweb/apps/noclook/schema/core.py | 2 +- src/niweb/apps/noclook/schema/types.py | 14 +++++++++++++- .../apps/noclook/tests/schema/test_mutations.py | 8 +++----- src/niweb/apps/noclook/tests/schema/test_schema.py | 14 +++++++------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index e6115e391..b60acfd7e 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -257,7 +257,7 @@ def __init_subclass_with_meta__( cls, **options, ): - super(NIObjectType, cls).__init_subclass_with_meta__( + super(NIRelationType, cls).__init_subclass_with_meta__( **options ) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 2562fb704..1555b3511 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -29,6 +29,18 @@ def resolve_roles_list(self, info, **kwargs): return ret +class Role(NIRelationType): + name = graphene.String(required=True) + + def resolve_name(self, info, **kwargs): + if self.name: + return self.name + else: + raise Exception('This must not be a role relationship') + + class Meta: + interfaces = (relay.Node, ) + class User(DjangoObjectType): ''' The django user type @@ -84,7 +96,7 @@ class Contact(NIObjectType): other_email = NIStringField() PGP_fingerprint = NIStringField() member_of_groups = NIListField(type_args=(Group,), rel_name='Member_of', rel_method='get_outgoing_relations') - works_for = NIRelationField(rel_name='Works_for') + works_for = NIRelationField(rel_name='Works_for', type_args=(Role, )) class NIMetaType: ni_type = 'Contact' diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index 68c1541bd..da86b99cb 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -25,7 +25,7 @@ def test_group(self): OrderedDict([ ('group', OrderedDict([ - ('handle_id', '9'), + ('handle_id', '17'), ('name', 'New test group') ])), ('clientMutationId', None) @@ -34,8 +34,6 @@ def test_group(self): ]) result = schema.execute(query, context=self.context) - #from pprint import pformat - #raise Exception(pformat(result.data)) assert not result.errors, result.errors assert result.data == expected @@ -43,7 +41,7 @@ def test_group(self): ## update ## query = """ mutation update_test_group { - update_group(input: {handle_id: 9, name: "A test group"}){ + update_group(input: {handle_id: 17, name: "A test group"}){ group { handle_id name @@ -58,7 +56,7 @@ def test_group(self): OrderedDict([ ('group', OrderedDict([ - ('handle_id', '9'), + ('handle_id', '17'), ('name', 'A test group') ])), ('clientMutationId', None) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 53e69245d..77cf416a2 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -30,7 +30,7 @@ def test_get_contacts(self): OrderedDict([('edges', [ OrderedDict([('node', - OrderedDict([('handle_id', '12'), + OrderedDict([('handle_id', '29'), ('name', 'John Smith'), ('first_name', 'John'), ('last_name', 'Smith'), @@ -38,7 +38,7 @@ def test_get_contacts(self): [OrderedDict([('name', 'group2')])])]))]), OrderedDict([('node', - OrderedDict([('handle_id', '11'), + OrderedDict([('handle_id', '28'), ('name', 'Jane Doe'), ('first_name', 'Jane'), ('last_name', 'Doe'), @@ -56,7 +56,7 @@ def test_get_contacts(self): # getNodeById query = ''' query { - getNodeById(handle_id: 12){ + getNodeById(handle_id: 29){ handle_id } } @@ -86,7 +86,7 @@ def test_get_contacts(self): expected = OrderedDict([('groups', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '15'), + OrderedDict([('handle_id', '32'), ('name', 'group1')] ))])] @@ -95,7 +95,7 @@ def test_get_contacts(self): result = schema.execute(query, context=self.context) - + assert not result.errors, result.errors assert result.data == expected @@ -121,10 +121,10 @@ def test_get_contacts(self): expected = OrderedDict([('groups', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '15'), + OrderedDict([('handle_id', '32'), ('name', 'group1')]))]), OrderedDict([('node', - OrderedDict([('handle_id', '16'), + OrderedDict([('handle_id', '33'), ('name', 'group2')]))])])]))]) From ce13965a0488fedd5adb0c1b2db0b579bf48386d Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 11 Jun 2019 15:08:52 +0200 Subject: [PATCH 110/520] Prototype of delete mutation --- src/niweb/apps/noclook/schema/mutations.py | 19 +++++++++++++++++++ src/niweb/apps/noclook/schema/types.py | 21 --------------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index e443c7a19..16e8f050e 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -2,6 +2,7 @@ __author__ = 'ffuentes' import graphene +import norduniclient as nc from apps.noclook import helpers from apps.noclook.forms import * @@ -29,6 +30,22 @@ class NIMetaClass: class Meta: abstract = False +class DeleteRole(relay.ClientIDMutation): + class Input: + relation_id = graphene.Int(required=True) + + deleted = graphene.Boolean(required=True) + + @classmethod + def mutate_and_get_payload(cls, root, info, **input): + relation_id = input.get("relation_id", None) + if relation_id: + role = nc.models.RoleRelationship.get_relationship_model(nc.graphdb.manager, relation_id) + role.delete() + return DeleteRole(deleted=True) + else: + return DeleteRole(deleted=False) + class NOCRootMutation(graphene.ObjectType): create_group = NIGroupMutationFactory.get_create_mutation().Field() update_group = NIGroupMutationFactory.get_update_mutation().Field() @@ -37,3 +54,5 @@ class NOCRootMutation(graphene.ObjectType): create_contact = NIContactMutationFactory.get_create_mutation().Field() update_contact = NIContactMutationFactory.get_update_mutation().Field() delete_contact = NIContactMutationFactory.get_delete_mutation().Field() + + delete_role = DeleteRole.Field() diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 1555b3511..4cf26ac66 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -8,27 +8,6 @@ from .core import * from ..models import * -def resolve_roles_list(self, info, **kwargs): - """ - This method is only present here to illustrate how a manual resolver - could be used - """ - neo4jnode = self.get_node() - relations = neo4jnode.get_outgoing_relations() - roles = relations.get('Is') - - # this may be the worst way to do it, but it's just for a PoC - handle_id_list = [] - if roles: - for role in roles: - role = role['node'] - role_id = role.data.get('handle_id') - handle_id_list.append(role_id) - - ret = NodeHandle.objects.filter(handle_id__in=handle_id_list) - - return ret - class Role(NIRelationType): name = graphene.String(required=True) From ce3664f8bf21992de2d38bdb16e6f3d487b4063f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 12 Jun 2019 08:09:02 +0200 Subject: [PATCH 111/520] bugfix in contact list and organization form --- src/niweb/apps/noclook/helpers.py | 10 ++++++++++ src/niweb/apps/noclook/views/list.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index b2c8da107..68d2467d3 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -979,6 +979,11 @@ def link_contact_role_for_organization(user, node, contact_handle_id, role_name) if six.PY2: role_name = role_name.encode('utf-8') + nc.models.RoleRelationship.remove_role_in_organization( + node.handle_id, + role_name + ) + relationship = nc.models.RoleRelationship.link_contact_organization( contact_handle_id, node.handle_id, @@ -1013,6 +1018,11 @@ def create_contact_role_for_organization(user, node, contact_name, role_name): contact_name = contact_name.encode('utf-8') role_name = role_name.encode('utf-8') + nc.models.RoleRelationship.remove_role_in_organization( + node.handle_id, + role_name + ) + first_name, last_name = contact_name.split(' ') # create or get contact diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index eb7c0f9e7..1db1df193 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -615,7 +615,10 @@ def list_contacts(request): if con_handle_id in contact_list.keys(): org_list = contact_list[con_handle_id]['org_list'] - new_org_name = row['org']['name'] + 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] = { From 762c20c0b05e0906113fa1a14f9e9d299b2d0550 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 12 Jun 2019 09:26:36 +0200 Subject: [PATCH 112/520] Show role name in template --- .../templates/noclook/edit/includes/works_for_group.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c0aceade0..65c1f23c5 100644 --- 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 @@ -11,7 +11,7 @@

    Remove organization

    {% noclook_get_type item.node.handle_id as node_type %} - {{ node_type }} {{ item.node.data.name }} + {{ node_type }} {{ item.node.data.name }}{% if item.relationship.name %} (as {{ item.relationship.name }}){% endif %}
    Delete From ec34be170886c2998beb89368ea8ba06ce269e37 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 12 Jun 2019 11:12:34 +0200 Subject: [PATCH 113/520] More generic relationship method --- src/niweb/apps/noclook/schema/mutations.py | 26 +++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 16e8f050e..27b8fd8ea 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -4,7 +4,7 @@ import graphene import norduniclient as nc -from apps.noclook import helpers +from apps.noclook import activitylog, helpers from apps.noclook.forms import * from .core import NIMutationFactory, CreateNIMutation @@ -30,21 +30,27 @@ class NIMetaClass: class Meta: abstract = False -class DeleteRole(relay.ClientIDMutation): +class DeleteRelationship(relay.ClientIDMutation): class Input: relation_id = graphene.Int(required=True) - deleted = graphene.Boolean(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) - if relation_id: - role = nc.models.RoleRelationship.get_relationship_model(nc.graphdb.manager, relation_id) - role.delete() - return DeleteRole(deleted=True) - else: - return DeleteRole(deleted=False) + success = False + + try: + relationship = nc.get_relationship_model(nc.graphdb.manager, relation_id) + 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 NOCRootMutation(graphene.ObjectType): create_group = NIGroupMutationFactory.get_create_mutation().Field() @@ -55,4 +61,4 @@ class NOCRootMutation(graphene.ObjectType): update_contact = NIContactMutationFactory.get_update_mutation().Field() delete_contact = NIContactMutationFactory.get_delete_mutation().Field() - delete_role = DeleteRole.Field() + delete_relationship = DeleteRelationship.Field() From 50f0ced5ce829ee5b8695d37d869f7bbd5736e24 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 12 Jun 2019 11:37:28 +0200 Subject: [PATCH 114/520] Changes in the mutation factory and payload naming --- src/niweb/apps/noclook/schema/core.py | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index b60acfd7e..6c0b089f2 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -692,7 +692,7 @@ def __init_subclass_with_meta__( 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', cls.__name__) + mutation_name = getattr(ni_metaclass, 'mutation_name', None) is_create = getattr(ni_metaclass, 'is_create', False) # build fields into Input @@ -719,14 +719,17 @@ def __init_subclass_with_meta__( inner_class = type('Input', (object,), inner_fields) setattr(cls, 'Input', inner_class) - # add return type - if graphql_type: - setattr(cls, graphql_type.__name__.lower(), graphene.Field(graphql_type)) + 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): '''Django form to graphene field conversor @@ -830,8 +833,8 @@ def mutate_and_get_payload(cls, root, info, **input): graphql_type = cls.get_graphql_type() init_params = {} - if graphql_type: - init_params[graphql_type.__name__.lower()] = ret + for key, value in ret.items(): + init_params[key] = value return cls(**init_params) @@ -905,7 +908,7 @@ def do_request(cls, request, **kwargs): # process relations if implemented cls.process_relations(form, nodehandler) - return nh + return { graphql_type.__name__.lower(): nh } else: raise GraphQLError('Form is not valid: {}'.format(form.errors)) else: @@ -921,14 +924,18 @@ class NIMetaClass: request_path = None graphql_type = None + @classmethod + def add_return_type(cls, graphql_type): + setattr(cls, 'success', graphene.Boolean(required=True)) + @classmethod def do_request(cls, request, **kwargs): handle_id = request.POST.get('handle_id') nh, node = helpers.get_nh_node(handle_id) - helpers.delete_node(request.user, node.handle_id) + success = helpers.delete_node(request.user, node.handle_id) - return None + return {'success': success} class NIMutationFactory(): ''' @@ -980,7 +987,6 @@ def __init_subclass__(cls, **kwargs): class_name = 'CreateNI{}Mutation'.format(node_type.capitalize()) attr_dict = { 'django_form': create_form, - 'mutation_name': class_name, 'request_path': request_path, 'is_create': True, 'graphql_type': graphql_type, @@ -998,7 +1004,6 @@ def __init_subclass__(cls, **kwargs): class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) attr_dict['django_form'] = update_form - attr_dict['mutation_name'] = class_name attr_dict['is_create'] = False update_metaclass = type(metaclass_name, (object,), attr_dict) @@ -1012,7 +1017,6 @@ def __init_subclass__(cls, **kwargs): class_name = 'DeleteNI{}Mutation'.format(node_type.capitalize()) del attr_dict['django_form'] - attr_dict['mutation_name'] = class_name delete_metaclass = type(metaclass_name, (object,), attr_dict) cls._delete_mutation = type( From d3eb74e4c5d6d1321ee250eec776ea4ec11cecce Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 12 Jun 2019 13:59:15 +0200 Subject: [PATCH 115/520] Added Organizations to the schema and WIP towards exclude fields --- src/niweb/apps/noclook/schema/core.py | 24 +++++++++++++++++++--- src/niweb/apps/noclook/schema/mutations.py | 19 +++++++++++++++++ src/niweb/apps/noclook/schema/query.py | 2 +- src/niweb/apps/noclook/schema/types.py | 18 ++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 6c0b089f2..75e69f133 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -173,11 +173,14 @@ class NIBasicField(): Super class of the type fields ''' def __init__(self, field_type=graphene.String, manual_resolver=False, - type_kwargs=None, **kwargs): + type_kwargs=None, required=False, req_on_create=False, + **kwargs): self.field_type = field_type self.manual_resolver = manual_resolver self.type_kwargs = type_kwargs + self.required = required + self.req_on_create = req_on_create def get_resolver(self, **kwargs): field_name = kwargs.get('field_name') @@ -206,9 +209,10 @@ class NIIntField(NIBasicField): Int type ''' def __init__(self, field_type=graphene.Int, manual_resolver=False, - type_kwargs=None, **kwargs): + type_kwargs=None, required=False, req_on_create=False, + **kwargs): super(NIIntField, self).__init__(field_type, manual_resolver, - type_kwargs, **kwargs) + type_kwargs, required, req_on_create, **kwargs) class NIListField(NIBasicField): ''' @@ -694,6 +698,8 @@ def __init_subclass_with_meta__( django_form = getattr(ni_metaclass, 'django_form', None) mutation_name = getattr(ni_metaclass, 'mutation_name', None) is_create = getattr(ni_metaclass, 'is_create', False) + fields = getattr(ni_metaclass, 'fields', None) + exclude = getattr(ni_metaclass, 'exclude', None) # build fields into Input inner_fields = {} @@ -857,6 +863,8 @@ class NIMetaClass: request_path = None is_create = True graphql_type = None + fields = None + exclude = None @classmethod def do_request(cls, request, **kwargs): @@ -968,6 +976,10 @@ def __init_subclass__(cls, **kwargs): 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_fields = getattr(ni_metaclass, 'create_fields', None) + create_exclude = getattr(ni_metaclass, 'create_exclude', None) + update_fields = getattr(ni_metaclass, 'update_fields', None) + update_exclude = getattr(ni_metaclass, 'update_exclude', None) # we'll retrieve these values NI type/metatype from the GraphQLType nimetatype = getattr(graphql_type, 'NIMetaType') @@ -990,6 +1002,8 @@ def __init_subclass__(cls, **kwargs): 'request_path': request_path, 'is_create': True, 'graphql_type': graphql_type, + 'fields': create_fields, + 'exclude': create_exclude, } create_metaclass = type(metaclass_name, (object,), attr_dict) @@ -1005,6 +1019,8 @@ def __init_subclass__(cls, **kwargs): class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) attr_dict['django_form'] = update_form attr_dict['is_create'] = False + attr_dict['fields'] = update_fields + attr_dict['exclude'] = update_exclude update_metaclass = type(metaclass_name, (object,), attr_dict) cls._update_mutation = type( @@ -1017,6 +1033,8 @@ def __init_subclass__(cls, **kwargs): class_name = 'DeleteNI{}Mutation'.format(node_type.capitalize()) del attr_dict['django_form'] + del attr_dict['fields'] + del attr_dict['exclude'] delete_metaclass = type(metaclass_name, (object,), attr_dict) cls._delete_mutation = type( diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 27b8fd8ea..d2ed3f964 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -30,6 +30,21 @@ class NIMetaClass: class Meta: abstract = False +class NIOrganizationMutationFactory(NIMutationFactory): + class NIMetaClass: + create_form = NewOrganizationForm + update_form = EditOrganizationForm + request_path = '/' + graphql_type = Organization + # create_fields or create_exclude + # update_fields or update_exclude + update_exclude = ('abuse_contact', 'primary_contact', + 'secondary_contact', 'it_technical_contact', + 'it_security_contact', 'it_manager_contact') + + class Meta: + abstract = False + class DeleteRelationship(relay.ClientIDMutation): class Input: relation_id = graphene.Int(required=True) @@ -61,4 +76,8 @@ class NOCRootMutation(graphene.ObjectType): update_contact = NIContactMutationFactory.get_update_mutation().Field() delete_contact = NIContactMutationFactory.get_delete_mutation().Field() + create_organization = NIOrganizationMutationFactory.get_create_mutation().Field() + update_organization = NIOrganizationMutationFactory.get_update_mutation().Field() + delete_organization = NIOrganizationMutationFactory.get_delete_mutation().Field() + delete_relationship = DeleteRelationship.Field() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index d21f825d6..329f7b936 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -62,4 +62,4 @@ def resolve_getRelationById(self, info, **kwargs): return rel class NIMeta: - graphql_types = [ Group, Contact ] + graphql_types = [ Group, Contact, Organization ] diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 4cf26ac66..6e433e720 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -80,3 +80,21 @@ class Contact(NIObjectType): class NIMetaType: ni_type = 'Contact' ni_metatype = 'relation' + +class Organization(NIObjectType): + ''' + The group type is used to group contacts + ''' + name = NIStringField(type_kwargs={ 'required': True }) + description = NIStringField(type_kwargs={ 'required': True }) + phone = NIStringField() + website = NIStringField() + customer_id = NIStringField() + additional_info = NIStringField() + + # add relation + works_for = NIRelationField(rel_name='Works_for', type_args=(Role, )) + + class NIMetaType: + ni_type = 'Organization' + ni_metatype = 'relation' From 82b06475fa7b90fa744ca1566be8be9b31513449 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 12 Jun 2019 14:48:27 +0200 Subject: [PATCH 116/520] Filtering of the fields of the django form in the mutation factory --- src/niweb/apps/noclook/schema/core.py | 34 ++++++++++++++++------ src/niweb/apps/noclook/schema/mutations.py | 4 +-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 75e69f133..adaf62ee6 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -698,9 +698,12 @@ def __init_subclass_with_meta__( django_form = getattr(ni_metaclass, 'django_form', None) mutation_name = getattr(ni_metaclass, 'mutation_name', None) is_create = getattr(ni_metaclass, 'is_create', False) - fields = getattr(ni_metaclass, 'fields', None) + 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: @@ -711,10 +714,21 @@ def __init_subclass_with_meta__( 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: - inner_fields[field_name] = graphene_field + add_field = True + elif include: + if field_name in include: + add_field = True + elif exclude: + if field_name not in exclude: + add_field = True else: + add_field = True + + if add_field: inner_fields[field_name] = graphene_field # add handle_id @@ -737,7 +751,7 @@ def add_return_type(cls, graphql_type): setattr(cls, graphql_type.__name__.lower(), graphene.Field(graphql_type)) @classmethod - def form_to_graphene_field(cls, form_field): + def form_to_graphene_field(cls, form_field, include=None, exclude=None): '''Django form to graphene field conversor ''' graphene_field = None @@ -863,7 +877,7 @@ class NIMetaClass: request_path = None is_create = True graphql_type = None - fields = None + include = None exclude = None @classmethod @@ -895,6 +909,8 @@ class UpdateNIMutation(AbstractNIMutation): class NIMetaClass: request_path = None graphql_type = None + include = None + exclude = None @classmethod def do_request(cls, request, **kwargs): @@ -976,9 +992,9 @@ def __init_subclass__(cls, **kwargs): 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_fields = getattr(ni_metaclass, 'create_fields', None) + create_include = getattr(ni_metaclass, 'create_include', None) create_exclude = getattr(ni_metaclass, 'create_exclude', None) - update_fields = getattr(ni_metaclass, 'update_fields', None) + update_include = getattr(ni_metaclass, 'update_include', None) update_exclude = getattr(ni_metaclass, 'update_exclude', None) # we'll retrieve these values NI type/metatype from the GraphQLType @@ -1002,7 +1018,7 @@ def __init_subclass__(cls, **kwargs): 'request_path': request_path, 'is_create': True, 'graphql_type': graphql_type, - 'fields': create_fields, + 'include': create_include, 'exclude': create_exclude, } @@ -1019,7 +1035,7 @@ def __init_subclass__(cls, **kwargs): class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) attr_dict['django_form'] = update_form attr_dict['is_create'] = False - attr_dict['fields'] = update_fields + attr_dict['include'] = update_include attr_dict['exclude'] = update_exclude update_metaclass = type(metaclass_name, (object,), attr_dict) @@ -1033,7 +1049,7 @@ def __init_subclass__(cls, **kwargs): class_name = 'DeleteNI{}Mutation'.format(node_type.capitalize()) del attr_dict['django_form'] - del attr_dict['fields'] + del attr_dict['include'] del attr_dict['exclude'] delete_metaclass = type(metaclass_name, (object,), attr_dict) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index d2ed3f964..e6e8fb789 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -36,8 +36,8 @@ class NIMetaClass: update_form = EditOrganizationForm request_path = '/' graphql_type = Organization - # create_fields or create_exclude - # update_fields or update_exclude + # create_include or create_exclude + # update_include or update_exclude update_exclude = ('abuse_contact', 'primary_contact', 'secondary_contact', 'it_technical_contact', 'it_security_contact', 'it_manager_contact') From 15cc65c76f44f3790f7a79d97ad0a475770bcda4 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 13 Jun 2019 13:19:19 +0200 Subject: [PATCH 117/520] Removed changes made on NIFields --- src/niweb/apps/noclook/schema/core.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index adaf62ee6..7fda091be 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -173,14 +173,11 @@ class NIBasicField(): Super class of the type fields ''' def __init__(self, field_type=graphene.String, manual_resolver=False, - type_kwargs=None, required=False, req_on_create=False, - **kwargs): + type_kwargs=None, **kwargs): self.field_type = field_type self.manual_resolver = manual_resolver self.type_kwargs = type_kwargs - self.required = required - self.req_on_create = req_on_create def get_resolver(self, **kwargs): field_name = kwargs.get('field_name') @@ -209,10 +206,9 @@ class NIIntField(NIBasicField): Int type ''' def __init__(self, field_type=graphene.Int, manual_resolver=False, - type_kwargs=None, required=False, req_on_create=False, - **kwargs): + type_kwargs=None, **kwargs): super(NIIntField, self).__init__(field_type, manual_resolver, - type_kwargs, required, req_on_create, **kwargs) + type_kwargs, **kwargs) class NIListField(NIBasicField): ''' @@ -1035,7 +1031,7 @@ def __init_subclass__(cls, **kwargs): class_name = 'UpdateNI{}Mutation'.format(node_type.capitalize()) attr_dict['django_form'] = update_form attr_dict['is_create'] = False - attr_dict['include'] = update_include + attr_dict['include'] = update_include attr_dict['exclude'] = update_exclude update_metaclass = type(metaclass_name, (object,), attr_dict) From f6057d6fda8ab0b8f155167e882726e6832feb16 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 17 Jun 2019 10:49:20 +0200 Subject: [PATCH 118/520] Role detail page --- src/niweb/apps/noclook/urls.py | 2 ++ src/niweb/apps/noclook/views/detail.py | 42 ++++++++++++++++++++++++++ src/niweb/apps/noclook/views/list.py | 21 +++++++++++++ 3 files changed, 65 insertions(+) diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index bd654b72e..f7625957e 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -87,6 +87,7 @@ 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'^router/$', _list.list_routers), url(r'^rack/$', _list.list_racks), url(r'^odf/$', _list.list_odfs), @@ -113,6 +114,7 @@ url(r'^optical-filter/(?P\d+)/$', detail.optical_filter_detail), url(r'^port/(?P\d+)/$', detail.port_detail), url(r'^site/(?P\d+)/$', detail.site_detail), + url(r'^role/detail/$', detail.role_detail), url(r'^rack/(?P\d+)/$', detail.rack_detail), url(r'^site-owner/(?P\d+)/$', detail.site_owner_detail), url(r'^service/(?P\d+)/$', detail.service_detail), diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 999c2af00..a7eea0bd6 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -1,5 +1,6 @@ # -*- 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 @@ -7,6 +8,7 @@ from apps.noclook.models import NodeHandle from apps.noclook import helpers +from apps.noclook.views.helpers import Table, TableRow import norduniclient as nc logger = logging.getLogger(__name__) @@ -661,3 +663,43 @@ def group_detail(request, handle_id): location_path = node.get_location_path() return render(request, 'noclook/detail/group_detail.html', {'node_handle': nh, 'node': node, 'location_path': location_path}) + +@login_required +def role_detail(request, role_name): + # Get node from neo4j-database + node = nh.get_node() + # Get location + contact_list = nc.models.RoleRelationship.get_contacts_with_role(role_name) + location_path = node.get_location_path() + return render(request, 'noclook/detail/procedure_detail.html', + {'node_handle': nh, 'node': node, 'location_path': location_path}) + +def _contact_with_role_table(con): + from pprint import pformat + #raise Exception(pformat(vars(con))) + + contact_link = { + 'url': u'/contact/{}/'.format(con.handle_id), + 'name': u'{}'.format(con.node_name) + } + + row = TableRow(contact_link) + return row + +@login_required +def role_detail(request): + role_name = request.GET.get('name', None) + + if role_name: + con_list = nc.models.RoleRelationship.get_contacts_with_role(role_name) + contact_list = [ NodeHandle.objects.get(handle_id=x.data['handle_id']) for x in con_list ] + urls = helpers.get_node_urls(contact_list) + + table = Table('Name') + table.rows = [_contact_with_role_table(item) for item in contact_list] + table.no_badges=True + + return render(request, 'noclook/list/list_generic.html', + {'table': table, 'name': 'Contacts', 'urls': urls}) + else: + raise Http404("The role doesn't exists") diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index 1db1df193..04b0e5626 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -7,6 +7,7 @@ from apps.noclook.views.helpers import Table, TableRow from apps.noclook.helpers import get_node_urls, neo4j_data_age import norduniclient as nc +import urllib __author__ = 'lundberg' @@ -636,3 +637,23 @@ def list_contacts(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Contacts', 'urls': urls}) + +def _role_table(role_name): + name_param = { 'name': role_name } + role_link = { + 'url': u'/role/detail/?{}'.format(urllib.parse.urlencode(name_param)), + 'name': u'{}'.format(role_name) + } + row = TableRow(role_link) + return row + +@login_required +def list_roles(request): + role_list = nc.models.RoleRelationship.get_all_roles(nc.graphdb.manager) + + table = Table('Name') + table.rows = [_role_table(role_name) for role_name in role_list] + table.no_badges=True + + return render(request, 'noclook/list/list_generic.html', + {'table': table, 'name': 'Roles', 'urls': role_list}) From 5120cd78fd8ce67454dd82d5f3036aea42b0fba0 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 17 Jun 2019 13:42:32 +0200 Subject: [PATCH 119/520] Deleted previous role_detail view function --- src/niweb/apps/noclook/views/detail.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index a7eea0bd6..bc07a2447 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -664,16 +664,6 @@ def group_detail(request, handle_id): return render(request, 'noclook/detail/group_detail.html', {'node_handle': nh, 'node': node, 'location_path': location_path}) -@login_required -def role_detail(request, role_name): - # Get node from neo4j-database - node = nh.get_node() - # Get location - contact_list = nc.models.RoleRelationship.get_contacts_with_role(role_name) - location_path = node.get_location_path() - return render(request, 'noclook/detail/procedure_detail.html', - {'node_handle': nh, 'node': node, 'location_path': location_path}) - def _contact_with_role_table(con): from pprint import pformat #raise Exception(pformat(vars(con))) From 61bb056960fecc3f31d9089f693a56c835ef5727 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 18 Jun 2019 09:26:18 +0200 Subject: [PATCH 120/520] WIP: Added relations_processors for update operations, --- src/niweb/apps/noclook/schema/core.py | 18 ++++++++++++++++-- src/niweb/apps/noclook/schema/mutations.py | 10 ++++++++++ .../noclook/tests/schema/test_mutations.py | 11 +++++------ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 7fda091be..4336b978c 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -896,7 +896,7 @@ def do_request(cls, request, **kwargs): 'A {} with that name already exists.'.format(node_type) ) helpers.form_update_node(request.user, nh.handle_id, form) - return nh + return { graphql_type.__name__.lower(): nh } else: # get the errors and return them raise GraphQLError('Form errors: {}'.format(form.errors)) @@ -937,7 +937,10 @@ def do_request(cls, request, **kwargs): @classmethod def process_relations(cls, form, nodehandler): - pass + relations_processors = getattr(cls, 'relations_processors', None) + if relations_processors: + for relation_name, relation_f in relations_processors.items(): + relation_f(form, nodehandler, relation_name) class DeleteNIMutation(AbstractNIMutation): class NIMetaClass: @@ -993,6 +996,9 @@ def __init_subclass__(cls, **kwargs): update_include = getattr(ni_metaclass, 'update_include', None) update_exclude = getattr(ni_metaclass, 'update_exclude', None) + # check for relationship processors + relations_processors = getattr(cls, 'relations_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() @@ -1033,6 +1039,10 @@ def __init_subclass__(cls, **kwargs): 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) cls._update_mutation = type( @@ -1047,6 +1057,10 @@ def __init_subclass__(cls, **kwargs): del attr_dict['django_form'] del attr_dict['include'] del attr_dict['exclude'] + + if relations_processors: + del attr_dict['relations_processors'] + delete_metaclass = type(metaclass_name, (object,), attr_dict) cls._delete_mutation = type( diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index e6e8fb789..a22ab9586 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -21,6 +21,16 @@ class Meta: abstract = False class NIContactMutationFactory(NIMutationFactory): + @classmethod + def process_works_for(cls, form, nodehandler, relation_name): + organization_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) + role_name = form.cleaned_data['role_name'] + helpers.set_works_for(request.user, nodehandler, organization_nh.handle_id, role_name) + + relations_processors = { + 'relationship_works_for': process_works_for + } + class NIMetaClass: create_form = NewContactForm update_form = EditContactForm diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index da86b99cb..d3cf52784 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -2,8 +2,9 @@ __author__ = 'ffuentes' from collections import OrderedDict -from . import Neo4jGraphQLTest from niweb.schema import schema +from pprint import pformat +from . import Neo4jGraphQLTest class QueryTest(Neo4jGraphQLTest): def test_group(self): @@ -73,9 +74,7 @@ def test_group(self): query = """ mutation delete_test_group { delete_group(input: {handle_id: 9}){ - group{ - handle_id - } + success } } """ @@ -83,11 +82,11 @@ def test_group(self): expected = OrderedDict([ ('delete_group', OrderedDict([ - ('group', None), + ('success', True), ]) ) ]) result = schema.execute(query, context=self.context) - assert not result.errors + assert not result.errors, pformat(result.errors, indent=1) assert result.data == expected From 5996a37ac2e80d92eb94b722c33d76c50415ec43 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 18 Jun 2019 11:41:45 +0200 Subject: [PATCH 121/520] Fixes and improvements on relation resolving --- src/niweb/apps/noclook/schema/core.py | 24 +++++-- src/niweb/apps/noclook/schema/types.py | 7 +- .../apps/noclook/tests/schema/test_schema.py | 65 ++++++++++--------- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 4336b978c..453beb6ad 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -655,6 +655,15 @@ def __init__(self, field_type=graphene.List, manual_resolver=False, 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, 'NIMeta', None) + if nimeta: + nimodel = getattr(nimeta, 'nimodel', None) + field_name = kwargs.get('field_name') rel_name = kwargs.get('rel_name') @@ -666,13 +675,14 @@ def get_resolver(self, **kwargs): ) def resolve_node_relation(self, info, **kwargs): ret = [] - reldicts = self.get_node().relationships[rel_name] - - for reldict in reldicts: - relbundle = nc.get_relationship_bundle(nc.graphdb.manager, relationship_id=reldict['relationship_id']) - relation = nc.models.BaseRelationshipModel(nc.graphdb.manager) - relation.load(relbundle) - ret.append(relation) + 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 diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 6e433e720..38997a0f1 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -3,6 +3,7 @@ import graphene +from norduniclient.models import RoleRelationship from django.contrib.auth.models import User from graphene import relay from .core import * @@ -17,8 +18,8 @@ def resolve_name(self, info, **kwargs): else: raise Exception('This must not be a role relationship') - class Meta: - interfaces = (relay.Node, ) + class NIMeta: + nimodel = RoleRelationship class User(DjangoObjectType): ''' @@ -91,7 +92,7 @@ class Organization(NIObjectType): website = NIStringField() customer_id = NIStringField() additional_info = NIStringField() - + # add relation works_for = NIRelationField(rel_name='Works_for', type_args=(Role, )) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 77cf416a2..ed09940eb 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -2,14 +2,15 @@ __author__ = 'ffuentes' from collections import OrderedDict -from . import Neo4jGraphQLTest from niweb.schema import schema +from pprint import pformat +from . import Neo4jGraphQLTest class QueryTest(Neo4jGraphQLTest): def test_get_contacts(self): # test contacts query = ''' - query getLastTenContacts { + query getLastTwoContacts { contacts(first: 2, orderBy: handle_id_DESC) { edges { node { @@ -20,38 +21,44 @@ def test_get_contacts(self): member_of_groups { name } + works_for{ + name + } } } } } ''' - expected = OrderedDict([('contacts', + expected = OrderedDict([('contacts', OrderedDict([('edges', - [ - OrderedDict([('node', + [OrderedDict([('node', OrderedDict([('handle_id', '29'), - ('name', 'John Smith'), - ('first_name', 'John'), - ('last_name', 'Smith'), - ('member_of_groups', - [OrderedDict([('name', - 'group2')])])]))]), - OrderedDict([('node', + ('name', 'John Smith'), + ('first_name', 'John'), + ('last_name', 'Smith'), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])]), + ('works_for', + [OrderedDict([('name', + 'role2')])])]))]), + OrderedDict([('node', OrderedDict([('handle_id', '28'), - ('name', 'Jane Doe'), - ('first_name', 'Jane'), - ('last_name', 'Doe'), - ('member_of_groups', - [OrderedDict([('name', - 'group1')])])]))]), - ])]))]) - + ('name', 'Jane Doe'), + ('first_name', 'Jane'), + ('last_name', 'Doe'), + ('member_of_groups', + [OrderedDict([('name', + 'group1')])]), + ('works_for', + [OrderedDict([('name', + 'role1')])])]))])])]))]) result = schema.execute(query, context=self.context) - assert not result.errors, result.errors - assert result.data == expected + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) # getNodeById query = ''' @@ -63,7 +70,7 @@ def test_get_contacts(self): ''' result = schema.execute(query, context=self.context) - assert not result.errors, result.errors + assert not result.errors, pformat(result.errors, indent=1) # filter tests query = ''' @@ -95,9 +102,9 @@ def test_get_contacts(self): result = schema.execute(query, context=self.context) - - assert not result.errors, result.errors - assert result.data == expected + + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) query = ''' { @@ -130,8 +137,8 @@ def test_get_contacts(self): result = schema.execute(query, context=self.context) - assert not result.errors, result.errors - assert result.data == expected + assert not result.errors, pformat(result.errors, indent=1) + assert result.data == expected, pformat(result.data, indent=1) def test_getnodebyhandle_id(self): query = ''' @@ -156,4 +163,4 @@ def test_dropdown(self): ''' result = schema.execute(query, context=self.context) - assert not result.errors, result.errors + assert not result.errors, pformat(result.errors, indent=1) From 0b896a5a89f99a6e678f1df46c145c2f7adbb63e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 07:43:51 +0200 Subject: [PATCH 122/520] WIP: relation_processor function defined outside the class --- src/niweb/apps/noclook/schema/core.py | 13 ++++++++----- src/niweb/apps/noclook/schema/mutations.py | 18 ++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 453beb6ad..55ab11bd9 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -936,7 +936,7 @@ def do_request(cls, request, **kwargs): helpers.form_update_node(request.user, nodehandler.handle_id, form) # process relations if implemented - cls.process_relations(form, nodehandler) + cls.process_relations(request, form, nodehandler) return { graphql_type.__name__.lower(): nh } else: @@ -946,11 +946,14 @@ def do_request(cls, request, **kwargs): raise GraphQLError('Form errors: {}'.format(form.errors)) @classmethod - def process_relations(cls, form, nodehandler): - relations_processors = getattr(cls, 'relations_processors', None) + def process_relations(cls, request, form, nodehandler): + from pprint import pformat + 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(form, nodehandler, relation_name) + relation_f(request, form, nodehandler, relation_name) class DeleteNIMutation(AbstractNIMutation): class NIMetaClass: @@ -1007,7 +1010,7 @@ def __init_subclass__(cls, **kwargs): update_exclude = getattr(ni_metaclass, 'update_exclude', None) # check for relationship processors - relations_processors = getattr(cls, 'relations_processors', None) + relations_processors = getattr(ni_metaclass, 'relations_processors', None) # we'll retrieve these values NI type/metatype from the GraphQLType nimetatype = getattr(graphql_type, 'NIMetaType') diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index a22ab9586..8c137790d 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -20,22 +20,20 @@ class NIMetaClass: class Meta: abstract = False -class NIContactMutationFactory(NIMutationFactory): - @classmethod - def process_works_for(cls, form, nodehandler, relation_name): - organization_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) - role_name = form.cleaned_data['role_name'] - helpers.set_works_for(request.user, nodehandler, organization_nh.handle_id, role_name) - - relations_processors = { - 'relationship_works_for': process_works_for - } +def process_works_for(request, form, nodehandler, relation_name): + organization_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) + role_name = form.cleaned_data['role_name'] + helpers.set_works_for(request.user, nodehandler, organization_nh.handle_id, role_name) +class NIContactMutationFactory(NIMutationFactory): class NIMetaClass: create_form = NewContactForm update_form = EditContactForm request_path = '/' graphql_type = Contact + relations_processors = { + 'relationship_works_for': process_works_for + } class Meta: abstract = False From 1a64507b3e9addc2222def43ff2a93a2282f0418 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 09:25:34 +0200 Subject: [PATCH 123/520] Added organization resolution for Role --- src/niweb/apps/noclook/schema/types.py | 61 +++++++++++++------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 38997a0f1..1b06ed53d 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -9,18 +9,6 @@ from .core import * from ..models import * -class Role(NIRelationType): - name = graphene.String(required=True) - - def resolve_name(self, info, **kwargs): - if self.name: - return self.name - else: - raise Exception('This must not be a role relationship') - - class NIMeta: - nimodel = RoleRelationship - class User(DjangoObjectType): ''' The django user type @@ -60,6 +48,37 @@ class NIMetaType: ni_type = 'Group' ni_metatype = 'logical' +class Organization(NIObjectType): + ''' + The group type is used to group contacts + ''' + name = NIStringField(type_kwargs={ 'required': True }) + description = NIStringField(type_kwargs={ 'required': True }) + phone = NIStringField() + website = NIStringField() + customer_id = NIStringField() + additional_info = NIStringField() + + class NIMetaType: + ni_type = 'Organization' + ni_metatype = 'relation' + +class Role(NIRelationType): + name = graphene.String(required=True) + organization = graphene.Field(Organization) + + def resolve_name(self, info, **kwargs): + if self.name: + return self.name + else: + raise Exception('This must not be a role relationship') + + def resolve_organization(self, info, **kwargs): + return NodeHandle.objects.get(handle_id=self.end) + + class NIMeta: + nimodel = RoleRelationship + class Contact(NIObjectType): ''' A contact in the SRI system @@ -81,21 +100,3 @@ class Contact(NIObjectType): class NIMetaType: ni_type = 'Contact' ni_metatype = 'relation' - -class Organization(NIObjectType): - ''' - The group type is used to group contacts - ''' - name = NIStringField(type_kwargs={ 'required': True }) - description = NIStringField(type_kwargs={ 'required': True }) - phone = NIStringField() - website = NIStringField() - customer_id = NIStringField() - additional_info = NIStringField() - - # add relation - works_for = NIRelationField(rel_name='Works_for', type_args=(Role, )) - - class NIMetaType: - ni_type = 'Organization' - ni_metatype = 'relation' From 1d26c987a5256e8da106682b04d27296494150a9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 09:59:49 +0200 Subject: [PATCH 124/520] Quickfix: prevention of empty role names --- src/niweb/apps/noclook/forms/common.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 4617bf348..5dca55f4d 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -193,13 +193,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): @@ -232,8 +232,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') @@ -777,7 +777,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) @@ -934,6 +934,16 @@ def __init__(self, *args, **kwargs): relationship_member_of = relationship_field('group', True) role_name = forms.CharField(required=False) + def clean(self): + """ + Check empty role names + """ + cleaned_data = super(EditContactForm, self).clean() + role_name = cleaned_data.get("role_name") + + if not role_name: + cleaned_data['relationship_works_for'] = None + class NewProcedureForm(forms.Form): name = forms.CharField() description = description_field('procedure') From 483fc2bdae72f157997e97a67b86f8d90b473a5b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 10:34:32 +0200 Subject: [PATCH 125/520] Added getRoleById --- src/niweb/apps/noclook/schema/query.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 329f7b936..9e2e68c26 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -25,6 +25,7 @@ class NOCRootQuery(NOCAutoQuery): getAvailableDropdowns = graphene.List(graphene.String) getChoicesForDropdown = graphene.List(KeyValue, name=graphene.String(required=True)) getRelationById = graphene.Field(NIRelationType, relation_id=graphene.Int(required=True)) + getRoleById = graphene.Field(Role, relation_id=graphene.Int(required=True)) def resolve_getAvailableDropdowns(self, info, **kwargs): django_dropdowns = [d.name for d in DropdownModel.objects.all()] @@ -61,5 +62,13 @@ def resolve_getRelationById(self, info, **kwargs): return rel + def resolve_getRoleById(self, info, **kwargs): + relation_id = kwargs.get('relation_id') + rel = nc.models.RoleRelationship.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) + rel.relation_id = rel.id + rel.id = None + + return rel + class NIMeta: graphql_types = [ Group, Contact, Organization ] From 408b97fa5b64334b4d6820fe8d2f345e349d96dd Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 10:43:24 +0200 Subject: [PATCH 126/520] Added prodcedure --- src/niweb/apps/noclook/schema/__init__.py | 1 + src/niweb/apps/noclook/schema/mutations.py | 28 ++++++++++++++++------ src/niweb/apps/noclook/schema/query.py | 2 +- src/niweb/apps/noclook/schema/types.py | 11 +++++++++ 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index d5745f338..8de1965c2 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -17,6 +17,7 @@ Neo4jChoice, Group, Contact, + Procedure, NodeHandler, ] diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 8c137790d..f15aa0836 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -20,6 +20,16 @@ class NIMetaClass: 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 process_works_for(request, form, nodehandler, relation_name): organization_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) role_name = form.cleaned_data['role_name'] @@ -76,13 +86,17 @@ def mutate_and_get_payload(cls, root, info, **input): return DeleteRelationship(success=success, relation_id=relation_id) 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() - - create_contact = NIContactMutationFactory.get_create_mutation().Field() - update_contact = NIContactMutationFactory.get_update_mutation().Field() - delete_contact = NIContactMutationFactory.get_delete_mutation().Field() + create_group = NIGroupMutationFactory.get_create_mutation().Field() + update_group = NIGroupMutationFactory.get_update_mutation().Field() + delete_group = NIGroupMutationFactory.get_delete_mutation().Field() + + create_procedure = NIProcedureMutationFactory.get_create_mutation().Field() + update_procedure = NIProcedureMutationFactory.get_update_mutation().Field() + delete_procedure = NIProcedureMutationFactory.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() create_organization = NIOrganizationMutationFactory.get_create_mutation().Field() update_organization = NIOrganizationMutationFactory.get_update_mutation().Field() diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 9e2e68c26..f6eef2642 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -71,4 +71,4 @@ def resolve_getRoleById(self, info, **kwargs): return rel class NIMeta: - graphql_types = [ Group, Contact, Organization ] + graphql_types = [ Group, Contact, Organization, Procedure ] diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 1b06ed53d..9c20988da 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -48,6 +48,17 @@ class NIMetaType: ni_type = 'Group' ni_metatype = 'logical' +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 = 'logical' + class Organization(NIObjectType): ''' The group type is used to group contacts From 5c45fb0cfc24ef757e83b70c877c09b42e043eea Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 11:01:01 +0200 Subject: [PATCH 127/520] Section markers added to the code --- src/niweb/apps/noclook/schema/core.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 55ab11bd9..411360a30 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -19,6 +19,7 @@ from ..models import NodeType, NodeHandle +########## CONNECTION FILTER BUILD FUNCTIONS def build_match_predicate(field, value, type): # string quoting if isinstance(type, graphene.String): @@ -142,7 +143,9 @@ def build_not_ends_with_predicate(field, value, type): 'ends_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_ends_with_predicate }, 'not_ends_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_ends_with_predicate }, } +########## END CONNECTION FILTER BUILD FUNCTIONS +########## KEYVALUE TYPES class KeyValue(graphene.Interface): name = graphene.String(required=True) value = graphene.String(required=True) @@ -168,6 +171,9 @@ def resolve_nidata(self, info, **kwargs): return ret +########## END KEYVALUE TYPES + +########## BASIC NI FIELDS class NIBasicField(): ''' Super class of the type fields @@ -248,6 +254,10 @@ def resolve_relationship_list(self, info, **kwargs): return resolve_relationship_list +########## END BASIC NI FIELDS + + +########## RELATION AND NODE TYPES class NIRelationType(graphene.ObjectType): ''' This class represents a relationship and its properties @@ -642,7 +652,10 @@ def build_filter_query(cls, filter, nodetype): 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 @@ -688,6 +701,9 @@ def resolve_node_relation(self, info, **kwargs): return resolve_node_relation +########## END RELATION FIELD + +########## MUTATION FACTORY class NodeHandler(NIObjectType): name = NIStringField(type_kwargs={ 'required': True }) @@ -1096,10 +1112,9 @@ def get_update_mutation(cls, *args, **kwargs): def get_delete_mutation(cls, *args, **kwargs): return cls._delete_mutation -# TODO: create a new mutation factory for relationships types -# update relationship attributes or delete the relation itself -# what about creating relationships between two related entities? +########## END MUTATION FACTORY +########## EXCEPTION AND AUTOQUERY class GraphQLAuthException(Exception): ''' Simple auth exception @@ -1185,3 +1200,4 @@ def __init_subclass__(cls, **kwargs): setattr(cls, field_name, graphene.Field(graphql_type, handle_id=graphene.Int())) setattr(cls, resolver_name, graphql_type.get_byid_resolver()) +########## END EXCEPTION AND AUTOQUERY From 7e8d84c41fa6a12ae3de96136af122d26c677f73 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 11:49:54 +0200 Subject: [PATCH 128/520] WIP: Added new scalars for the form conversion in the mutation factory --- src/niweb/apps/noclook/schema/core.py | 99 +++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 411360a30..5bb6f42b8 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -10,15 +10,114 @@ from collections import OrderedDict from django import forms from django.db.models import Q +from django.forms.utils import ValidationError from django.test import RequestFactory from graphene import relay +from graphene.types import Scalar from graphene_django import DjangoObjectType from graphene_django.types import DjangoObjectTypeOptions from graphql import GraphQLError +from io import StringIO from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible from ..models import NodeType, NodeHandle +########## CUSTOM SCALARS FOR FORM FIELD CONVERSION +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): + value = u'\n'.join(value) + return value + + @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 + +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 DateTime(Scalar): + # http://docs.graphene-python.org/en/latest/types/scalars/#custom-scalars + '''DateTime Scalar Description''' + + @staticmethod + def serialize(dt): + return dt.isoformat() + + @staticmethod + def parse_literal(node): + if isinstance(node, ast.StringValue): + return datetime.datetime.strptime( + node.value, "%Y-%m-%dT%H:%M:%S.%f") + + @staticmethod + def parse_value(value): + return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") + +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 + +########## END CUSTOM SCALARS FOR FORM FIELD CONVERSION + ########## CONNECTION FILTER BUILD FUNCTIONS def build_match_predicate(field, value, type): # string quoting From 0a36d945abcb014d420574690c10a925dbefd796 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 12:15:28 +0200 Subject: [PATCH 129/520] Stub of Host type and small corrections and improvements --- src/niweb/apps/noclook/schema/__init__.py | 3 ++- src/niweb/apps/noclook/schema/core.py | 2 +- src/niweb/apps/noclook/schema/query.py | 2 +- src/niweb/apps/noclook/schema/types.py | 19 +++++++++++++++---- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/niweb/apps/noclook/schema/__init__.py b/src/niweb/apps/noclook/schema/__init__.py index 8de1965c2..dfa753af7 100644 --- a/src/niweb/apps/noclook/schema/__init__.py +++ b/src/niweb/apps/noclook/schema/__init__.py @@ -15,10 +15,11 @@ Dropdown, Choice, Neo4jChoice, + NodeHandler, Group, Contact, Procedure, - NodeHandler, + Host, ] NOCSCHEMA_QUERIES = [ diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 5bb6f42b8..71d05ff09 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -266,7 +266,7 @@ def resolve_nidata(self, info, **kwargs): alldata = self.get_node().data for key, value in alldata.items(): - ret.append(DictEntryType(key=key, value=value)) + ret.append(DictEntryType(name=key, value=value)) return ret diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index f6eef2642..886869bee 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -71,4 +71,4 @@ def resolve_getRoleById(self, info, **kwargs): return rel class NIMeta: - graphql_types = [ Group, Contact, Organization, Procedure ] + graphql_types = [ Group, Contact, Organization, Procedure, Host ] diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 9c20988da..13c81c2be 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -9,6 +9,12 @@ from .core import * from ..models import * +# further centralization? +NIMETA_LOGICAL = 'logical' +NIMETA_RELATION = 'relation' +NIMETA_PHYSICAL = 'physical' +NIMETA_LOCATION = 'location' + class User(DjangoObjectType): ''' The django user type @@ -46,7 +52,7 @@ class Group(NIObjectType): class NIMetaType: ni_type = 'Group' - ni_metatype = 'logical' + ni_metatype = NIMETA_LOGICAL class Procedure(NIObjectType): ''' @@ -57,7 +63,7 @@ class Procedure(NIObjectType): class NIMetaType: ni_type = 'Procedure' - ni_metatype = 'logical' + ni_metatype = NIMETA_LOGICAL class Organization(NIObjectType): ''' @@ -72,7 +78,7 @@ class Organization(NIObjectType): class NIMetaType: ni_type = 'Organization' - ni_metatype = 'relation' + ni_metatype = NIMETA_RELATION class Role(NIRelationType): name = graphene.String(required=True) @@ -110,4 +116,9 @@ class Contact(NIObjectType): class NIMetaType: ni_type = 'Contact' - ni_metatype = 'relation' + ni_metatype = NIMETA_RELATION + +class Host(NIObjectType): + class NIMetaType: + ni_type = 'Host' + ni_metatype = NIMETA_LOGICAL From f0d55995da23969fd8543d8e922bcfc10c8e62f9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 12:41:15 +0200 Subject: [PATCH 130/520] Host type improved and correction of IPAddr scalar --- src/niweb/apps/noclook/schema/core.py | 8 ++++++-- src/niweb/apps/noclook/schema/types.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 71d05ff09..bb588ea6f 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -29,8 +29,9 @@ class IPAddr(Scalar): def serialize(value): # this would be to_python method if isinstance(value, list): - value = u'\n'.join(value) - return value + return value + else: + return none @staticmethod def parse_value(value): @@ -48,6 +49,9 @@ def parse_value(value): 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 diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 13c81c2be..f0940d050 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -119,6 +119,27 @@ class NIMetaType: ni_metatype = NIMETA_RELATION 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 From 62fe07422cbb7c98165da7a3b1ce9d1017b7417e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 13:45:25 +0200 Subject: [PATCH 131/520] Bugfix on incoming/outgoing relation resolution and wip for filter build --- src/niweb/apps/noclook/schema/core.py | 47 ++++++++++++++++++-------- src/niweb/apps/noclook/schema/types.py | 2 +- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index bb588ea6f..52d129edf 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -497,10 +497,11 @@ def resolve_incoming(self, info, **kwargs): ''' incoming_rels = self.get_node().incoming ret = [] - for rel_name, rel in incoming_rels.items(): - relation_id = rel[0]['relationship_id'] - rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) - ret.append(DictRelationType(name=rel_name, relation=rel)) + for rel_name, rel_list in incoming_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 @@ -510,10 +511,11 @@ def resolve_outgoing(self, info, **kwargs): ''' outgoing_rels = self.get_node().outgoing ret = [] - for rel_name, rel in outgoing_rels.items(): - relation_id = rel[0]['relationship_id'] - rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) - ret.append(DictRelationType(name=rel_name, relation=rel)) + 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 @@ -536,9 +538,15 @@ def get_filter_input_fields(cls): input_fields = {} for name, field in cls.__dict__.items(): - if field and not isinstance(field, str) and getattr(field, '__module__', None) == 'graphene.types.scalars': - input_field = type(field) - input_fields[name] = input_field + # string or string like fields + if field: + if isinstance(field, str) and getattr(field, '__module__', None) == 'graphene.types.scalars': + input_field = type(field) + input_fields[name] = input_field + elif isinstance(field, graphene.types.structures.List): + continue + input_field = type(field) + input_fields[name] = input_field, field._of_type input_fields['handle_id'] = graphene.Int @@ -559,24 +567,33 @@ def build_filter_and_order(cls): input_fields = cls.get_filter_input_fields() for field_name, input_field in input_fields.items(): + # creating field instance + if hasattr(input_field, '__call__'): + field_instance = input_field() + else: # it must be a list then + field_instance = input_field[0](of_type=input_field[1]) + # adding order attributes enum_options.append(['{}_ASC'.format(field_name), '{}_ASC'.format(field_name)]) enum_options.append(['{}_DESC'.format(field_name), '{}_DESC'.format(field_name)]) # adding filter attributes for suffix, suffix_attr in 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(input_field(), graphene.String): + if not suffix_attr['only_strings'] or isinstance(field_instance, graphene.String): if 'wrapper_field' not in suffix_attr or not suffix_attr['wrapper_field']: - filter_attrib[fmt_filter_field] = input_field() + field_instance = None + + filter_attrib[fmt_filter_field] = field_instance cls.filter_names[fmt_filter_field] = { 'field' : field_name, 'suffix': suffix, - 'field_type': input_field(), + 'field_type': field_instance, } else: the_field = input_field @@ -587,7 +604,7 @@ def build_filter_and_order(cls): cls.filter_names[fmt_filter_field] = { 'field' : field_name, 'suffix': suffix, - 'field_type': input_field(), + 'field_type': field_instance, } simple_filter_input = type('{}NestedFilter'.format(ni_type), (graphene.InputObjectType, ), filter_attrib) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index f0940d050..18d8b77ea 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -112,7 +112,7 @@ class Contact(NIObjectType): other_email = NIStringField() PGP_fingerprint = NIStringField() member_of_groups = NIListField(type_args=(Group,), rel_name='Member_of', rel_method='get_outgoing_relations') - works_for = NIRelationField(rel_name='Works_for', type_args=(Role, )) + roles = NIRelationField(rel_name='Works_for', type_args=(Role, )) class NIMetaType: ni_type = 'Contact' From 4cac46db3b50008765cd3d45c241836a51b3b29e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Jun 2019 14:21:45 +0200 Subject: [PATCH 132/520] More wip for the filter fields building --- src/niweb/apps/noclook/schema/core.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 52d129edf..27b3a08e1 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -540,13 +540,16 @@ def get_filter_input_fields(cls): for name, field in cls.__dict__.items(): # string or string like fields if field: - if isinstance(field, str) and getattr(field, '__module__', None) == 'graphene.types.scalars': + if isinstance(field, graphene.types.scalars.String) or\ + isinstance(field, graphene.types.scalars.Int): input_field = type(field) input_fields[name] = input_field elif isinstance(field, graphene.types.structures.List): continue - input_field = type(field) - input_fields[name] = input_field, field._of_type + #input_field = type(field) + filter_attrib = { 'handle_id': graphene.Int() } + binput_field = type('{}InputField'.format(name), (graphene.InputObjectType, ), filter_attrib) + input_fields[name] = binput_field, field._of_type input_fields['handle_id'] = graphene.Int @@ -568,10 +571,15 @@ def build_filter_and_order(cls): for field_name, input_field in input_fields.items(): # creating field instance + field_instance = None + the_field = None + if hasattr(input_field, '__call__'): field_instance = input_field() - else: # it must be a list then - field_instance = input_field[0](of_type=input_field[1]) + the_field = input_field + else: # it must be a list other_node + field_instance = input_field[0] + the_field = input_field[1] # adding order attributes enum_options.append(['{}_ASC'.format(field_name), '{}_ASC'.format(field_name)]) @@ -587,8 +595,6 @@ def build_filter_and_order(cls): if not suffix_attr['only_strings'] or isinstance(field_instance, graphene.String): if 'wrapper_field' not in suffix_attr or not suffix_attr['wrapper_field']: - field_instance = None - filter_attrib[fmt_filter_field] = field_instance cls.filter_names[fmt_filter_field] = { 'field' : field_name, @@ -596,7 +602,6 @@ def build_filter_and_order(cls): 'field_type': field_instance, } else: - the_field = input_field for wrapper_field in suffix_attr['wrapper_field']: the_field = wrapper_field(the_field) From 2abfbe8be28d7c3d8de47e47c24fdea5bb506be1 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 21 Jun 2019 08:02:57 +0200 Subject: [PATCH 133/520] Small ui/ux fix, title for role detail page --- .../noclook/templates/noclook/detail/role_detail.html | 6 +++++- src/niweb/apps/noclook/views/detail.py | 8 +++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html index 9edb4ff12..0ecd7da97 100644 --- a/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html +++ b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html @@ -1,6 +1,10 @@ -{% extends "noclook/detail/detail.html" %} +{% extends "noclook/list/list_generic.html" %} {% load table_tags %} +{% block before_table %} +

    Role {{name}}

    +{% endblock %} + {% block edit_link %} {% if user.is_staff %} Edit diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index bc07a2447..1b5baf013 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -665,9 +665,6 @@ def group_detail(request, handle_id): {'node_handle': nh, 'node': node, 'location_path': location_path}) def _contact_with_role_table(con): - from pprint import pformat - #raise Exception(pformat(vars(con))) - contact_link = { 'url': u'/contact/{}/'.format(con.handle_id), 'name': u'{}'.format(con.node_name) @@ -689,7 +686,8 @@ def role_detail(request): table.rows = [_contact_with_role_table(item) for item in contact_list] table.no_badges=True - return render(request, 'noclook/list/list_generic.html', - {'table': table, 'name': 'Contacts', 'urls': urls}) + return render(request, 'noclook/detail/role_detail.html', + {'table': table, 'name': role_name, 'slug': 'role', + 'urls': urls}) else: raise Http404("The role doesn't exists") From 1ace7927a505049ed3039556fd01a35102f50b80 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 21 Jun 2019 15:10:46 +0200 Subject: [PATCH 134/520] Improved nester filter construction --- src/niweb/apps/noclook/schema/core.py | 98 ++++++++++++++++++++------ src/niweb/apps/noclook/schema/types.py | 3 +- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 27b3a08e1..f5d4931e5 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -7,7 +7,7 @@ from apps.noclook import helpers from apps.noclook.models import NodeType, NodeHandle -from collections import OrderedDict +from collections import OrderedDict, Iterable from django import forms from django.db.models import Q from django.forms.utils import ValidationError @@ -399,6 +399,44 @@ def resolve_nidata(self, info, **kwargs): return ret + @classmethod + def get_filter_input_fields(cls): + ''' + Method used by build_filter_and_order for a Relatio 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 + class Meta: interfaces = (relay.Node, ) @@ -533,10 +571,14 @@ def get_type_name(cls): @classmethod def get_filter_input_fields(cls): ''' - Method used by build_filter_and_order + 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: @@ -545,10 +587,21 @@ def get_filter_input_fields(cls): input_field = type(field) input_fields[name] = input_field elif isinstance(field, graphene.types.structures.List): - continue - #input_field = type(field) - filter_attrib = { 'handle_id': graphene.Int() } - binput_field = type('{}InputField'.format(name), (graphene.InputObjectType, ), filter_attrib) + # 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 = {} + for a, b in field_of_type.get_filter_input_fields().items(): + filter_attrib[a] = b() + + binput_field = type('{}InputField'.format(name_fot), (graphene.InputObjectType, ), filter_attrib) input_fields[name] = binput_field, field._of_type input_fields['handle_id'] = graphene.Int @@ -574,16 +627,17 @@ def build_filter_and_order(cls): field_instance = None the_field = None - if hasattr(input_field, '__call__'): + # is a plain scalar field? + if not isinstance(input_field, Iterable): field_instance = input_field() the_field = input_field + + # adding order attributes (only for scalar fields) + enum_options.append(['{}_ASC'.format(field_name), '{}_ASC'.format(field_name)]) + enum_options.append(['{}_DESC'.format(field_name), '{}_DESC'.format(field_name)]) else: # it must be a list other_node field_instance = input_field[0] - the_field = input_field[1] - - # adding order attributes - enum_options.append(['{}_ASC'.format(field_name), '{}_ASC'.format(field_name)]) - enum_options.append(['{}_DESC'.format(field_name), '{}_DESC'.format(field_name)]) + the_field = input_field[0] # adding filter attributes for suffix, suffix_attr in filter_array.items(): @@ -602,6 +656,9 @@ def build_filter_and_order(cls): 'field_type': field_instance, } else: + #if isinstance(input_field, Iterable): + # the_field = field_instance + for wrapper_field in suffix_attr['wrapper_field']: the_field = wrapper_field(the_field) @@ -798,7 +855,7 @@ def get_resolver(self, **kwargs): if self.type_args != (NIRelationType,): rel_type = self.type_args[0] - nimeta = getattr(rel_type, 'NIMeta', None) + nimeta = getattr(rel_type, 'NIMetaType', None) if nimeta: nimodel = getattr(nimeta, 'nimodel', None) @@ -825,7 +882,6 @@ def resolve_node_relation(self, info, **kwargs): return ret return resolve_node_relation - ########## END RELATION FIELD ########## MUTATION FACTORY @@ -866,15 +922,16 @@ def __init_subclass_with_meta__( if hasattr(django_form, 'Meta') and hasattr(django_form.Meta, 'exclude'): if field not in django_form.Meta.exclude: add_field = True - elif include: - if field_name in include: - add_field = True - elif exclude: - if field_name not in 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 @@ -1088,7 +1145,6 @@ def do_request(cls, request, **kwargs): @classmethod def process_relations(cls, request, form, nodehandler): - from pprint import pformat nimetaclass = getattr(cls, 'NIMetaClass') relations_processors = getattr(nimetaclass, 'relations_processors', None) diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index 18d8b77ea..c1b2ca9cf 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -93,8 +93,9 @@ def resolve_name(self, info, **kwargs): def resolve_organization(self, info, **kwargs): return NodeHandle.objects.get(handle_id=self.end) - class NIMeta: + class NIMetaType: nimodel = RoleRelationship + filter_exclude = ('type') class Contact(NIObjectType): ''' From d478072d759e1f9e9aabf1a7d8c9d562db9abfb9 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 24 Jun 2019 13:07:32 +0200 Subject: [PATCH 135/520] Test fixing --- src/niweb/apps/noclook/schema/core.py | 33 +++---------------- .../apps/noclook/tests/schema/test_schema.py | 6 ++-- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index f5d4931e5..91cf9c766 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -142,37 +142,13 @@ def build_not_predicate(field, value, type): return ret def build_in_predicate(field, values, type): # a list predicate builder - # string quoting - filter_strings = False - if isinstance(type, graphene.String): - filter_strings = True - - subpredicates = [] - for value in values: - if filter_strings: - value = "'{}'".format(value) - subpredicates.append( - """n.{field} = {value}""".format(field=field, value=value) - ) - - ret = ' OR '.join(subpredicates) + in_string = '{}'.format(', '.join(["'{}'".format(str(x[0])) for x in values])) + ret = 'n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) return ret def build_not_in_predicate(field, values, type): # a list predicate builder - # string quoting - filter_strings = False - if isinstance(type, graphene.String): - filter_strings = True - - subpredicates = [] - for value in values: - if filter_strings: - value = "'{}'".format(value) - subpredicates.append( - """n.{field} <> {value}""".format(field=field, value=value) - ) - - ret = ' AND '.join(subpredicates) + in_string = '{}'.format(', '.join(["'{}'".format(str(x[0])) for x in values])) + ret = 'NOT n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) return ret def build_lt_predicate(field, value, type): @@ -720,6 +696,7 @@ def generic_list_resolver(self, info, **args): nodes = None if filter: q = cls.build_filter_query(filter, type_name) + #raise Exception(q) nodes = nc.query_to_list(nc.graphdb.manager, q) nodes = [ node['n'].properties for node in nodes] else: diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index ed09940eb..9cd805d79 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -21,7 +21,7 @@ def test_get_contacts(self): member_of_groups { name } - works_for{ + roles{ name } } @@ -40,7 +40,7 @@ def test_get_contacts(self): ('member_of_groups', [OrderedDict([('name', 'group2')])]), - ('works_for', + ('roles', [OrderedDict([('name', 'role2')])])]))]), OrderedDict([('node', @@ -51,7 +51,7 @@ def test_get_contacts(self): ('member_of_groups', [OrderedDict([('name', 'group1')])]), - ('works_for', + ('roles', [OrderedDict([('name', 'role1')])])]))])])]))]) From 44a1eeae77172927223df9eb458a2f7f678bf744 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 25 Jun 2019 12:52:45 +0200 Subject: [PATCH 136/520] WIP of the filtering of nested input fields in graphql connections --- src/niweb/apps/noclook/schema/core.py | 295 ++++++++++++++++++-------- 1 file changed, 205 insertions(+), 90 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 91cf9c766..7e317f7d3 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -123,105 +123,205 @@ def get_roles_dropdown(): ########## END CUSTOM SCALARS FOR FORM FIELD CONVERSION ########## CONNECTION FILTER BUILD FUNCTIONS -def build_match_predicate(field, value, type): - # string quoting - if isinstance(type, graphene.String): - value = "'{}'".format(value) +class classproperty(object): + def __init__(self, f): + self.f = f + def __get__(self, obj, owner): + return self.f(owner) - ret = """n.{field} = {value}""".format(field=field, value=value) +class AbstractQueryBuilder: + @staticmethod + def build_match_predicate(field, value, type): + pass - return ret + @staticmethod + def build_not_predicate(field, value, type): + pass -def build_not_predicate(field, value, type): - # string quoting - if isinstance(type, graphene.String): - value = "'{}'".format(value) + @staticmethod + def build_in_predicate(field, value, type): + pass - ret = """n.{field} <> {value}""".format(field=field, value=value) + @staticmethod + def build_not_in_predicate(field, value, type): + pass - return ret + @staticmethod + def build_lt_predicate(field, value, type): + pass -def build_in_predicate(field, values, type): # a list predicate builder - in_string = '{}'.format(', '.join(["'{}'".format(str(x[0])) for x in values])) - ret = 'n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) - return ret + @staticmethod + def build_lte_predicate(field, value, type): + pass -def build_not_in_predicate(field, values, type): # a list predicate builder - in_string = '{}'.format(', '.join(["'{}'".format(str(x[0])) for x in values])) - ret = 'NOT n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) - return ret + @staticmethod + def build_gt_predicate(field, value, type): + pass -def build_lt_predicate(field, value, type): - # string quoting - if isinstance(type, graphene.String): - value = "'{}'".format(value) + @staticmethod + def build_gte_predicate(field, value, type): + pass - ret = """n.{field} < {value}""".format(field=field, value=value) + @staticmethod + def build_contains_predicate(field, value, type): + pass - return ret + @staticmethod + def build_not_contains_predicate(field, value, type): + pass -def build_lte_predicate(field, value, type): - # string quoting - if isinstance(type, graphene.String): - value = "'{}'".format(value) + @staticmethod + def build_starts_with_predicate(field, value, type): + pass - ret = """n.{field} <= {value}""".format(field=field, value=value) + @staticmethod + def build_not_starts_with_predicate(field, value, type): + pass - return ret + @staticmethod + def build_ends_with_predicate(field, value, type): + pass -def build_gt_predicate(field, value, type): - # string quoting - if isinstance(type, graphene.String): - value = "'{}'".format(value) + @staticmethod + def build_not_ends_with_predicate(field, value, type): + pass + + @classproperty + def filter_array(cls): + return { + '': { '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 }, + } - ret = """n.{field} > {value}""".format(field=field, value=value) +class ScalarQueryBuilder(AbstractQueryBuilder): + @staticmethod + def build_match_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) - return ret + ret = """n.{field} = {value}""".format(field=field, value=value) + + return ret -def build_gte_predicate(field, value, type): - # string quoting - if isinstance(type, graphene.String): - value = "'{}'".format(value) + @staticmethod + def build_not_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) - ret = """n.{field} >= {value}""".format(field=field, value=value) + ret = """n.{field} <> {value}""".format(field=field, value=value) - return ret + return ret + + @staticmethod + def build_in_predicate(field, values, type): # a list predicate builder + #raise Exception(values) + in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) + ret = 'n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) + return ret + + @staticmethod + def build_not_in_predicate(field, values, type): # a list predicate builder + in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) + ret = 'NOT n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) + return ret + + @staticmethod + def build_lt_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} < {value}""".format(field=field, value=value) + + return ret + + @staticmethod + def build_lte_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} <= {value}""".format(field=field, value=value) + + return ret + + @staticmethod + def build_gt_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} > {value}""".format(field=field, value=value) + + return ret + + @staticmethod + def build_gte_predicate(field, value, type): + # string quoting + if isinstance(type, graphene.String): + value = "'{}'".format(value) + + ret = """n.{field} >= {value}""".format(field=field, value=value) + + return ret + + @staticmethod + def build_contains_predicate(field, value, type): + return """n.{field} CONTAINS '{value}'""".format(field=field, value=value) + + @staticmethod + def build_not_contains_predicate(field, value, type): + return """NOT n.{field} CONTAINS '{value}'""".format(field=field, value=value) + + @staticmethod + def build_starts_with_predicate(field, value, type): + return """n.{field} STARTS WITH '{value}'""".format(field=field, value=value) + + @staticmethod + def build_not_starts_with_predicate(field, value, type): + return """NOT n.{field} STARTS WITH '{value}'""".format(field=field, value=value) + + @staticmethod + def build_ends_with_predicate(field, value, type): + return """n.{field} ENDS WITH '{value}'""".format(field=field, value=value) + + @staticmethod + def build_not_ends_with_predicate(field, value, type): + return """NOT n.{field} ENDS WITH '{value}'""".format(field=field, value=value) + +class InputFieldQueryBuilder(AbstractQueryBuilder): + @staticmethod + def build_in_predicate(field, values, type): # a list predicate builder + subqueries = [] + for element in values: + for name, field in element.__dict__: + pass + + in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) + ret = 'n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) + return ret + + @staticmethod + def build_not_in_predicate(field, values, type): # a list predicate builder + in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) + ret = 'NOT n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) + return ret -def build_contains_predicate(field, value, type): - return """n.{field} CONTAINS '{value}'""".format(field=field, value=value) - -def build_not_contains_predicate(field, value, type): - return """NOT n.{field} CONTAINS '{value}'""".format(field=field, value=value) - -def build_starts_with_predicate(field, value, type): - return """n.{field} STARTS WITH '{value}'""".format(field=field, value=value) - -def build_not_starts_with_predicate(field, value, type): - return """NOT n.{field} STARTS WITH '{value}'""".format(field=field, value=value) - -def build_ends_with_predicate(field, value, type): - return """n.{field} ENDS WITH '{value}'""".format(field=field, value=value) - -def build_not_ends_with_predicate(field, value, type): - return """NOT n.{field} ENDS WITH '{value}'""".format(field=field, value=value) - -filter_array = { - '': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_match_predicate }, - 'not': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_not_predicate }, - 'in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False, 'qpredicate': build_in_predicate }, - 'not_in': { 'wrapper_field': [graphene.NonNull, graphene.List], 'only_strings': False, 'qpredicate': build_not_in_predicate }, - 'lt': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_lt_predicate }, - 'lte': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_lte_predicate }, - 'gt': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_gt_predicate }, - 'gte': { 'wrapper_field': None, 'only_strings': False, 'qpredicate': build_gte_predicate }, - - 'contains': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_contains_predicate }, - 'not_contains': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_contains_predicate }, - 'starts_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_starts_with_predicate }, - 'not_starts_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_starts_with_predicate }, - 'ends_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_ends_with_predicate }, - 'not_ends_with': { 'wrapper_field': None, 'only_strings': True, 'qpredicate': build_not_ends_with_predicate }, -} ########## END CONNECTION FILTER BUILD FUNCTIONS ########## KEYVALUE TYPES @@ -612,18 +712,19 @@ def build_filter_and_order(cls): enum_options.append(['{}_ASC'.format(field_name), '{}_ASC'.format(field_name)]) enum_options.append(['{}_DESC'.format(field_name), '{}_DESC'.format(field_name)]) else: # it must be a list other_node - field_instance = input_field[0] + field_instance = input_field[0]() the_field = input_field[0] # adding filter attributes - for suffix, suffix_attr in filter_array.items(): + 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): + if not suffix_attr['only_strings'] \ + or isinstance(field_instance, graphene.String): 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] = { @@ -632,13 +733,11 @@ def build_filter_and_order(cls): 'field_type': field_instance, } else: - #if isinstance(input_field, Iterable): - # the_field = field_instance - + wrapped_field = the_field for wrapper_field in suffix_attr['wrapper_field']: - the_field = wrapper_field(the_field) + wrapped_field = wrapper_field(wrapped_field) - filter_attrib[fmt_filter_field] = the_field + filter_attrib[fmt_filter_field] = wrapped_field cls.filter_names[fmt_filter_field] = { 'field' : field_name, 'suffix': suffix, @@ -696,7 +795,6 @@ def generic_list_resolver(self, info, **args): nodes = None if filter: q = cls.build_filter_query(filter, type_name) - #raise Exception(q) nodes = nc.query_to_list(nc.graphdb.manager, q) nodes = [ node['n'].properties for node in nodes] else: @@ -740,6 +838,9 @@ def build_filter_query(cls, filter, nodetype): for and_filter in and_filters: # iterate though values of a nested filter for filter_key, filter_value in and_filter.items(): + # choose filter array for query building + filter_array = ScalarQueryBuilder.filter_array + filter_field = cls.filter_names[filter_key] field = filter_field['field'] suffix = filter_field['suffix'] @@ -765,6 +866,9 @@ def build_filter_query(cls, filter, nodetype): for or_filter in or_filters: # iterate though values of a nested filter for filter_key, filter_value in or_filter.items(): + # choose filter array for query building + filter_array = ScalarQueryBuilder.filter_array + filter_field = cls.filter_names[filter_key] field = filter_field['field'] suffix = filter_field['suffix'] @@ -800,11 +904,22 @@ def build_filter_query(cls, filter, nodetype): if build_query != '': build_query = 'WHERE {}'.format(build_query) + # additional clauses + match_additional_clauses = [] + + # remove redundant + match_additional_clauses = list(set(match_additional_clauses)) + match_additional = ''.join(match_additional_clauses) + optional = '' + if match_additional: + optional = "OPTIONAL " + q = """ - MATCH (n:{label}) + {optional}MATCH (n:{label}){match_additional} {build_query} RETURN distinct n - """.format(label=nodetype, build_query=build_query) + """.format(optional=optional, label=nodetype, match_additional="", + build_query=build_query) return q From c39c23f877388822db8c16a0fabc8b8206477fb4 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 25 Jun 2019 15:52:20 +0200 Subject: [PATCH 137/520] Fix for non existent types and further filter wip --- src/niweb/apps/noclook/schema/core.py | 88 ++++++++++++++++----------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 7e317f7d3..17c5e3ead 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -305,8 +305,12 @@ def build_not_ends_with_predicate(field, value, type): return """NOT n.{field} ENDS WITH '{value}'""".format(field=field, value=value) class InputFieldQueryBuilder(AbstractQueryBuilder): - @staticmethod - def build_in_predicate(field, values, type): # a list predicate builder + @classmethod + def build_match_predicate(cls, field, value, type): + pass + + @classmethod + def build_in_predicate(cls, field, values, type): # a list predicate builder subqueries = [] for element in values: for name, field in element.__dict__: @@ -316,8 +320,8 @@ def build_in_predicate(field, values, type): # a list predicate builder ret = 'n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) return ret - @staticmethod - def build_not_in_predicate(field, values, type): # a list predicate builder + @classmethod + def build_not_in_predicate(cls, field, values, type): # a list predicate builder in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) ret = 'NOT n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) return ret @@ -513,6 +517,10 @@ def get_filter_input_fields(cls): return input_fields + @classproperty + def match_additional_clause(cls): + return "[r{}:{}]".format('{}',cls) + class Meta: interfaces = (relay.Node, ) @@ -839,7 +847,10 @@ def build_filter_query(cls, filter, nodetype): # iterate though values of a nested filter for filter_key, filter_value in and_filter.items(): # choose filter array for query building - filter_array = ScalarQueryBuilder.filter_array + if isinstance(filter_value, int) or isinstance(filter_value, str): + filter_array = ScalarQueryBuilder.filter_array + else: + filter_array = InputFieldQueryBuilder.filter_array filter_field = cls.filter_names[filter_key] field = filter_field['field'] @@ -909,20 +920,21 @@ def build_filter_query(cls, filter, nodetype): # remove redundant match_additional_clauses = list(set(match_additional_clauses)) - match_additional = ''.join(match_additional_clauses) - optional = '' - if match_additional: - optional = "OPTIONAL " + match_additional = ', '.join(match_additional_clauses) q = """ - {optional}MATCH (n:{label}){match_additional} + MATCH (n:{label}){match_additional} {build_query} RETURN distinct n - """.format(optional=optional, label=nodetype, match_additional="", + """.format(label=nodetype, match_additional="", build_query=build_query) return q + @classproperty + def match_additional_clause(cls): + return "[m{}:{}]".format('{}',cls.NIMetaType.ni_type) + class Meta: model = NodeHandle interfaces = (relay.Node, ) @@ -1444,33 +1456,35 @@ def __init_subclass__(cls, **kwargs): assert ni_metatype, '{} has not set its ni_metatype attribute'.format(cls.__name__) node_type = NodeType.objects.filter(type=ni_type).first() - type_name = node_type.type - type_slug = node_type.slug - - # 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()) + if node_type: + type_name = node_type.type + type_slug = node_type.slug + + # 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) + ## 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, handle_id=graphene.Int())) - setattr(cls, resolver_name, graphql_type.get_byid_resolver()) + setattr(cls, field_name, graphene.Field(graphql_type, handle_id=graphene.Int())) + setattr(cls, resolver_name, graphql_type.get_byid_resolver()) ########## END EXCEPTION AND AUTOQUERY From eff366f7f09f4ea78a642380930d97a2b3fa4e2d Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 28 Jun 2019 08:04:10 +0200 Subject: [PATCH 138/520] Role name is not mandatory anymore. WIP on filtering nested objects. --- src/niweb/apps/noclook/schema/core.py | 58 +++++++++++++++++++++------ 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 17c5e3ead..2bff5c8b4 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -685,6 +685,8 @@ def get_filter_input_fields(cls): for a, b in field_of_type.get_filter_input_fields().items(): filter_attrib[a] = b() + filter_attrib['_of_type'] = field._of_type + binput_field = type('{}InputField'.format(name_fot), (graphene.InputObjectType, ), filter_attrib) input_fields[name] = binput_field, field._of_type @@ -842,15 +844,44 @@ def build_filter_query(cls, filter, nodetype): and_filters = filter.get('AND', []) and_predicates = [] + # additional clauses + match_additional_nodes = [] + match_additional_rels = [] + + # embed entity index + rel_index, node_index = 1, 1 + # iterate through the nested filters for and_filter in and_filters: # iterate though values of a nested filter for filter_key, filter_value in and_filter.items(): # choose filter array for query building + filter_array, queryBuilder = None, None + if isinstance(filter_value, int) or isinstance(filter_value, str): filter_array = ScalarQueryBuilder.filter_array + queryBuilder = ScalarQueryBuilder else: + # set of type + 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 issubclass(of_type, NIObjectType): + additional_clause.format(node_index) + node_index = node_index + 1 + match_additional_nodes.append(additional_clause) + elif issubclass(of_type, NIRelationType): + additional_clause.format(rel_index) + rel_index = rel_index + 1 + match_additional_rels.append(additional_clause) filter_field = cls.filter_names[filter_key] field = filter_field['field'] @@ -861,12 +892,13 @@ def build_filter_query(cls, filter, nodetype): # the predicate building function for fa_suffix, fa_value in filter_array.items(): if fa_suffix != '': - fa_suffix = '_{}'.format(fa_suffix) + fa_suffix = '_{}'.format(fa_suffix) # get the predicate if suffix == fa_suffix: - build_preficate_func = fa_value['qpredicate'] - predicate = build_preficate_func(field, filter_value, field_type) + build_predicate_func = fa_value['qpredicate'] + predicate = build_predicate_func(field, filter_value, field_type) + if predicate: and_predicates.append(predicate) @@ -915,25 +947,27 @@ def build_filter_query(cls, filter, nodetype): if build_query != '': build_query = 'WHERE {}'.format(build_query) - # additional clauses - match_additional_clauses = [] + # remove redundant additional clauses + match_additional_nodes = list(set(match_additional_nodes)) + match_1 = ', '.join(match_additional_nodes) + + match_additional_rels = list(set(match_additional_rels)) + match_2 = ', '.join(match_additional_rels) - # remove redundant - match_additional_clauses = list(set(match_additional_clauses)) - match_additional = ', '.join(match_additional_clauses) + node_match_clause = "(n:{label})".format(label=nodetype) q = """ - MATCH (n:{label}){match_additional} + MATCH {node_match_clause}{match_additional} {build_query} RETURN distinct n - """.format(label=nodetype, match_additional="", - build_query=build_query) + """.format(node_match_clause=node_match_clause, + match_additional="", build_query=build_query) return q @classproperty def match_additional_clause(cls): - return "[m{}:{}]".format('{}',cls.NIMetaType.ni_type) + return "{}-[]-(m{}:{})".format('{}',cls.NIMetaType.ni_type) class Meta: model = NodeHandle From 8c7835fbc2137624ea0e71031b051c505b654b91 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 28 Jun 2019 15:04:47 +0200 Subject: [PATCH 139/520] Filter subquery wip, first functional version --- src/niweb/apps/noclook/schema/core.py | 132 +++++++++++++++++-------- src/niweb/apps/noclook/schema/query.py | 2 - src/niweb/apps/noclook/schema/types.py | 12 +-- 3 files changed, 92 insertions(+), 54 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 2bff5c8b4..55cb1ab6a 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -208,7 +208,7 @@ def filter_array(cls): class ScalarQueryBuilder(AbstractQueryBuilder): @staticmethod - def build_match_predicate(field, value, type): + def build_match_predicate(field, value, type, **kwargs): # string quoting if isinstance(type, graphene.String): value = "'{}'".format(value) @@ -218,7 +218,7 @@ def build_match_predicate(field, value, type): return ret @staticmethod - def build_not_predicate(field, value, type): + def build_not_predicate(field, value, type, **kwargs): # string quoting if isinstance(type, graphene.String): value = "'{}'".format(value) @@ -228,20 +228,19 @@ def build_not_predicate(field, value, type): return ret @staticmethod - def build_in_predicate(field, values, type): # a list predicate builder - #raise Exception(values) + def build_in_predicate(field, values, type, **kwargs): # a list predicate builder in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) ret = 'n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) return ret @staticmethod - def build_not_in_predicate(field, values, type): # a list predicate builder + def build_not_in_predicate(field, values, type, **kwargs): # a list predicate builder in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) ret = 'NOT n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) return ret @staticmethod - def build_lt_predicate(field, value, type): + def build_lt_predicate(field, value, type, **kwargs): # string quoting if isinstance(type, graphene.String): value = "'{}'".format(value) @@ -251,7 +250,7 @@ def build_lt_predicate(field, value, type): return ret @staticmethod - def build_lte_predicate(field, value, type): + def build_lte_predicate(field, value, type, **kwargs): # string quoting if isinstance(type, graphene.String): value = "'{}'".format(value) @@ -261,7 +260,7 @@ def build_lte_predicate(field, value, type): return ret @staticmethod - def build_gt_predicate(field, value, type): + def build_gt_predicate(field, value, type, **kwargs): # string quoting if isinstance(type, graphene.String): value = "'{}'".format(value) @@ -271,7 +270,7 @@ def build_gt_predicate(field, value, type): return ret @staticmethod - def build_gte_predicate(field, value, type): + def build_gte_predicate(field, value, type, **kwargs): # string quoting if isinstance(type, graphene.String): value = "'{}'".format(value) @@ -281,49 +280,51 @@ def build_gte_predicate(field, value, type): return ret @staticmethod - def build_contains_predicate(field, value, type): + def build_contains_predicate(field, value, type, **kwargs): return """n.{field} CONTAINS '{value}'""".format(field=field, value=value) @staticmethod - def build_not_contains_predicate(field, value, type): + def build_not_contains_predicate(field, value, type, **kwargs): return """NOT n.{field} CONTAINS '{value}'""".format(field=field, value=value) @staticmethod - def build_starts_with_predicate(field, value, type): + def build_starts_with_predicate(field, value, type, **kwargs): return """n.{field} STARTS WITH '{value}'""".format(field=field, value=value) @staticmethod - def build_not_starts_with_predicate(field, value, type): + def build_not_starts_with_predicate(field, value, type, **kwargs): return """NOT n.{field} STARTS WITH '{value}'""".format(field=field, value=value) @staticmethod - def build_ends_with_predicate(field, value, type): + def build_ends_with_predicate(field, value, type, **kwargs): return """n.{field} ENDS WITH '{value}'""".format(field=field, value=value) @staticmethod - def build_not_ends_with_predicate(field, value, type): + def build_not_ends_with_predicate(field, value, type, **kwargs): return """NOT n.{field} ENDS WITH '{value}'""".format(field=field, value=value) class InputFieldQueryBuilder(AbstractQueryBuilder): @classmethod - def build_match_predicate(cls, field, value, type): + def build_match_predicate(cls, field, value, type, **kwargs): pass @classmethod - def build_in_predicate(cls, field, values, type): # a list predicate builder - subqueries = [] - for element in values: - for name, field in element.__dict__: - pass + def build_in_predicate(cls, field, values, type, **kwargs): # a list predicate builder + neo4j_var = kwargs.get('neo4j_var') - in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) - ret = 'n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) - return ret + ret = [] + for value in values: + for k, v in value.items(): + if isinstance(v, str): + v = "'{}'".format(v) + + ret.append('{}.{} = {}'.format(neo4j_var, k, v)) + + return '({})'.format(' OR '.join(ret)) @classmethod - def build_not_in_predicate(cls, field, values, type): # a list predicate builder - in_string = '{}'.format(', '.join(["'{}'".format(str(x)) for x in values])) - ret = 'NOT n.{field} IN [{in_string}]'.format(field=field, in_string=in_string) + def build_not_in_predicate(cls, field, values, type, **kwargs): # a list predicate builder + ret = '' return ret ########## END CONNECTION FILTER BUILD FUNCTIONS @@ -462,7 +463,6 @@ def __init_subclass_with_meta__( def resolve_relation_id(self, info, **kwargs): self.relation_id = self.id - self.id = None return self.relation_id @@ -479,6 +479,12 @@ def resolve_nidata(self, info, **kwargs): return ret + def resolve_start_node(self, info, **kwargs): + return NodeHandle.objects.get(handle_id=self.start) + + def resolve_end_node(self, info, **kwargs): + return NodeHandle.objects.get(handle_id=self.end) + @classmethod def get_filter_input_fields(cls): ''' @@ -519,7 +525,17 @@ def get_filter_input_fields(cls): @classproperty def match_additional_clause(cls): - return "[r{}:{}]".format('{}',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, ) @@ -848,8 +864,17 @@ def build_filter_query(cls, filter, nodetype): match_additional_nodes = [] match_additional_rels = [] + and_node_predicates = [] + and_rels_predicates = [] + # embed entity index - rel_index, node_index = 1, 1 + rel_idx, node_idx, subnode_idx, subrel_idx = 1, 1, 1, 1 + idxdict = { + 'rel_idx': 1, + 'node_idx': 1, + 'subnode_idx': 1, + 'subrel_idx': 1, + } # iterate through the nested filters for and_filter in and_filters: @@ -857,13 +882,19 @@ def build_filter_query(cls, filter, nodetype): for filter_key, filter_value in and_filter.items(): # choose filter array for query building filter_array, queryBuilder = None, None + is_nested_query = False + neo4j_var = '' if isinstance(filter_value, int) or isinstance(filter_value, str): filter_array = ScalarQueryBuilder.filter_array queryBuilder = ScalarQueryBuilder - else: + elif isinstance(filter_value, list) and not (\ + isinstance(filter_value[0], str) or isinstance(filter_value[0], int)\ + ): # set of type + is_nested_query = True of_type = None + if isinstance(filter_value, list): of_type = filter_value[0]._of_type else: @@ -874,14 +905,20 @@ def build_filter_query(cls, filter, nodetype): additional_clause = of_type.match_additional_clause if additional_clause: + # format var name and additional match if issubclass(of_type, NIObjectType): - additional_clause.format(node_index) - node_index = node_index + 1 + #additional_clause.format(node_index) + idxdict['node_idx'] = idxdict['node_idx'] + 1 match_additional_nodes.append(additional_clause) elif issubclass(of_type, NIRelationType): - additional_clause.format(rel_index) - rel_index = rel_index + 1 + 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) + else: + filter_array = ScalarQueryBuilder.filter_array + queryBuilder = ScalarQueryBuilder filter_field = cls.filter_names[filter_key] field = filter_field['field'] @@ -897,7 +934,8 @@ def build_filter_query(cls, filter, nodetype): # get the predicate if suffix == fa_suffix: build_predicate_func = fa_value['qpredicate'] - predicate = build_predicate_func(field, filter_value, field_type) + + predicate = build_predicate_func(field, filter_value, field_type, neo4j_var=neo4j_var) if predicate: and_predicates.append(predicate) @@ -949,29 +987,37 @@ def build_filter_query(cls, filter, nodetype): # remove redundant additional clauses match_additional_nodes = list(set(match_additional_nodes)) - match_1 = ', '.join(match_additional_nodes) - match_additional_rels = list(set(match_additional_rels)) - match_2 = ', '.join(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) q = """ - MATCH {node_match_clause}{match_additional} + MATCH {node_match_clause} {build_query} RETURN distinct n - """.format(node_match_clause=node_match_clause, - match_additional="", build_query=build_query) + """.format(node_match_clause=node_match_clause, build_query=build_query) return q @classproperty def match_additional_clause(cls): - return "{}-[]-(m{}:{})".format('{}',cls.NIMetaType.ni_type) + return "{}-[]-(m{}:{})".format('{}', cls.neo4j_var_name, '{}', + cls.NIMetaType.ni_type) + + neo4j_var_name = "m" class Meta: model = NodeHandle interfaces = (relay.Node, ) + +## add start and end fields +setattr(NIRelationType, 'start_node', graphene.Field(NIObjectType)) +setattr(NIRelationType, 'end_node', graphene.Field(NIObjectType)) ########## END RELATION AND NODE TYPES diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 886869bee..ef5451416 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -58,7 +58,6 @@ def resolve_getRelationById(self, info, **kwargs): relation_id = kwargs.get('relation_id') rel = nc.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) rel.relation_id = rel.id - rel.id = None return rel @@ -66,7 +65,6 @@ def resolve_getRoleById(self, info, **kwargs): relation_id = kwargs.get('relation_id') rel = nc.models.RoleRelationship.get_relationship_model(nc.graphdb.manager, relationship_id=relation_id) rel.relation_id = rel.id - rel.id = None return rel diff --git a/src/niweb/apps/noclook/schema/types.py b/src/niweb/apps/noclook/schema/types.py index c1b2ca9cf..01fbc5565 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -81,17 +81,11 @@ class NIMetaType: ni_metatype = NIMETA_RELATION class Role(NIRelationType): - name = graphene.String(required=True) - organization = graphene.Field(Organization) + name = graphene.String() + end_node = graphene.Field(Organization) def resolve_name(self, info, **kwargs): - if self.name: - return self.name - else: - raise Exception('This must not be a role relationship') - - def resolve_organization(self, info, **kwargs): - return NodeHandle.objects.get(handle_id=self.end) + return getattr(self, 'name', None) class NIMetaType: nimodel = RoleRelationship From 9a0c371b3d98f4ecad683e9cc7b86db12753dd29 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 1 Jul 2019 15:00:38 +0200 Subject: [PATCH 140/520] WIP: Added filter functions for nested entities, a test is required --- src/niweb/apps/noclook/schema/core.py | 168 ++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 9 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 55cb1ab6a..61d45603c 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -304,29 +304,179 @@ def build_not_ends_with_predicate(field, value, type, **kwargs): return """NOT n.{field} ENDS WITH '{value}'""".format(field=field, value=value) class InputFieldQueryBuilder(AbstractQueryBuilder): + standard_expression = """{neo4j_var}.{field} {op} {value}""" + id_expression = """ID({neo4j_var}) {op} {value}""" + @classmethod - def build_match_predicate(cls, field, value, type, **kwargs): - pass + def format_expression(cls, key, value, neo4j_var, op, add_quotes=True): + # string quoting + if isinstance(value, str) and add_quotes: + value = "'{}'".format(value) + + if key is 'relation_id': + ret = cls.id_expression.format( + neo4j_var=neo4j_var, + op=op, + value=value, + ) + else: + ret = cls.standard_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 build_in_predicate(cls, field, values, type, **kwargs): # a list predicate builder + def multiple_value_predicate(cls, field, values, type, op, not_in=False, **kwargs): # a list predicate builder neo4j_var = kwargs.get('neo4j_var') - ret = [] + all_values = [] + field_name = "" + for value in values: for k, v in value.items(): if isinstance(v, str): v = "'{}'".format(v) - ret.append('{}.{} = {}'.format(neo4j_var, k, v)) + field_name = k + all_values.append(v) + + the_value = "[{}]".format(', '.join([str(x) for x in all_values])) - return '({})'.format(' OR '.join(ret)) + ret = InputFieldQueryBuilder.format_expression(field_name, the_value, neo4j_var, op, False) + + 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 - ret = '' + 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 build_contains_predicate(field, value, type): + op = "CONTAINS" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_not_contains_predicate(field, value, type): + op = "CONTAINS" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, True, **kwargs) + + return ret + + @staticmethod + def build_starts_with_predicate(field, value, type): + op = "STARTS WITH" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_not_starts_with_predicate(field, value, type): + op = "STARTS WITH" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, True, **kwargs) + + return ret + + @staticmethod + def build_ends_with_predicate(field, value, type): + op = "ENDS WITH" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, **kwargs) + + return ret + + @staticmethod + def build_not_ends_with_predicate(field, value, type): + op = "ENDS WITH" + ret = InputFieldQueryBuilder.single_value_predicate(field, value, type, + op, True, **kwargs) + + return ret ########## END CONNECTION FILTER BUILD FUNCTIONS ########## KEYVALUE TYPES @@ -889,8 +1039,8 @@ def build_filter_query(cls, filter, nodetype): 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)\ - ): + 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 From f4f3cd8cf763dc553f822615ce841d3119d724f0 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 2 Jul 2019 12:42:23 +0200 Subject: [PATCH 141/520] Unification of AND et OR filters --- src/niweb/apps/noclook/schema/core.py | 178 +++++++++++++------------- 1 file changed, 90 insertions(+), 88 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 61d45603c..1881ba2ff 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -1010,6 +1010,10 @@ def build_filter_query(cls, filter, nodetype): and_filters = filter.get('AND', []) and_predicates = [] + # build OR block + or_filters = filter.get('OR', []) + or_predicates = [] + # additional clauses match_additional_nodes = [] match_additional_rels = [] @@ -1018,7 +1022,6 @@ def build_filter_query(cls, filter, nodetype): and_rels_predicates = [] # embed entity index - rel_idx, node_idx, subnode_idx, subrel_idx = 1, 1, 1, 1 idxdict = { 'rel_idx': 1, 'node_idx': 1, @@ -1026,100 +1029,99 @@ def build_filter_query(cls, filter, nodetype): 'subrel_idx': 1, } - # iterate through the nested filters - for and_filter in and_filters: - # iterate though values of a nested filter - for filter_key, filter_value in and_filter.items(): - # choose filter array for query building - filter_array, queryBuilder = None, None - is_nested_query = False - neo4j_var = '' - - 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 + operations = { + 'AND': { + 'filters': filter.get('AND', []), + 'predicates': [], + }, + 'OR': { + 'filters': filter.get('OR', []), + 'predicates': [], + }, + } + + 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 = '' + + 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: + # format var name and additional match + if issubclass(of_type, NIObjectType): + neo4j_var = '{}{}'.format(of_type.neo4j_var_name, idxdict['node_idx']) + 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) else: - of_type = filter_value._of_type - - filter_array = InputFieldQueryBuilder.filter_array - queryBuilder = InputFieldQueryBuilder - additional_clause = of_type.match_additional_clause - - if additional_clause: - # format var name and additional match - if issubclass(of_type, NIObjectType): - #additional_clause.format(node_index) - idxdict['node_idx'] = idxdict['node_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) - else: - filter_array = ScalarQueryBuilder.filter_array - queryBuilder = ScalarQueryBuilder + filter_array = ScalarQueryBuilder.filter_array + queryBuilder = ScalarQueryBuilder - filter_field = cls.filter_names[filter_key] - field = filter_field['field'] - suffix = filter_field['suffix'] - field_type = filter_field['field_type'] + filter_field = cls.filter_names[filter_key] + field = filter_field['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(): - if fa_suffix != '': - fa_suffix = '_{}'.format(fa_suffix) + # iterate through the keys of the filter array and extracts + # the predicate building function + for fa_suffix, fa_value in filter_array.items(): + if fa_suffix != '': + fa_suffix = '_{}'.format(fa_suffix) - # get the predicate - if suffix == fa_suffix: - build_predicate_func = fa_value['qpredicate'] + # 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) + predicate = build_predicate_func(field, filter_value, field_type, neo4j_var=neo4j_var) - if predicate: - and_predicates.append(predicate) + if predicate: + predicates.append(predicate) - # build OR block - or_filters = filter.get('OR', []) - or_predicates = [] + operations[operation]['predicates'] = predicates - for or_filter in or_filters: - # iterate though values of a nested filter - for filter_key, filter_value in or_filter.items(): - # choose filter array for query building - filter_array = ScalarQueryBuilder.filter_array - - filter_field = cls.filter_names[filter_key] - field = filter_field['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(): - if fa_suffix != '': - fa_suffix = '_{}'.format(fa_suffix) - - # get the predicate - if suffix == fa_suffix: - build_preficate_func = fa_value['qpredicate'] - predicate = build_preficate_func(field, filter_value, field_type) - if predicate: - or_predicates.append(predicate) - - and_query = ' AND '.join(and_predicates) - or_query = ' OR '.join(or_predicates) + and_query = ' AND '.join(operations['AND']['predicates']) + or_query = ' OR '.join(operations['OR']['predicates']) if and_query and or_query: build_query = '{} OR {}'.format( @@ -1156,7 +1158,7 @@ def build_filter_query(cls, filter, nodetype): @classproperty def match_additional_clause(cls): - return "{}-[]-(m{}:{})".format('{}', cls.neo4j_var_name, '{}', + return "({})-[{}]-({}{}:{})".format('{}', '{}', cls.neo4j_var_name, '{}', cls.NIMetaType.ni_type) neo4j_var_name = "m" From 4f0bea7355b097106fb5a45fd2b9f9f0056a61e7 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 2 Jul 2019 13:05:47 +0200 Subject: [PATCH 142/520] Subentity filtering testing --- .../apps/noclook/tests/schema/test_schema.py | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 9cd805d79..7b5578d21 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -60,6 +60,144 @@ def test_get_contacts(self): assert not result.errors, pformat(result.errors, indent=1) assert result.data == expected, pformat(result.data, indent=1) + # subquery test + query = ''' + query { + contacts(filter: {AND: [ + { + member_of_groups: { name: "group2" }, + roles: { name: "role2"} + } + ]}){ + edges{ + node{ + handle_id + name + roles{ + name + } + member_of_groups{ + name + handle_id + } + } + } + } + } + ''' + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('handle_id', '29'), + ('name', 'John Smith'), + ('roles', + [OrderedDict([('name', + 'role2')])]), + ('member_of_groups', + [OrderedDict([('name', + 'group2'), + ('handle_id', + '33')])])]))])])]))]) + + + 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{ + handle_id + name + member_of_groups{ + name + } + roles{ + name + } + } + } + } + } + ''' + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('handle_id', '29'), + ('name', 'John Smith'), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])]), + ('roles', + [OrderedDict([('name', + 'role2')])])]))]), + OrderedDict([('node', + OrderedDict([('handle_id', '28'), + ('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: { handle_id: 33 }, + roles: { relation_id: 20} + } + ]}){ + edges{ + node{ + handle_id + name + roles{ + name + } + member_of_groups{ + name + handle_id + } + } + } + } + } + ''' + expected = OrderedDict([('contacts', + OrderedDict([('edges', + [OrderedDict([('node', + OrderedDict([('handle_id', '29'), + ('name', 'John Smith'), + ('roles', + [OrderedDict([('name', + 'role2')])]), + ('member_of_groups', + [OrderedDict([('name', + 'group2'), + ('handle_id', + '33')])])]))])])]))]) + + + 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) + # getNodeById query = ''' query { @@ -69,8 +207,13 @@ def test_get_contacts(self): } ''' + expected = OrderedDict([ + ('getNodeById', OrderedDict([('handle_id', '29')])) + ]) + 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 = ''' From 77de20249115c5ef377ab9d138abc0b2259d58b3 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 3 Jul 2019 14:35:55 +0200 Subject: [PATCH 143/520] Tests extended with nested filters and ChoiceScalar --- src/niweb/apps/noclook/schema/core.py | 30 +++- src/niweb/apps/noclook/schema/mutations.py | 4 +- .../noclook/tests/schema/test_mutations.py | 138 ++++++++++++++++++ .../apps/noclook/tests/schema/test_schema.py | 56 +++---- 4 files changed, 190 insertions(+), 38 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 1881ba2ff..f01b372a0 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -17,6 +17,7 @@ from graphene_django import DjangoObjectType from graphene_django.types import DjangoObjectTypeOptions from graphql import GraphQLError +from graphql.language.ast import IntValue from io import StringIO from norduniclient.exceptions import UniqueNodeError, NoRelationshipPossible @@ -120,6 +121,33 @@ def get_roles_dropdown(): return ret +class ChoiceScalar(Scalar): + @staticmethod + def coerce_choice(value): + num = None + try: + num = int(value) + except ValueError: + try: + num = int(float(value)) + except ValueError: + return None + 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) + else: + return graphene.String.parse_literal(ast) + ########## END CUSTOM SCALARS FOR FORM FIELD CONVERSION ########## CONNECTION FILTER BUILD FUNCTIONS @@ -1314,7 +1342,7 @@ def form_to_graphene_field(cls, form_field, include=None, exclude=None): elif isinstance(form_field, forms.CharField): graphene_field = graphene.String(**graph_kwargs) elif isinstance(form_field, forms.ChoiceField): - graphene_field = graphene.String(**graph_kwargs) + graphene_field = ChoiceScalar(**graph_kwargs) elif isinstance(form_field, forms.FloatField): graphene_field = graphene.Float(**graph_kwargs) elif isinstance(form_field, forms.IntegerField): diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index f15aa0836..b493efc84 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -45,6 +45,8 @@ class NIMetaClass: 'relationship_works_for': process_works_for } + update_exclude = ('relationship_member_of') + class Meta: abstract = False @@ -89,7 +91,7 @@ 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() - + create_procedure = NIProcedureMutationFactory.get_create_mutation().Field() update_procedure = NIProcedureMutationFactory.get_update_mutation().Field() delete_procedure = NIProcedureMutationFactory.get_delete_mutation().Field() diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index d3cf52784..327cde07e 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -8,6 +8,7 @@ class QueryTest(Neo4jGraphQLTest): def test_group(self): + ### Simple entity ### ## create ## query = ''' mutation create_test_group { @@ -90,3 +91,140 @@ def test_group(self): result = schema.execute(query, context=self.context) assert not result.errors, pformat(result.errors, indent=1) assert result.data == expected + + ### Composite entities ### + ## create ## + query = """ + mutation create_test_contact { + create_contact( + input: { + first_name: "Jane" + last_name: "Smith" + title: "" + salutation: "Ms" + contact_type: "person" + phone: " 823-971-5606" + mobile: "617-372-0822" + email: "jsmith@mashable.com" + other_email: "jsmith1@mashable.com" + } + ){ + contact{ + handle_id + name + first_name + last_name + title + salutation + contact_type + phone + mobile + email + other_email + } + } + } + """ + + expected = OrderedDict([('create_contact', + OrderedDict([('contact', + OrderedDict([('handle_id', '18'), + ('name', 'Jane Smith'), + ('first_name', 'Jane'), + ('last_name', 'Smith'), + ('title', None), + ('salutation', 'Ms'), + ('contact_type', 'person'), + ('phone', '823-971-5606'), + ('mobile', '617-372-0822'), + ('email', 'jsmith@mashable.com'), + ('other_email', + 'jsmith1@mashable.com')]))]))]) + + 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) + + ## update ## + query = """ + mutation update_test_contact { + update_contact( + input: { + handle_id: 18 + first_name: "Janet" + last_name: "Doe" + contact_type: "person" + relationship_works_for: 10 + role_name: "IT-manager" + } + ){ + contact{ + handle_id + name + first_name + last_name + title + salutation + contact_type + phone + mobile + email + other_email + roles{ + name + end_node{ + handle_id + name + } + } + } + } + } + """ + + expected = OrderedDict([('update_contact', + OrderedDict([('contact', + OrderedDict([('handle_id', '18'), + ('name', 'Janet Doe'), + ('first_name', 'Janet'), + ('last_name', 'Doe'), + ('title', None), + ('salutation', None), + ('contact_type', 'person'), + ('phone', None), + ('mobile', None), + ('email', None), + ('other_email', None), + ('roles', + [OrderedDict([ + ('name', 'IT-manager'), + ('end_node', + OrderedDict([('handle_id', + '10'), + ('name', + 'organization2')]))])])]))]))]) + + 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_contact { + delete_contact(input: {handle_id: 18}){ + success + } + } + """ + + 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) diff --git a/src/niweb/apps/noclook/tests/schema/test_schema.py b/src/niweb/apps/noclook/tests/schema/test_schema.py index 7b5578d21..43ba0ee2c 100644 --- a/src/niweb/apps/noclook/tests/schema/test_schema.py +++ b/src/niweb/apps/noclook/tests/schema/test_schema.py @@ -14,7 +14,6 @@ def test_get_contacts(self): contacts(first: 2, orderBy: handle_id_DESC) { edges { node { - handle_id name first_name last_name @@ -33,8 +32,7 @@ def test_get_contacts(self): expected = OrderedDict([('contacts', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '29'), - ('name', 'John Smith'), + OrderedDict([('name', 'John Smith'), ('first_name', 'John'), ('last_name', 'Smith'), ('member_of_groups', @@ -44,8 +42,7 @@ def test_get_contacts(self): [OrderedDict([('name', 'role2')])])]))]), OrderedDict([('node', - OrderedDict([('handle_id', '28'), - ('name', 'Jane Doe'), + OrderedDict([('name', 'Jane Doe'), ('first_name', 'Jane'), ('last_name', 'Doe'), ('member_of_groups', @@ -71,7 +68,6 @@ def test_get_contacts(self): ]}){ edges{ node{ - handle_id name roles{ name @@ -88,8 +84,7 @@ def test_get_contacts(self): expected = OrderedDict([('contacts', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '29'), - ('name', 'John Smith'), + OrderedDict([('name', 'John Smith'), ('roles', [OrderedDict([('name', 'role2')])]), @@ -97,7 +92,7 @@ def test_get_contacts(self): [OrderedDict([('name', 'group2'), ('handle_id', - '33')])])]))])])]))]) + '34')])])]))])])]))]) result = schema.execute(query, context=self.context) @@ -115,7 +110,6 @@ def test_get_contacts(self): ]}){ edges{ node{ - handle_id name member_of_groups{ name @@ -131,8 +125,7 @@ def test_get_contacts(self): expected = OrderedDict([('contacts', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '29'), - ('name', 'John Smith'), + OrderedDict([('name', 'John Smith'), ('member_of_groups', [OrderedDict([('name', 'group2')])]), @@ -140,8 +133,7 @@ def test_get_contacts(self): [OrderedDict([('name', 'role2')])])]))]), OrderedDict([('node', - OrderedDict([('handle_id', '28'), - ('name', 'Jane Doe'), + OrderedDict([('name', 'Jane Doe'), ('member_of_groups', [OrderedDict([('name', 'group1')])]), @@ -158,8 +150,8 @@ def test_get_contacts(self): query { contacts(filter: {AND: [ { - member_of_groups: { handle_id: 33 }, - roles: { relation_id: 20} + member_of_groups: { name: "group2" }, + roles: { name: "role2" } } ]}){ edges{ @@ -181,7 +173,7 @@ def test_get_contacts(self): expected = OrderedDict([('contacts', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '29'), + OrderedDict([('handle_id', '30'), ('name', 'John Smith'), ('roles', [OrderedDict([('name', @@ -190,7 +182,7 @@ def test_get_contacts(self): [OrderedDict([('name', 'group2'), ('handle_id', - '33')])])]))])])]))]) + '34')])])]))])])]))]) result = schema.execute(query, context=self.context) @@ -201,14 +193,14 @@ def test_get_contacts(self): # getNodeById query = ''' query { - getNodeById(handle_id: 29){ + getNodeById(handle_id: 30){ handle_id } } ''' expected = OrderedDict([ - ('getNodeById', OrderedDict([('handle_id', '29')])) + ('getNodeById', OrderedDict([('handle_id', '30')])) ]) result = schema.execute(query, context=self.context) @@ -226,7 +218,6 @@ def test_get_contacts(self): }, orderBy: handle_id_ASC){ edges{ node{ - handle_id name } } @@ -236,8 +227,7 @@ def test_get_contacts(self): expected = OrderedDict([('groups', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '32'), - ('name', + OrderedDict([('name', 'group1')] ))])] )])) @@ -253,7 +243,6 @@ def test_get_contacts(self): { groups(first: 10, filter:{ OR:[{ - name: "group1", name_in: ["group1", "group2"] },{ name: "group2", @@ -261,7 +250,6 @@ def test_get_contacts(self): }, orderBy: handle_id_ASC){ edges{ node{ - handle_id name } } @@ -271,11 +259,9 @@ def test_get_contacts(self): expected = OrderedDict([('groups', OrderedDict([('edges', [OrderedDict([('node', - OrderedDict([('handle_id', '32'), - ('name', 'group1')]))]), + OrderedDict([('name', 'group1')]))]), OrderedDict([('node', - OrderedDict([('handle_id', '33'), - ('name', + OrderedDict([('name', 'group2')]))])])]))]) result = schema.execute(query, context=self.context) @@ -283,19 +269,17 @@ def test_get_contacts(self): assert not result.errors, pformat(result.errors, indent=1) assert result.data == expected, pformat(result.data, indent=1) - def test_getnodebyhandle_id(self): + def test_dropdown(self): query = ''' - query { - getNodeById(handle_id: 1){ - handle_id - } + { + getAvailableDropdowns } ''' result = schema.execute(query, context=self.context) - #assert not result.errors, result.errors + assert not result.errors, pformat(result.errors, indent=1) + assert 'contact_type' in result.data['getAvailableDropdowns'], pformat(result.data, indent=1) - def test_dropdown(self): query = ''' query{ getChoicesForDropdown(name:"contact_type"){ From 14c9e8bd1f703257e2a4448a57bbbae4afe86b1a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 4 Jul 2019 12:41:13 +0200 Subject: [PATCH 144/520] New relation processor, tests and slight change in by id query --- src/niweb/apps/noclook/schema/core.py | 6 ++++- src/niweb/apps/noclook/schema/mutations.py | 9 +++++--- .../noclook/tests/schema/test_mutations.py | 22 ++++++++++++------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index f01b372a0..f2fa89a41 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -972,7 +972,11 @@ def generic_byid_resolver(self, info, **args): if info.context and info.context.user.is_authenticated: if handle_id: - ret = NodeHandle.objects.filter(node_type=node_type).get(handle_id=handle_id) + 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') diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index b493efc84..acd462461 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -35,6 +35,10 @@ def process_works_for(request, form, nodehandler, relation_name): role_name = form.cleaned_data['role_name'] helpers.set_works_for(request.user, nodehandler, organization_nh.handle_id, role_name) +def process_member_of(request, form, nodehandler, relation_name): + group_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) + helpers.set_member_of(request.user, nodehandler, group_nh.handle_id) + class NIContactMutationFactory(NIMutationFactory): class NIMetaClass: create_form = NewContactForm @@ -42,11 +46,10 @@ class NIMetaClass: request_path = '/' graphql_type = Contact relations_processors = { - 'relationship_works_for': process_works_for + 'relationship_works_for': process_works_for, + 'relationship_member_of': process_member_of, } - update_exclude = ('relationship_member_of') - class Meta: abstract = False diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index 327cde07e..d0fef699a 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -74,7 +74,7 @@ def test_group(self): ## delete ## query = """ mutation delete_test_group { - delete_group(input: {handle_id: 9}){ + delete_group(input: {handle_id: 17}){ success } } @@ -156,6 +156,7 @@ def test_group(self): contact_type: "person" relationship_works_for: 10 role_name: "IT-manager" + relationship_member_of: 16 } ){ contact{ @@ -177,6 +178,9 @@ def test_group(self): name } } + member_of_groups{ + name + } } } } @@ -196,13 +200,15 @@ def test_group(self): ('email', None), ('other_email', None), ('roles', - [OrderedDict([ - ('name', 'IT-manager'), - ('end_node', - OrderedDict([('handle_id', - '10'), - ('name', - 'organization2')]))])])]))]))]) + [OrderedDict([('name', 'IT-manager'), + ('end_node', + OrderedDict([('handle_id', + '10'), + ('name', + 'organization2')]))])]), + ('member_of_groups', + [OrderedDict([('name', + 'group2')])])]))]))]) result = schema.execute(query, context=self.context) assert not result.errors, pformat(result.errors, indent=1) From 4ffd745d327d2d92c737561cabc7885ae0804ef7 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 4 Jul 2019 14:31:29 +0200 Subject: [PATCH 145/520] Added specific Organization mutation --- src/niweb/apps/noclook/schema/mutations.py | 65 ++++++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index acd462461..54a96e2a5 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -6,6 +6,7 @@ from apps.noclook import activitylog, helpers from apps.noclook.forms import * +from apps.noclook.models import Dropdown as DropdownModel from .core import NIMutationFactory, CreateNIMutation from .types import * @@ -53,6 +54,10 @@ class NIMetaClass: class Meta: abstract = False +def process_abuse_contact(request, form, nodehandler, relation_name): + group_nh = NodeHandle.objects.get(pk=form.cleaned_data[relation_name]) + helpers.set_member_of(request.user, nodehandler, group_nh.handle_id) + class NIOrganizationMutationFactory(NIMutationFactory): class NIMetaClass: create_form = NewOrganizationForm @@ -60,14 +65,64 @@ class NIMetaClass: request_path = '/' graphql_type = Organization # create_include or create_exclude - # update_include or update_exclude - update_exclude = ('abuse_contact', 'primary_contact', - 'secondary_contact', 'it_technical_contact', - 'it_security_contact', 'it_manager_contact') class Meta: abstract = False +class UpdateNIOrganizationMutation(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() + handle_id = request.POST.get('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 = form_class(request.POST.copy()) + 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', 'customer_id', 'type', 'additional_info', + ] + helpers.form_update_node(request.user, organization.handle_id, form, property_keys) + # Set contacts + contact_fields = DropdownModel.get('organization_contact_types').as_choices(empty=False) + for field in contact_fields: + if field[0] in form.cleaned_data: + contact_data = form.cleaned_data[field[0]] + if contact_data: + if isinstance(contact_data, six.string_types): + if contact_data: + helpers.create_contact_role_for_organization(request.user, organization, contact_data, field[1]) + else: + helpers.link_contact_role_for_organization(request.user, organization, contact_data, field[1]) + + # 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) + + return { graphql_type.__name__.lower(): nh } + else: + # get the errors and return them + raise GraphQLError('Form errors: {}'.format(form.errors)) + + class NIMetaClass: + django_form = EditOrganizationForm + request_path = '/' + graphql_type = Organization + class DeleteRelationship(relay.ClientIDMutation): class Input: relation_id = graphene.Int(required=True) @@ -104,7 +159,7 @@ class NOCRootMutation(graphene.ObjectType): delete_contact = NIContactMutationFactory.get_delete_mutation().Field() create_organization = NIOrganizationMutationFactory.get_create_mutation().Field() - update_organization = NIOrganizationMutationFactory.get_update_mutation().Field() + update_organization = UpdateNIOrganizationMutation.Field() delete_organization = NIOrganizationMutationFactory.get_delete_mutation().Field() delete_relationship = DeleteRelationship.Field() From ce3e9c9c325895e1fecab5a8d9d6c991cf1c004e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Jul 2019 14:49:16 +0200 Subject: [PATCH 146/520] Fix: Display only unique organizations for a contact in the list --- src/niweb/apps/noclook/views/list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index 04b0e5626..d2673351a 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -603,7 +603,7 @@ def list_contacts(request): q = """ MATCH (con:Contact) OPTIONAL MATCH (con)-[:Works_for]->(org:Organization) - RETURN con.handle_id AS con_handle_id, con, org + RETURN con.handle_id AS con_handle_id, con, count(DISTINCT org), org """ con_list = nc.query_to_list(nc.graphdb.manager, q) From 77362e41129a361d533546d8149b5c4977dfdd3b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 15 Jul 2019 15:05:53 +0200 Subject: [PATCH 147/520] Fix: Show parent organization in org list, and slight ui/ux improvement --- .../edit/includes/parent_of_group.html | 4 ++-- src/niweb/apps/noclook/views/list.py | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) 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 index be9dde55b..3f03e7a8b 100644 --- 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 @@ -1,7 +1,7 @@ {% load noclook_tags %} {% load crispy_forms_tags %} {% blockvar organization_title %} - {{ node_handle.node_type }} Parent Organization (optional) + Edit organization hierarchy (optional) {% endblockvar %} {% accordion organization_title 'organization-edit' '#edit-accordion' %} {% if relations.Parent_of %} @@ -20,7 +20,7 @@

    Remove organization

    {% endfor %}
    {% endif %} -

    Add parent organization

    +

    Add child organization

    {{ form.relationship_parent_of | as_crispy_field }}
    diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index d2673351a..f74a74643 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -560,28 +560,37 @@ def list_sites(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Sites', 'urls': urls}) -def _organization_table(org): +def _organization_table(org, parent_org): organization_link = { 'url': u'/organization/{}/'.format(org.get('handle_id')), 'name': u'{}'.format(org.get('name', '')) } + if parent_org: + parent_org_link = { + 'url': u'/organization/{}/'.format(parent_org.get('handle_id')), + 'name': u'{}'.format(parent_org.get('name', '')) + } + else: + parent_org_link = '' + name = org.get('customer_id') - row = TableRow(organization_link, name) + row = TableRow(organization_link, parent_org_link) return row @login_required def list_organizations(request): q = """ MATCH (org:Organization) - RETURN org + 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', 'ID') - table.rows = [_organization_table(item['org']) for item in org_list] + table = Table('Name', 'Parent Org.') + table.rows = [_organization_table(item['org'], item['parent_org']) for item in org_list] table.no_badges=True return render(request, 'noclook/list/list_generic.html', From 319d1484eab74bf5b884fb813b059c3bbb728494 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 16 Jul 2019 10:07:24 +0200 Subject: [PATCH 148/520] Fix: organization column added --- src/niweb/apps/noclook/views/detail.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 1b5baf013..ce38398c1 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -664,13 +664,18 @@ def group_detail(request, handle_id): return render(request, 'noclook/detail/group_detail.html', {'node_handle': nh, 'node': node, 'location_path': location_path}) -def _contact_with_role_table(con): +def _contact_with_role_table(con, org=None): contact_link = { 'url': u'/contact/{}/'.format(con.handle_id), 'name': u'{}'.format(con.node_name) } - row = TableRow(contact_link) + 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 @@ -679,11 +684,17 @@ def role_detail(request): if role_name: con_list = nc.models.RoleRelationship.get_contacts_with_role(role_name) - contact_list = [ NodeHandle.objects.get(handle_id=x.data['handle_id']) for x in con_list ] - urls = helpers.get_node_urls(contact_list) + 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') - table.rows = [_contact_with_role_table(item) for item in contact_list] + table = Table('Name', 'Organization') + table.rows = rows table.no_badges=True return render(request, 'noclook/detail/role_detail.html', From 9f8a6537067080be67113e9c2ca041c37e8fe6fa Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 16 Jul 2019 12:18:59 +0200 Subject: [PATCH 149/520] Fix: Contacts with same name validation --- src/niweb/apps/noclook/forms/common.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 5dca55f4d..d895896c2 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -6,7 +6,7 @@ import json import csv from apps.noclook import helpers -from apps.noclook.models import NodeHandle, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown +from apps.noclook.models import NodeType, NodeHandle, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown from .. import unique_ids import norduniclient as nc from dynamic_preferences.registries import global_preferences_registry @@ -906,7 +906,7 @@ def __init__(self, *args, **kwargs): title = forms.CharField(required=False) PGP_fingerprint = forms.CharField(required=False) - def clean(self): + def clean(self, is_create=True): """ Sets name from first and second name """ @@ -919,7 +919,13 @@ def clean(self): first_name = first_name.encode('utf-8') last_name = last_name.encode('utf-8') - cleaned_data['name'] = '{} {}'.format(first_name, last_name) + full_name = '{} {}'.format(first_name, last_name) + node_type = NodeType.objects.get(type="Contact") + + if is_create and self.data and NodeHandle.objects.filter(node_name=full_name, node_type=node_type): + raise ValidationError('A contact with that name already exists') + + cleaned_data['name'] = full_name return cleaned_data @@ -938,7 +944,7 @@ def clean(self): """ Check empty role names """ - cleaned_data = super(EditContactForm, self).clean() + cleaned_data = super(EditContactForm, self).clean(False) role_name = cleaned_data.get("role_name") if not role_name: From bded215d2818b76a0610502156cf2e394ff49f8a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 16 Jul 2019 15:17:46 +0200 Subject: [PATCH 150/520] Fix: Clean field on group edit --- src/niweb/apps/noclook/forms/common.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index d895896c2..c33f09d76 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -969,3 +969,7 @@ def __init__(self, *args, **kwargs): self.fields['relationship_member_of'].choices = get_node_type_tuples('Contact') relationship_member_of = relationship_field('contact', True) + + def clean(self): + self.data = self.data.copy() + del self.data['relationship_member_of'] From 55f6682caa3ebe26c7f13f1ad585adaf927a5ee7 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 17 Jul 2019 08:19:50 +0200 Subject: [PATCH 151/520] Fix: Removed salutation and text change --- src/niweb/apps/noclook/forms/common.py | 1 - .../apps/noclook/templates/noclook/create/create_contact.html | 3 --- .../apps/noclook/templates/noclook/edit/edit_contact.html | 1 - .../templates/noclook/edit/includes/member_of_group.html | 2 +- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index c33f09d76..484575468 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -899,7 +899,6 @@ def __init__(self, *args, **kwargs): contact_type = forms.ChoiceField(widget=forms.widgets.Select) mobile = forms.CharField(required=False) phone = forms.CharField(required=False) - salutation = forms.CharField(required=False) email = forms.CharField(required=False) other_email = forms.CharField(required=False) name = forms.CharField(required=False, widget=forms.widgets.HiddenInput) diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_contact.html b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html index d18a8f804..d6c3460f4 100644 --- a/src/niweb/apps/noclook/templates/noclook/create/create_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/create/create_contact.html @@ -22,9 +22,6 @@

    Additional info (optional)

    {{ form.title }}
    - - {{ form.salutation }} -
    {{ form.contact_type }}
    diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html index 436c02bf4..3f0892e07 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -29,7 +29,6 @@
    {% accordion 'Additional info (optional)' 'additional-edit' '#edit-accordion' %} {{ form.title | as_crispy_field }} - {{ form.salutation | as_crispy_field }} {{ form.contact_type | as_crispy_field }} {{ form.phone | as_crispy_field }} {{ form.mobile | as_crispy_field }} 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 index db2044353..9e58e1c30 100644 --- 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 @@ -6,7 +6,7 @@ {% accordion memberof_title 'memberof-edit' '#edit-accordion' %} {% if relations.Member_of %} {% load noclook_tags %} -

    Remove group

    +

    Remove from group

    {% for item in relations.Member_of %}
    From 1dc143836cb7b1c971b2422dd996235ef5cf94cc Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 17 Jul 2019 11:25:38 +0200 Subject: [PATCH 152/520] Fix: Change in text for organizations, functionality on set parent, list --- src/niweb/apps/noclook/helpers.py | 2 +- .../edit/includes/parent_of_group.html | 4 +- src/niweb/apps/noclook/views/list.py | 42 ++++++++++++++----- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 68d2467d3..5ad8dd1d6 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -889,7 +889,7 @@ def set_parent_of(user, node, child_org_id): :param child_org_id: unique id :return: norduniclient model, boolean """ - result = node.set_child(child_org_id) + result = node.set_parent(child_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') 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 index 3f03e7a8b..cdc881667 100644 --- 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 @@ -1,7 +1,7 @@ {% load noclook_tags %} {% load crispy_forms_tags %} {% blockvar organization_title %} - Edit organization hierarchy (optional) + Parent Organization (optional) {% endblockvar %} {% accordion organization_title 'organization-edit' '#edit-accordion' %} {% if relations.Parent_of %} @@ -20,7 +20,7 @@

    Remove organization

    {% endfor %}
    {% endif %} -

    Add child organization

    +

    Add parent organization

    {{ form.relationship_parent_of | as_crispy_field }}
    diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index f74a74643..c45986742 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -560,21 +560,25 @@ def list_sites(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Sites', 'urls': urls}) -def _organization_table(org, parent_org): +def _organization_table(org, parent_orgs): organization_link = { 'url': u'/organization/{}/'.format(org.get('handle_id')), 'name': u'{}'.format(org.get('name', '')) } - if parent_org: - parent_org_link = { - 'url': u'/organization/{}/'.format(parent_org.get('handle_id')), - 'name': u'{}'.format(parent_org.get('name', '')) - } - else: - parent_org_link = '' - + + 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('customer_id') - row = TableRow(organization_link, parent_org_link) + row = TableRow(organization_link, parent_links) return row @login_required @@ -590,7 +594,23 @@ def list_organizations(request): urls = get_node_urls(org_list) table = Table('Name', 'Parent Org.') - table.rows = [_organization_table(item['org'], item['parent_org']) for item in org_list] + 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', From a0c53153e500f2f2e6d627068a4935689c95f254 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 17 Jul 2019 11:35:23 +0200 Subject: [PATCH 153/520] Fix: Removed contacts section from edit organization --- src/niweb/apps/noclook/forms/common.py | 47 ------------------- .../noclook/edit/edit_organization.html | 8 ---- src/niweb/apps/noclook/views/edit.py | 12 ----- 3 files changed, 67 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 484575468..905c8ed9a 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -834,59 +834,12 @@ def __init__(self, *args, **kwargs): 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]: - for field in Dropdown.get('organization_contact_types').as_choices(empty=False): - possible_contact = helpers.get_contact_for_orgrole(args[0]['handle_id'], field[1]) - if possible_contact: - field_name = field[0].decode('utf8') if six.PY2 else field[0] - args[0][field_name] = possible_contact.handle_id - 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 - if 'handle_id' in args[0]: - organization_id = args[0]['handle_id'] - contact_choices = get_contacts_for_organization(organization_id) - 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 - - def clean(self): - """ - Sets name from first and second name - """ - cleaned_data = super(EditOrganizationForm, self).clean() - contact_fields = Dropdown.get('organization_contact_types').as_values() - - for field in contact_fields: - 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 field in self._errors: - del self._errors[field] - relationship_parent_of = relationship_field('organization', True) relationship_uses_a = relationship_field('procedure', True) - abuse_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Abuse") - primary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Primary contact at incidents") # Primary contact at incidents - secondary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Secondary contact at incidents") # Secondary contact at incidents - it_technical_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-technical") # IT-technical - it_security_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-security") # IT-security - it_manager_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-manager") # IT-manager class NewContactForm(forms.Form): diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html index 2d67d43ef..77d11292c 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html @@ -46,14 +46,6 @@ {{ form.type | as_crispy_field}} {{ form.additional_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/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 2e8a1483a..21ea2c485 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -967,18 +967,6 @@ def edit_organization(request, handle_id): 'name', 'description', 'phone', 'website', 'customer_id', 'type', 'additional_info', ] helpers.form_update_node(request.user, organization.handle_id, form, property_keys) - # Set contacts - contact_fields = Dropdown.get('organization_contact_types').as_choices(empty=False) - for field in contact_fields: - if field[0] in form.cleaned_data: - contact_data = form.cleaned_data[field[0]] - if contact_data: - if isinstance(contact_data, six.string_types): - if contact_data: - helpers.create_contact_role_for_organization(request.user, organization, contact_data, field[1]) - else: - helpers.link_contact_role_for_organization(request.user, organization, contact_data, field[1]) - # Set child organizations if form.cleaned_data['relationship_parent_of']: organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) From 052fe9d4b41726c949bc52361f90c94cec971a7b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 17 Jul 2019 12:07:50 +0200 Subject: [PATCH 154/520] Fix: Removed Contacts section in create Organization --- src/niweb/apps/noclook/forms/common.py | 7 ------- .../noclook/create/create_organization.html | 18 ------------------ src/niweb/apps/noclook/views/create.py | 6 ------ 3 files changed, 31 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 905c8ed9a..850c1c70f 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -819,13 +819,6 @@ class NewOrganizationForm(forms.Form): customer_id = forms.CharField(required=False) type = forms.ChoiceField(widget=forms.widgets.Select, required=False) additional_info = forms.CharField(widget=forms.widgets.Textarea, required=False, label="Additional info for incident Mgmt") - # these fields will be replaced by selects in the edit form - abuse_contact = forms.CharField(required=False) - primary_contact = forms.CharField(required=False) # Primary contact at incidents - secondary_contact = forms.CharField(required=False) # Secondary contact at incidents - it_technical_contact = forms.CharField(required=False) # IT-technical - it_security_contact = forms.CharField(required=False) # IT-security - it_manager_contact = forms.CharField(required=False) # IT-manager def __init__(self, *args, **kwargs): super(NewOrganizationForm, self).__init__(*args, **kwargs) diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_organization.html b/src/niweb/apps/noclook/templates/noclook/create/create_organization.html index 815e12bfe..ad17a338c 100644 --- a/src/niweb/apps/noclook/templates/noclook/create/create_organization.html +++ b/src/niweb/apps/noclook/templates/noclook/create/create_organization.html @@ -33,24 +33,6 @@

    Additional info (optional)


    {{ form.additional_info }} -

    Contacts (optional)

    - - {{ form.abuse_contact }} -
    - - {{ form.primary_contact }} -
    - - {{ form.secondary_contact }} -
    - - {{ form.it_technical_contact }} -
    - - {{ form.it_security_contact }} -
    - - {{ form.it_manager_contact }}
    Cancel diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index e9914911b..cd623b16b 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -556,12 +556,6 @@ def new_organization(request, **kwargs): ] helpers.form_update_node(request.user, nh.handle_id, form, property_keys) - contact_fields = Dropdown.get('organization_contact_types').as_choices(empty=False) - for field in contact_fields: - contact_name = form.cleaned_data[field[0]] - if contact_name: - helpers.create_contact_role_for_organization(request.user, nh, contact_name, field[1]) - return redirect(nh.get_absolute_url()) else: form = forms.NewOrganizationForm() From af4823cf0b443f1d04c2e7807b897b723ca7e12c Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 17 Jul 2019 12:54:47 +0200 Subject: [PATCH 155/520] Fix: Contacts sorted alphabetically in group edit --- .../noclook/edit/includes/of_member_group.html | 11 ++++++----- src/niweb/apps/noclook/views/edit.py | 6 +++++- 2 files changed, 11 insertions(+), 6 deletions(-) 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 index 4db7afb17..29912339c 100644 --- 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 @@ -42,14 +42,15 @@

    Remove contact

    - {% for item in relations.Member_of %} + {{ foo }} + {% for contact in contacts %} -
    +
    - {% noclook_get_type item.node.handle_id as node_type %} - {{ node_type }} {{ item.node.data.name }} + {% noclook_get_type contact.handle_id as node_type %} + {{ node_type }} {{ contact.data.name }}
    Delete diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 21ea2c485..6a66eb982 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -1049,8 +1049,12 @@ def edit_group(request, handle_id): return redirect('%sedit' % nh.get_absolute_url()) else: form = forms.EditGroupForm(group.data) + + contacts = [x['node'] for x in relations['Member_of']] + 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}) + {'node_handle': nh, 'form': form, 'node': group, 'relations': relations, 'contacts': contacts }) EDIT_FUNC = { 'cable': edit_cable, From 0a54c2583b728b92129f2296c4c5673b4aeaacdf Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 17 Jul 2019 13:31:44 +0200 Subject: [PATCH 156/520] Fix: Added description for groups --- src/niweb/apps/noclook/forms/common.py | 6 ++++-- .../noclook/templates/noclook/create/create_group.html | 3 +++ .../apps/noclook/templates/noclook/edit/edit_group.html | 1 + src/niweb/apps/noclook/views/edit.py | 7 +++++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 850c1c70f..deb22a045 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -906,6 +906,7 @@ class EditProcedureForm(NewProcedureForm): class NewGroupForm(forms.Form): name = forms.CharField() + description = description_field('group') class EditGroupForm(NewGroupForm): @@ -916,5 +917,6 @@ def __init__(self, *args, **kwargs): relationship_member_of = relationship_field('contact', True) def clean(self): - self.data = self.data.copy() - del self.data['relationship_member_of'] + if 'relationship_member_of' in self.data: + self.data = self.data.copy() + del self.data['relationship_member_of'] diff --git a/src/niweb/apps/noclook/templates/noclook/create/create_group.html b/src/niweb/apps/noclook/templates/noclook/create/create_group.html index d05ae62a0..3fbc914c8 100644 --- a/src/niweb/apps/noclook/templates/noclook/create/create_group.html +++ b/src/niweb/apps/noclook/templates/noclook/create/create_group.html @@ -15,6 +15,9 @@

    Main information

    {{ form.name }}
    + + {{ form.description }} +
    Cancel diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html index 2ff3a5d9a..0ea6737b7 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_group.html @@ -20,6 +20,7 @@ {{ block.super }}
    {{ form.name | as_crispy_field}} + {{ form.description | as_crispy_field}}
    diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 6a66eb982..a86608f52 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -1050,8 +1050,11 @@ def edit_group(request, handle_id): else: form = forms.EditGroupForm(group.data) - contacts = [x['node'] for x in relations['Member_of']] - contacts = sorted(contacts, key=lambda x: x.data['name'], reverse=False) + contacts = [] + + if 'Member_of' in relations: + contacts = [x['node'] for x in relations['Member_of']] + 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 }) From 058e14dc38a3ae4af9b654712b61444e2e9bc0af Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 17 Jul 2019 15:16:35 +0200 Subject: [PATCH 157/520] WIP: Major changes for roles, now a dual entity in both db --- src/niweb/apps/noclook/admin.py | 3 +- .../noclook/management/commands/csvimport.py | 32 +++++++++++++++- src/niweb/apps/noclook/models.py | 8 ++++ src/niweb/apps/noclook/urls.py | 2 +- src/niweb/apps/noclook/views/detail.py | 37 +++++++++---------- src/niweb/apps/noclook/views/list.py | 18 ++++----- 6 files changed, 67 insertions(+), 33 deletions(-) diff --git a/src/niweb/apps/noclook/admin.py b/src/niweb/apps/noclook/admin.py index c1d2c37b9..09ff46c88 100644 --- a/src/niweb/apps/noclook/admin.py +++ b/src/niweb/apps/noclook/admin.py @@ -4,7 +4,7 @@ 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 +from .models import NodeHandle, NodeType, Role, UniqueIdGenerator, NordunetUniqueId, OpticalNodeType, ServiceType, ServiceClass, Dropdown, Choice class UserModelAdmin(UserAdmin): inlines = [ApiKeyInline] @@ -99,3 +99,4 @@ class ChoiceAdmin(admin.ModelAdmin): admin.site.register(ServiceClass) admin.site.register(Dropdown, DropdownAdmin) admin.site.register(Choice, ChoiceAdmin) +admin.site.register(Role) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 8cec5afae..371dbcdd6 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' -from apps.noclook.models import User, NodeType, NodeHandle, NODE_META_TYPE_CHOICES +from apps.noclook.models import User, NodeType, NodeHandle, Role, NODE_META_TYPE_CHOICES from apps.nerds.lib.consumer_util import get_user from django.core.management.base import BaseCommand, CommandError from pprint import pprint @@ -26,10 +26,15 @@ def add_arguments(self, parser): type=argparse.FileType('r')) parser.add_argument("-s", "--secroles", help="security roles CSV file", type=argparse.FileType('r')) + parser.add_argument("-f", "--fixroles", help="regenerate roles in intermediate setup") parser.add_argument('-d', "--delimiter", nargs='?', default=';', help='Delimiter to use use. Default ";".') def handle(self, *args, **options): + if options['fixroles']: + self.fix_roles() + return + relation_meta_type = 'Relation' logical_meta_type = 'Logical' @@ -245,6 +250,31 @@ def handle(self, *args, **options): 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 + ''' + # get all unique role string in all Works_for relation in neo4j db + role_names = nc.models.RoleRelationship.get_all_roles() + + # 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() + + # update the relation in neo4j using the relation_id to add the handle_id + q = """ + MATCH (c:Contact)-[r:Works_for]->(o:Organization) + WHERE r.name = "{role_name}" + SET r.handle_id = {handle_id} + RETURN r + """.format(role_name=role_name, handle_id=role.handle_id) + + ret = nc.core.query_to_list(nc.graphdb.manager, q) def count_lines(self, file): ''' diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index 26364ff83..487aa8ae4 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -129,6 +129,14 @@ def delete(self, **kwargs): delete.alters_data = True +@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) + # Data only present in the relational database + description = models.TextField() + @python_2_unicode_compatible class UniqueIdGenerator(models.Model): diff --git a/src/niweb/apps/noclook/urls.py b/src/niweb/apps/noclook/urls.py index f7625957e..11a0672a7 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -114,7 +114,7 @@ url(r'^optical-filter/(?P\d+)/$', detail.optical_filter_detail), url(r'^port/(?P\d+)/$', detail.port_detail), url(r'^site/(?P\d+)/$', detail.site_detail), - url(r'^role/detail/$', detail.role_detail), + url(r'^role/(?P\d+)/$', detail.role_detail), url(r'^rack/(?P\d+)/$', detail.rack_detail), url(r'^site-owner/(?P\d+)/$', detail.site_owner_detail), url(r'^service/(?P\d+)/$', detail.service_detail), diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index ce38398c1..3fa234ea0 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -6,7 +6,7 @@ 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 @@ -679,26 +679,23 @@ def _contact_with_role_table(con, org=None): return row @login_required -def role_detail(request): - role_name = request.GET.get('name', None) +def role_detail(request, handle_id): + role = get_object_or_404(Role, pk=handle_id) - if role_name: - con_list = nc.models.RoleRelationship.get_contacts_with_role(role_name) - urls = [] - rows = [] + con_list = nc.models.RoleRelationship.get_contacts_with_role_id(role.handle_id) + 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)) + 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 + 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}) - else: - raise Http404("The role doesn't exists") + return render(request, 'noclook/detail/role_detail.html', + {'table': table, 'name': role.name, 'slug': 'role', + 'urls': urls}) diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index c45986742..e6475b0e4 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 +from apps.noclook.models import NodeType, Role from apps.noclook.views.helpers import Table, TableRow from apps.noclook.helpers import get_node_urls, neo4j_data_age import norduniclient as nc @@ -667,21 +667,19 @@ def list_contacts(request): return render(request, 'noclook/list/list_generic.html', {'table': table, 'name': 'Contacts', 'urls': urls}) -def _role_table(role_name): - name_param = { 'name': role_name } +def _role_table(role): role_link = { - 'url': u'/role/detail/?{}'.format(urllib.parse.urlencode(name_param)), - 'name': u'{}'.format(role_name) - } - row = TableRow(role_link) - return row + 'url': u'/role/{}/'.format(role.handle_id), + 'name': u'{}'.format(role.name) + } + return TableRow(role_link) @login_required def list_roles(request): - role_list = nc.models.RoleRelationship.get_all_roles(nc.graphdb.manager) + role_list = Role.objects.all() table = Table('Name') - table.rows = [_role_table(role_name) for role_name in role_list] + table.rows = [_role_table(role) for role in role_list] table.no_badges=True return render(request, 'noclook/list/list_generic.html', From ebb5af1b9583b1279a829b406f7e8fcf89ba5b95 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 18 Jul 2019 13:57:19 +0200 Subject: [PATCH 158/520] WIP: Added modified function --- src/niweb/apps/noclook/management/commands/csvimport.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 371dbcdd6..9aeffed6d 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -26,7 +26,8 @@ def add_arguments(self, parser): type=argparse.FileType('r')) parser.add_argument("-s", "--secroles", help="security roles CSV file", type=argparse.FileType('r')) - parser.add_argument("-f", "--fixroles", help="regenerate roles in intermediate setup") + parser.add_argument("-f", "--fixroles", + action='store_true', help="regenerate roles in intermediate setup") parser.add_argument('-d', "--delimiter", nargs='?', default=';', help='Delimiter to use use. Default ";".') @@ -200,10 +201,12 @@ def handle(self, *args, **options): # add role relatioship role_name = node['contact_role'] + role = Role.objects.get_or_create(name = role_name)[0] nc.models.RoleRelationship.link_contact_organization( new_contact.handle_id, new_org.handle_id, + role.handle_id, role_name ) @@ -241,10 +244,12 @@ def handle(self, *args, **options): )[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.handle_id, role_name ) From 31c4e194947e7dfbcd3a0d4bb4134fa1a0463ef3 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 19 Jul 2019 09:13:08 +0200 Subject: [PATCH 159/520] WIP: Added create role view --- src/niweb/apps/noclook/forms/common.py | 8 +++++++- src/niweb/apps/noclook/models.py | 6 ++++++ src/niweb/apps/noclook/views/create.py | 13 +++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index deb22a045..3289ecbf4 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -6,7 +6,7 @@ import json import csv from apps.noclook import helpers -from apps.noclook.models import NodeType, NodeHandle, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown +from apps.noclook.models import NodeType, NodeHandle, Role, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown from .. import unique_ids import norduniclient as nc from dynamic_preferences.registries import global_preferences_registry @@ -920,3 +920,9 @@ 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 = '__all__' diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index 487aa8ae4..cc2b36d0b 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -137,6 +137,12 @@ class Role(models.Model): # Data only present in the relational database description = models.TextField() + def get_absolute_url(self): + return self.url() + + def url(self): + return '/role/{}'.format(self.handle_id) + @python_2_unicode_compatible class UniqueIdGenerator(models.Model): diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index cd623b16b..31575e207 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -21,6 +21,7 @@ ("contact", "Contact"), ("organization", "Organization"), ("group", "Group"), + ("role", "Role"), ] @@ -612,6 +613,17 @@ def new_group(request, **kwargs): 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, @@ -631,6 +643,7 @@ def new_group(request, **kwargs): 'provider': new_provider, 'procedure': new_procedure, 'rack': new_rack, + 'role': new_role, 'service': new_service, 'site': new_site, 'site-owner': new_site_owner, From 699ff49254dc901843b72c6b584f4cca82be918f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 19 Jul 2019 11:11:31 +0200 Subject: [PATCH 160/520] WIP: Added detail view for roles --- src/niweb/apps/noclook/models.py | 3 + .../templates/noclook/detail/role_detail.html | 116 +++++++++++++++++- src/niweb/apps/noclook/views/detail.py | 2 +- src/niweb/apps/noclook/views/list.py | 4 +- 4 files changed, 120 insertions(+), 5 deletions(-) diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index cc2b36d0b..d07fbc7c6 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -137,6 +137,9 @@ class Role(models.Model): # Data only present in the relational database description = models.TextField() + def __str__(self): + return 'Role %s' % (self.name) + def get_absolute_url(self): return self.url() diff --git a/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html index 0ecd7da97..43e5e0ba2 100644 --- a/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html +++ b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html @@ -1,12 +1,124 @@ -{% extends "noclook/list/list_generic.html" %} +{% 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}}

    + + + + +
    description{{ node_handle.description }}
    +
    + {% 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 edit_link %} + +{% block content_footer %} +
    {% if user.is_staff %} Edit {% endif %} +
    {% endblock %} diff --git a/src/niweb/apps/noclook/views/detail.py b/src/niweb/apps/noclook/views/detail.py index 3fa234ea0..2fa922f8f 100644 --- a/src/niweb/apps/noclook/views/detail.py +++ b/src/niweb/apps/noclook/views/detail.py @@ -698,4 +698,4 @@ def role_detail(request, handle_id): return render(request, 'noclook/detail/role_detail.html', {'table': table, 'name': role.name, 'slug': 'role', - 'urls': urls}) + 'urls': urls, 'node_handle': role }) diff --git a/src/niweb/apps/noclook/views/list.py b/src/niweb/apps/noclook/views/list.py index e6475b0e4..651a9bdd6 100644 --- a/src/niweb/apps/noclook/views/list.py +++ b/src/niweb/apps/noclook/views/list.py @@ -672,13 +672,13 @@ def _role_table(role): 'url': u'/role/{}/'.format(role.handle_id), 'name': u'{}'.format(role.name) } - return TableRow(role_link) + return TableRow(role_link, role.description) @login_required def list_roles(request): role_list = Role.objects.all() - table = Table('Name') + table = Table('Name', 'Description') table.rows = [_role_table(role) for role in role_list] table.no_badges=True From 7649252a3265fd6a6d2084389584dac55cb42d5a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 19 Jul 2019 12:07:18 +0200 Subject: [PATCH 161/520] WIP: Added edit Role --- src/niweb/apps/noclook/forms/common.py | 12 +++++++ .../migrations/0007_auto_20190410_1341.py | 35 +++++++++++++++++++ .../apps/noclook/migrations/0008_role.py | 23 ++++++++++++ .../migrations/0009_auto_20190717_1240.py | 20 +++++++++++ .../migrations/0010_auto_20190719_1005.py | 20 +++++++++++ src/niweb/apps/noclook/models.py | 6 ++-- .../templates/noclook/create/create_role.html | 25 +++++++++++++ .../templates/noclook/detail/role_detail.html | 2 ++ .../templates/noclook/edit/edit_role.html | 14 ++++++++ src/niweb/apps/noclook/views/edit.py | 21 ++++++++++- 10 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 src/niweb/apps/noclook/migrations/0007_auto_20190410_1341.py create mode 100644 src/niweb/apps/noclook/migrations/0008_role.py create mode 100644 src/niweb/apps/noclook/migrations/0009_auto_20190717_1240.py create mode 100644 src/niweb/apps/noclook/migrations/0010_auto_20190719_1005.py create mode 100644 src/niweb/apps/noclook/templates/noclook/create/create_role.html create mode 100644 src/niweb/apps/noclook/templates/noclook/edit/edit_role.html diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 3289ecbf4..5cb076ad4 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -926,3 +926,15 @@ class NewRoleForm(forms.ModelForm): class Meta: model = Role fields = '__all__' + +class EditRoleForm(forms.ModelForm): + def save(self, commit=True): + role = super(EditRoleForm, self).save(commit) + if 'name' in self.changed_data: + nc.models.RoleRelationship.update_roles_withid(role.handle_id, role.name) + + return role + + class Meta: + model = Role + fields = '__all__' 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/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/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/models.py b/src/niweb/apps/noclook/models.py index d07fbc7c6..107d89ed1 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -10,11 +10,11 @@ try: from neo4j.exceptions import CypherError except ImportError: - try: + try: # pre neo4j 1.4 from neo4j.v1.exceptions import CypherError except ImportError: - # neo4j 1.1 + # neo4j 1.1 from neo4j.v1.api import CypherError @@ -135,7 +135,7 @@ class Role(models.Model): handle_id = models.AutoField(primary_key=True) # Handle <-> Node data name = models.CharField(max_length=200) # Data only present in the relational database - description = models.TextField() + description = models.TextField(blank=True, null=True) def __str__(self): return 'Role %s' % (self.name) 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/detail/role_detail.html b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html index 43e5e0ba2..7f8187eba 100644 --- a/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html +++ b/src/niweb/apps/noclook/templates/noclook/detail/role_detail.html @@ -64,11 +64,13 @@ {% block content %} {{ block.super }}

    Role {{name}}

    + {% if node_handle.description %}
    description{{ node_handle.description }}
    + {% endif %}
    {% if table.no_badges %} {# Nothing to show #} 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..821413891 --- /dev/null +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_role.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/views/edit.py b/src/niweb/apps/noclook/views/edit.py index a86608f52..51070bc34 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -13,7 +13,7 @@ from django.utils import six from django.shortcuts import get_object_or_404, render, redirect import json -from apps.noclook.models import NodeHandle, Dropdown +from apps.noclook.models import NodeHandle, Role, Dropdown from apps.noclook import forms from apps.noclook import activitylog from apps.noclook import helpers @@ -1059,6 +1059,24 @@ def edit_group(request, handle_id): 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('%sedit' % nh.get_absolute_url()) + else: + form = forms.EditRoleForm(instance=role) + return render(request, 'noclook/edit/edit_role.html', + {'form': form, 'role': role}) + + EDIT_FUNC = { 'cable': edit_cable, 'customer': edit_customer, @@ -1082,6 +1100,7 @@ def edit_group(request, handle_id): 'provider': edit_provider, 'procedure': edit_procedure, 'rack': edit_rack, + 'role': edit_role, 'router': edit_router, 'site': edit_site, 'site-owner': edit_site_owner, From 22b92de0fbcba37e084f13765e3f1362b23cbd0b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 19 Jul 2019 13:51:03 +0200 Subject: [PATCH 162/520] Fix: Contact role linking in edit form adapted tot he new models --- src/niweb/apps/noclook/forms/common.py | 7 ++++--- src/niweb/apps/noclook/helpers.py | 15 ++++++++------- .../templates/noclook/edit/edit_contact.html | 7 +++++++ .../noclook/edit/includes/works_for_group.html | 2 +- src/niweb/apps/noclook/views/edit.py | 4 ++-- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 5cb076ad4..c939d68d9 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -880,19 +880,20 @@ def __init__(self, *args, **kwargs): super(EditContactForm, self).__init__(*args, **kwargs) 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) relationship_member_of = relationship_field('group', True) - role_name = forms.CharField(required=False) + role = forms.ChoiceField(required=False, widget=forms.widgets.Select) def clean(self): """ Check empty role names """ cleaned_data = super(EditContactForm, self).clean(False) - role_name = cleaned_data.get("role_name") + role_id = cleaned_data.get("role") - if not role_name: + if not role_id: cleaned_data['relationship_works_for'] = None class NewProcedureForm(forms.Form): diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 5ad8dd1d6..bbb55028d 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -21,7 +21,7 @@ import os from neo4j.v1.types import Node -from .models import NodeHandle, NodeType +from .models import NodeHandle, NodeType, Role from . import activitylog import norduniclient as nc from norduniclient.exceptions import UniqueNodeError, NodeNotFound @@ -221,7 +221,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 @@ -240,11 +240,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 @@ -912,7 +912,7 @@ def set_uses_a(user, node, procedure_id): activitylog.create_relationship(user, relationship) return relationship, created -def set_works_for(user, node, organization_id, role_name): +def set_works_for(user, node, organization_id, role_handle_id): """ :param user: Django user :param node: norduniclient model @@ -920,9 +920,10 @@ def set_works_for(user, node, organization_id, role_name): :param role_name: string for role name :return: norduniclient model, boolean """ - from pprint import pprint + role = Role.objects.get(handle_id=role_handle_id) + role_name = role.name contact_id = node.handle_id - relationship = nc.models.RoleRelationship.link_contact_organization(contact_id, organization_id, role_name) + relationship = nc.models.RoleRelationship.link_contact_organization(contact_id, organization_id, role_handle_id, role_name) if not relationship: relationship = RoleRelationship() diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html index 3f0892e07..69756ea4a 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -4,6 +4,8 @@ {% block js %} {{ block.super }} + + 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 index 65c1f23c5..1ae45892d 100644 --- 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 @@ -25,6 +25,6 @@

    Link contact to organization

    {{ form.relationship_works_for | as_crispy_field }}
    - {{ form.role_name | as_crispy_field }} + {{ form.role | as_crispy_field }}
    {% endaccordion %} diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 51070bc34..7456d622b 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -996,8 +996,8 @@ def edit_contact(request, handle_id): # Set relationships if form.cleaned_data['relationship_works_for']: organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_works_for']) - role_name = form.cleaned_data['role_name'] - helpers.set_works_for(request.user, contact, organization_nh.handle_id, role_name) + role_handle_id = form.cleaned_data['role'] + helpers.set_works_for(request.user, contact, organization_nh.handle_id, role_handle_id) 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) From 6d4fd22c0cd8c8134a651b411c4497bce68859f8 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 23 Jul 2019 10:43:13 +0200 Subject: [PATCH 163/520] WIP: Add specific contact with roles in Organization edit form. --- src/niweb/apps/noclook/admin.py | 13 +-- src/niweb/apps/noclook/forms/common.py | 57 ++++++++++++- src/niweb/apps/noclook/helpers.py | 84 ++++++++++++++----- .../migrations/0011_auto_20190719_1157.py | 29 +++++++ .../apps/noclook/migrations/0012_role_slug.py | 20 +++++ src/niweb/apps/noclook/models.py | 16 ++++ .../noclook/edit/edit_organization.html | 12 ++- src/niweb/apps/noclook/urls.py | 1 + src/niweb/apps/noclook/views/edit.py | 26 +++++- 9 files changed, 225 insertions(+), 33 deletions(-) create mode 100644 src/niweb/apps/noclook/migrations/0011_auto_20190719_1157.py create mode 100644 src/niweb/apps/noclook/migrations/0012_role_slug.py diff --git a/src/niweb/apps/noclook/admin.py b/src/niweb/apps/noclook/admin.py index 09ff46c88..dc4221da7 100644 --- a/src/niweb/apps/noclook/admin.py +++ b/src/niweb/apps/noclook/admin.py @@ -4,7 +4,7 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User -from .models import NodeHandle, NodeType, Role, UniqueIdGenerator, NordunetUniqueId, OpticalNodeType, ServiceType, ServiceClass, Dropdown, Choice +from .models import NodeHandle, NodeType, Role, RoleGroup, UniqueIdGenerator, NordunetUniqueId, OpticalNodeType, ServiceType, ServiceClass, Dropdown, Choice class UserModelAdmin(UserAdmin): inlines = [ApiKeyInline] @@ -13,7 +13,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 +21,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 +33,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 +46,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: @@ -99,4 +99,5 @@ class ChoiceAdmin(admin.ModelAdmin): admin.site.register(ServiceClass) admin.site.register(Dropdown, DropdownAdmin) admin.site.register(Choice, ChoiceAdmin) +admin.site.register(RoleGroup) admin.site.register(Role) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index c939d68d9..6119dc2d7 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -6,7 +6,7 @@ import json import csv from apps.noclook import helpers -from apps.noclook.models import NodeType, NodeHandle, Role, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown +from apps.noclook.models import NodeType, NodeHandle, RoleGroup, Role, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown from .. import unique_ids import norduniclient as nc from dynamic_preferences.registries import global_preferences_registry @@ -827,13 +827,68 @@ def __init__(self, *args, **kwargs): 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]: + organization_id = args[0]['handle_id'] + + # check or create the default roles + helpers.init_default_rolegroup() + + for field, roledict in helpers.DEFAULT_ROLES.items(): + role = Role.objects.get(slug=field) + possible_contact = helpers.get_contact_for_orgrole(organization_id, role) + + if possible_contact: + args[0][field] = possible_contact.handle_id + 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 + if 'handle_id' in args[0]: + organization_id = args[0]['handle_id'] + contact_choices = get_contacts_for_organization(organization_id) + 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) relationship_uses_a = relationship_field('procedure', True) + abuse_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Abuse") + primary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Primary contact at incidents") # Primary contact at incidents + secondary_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="Secondary contact at incidents") # Secondary contact at incidents + it_technical_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-technical") # IT-technical + it_security_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-security") # IT-security + it_manager_contact = forms.ChoiceField(widget=forms.widgets.Select, required=False, label="IT-manager") # IT-manager + + def clean(self): + """ + Sets name from first and second name + """ + cleaned_data = super(EditOrganizationForm, self).clean() + for field, roledict in helpers.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 field in self._errors: + del self._errors[field] + class NewContactForm(forms.Form): def __init__(self, *args, **kwargs): diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index bbb55028d..a59d02be2 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -21,7 +21,7 @@ import os from neo4j.v1.types import Node -from .models import NodeHandle, NodeType, Role +from .models import NodeHandle, NodeType, RoleGroup, Role from . import activitylog import norduniclient as nc from norduniclient.exceptions import UniqueNodeError, NodeNotFound @@ -969,26 +969,20 @@ def set_of_member(user, node, contact_id): return relationship, created -def link_contact_role_for_organization(user, node, contact_handle_id, role_name): +def link_contact_role_for_organization(user, node, contact_handle_id, role): """ :param user: Django user :param node: norduniclient organization model :param contact_handle_id: contact's handle_id - :param role_name: role name + :param role: the selected role :return: contact """ - if six.PY2: - role_name = role_name.encode('utf-8') - - nc.models.RoleRelationship.remove_role_in_organization( - node.handle_id, - role_name - ) relationship = nc.models.RoleRelationship.link_contact_organization( contact_handle_id, node.handle_id, - role_name + role.handle_id, + role.name ) if not relationship: @@ -1005,6 +999,21 @@ def link_contact_role_for_organization(user, node, contact_handle_id, role_name) 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.handle_id, + ) + + activitylog.delete_relationship(user, relationship) + relationship.delete() + + def create_contact_role_for_organization(user, node, contact_name, role_name): """ :param user: Django user @@ -1063,20 +1072,51 @@ def create_contact_role_for_organization(user, node, contact_name, role_name): return contact, relationship -def get_contact_for_orgrole(organization_id, role_name): +def get_contact_for_orgrole(organization_id, role): """ :param organization_id: Organization's handle_id - :param role_name: Role name + :param role_name: Role object """ - q = """ - MATCH (c:Contact)-[:Works_for {{ name: '{role_name}'}}]->(o:Organization) - WHERE o.handle_id = {organization_id} - RETURN c.handle_id AS handle_id - """.format(organization_id=organization_id, role_name=role_name) - d = nc.query_to_dict(nc.graphdb.manager, q) - - if 'handle_id' in d and d['handle_id']: - contact_handle_id = d['handle_id'] + contact_handle_id = nc.models.RoleRelationship.get_contact_with_role_in_organization( + organization_id, + role.handle_id, + ) + + if contact_handle_id: contact = NodeHandle.objects.get(handle_id=contact_handle_id) return contact + +DEFAULT_ROLEGROUP_NAME = 'default' +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': 'IT-technical', 'description': '' }, + 'it_security_contact': { 'name': 'IT-security', 'description': '' }, + 'it_manager_contact': { 'name': 'IT-manager', 'description': '' }, +} + +def init_default_rolegroup(): + default_rolegroup = RoleGroup.objects.filter(name=DEFAULT_ROLEGROUP_NAME) + + if not default_rolegroup: + # 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() 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/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/models.py b/src/niweb/apps/noclook/models.py index 107d89ed1..361b89935 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -129,13 +129,22 @@ def delete(self, **kwargs): delete.alters_data = True + +@python_2_unicode_compatible +class RoleGroup(models.Model): + name = models.CharField(max_length=100, unique=True) + hidden = models.BooleanField(default=False, blank=True) + + @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) + slug = models.CharField(max_length=20, unique=True, null=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) @@ -146,6 +155,13 @@ def get_absolute_url(self): def url(self): return '/role/{}'.format(self.handle_id) + def delete(self, **kwargs): + """ + Propagate the changes over the graph db + """ + nc.models.RoleRelationship.delete_roles_withid(self.handle_id) + super(Role, self).delete() + @python_2_unicode_compatible class UniqueIdGenerator(models.Model): diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html index 77d11292c..31ecf223b 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_organization.html @@ -24,9 +24,7 @@ select#id_secondary_contact,\ select#id_it_technical_contact,\ select#id_it_security_contact,\ - select#id_it_manager_contact").select2({ - tags: true, - }); + select#id_it_manager_contact").select2(); } ); @@ -46,6 +44,14 @@ {{ form.type | as_crispy_field}} {{ form.additional_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/urls.py b/src/niweb/apps/noclook/urls.py index 11a0672a7..5859ddbc5 100644 --- a/src/niweb/apps/noclook/urls.py +++ b/src/niweb/apps/noclook/urls.py @@ -46,6 +46,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'^(?P[-\w]+)/(?P\d+)/edit/disable-noclook-auto-manage/$', edit.disable_noclook_auto_manage), url(r'^host/(?P\d+)/edit/convert-to/(?P[-\w]+)/$', edit.convert_host), diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 7456d622b..739d5ca93 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -13,7 +13,7 @@ from django.utils import six from django.shortcuts import get_object_or_404, render, redirect import json -from apps.noclook.models import NodeHandle, Role, Dropdown +from apps.noclook.models import NodeHandle, Role, RoleGroup, Dropdown from apps.noclook import forms from apps.noclook import activitylog from apps.noclook import helpers @@ -967,6 +967,18 @@ def edit_organization(request, handle_id): 'name', 'description', 'phone', 'website', 'customer_id', 'type', 'additional_info', ] helpers.form_update_node(request.user, organization.handle_id, form, property_keys) + + # specific role setting + for field, roledict in helpers.DEFAULT_ROLES.items(): + if field in form.cleaned_data: + contact_id = form.cleaned_data[field] + role = Role.objects.get(slug=field) + + if contact_id: + # first we get delete any previous contact with that role on that organization + helpers.unlink_contact_with_role_from_org(request.user, organization, role) + helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + # Set child organizations if form.cleaned_data['relationship_parent_of']: organization_nh = NodeHandle.objects.get(pk=form.cleaned_data['relationship_parent_of']) @@ -1059,6 +1071,7 @@ def edit_group(request, handle_id): 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 @@ -1077,6 +1090,17 @@ def edit_role(request, handle_id): {'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, From 76502984f7a951caa2f594b6162b5bf02918615d Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 23 Jul 2019 12:27:35 +0200 Subject: [PATCH 164/520] WIP: Default roles protected and remove contact in organization form --- src/niweb/apps/noclook/forms/common.py | 7 +++++-- src/niweb/apps/noclook/helpers.py | 27 ++++++++++++++------------ src/niweb/apps/noclook/models.py | 17 ++++++++++++++-- src/niweb/apps/noclook/views/edit.py | 13 ++++++++++--- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 6119dc2d7..4a884a962 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -6,7 +6,10 @@ import json import csv from apps.noclook import helpers -from apps.noclook.models import NodeType, NodeHandle, RoleGroup, Role, UniqueIdGenerator, ServiceType, NordunetUniqueId, Dropdown +from apps.noclook.models import NodeType, NodeHandle, RoleGroup, Role,\ + UniqueIdGenerator, ServiceType,\ + NordunetUniqueId, Dropdown, DEFAULT_ROLES,\ + DEFAULT_ROLEGROUP_NAME from .. import unique_ids import norduniclient as nc from dynamic_preferences.registries import global_preferences_registry @@ -836,7 +839,7 @@ def __init__(self, *args, **kwargs): # check or create the default roles helpers.init_default_rolegroup() - for field, roledict in helpers.DEFAULT_ROLES.items(): + for field, roledict in DEFAULT_ROLES.items(): role = Role.objects.get(slug=field) possible_contact = helpers.get_contact_for_orgrole(organization_id, role) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index a59d02be2..c7f73a7af 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -21,7 +21,8 @@ import os from neo4j.v1.types import Node -from .models import NodeHandle, NodeType, RoleGroup, Role +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 @@ -1010,9 +1011,20 @@ def unlink_contact_with_role_from_org(user, organization, role): role.handle_id, ) - activitylog.delete_relationship(user, relationship) - relationship.delete() + 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.handle_id, + contact_id + ) + if relationship: + activitylog.delete_relationship(user, relationship) + relationship.delete() def create_contact_role_for_organization(user, node, contact_name, role_name): """ @@ -1087,15 +1099,6 @@ def get_contact_for_orgrole(organization_id, role): return contact -DEFAULT_ROLEGROUP_NAME = 'default' -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': 'IT-technical', 'description': '' }, - 'it_security_contact': { 'name': 'IT-security', 'description': '' }, - 'it_manager_contact': { 'name': 'IT-manager', 'description': '' }, -} def init_default_rolegroup(): default_rolegroup = RoleGroup.objects.filter(name=DEFAULT_ROLEGROUP_NAME) diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index 361b89935..f9edfb55d 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -130,6 +130,17 @@ def delete(self, **kwargs): delete.alters_data = True +DEFAULT_ROLEGROUP_NAME = 'default' +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': 'IT-technical', 'description': '' }, + 'it_security_contact': { 'name': 'IT-security', 'description': '' }, + 'it_manager_contact': { 'name': 'IT-manager', 'description': '' }, +} + + @python_2_unicode_compatible class RoleGroup(models.Model): name = models.CharField(max_length=100, unique=True) @@ -159,8 +170,10 @@ def delete(self, **kwargs): """ Propagate the changes over the graph db """ - nc.models.RoleRelationship.delete_roles_withid(self.handle_id) - super(Role, self).delete() + default_rolegroup = RoleGroup.objects.get(name=DEFAULT_ROLEGROUP_NAME) + if self.role_group != default_rolegroup: + nc.models.RoleRelationship.delete_roles_withid(self.handle_id) + super(Role, self).delete() @python_2_unicode_compatible diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index 739d5ca93..be3a894ec 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -973,11 +973,18 @@ def edit_organization(request, handle_id): 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: - # first we get delete any previous contact with that role on that organization - helpers.unlink_contact_with_role_from_org(request.user, organization, role) - helpers.link_contact_role_for_organization(request.user, organization, contact_id, role) + 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']: From dea79b01566b9188f27233ded53ef0f82f0702a3 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 24 Jul 2019 11:57:29 +0200 Subject: [PATCH 165/520] Tests fixed with the new helper functions and norduniclient changes --- src/niweb/apps/noclook/helpers.py | 67 ++------------ .../noclook/management/commands/csvimport.py | 2 +- src/niweb/apps/noclook/models.py | 3 + src/niweb/apps/noclook/tests/test_helpers.py | 88 ++++++++++++------- 4 files changed, 69 insertions(+), 91 deletions(-) diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index c7f73a7af..2062ab2a5 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -883,14 +883,14 @@ def attachment_content(attachment): content = f.read() return content -def set_parent_of(user, node, child_org_id): +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(child_org_id) + 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') @@ -1024,64 +1024,11 @@ def unlink_contact_and_role_from_org(user, organization, contact_id, role): if relationship: activitylog.delete_relationship(user, relationship) - relationship.delete() - -def create_contact_role_for_organization(user, node, contact_name, role_name): - """ - :param user: Django user - :param node: norduniclient organization model - :param contact_name: full name of the contact - :return: contact, role: New objects if they're not present in the db - """ - contact_type = NodeType.objects.get(type='Contact') - - # convert string if necesary - if six.PY2: - contact_name = contact_name.encode('utf-8') - role_name = role_name.encode('utf-8') - - nc.models.RoleRelationship.remove_role_in_organization( - node.handle_id, - role_name - ) - - first_name, last_name = contact_name.split(' ') - - # create or get contact - contact, created_contact = NodeHandle.objects.get_or_create( - node_name=contact_name, - node_type=contact_type, - node_meta_type='Relation', - creator=user, - modifier=user, - ) - - if created_contact: - activitylog.create_node(user, contact) - contact.get_node().add_property('first_name', first_name) - contact.get_node().add_property('last_name', last_name) - - relationship = nc.models.RoleRelationship.link_contact_organization( - contact.handle_id, - node.handle_id, - role_name - ) - - if not relationship: - relationship = RoleRelationship() - relationship.load_from_nodes(contact_id, organization_id) - - node = node.reload() - - created = False - for relation in node.relationships.get('Works_for'): - if relation['node'].handle_id == contact.handle_id: - created = relation.get('created') - - if created: - activitylog.create_relationship(user, relationship) - - return contact, relationship + nc.models.RoleRelationship.unlink_contact_with_role_organization( + contact_id, + organization.handle_id, + role.handle_id, + ) def get_contact_for_orgrole(organization_id, role): diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 9aeffed6d..69ea1cbe3 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -261,7 +261,7 @@ def fix_roles(self): role database representation in both databases ''' # get all unique role string in all Works_for relation in neo4j db - role_names = nc.models.RoleRelationship.get_all_roles() + role_names = nc.models.RoleRelationship.get_all_role_names() # create a role for each of them for role_name in role_names: diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index f9edfb55d..decbabd90 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -146,6 +146,9 @@ 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): diff --git a/src/niweb/apps/noclook/tests/test_helpers.py b/src/niweb/apps/noclook/tests/test_helpers.py index 31bc090c4..23a187b9c 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 @@ -13,14 +14,16 @@ def setUp(self): 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() - role = self.create_node('role1', 'role', meta='Logical') - self.role_node = role.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') @@ -48,33 +51,58 @@ def test_create_unique_node_handle_case_insensitive(self): 'Physical') def test_link_contact_role_for_organization(self): - thedata = { - 'role_name': 'IT-manager' - } - 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, - thedata['role_name'] - ) - - self.assertEqual(role.name, thedata['role_name']) - self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) - - def test_create_contact_role_for_organization(self): - thedata = { - 'contact_name': 'FirstName LastName', - 'role_name': 'IT-manager' - } - - self.assertEqual(len(self.organization_node.relationships), 0) - - contact, role = helpers.create_contact_role_for_organization(self.user, self.organization_node, - thedata['contact_name'], - thedata['role_name'], - ) - - self.assertEqual(contact.get_node(), self.organization_node.get_relations().get('Works_for')[0].get('node')) - self.assertEqual(contact.get_node().data.get('name'), thedata['contact_name']) - self.assertEqual(role.name, thedata['role_name']) + 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_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.handle_id + ) + 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) From 2eaa1e5c0ae8af0d5d3f388b82bcb913fa3a8e26 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 24 Jul 2019 12:31:33 +0200 Subject: [PATCH 166/520] CSVimport tests improved --- src/niweb/apps/noclook/tests/management/test_csvimport.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index 0601b7331..6082abb5a 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -6,7 +6,7 @@ from norduniclient.exceptions import UniqueNodeError, NodeNotFound import norduniclient.models as ncmodels -from apps.noclook.models import NodeHandle, NodeType, User +from apps.noclook.models import NodeHandle, NodeType, User, Role from ..neo4j_base import NeoTestCase @@ -113,6 +113,10 @@ def test_contacts_import(self): 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) + def test_secroles_import(self): # call csvimport command (verbose 0) call_command( From f02ecc46a4ebdd5359cab6ebaa78a22378cf6436 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 24 Jul 2019 13:14:38 +0200 Subject: [PATCH 167/520] Salutation removed from csvimport --- src/niweb/apps/noclook/management/commands/csvimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 69ea1cbe3..a90853957 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -182,7 +182,7 @@ def handle(self, *args, **options): graph_node = new_contact.get_node() for key in node.keys(): - if key not in ['node_type', 'contact_role', 'name', 'account_name'] and node[key]: + 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 From 498b1881464605a3581683ebb7cf2359d6fe871f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 24 Jul 2019 13:49:27 +0200 Subject: [PATCH 168/520] Added default role when this is not specified --- src/niweb/apps/noclook/forms/common.py | 14 ++++++++++---- .../noclook/management/commands/csvimport.py | 18 ++++++++++++++---- src/niweb/apps/noclook/models.py | 2 ++ .../noclook/tests/management/test_csvimport.py | 16 +++++++++++++--- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 4a884a962..bfa5620b3 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -9,7 +9,7 @@ from apps.noclook.models import NodeType, NodeHandle, RoleGroup, Role,\ UniqueIdGenerator, ServiceType,\ NordunetUniqueId, Dropdown, DEFAULT_ROLES,\ - DEFAULT_ROLEGROUP_NAME + DEFAULT_ROLE_KEY, DEFAULT_ROLEGROUP_NAME from .. import unique_ids import norduniclient as nc from dynamic_preferences.registries import global_preferences_registry @@ -879,7 +879,7 @@ def clean(self): Sets name from first and second name """ cleaned_data = super(EditOrganizationForm, self).clean() - for field, roledict in helpers.DEFAULT_ROLES.items(): + for field, roledict in DEFAULT_ROLES.items(): if field in self.data: value = self.data[field] if value: @@ -936,6 +936,11 @@ def clean(self, is_create=True): class EditContactForm(NewContactForm): def __init__(self, *args, **kwargs): super(EditContactForm, self).__init__(*args, **kwargs) + + # check or create the default roles + helpers.init_default_rolegroup() + + # 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')) @@ -946,13 +951,14 @@ def __init__(self, *args, **kwargs): def clean(self): """ - Check empty role names + Check empty role, set to employee """ cleaned_data = super(EditContactForm, self).clean(False) role_id = cleaned_data.get("role") if not role_id: - cleaned_data['relationship_works_for'] = None + default_role = Role.objects.get(slug=DEFAULT_ROLE_KEY) + cleaned_data['role'] = default_role.handle_id class NewProcedureForm(forms.Form): name = forms.CharField() diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index a90853957..5bdbcb562 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' -from apps.noclook.models import User, NodeType, NodeHandle, Role, NODE_META_TYPE_CHOICES +from apps.noclook import helpers +from apps.noclook.models import User, NodeType, NodeHandle, Role, 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 @@ -32,6 +33,10 @@ def add_arguments(self, parser): help='Delimiter to use use. Default ";".') def handle(self, *args, **options): + # init default roles and rolegroup + helpers.init_default_rolegroup() + + # check if the fixroles option has been called, do it and exit if options['fixroles']: self.fix_roles() return @@ -199,9 +204,13 @@ def handle(self, *args, **options): modifier = self.user, )[0] - # add role relatioship + # add role relatioship, use employee role if empty role_name = node['contact_role'] - role = Role.objects.get_or_create(name = role_name)[0] + + 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, @@ -258,7 +267,8 @@ def handle(self, *args, **options): def fix_roles(self): ''' This method is provided to update an existing setup into the new - role database representation in both databases + 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() diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index decbabd90..f3785a5ab 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -131,6 +131,7 @@ def delete(self, **kwargs): DEFAULT_ROLEGROUP_NAME = 'default' +DEFAULT_ROLE_KEY = 'employee' DEFAULT_ROLES = { 'abuse_contact': { 'name': 'Abuse', 'description': '' }, 'primary_contact': { 'name': 'Primary contact at incidents', 'description': '' }, @@ -138,6 +139,7 @@ def delete(self, **kwargs): 'it_technical_contact': { 'name': 'IT-technical', 'description': '' }, 'it_security_contact': { 'name': 'IT-security', 'description': '' }, 'it_manager_contact': { 'name': 'IT-manager', 'description': '' }, + DEFAULT_ROLE_KEY: { 'name': 'Employee', 'description': '' }, } diff --git a/src/niweb/apps/noclook/tests/management/test_csvimport.py b/src/niweb/apps/noclook/tests/management/test_csvimport.py index 6082abb5a..f3e9c2aca 100644 --- a/src/niweb/apps/noclook/tests/management/test_csvimport.py +++ b/src/niweb/apps/noclook/tests/management/test_csvimport.py @@ -6,7 +6,7 @@ from norduniclient.exceptions import UniqueNodeError, NodeNotFound import norduniclient.models as ncmodels -from apps.noclook.models import NodeHandle, NodeType, User, Role +from apps.noclook.models import NodeHandle, NodeType, User, Role, DEFAULT_ROLE_KEY from ..neo4j_base import NeoTestCase @@ -28,8 +28,9 @@ class CsvImportTest(NeoTestCase): "Honorable";"Caesar";"Newby";;"Computer Systems Analyst III";"Person";;;;;"China";"897-979-7799";"501-503-1550";;"cnewby0@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";"Franny";"Bainton";;"Software Consultant";"Person";;;;;"China";"877-832-9647";"138-608-6235";;"fbainton3@si.edu";;;"Mudo" -"Rev";"Kiri";"Janosevic";;"Physical Therapy Assistant";"Person";;;;;"China";"568-690-1854";"118-569-1303";;"kjanosevic4@umich.edu";;;"Youspan" +"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" @@ -117,6 +118,15 @@ def test_contacts_import(self): 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.handle_id, relations['Works_for'][0]['relationship']['handle_id']) + def test_secroles_import(self): # call csvimport command (verbose 0) call_command( From a6b4410815ded519678b304a7188610b4b337439 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 25 Jul 2019 14:58:16 +0200 Subject: [PATCH 169/520] Migrations squashed and data inicialization with RunPython --- src/niweb/apps/noclook/forms/common.py | 9 +- src/niweb/apps/noclook/helpers.py | 25 ----- .../noclook/management/commands/csvimport.py | 5 +- ...8_role_squashed_0013_auto_20190725_1153.py | 93 +++++++++++++++++++ .../migrations/0013_auto_20190725_1153.py | 25 +++++ .../noclook/migrations/common_dropdowns.csv | 6 -- src/niweb/apps/noclook/models.py | 12 ++- 7 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 src/niweb/apps/noclook/migrations/0008_role_squashed_0013_auto_20190725_1153.py create mode 100644 src/niweb/apps/noclook/migrations/0013_auto_20190725_1153.py diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index bfa5620b3..556e5e99e 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -836,9 +836,6 @@ def __init__(self, *args, **kwargs): if 'handle_id' in args[0]: organization_id = args[0]['handle_id'] - # check or create the default roles - helpers.init_default_rolegroup() - for field, roledict in DEFAULT_ROLES.items(): role = Role.objects.get(slug=field) possible_contact = helpers.get_contact_for_orgrole(organization_id, role) @@ -937,9 +934,6 @@ class EditContactForm(NewContactForm): def __init__(self, *args, **kwargs): super(EditContactForm, self).__init__(*args, **kwargs) - # check or create the default roles - helpers.init_default_rolegroup() - # init combos self.fields['relationship_works_for'].choices = get_node_type_tuples('Organization') self.fields['relationship_member_of'].choices = get_node_type_tuples('Group') @@ -994,9 +988,10 @@ class Meta: class EditRoleForm(forms.ModelForm): def save(self, commit=True): - role = super(EditRoleForm, self).save(commit) + role = super(EditRoleForm, self).save(False) if 'name' in self.changed_data: nc.models.RoleRelationship.update_roles_withid(role.handle_id, role.name) + role.save() return role diff --git a/src/niweb/apps/noclook/helpers.py b/src/niweb/apps/noclook/helpers.py index 2062ab2a5..35e338969 100644 --- a/src/niweb/apps/noclook/helpers.py +++ b/src/niweb/apps/noclook/helpers.py @@ -1045,28 +1045,3 @@ def get_contact_for_orgrole(organization_id, role): contact = NodeHandle.objects.get(handle_id=contact_handle_id) return contact - - -def init_default_rolegroup(): - default_rolegroup = RoleGroup.objects.filter(name=DEFAULT_ROLEGROUP_NAME) - - if not default_rolegroup: - # 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() diff --git a/src/niweb/apps/noclook/management/commands/csvimport.py b/src/niweb/apps/noclook/management/commands/csvimport.py index 5bdbcb562..ef25c1756 100644 --- a/src/niweb/apps/noclook/management/commands/csvimport.py +++ b/src/niweb/apps/noclook/management/commands/csvimport.py @@ -33,9 +33,6 @@ def add_arguments(self, parser): help='Delimiter to use use. Default ";".') def handle(self, *args, **options): - # init default roles and rolegroup - helpers.init_default_rolegroup() - # check if the fixroles option has been called, do it and exit if options['fixroles']: self.fix_roles() @@ -206,7 +203,7 @@ def handle(self, *args, **options): # 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: 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/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/common_dropdowns.csv b/src/niweb/apps/noclook/migrations/common_dropdowns.csv index 362d53232..5825606ea 100644 --- a/src/niweb/apps/noclook/migrations/common_dropdowns.csv +++ b/src/niweb/apps/noclook/migrations/common_dropdowns.csv @@ -52,11 +52,5 @@ organization_types,student_net,Student network organization_types,partner,Partner organization_types,provider,Service provider organization_types,supplier,Supplier -organization_contact_types,abuse_contact,Abuse -organization_contact_types,primary_contact,Primary contact at incidents -organization_contact_types,secondary_contact,Secondary contact at incidents -organization_contact_types,it_technical_contact,IT-technical -organization_contact_types,it_security_contact,IT-security -organization_contact_types,it_manager_contact,IT-manager contact_type,person,Person contact_type,group,Group diff --git a/src/niweb/apps/noclook/models.py b/src/niweb/apps/noclook/models.py index f3785a5ab..6aa8bc623 100644 --- a/src/niweb/apps/noclook/models.py +++ b/src/niweb/apps/noclook/models.py @@ -156,8 +156,8 @@ def __str__(self): 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) - slug = models.CharField(max_length=20, unique=True, null=True) + 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) @@ -171,6 +171,14 @@ def get_absolute_url(self): 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 From baa20e2667c2a9218842838f21edc430afe021f6 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 26 Jul 2019 10:24:21 +0200 Subject: [PATCH 170/520] Fix: Contacts with duplicate names are possible now. --- src/niweb/apps/noclook/forms/common.py | 4 ---- src/niweb/apps/noclook/views/create.py | 6 +----- src/niweb/apps/noclook/views/edit.py | 4 ++-- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index 556e5e99e..9de29859c 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -921,10 +921,6 @@ def clean(self, is_create=True): full_name = '{} {}'.format(first_name, last_name) node_type = NodeType.objects.get(type="Contact") - - if is_create and self.data and NodeHandle.objects.filter(node_name=full_name, node_type=node_type): - raise ValidationError('A contact with that name already exists') - cleaned_data['name'] = full_name return cleaned_data diff --git a/src/niweb/apps/noclook/views/create.py b/src/niweb/apps/noclook/views/create.py index 31575e207..3ae1f4364 100644 --- a/src/niweb/apps/noclook/views/create.py +++ b/src/niweb/apps/noclook/views/create.py @@ -568,11 +568,7 @@ def new_contact(request, **kwargs): if request.POST: form = forms.NewContactForm(request.POST) if form.is_valid(): - try: - nh = helpers.form_to_unique_node_handle(request, form, 'contact', 'Relation') - except UniqueNodeError: - form.add_error('name', 'A Contact with that name already exists.') - return render(request, 'noclook/create/create_contact.html', {'form': form}) + 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: diff --git a/src/niweb/apps/noclook/views/edit.py b/src/niweb/apps/noclook/views/edit.py index be3a894ec..8734aab90 100644 --- a/src/niweb/apps/noclook/views/edit.py +++ b/src/niweb/apps/noclook/views/edit.py @@ -13,7 +13,7 @@ from django.utils import six from django.shortcuts import get_object_or_404, render, redirect import json -from apps.noclook.models import NodeHandle, Role, RoleGroup, Dropdown +from apps.noclook.models import NodeHandle, Role, RoleGroup, Dropdown, DEFAULT_ROLES from apps.noclook import forms from apps.noclook import activitylog from apps.noclook import helpers @@ -969,7 +969,7 @@ def edit_organization(request, handle_id): helpers.form_update_node(request.user, organization.handle_id, form, property_keys) # specific role setting - for field, roledict in helpers.DEFAULT_ROLES.items(): + 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) From e10f4d527a38ec14a63187c60e976244cc3c460a Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 26 Jul 2019 10:24:48 +0200 Subject: [PATCH 171/520] Fix: External files brought to static folder --- .../apps/noclook/templates/noclook/edit/edit_contact.html | 4 ++-- src/niweb/niweb/assets/css/select2.min.css | 1 + src/niweb/niweb/assets/js/select2/select2.min.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 src/niweb/niweb/assets/css/select2.min.css create mode 100644 src/niweb/niweb/assets/js/select2/select2.min.js diff --git a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html index 69756ea4a..aa4e67b2d 100644 --- a/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html +++ b/src/niweb/apps/noclook/templates/noclook/edit/edit_contact.html @@ -4,8 +4,8 @@ {% block js %} {{ block.super }} - - + + + + + +{% endblock %} + +{% block content %} +
    +
    +
    +
    +
    +
    +
    +
    +

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

    +
    +
    +
    + {% if user.is_authenticated %} + + {% endif %} +
    +
    +{% endblock %} diff --git a/src/niweb/apps/noclook/views/other.py b/src/niweb/apps/noclook/views/other.py index cf3c6dec8..c469e8fed 100644 --- a/src/niweb/apps/noclook/views/other.py +++ b/src/niweb/apps/noclook/views/other.py @@ -31,7 +31,7 @@ def logout_page(request): """ Log users out and redirects them to the index. """ - response = render(request, 'noclook/index.html', {}) + response = render(request, 'noclook/logout.html', {}) response.delete_cookie(jwt_settings.JWT_COOKIE_NAME) request.session.flush() From 0c824810ff052cf177ebd4379fd7f46282af5861 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 28 Jan 2020 13:49:23 +0100 Subject: [PATCH 480/520] From cdn to local --- src/niweb/apps/noclook/templates/noclook/logout.html | 3 ++- src/niweb/niweb/assets/js/jquery/js.cookie.min.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/niweb/niweb/assets/js/jquery/js.cookie.min.js diff --git a/src/niweb/apps/noclook/templates/noclook/logout.html b/src/niweb/apps/noclook/templates/noclook/logout.html index 8704f7ae2..0e5bcecc2 100644 --- a/src/niweb/apps/noclook/templates/noclook/logout.html +++ b/src/niweb/apps/noclook/templates/noclook/logout.html @@ -1,9 +1,10 @@ {% extends "base.html" %} +{% load static %} {% block title %}{{ block.super }} | Welcome{% endblock %} {% block js %} - + {% endblock %} diff --git a/src/niweb/apps/noclook/views/other.py b/src/niweb/apps/noclook/views/other.py index c469e8fed..ff45b32cf 100644 --- a/src/niweb/apps/noclook/views/other.py +++ b/src/niweb/apps/noclook/views/other.py @@ -31,7 +31,7 @@ def logout_page(request): """ Log users out and redirects them to the index. """ - response = render(request, 'noclook/logout.html', {}) + response = render(request, 'noclook/logout.html', {'cookie_domain': settings.COOKIE_DOMAIN }) response.delete_cookie(jwt_settings.JWT_COOKIE_NAME) request.session.flush() From a22f05a2580957ee601bc7849481d854c63e5827 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 29 Jan 2020 14:55:04 +0100 Subject: [PATCH 482/520] Catch exception on context init --- src/niweb/apps/noclook/vakt/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/vakt/utils.py b/src/niweb/apps/noclook/vakt/utils.py index 8582a5933..cf3fe2f5a 100644 --- a/src/niweb/apps/noclook/vakt/utils.py +++ b/src/niweb/apps/noclook/vakt/utils.py @@ -83,7 +83,11 @@ def get_list_authaction(aamodel=AuthzAction): def get_context_by_name(name, cmodel=Context): - context = cmodel.objects.get(name=name) + try: + context = cmodel.objects.get(name=name) + except: + context = None + return context From ae343aed8d342dfe9c632b37cecc29b58f0e107b Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 7 Feb 2020 08:32:19 +0100 Subject: [PATCH 483/520] Swapped handle_id/id in a custom query --- src/niweb/apps/noclook/schema/query.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/schema/query.py b/src/niweb/apps/noclook/schema/query.py index 6433813ea..069493bfd 100644 --- a/src/niweb/apps/noclook/schema/query.py +++ b/src/niweb/apps/noclook/schema/query.py @@ -94,7 +94,7 @@ 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), handle_id=graphene.Int()) + checkExistentOrganizationId = graphene.Boolean(organization_id=graphene.String(required=True), id=graphene.ID()) # get roles lookup getAvailableRoleGroups = graphene.List(RoleGroup) @@ -185,9 +185,13 @@ def resolve_getRolesFromRoleGroup(self, info, **kwargs): return ret def resolve_checkExistentOrganizationId(self, info, **kwargs): - # django dropdown resolver + 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') - handle_id = kwargs.get('handle_id', None) ret = nc.models.OrganizationModel.check_existent_organization_id(organization_id, handle_id, nc.graphdb.manager) From a185b91421e61e5f3927c10e404152371f08078c Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 7 Feb 2020 12:44:12 +0100 Subject: [PATCH 484/520] New field and resolver which relates the entities with the relation_id --- src/niweb/apps/noclook/schema/fields.py | 68 +++++++++++++++++++++++-- src/niweb/apps/noclook/schema/types.py | 6 +++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/niweb/apps/noclook/schema/fields.py b/src/niweb/apps/noclook/schema/fields.py index e20277afb..31f696a7e 100644 --- a/src/niweb/apps/noclook/schema/fields.py +++ b/src/niweb/apps/noclook/schema/fields.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- __author__ = 'ffuentes' -import graphene - 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) @@ -144,7 +145,7 @@ 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: @@ -157,3 +158,62 @@ def resolve_relationship_list(instance, info, **kwargs): return ret return resolve_relationship_list + + +class IDRelation(graphene.ObjectType): + 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.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/types.py b/src/niweb/apps/noclook/schema/types.py index d62c3227c..063ce8e9f 100644 --- a/src/niweb/apps/noclook/schema/types.py +++ b/src/niweb/apps/noclook/schema/types.py @@ -58,6 +58,7 @@ class Group(NIObjectType): 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' @@ -109,6 +110,7 @@ class Organization(NIObjectType): 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() @@ -117,6 +119,7 @@ class Organization(NIObjectType): 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' @@ -186,11 +189,14 @@ class Contact(NIObjectType): 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: From b279a80e677b189200a6e524b3d484d61d8929b1 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 7 Feb 2020 13:58:24 +0100 Subject: [PATCH 485/520] Attribute name change --- src/niweb/apps/noclook/schema/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/schema/fields.py b/src/niweb/apps/noclook/schema/fields.py index 31f696a7e..8f01f763c 100644 --- a/src/niweb/apps/noclook/schema/fields.py +++ b/src/niweb/apps/noclook/schema/fields.py @@ -161,7 +161,7 @@ def resolve_relationship_list(instance, info, **kwargs): class IDRelation(graphene.ObjectType): - id = graphene.ID() + entity_id = graphene.ID() relation_id = graphene.Int() @@ -210,7 +210,7 @@ def resolve_relationship_list(instance, info, **kwargs): type_str, str(node_id) ) id_relation = IDRelation() - id_relation.id = id + id_relation.entity_id = id id_relation.relation_id = relation_id handle_id_list.append(id_relation) From 1f5e2c651eca934ffcb9388b287448a5adb1de37 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 10 Feb 2020 13:48:49 +0100 Subject: [PATCH 486/520] Added switches and routers --- .../noclook/management/commands/datafaker.py | 5 +- .../tests/stressload/data_generator.py | 47 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index 3b9ae36ba..8591a021e 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -10,6 +10,7 @@ class Command(BaseCommand): help = 'Create fake data for the Network module' + generated_types = ['Cable', 'Provider', 'Port', 'Host', 'Router', 'Switch'] def add_arguments(self, parser): parser.add_argument("--equipmentcables", @@ -33,6 +34,8 @@ def create_equipment_cables(self, numnodes): create_funcs = [ generator.create_cable, generator.create_host, + generator.create_router, + generator.create_switch, ] total_nodes = numnodes * len(create_funcs) @@ -47,7 +50,7 @@ def create_equipment_cables(self, numnodes): def delete_network_nodes(self): if settings.DEBUG: # guard against accidental deletion on the wrong environment - delete_types = ['Cable', 'Provider', 'Port', 'Host'] + delete_types = self.generated_types total_nodes = 0 diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index 6ad877258..725a8a8e4 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -219,10 +219,13 @@ def create_cable(self): return cable - def create_host(self, type_name="Host", metatype=META_TYPES[0]): + 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( - self.fake.hostname(), type_name, metatype) + name, type_name, metatype) # add context self.add_network_context(host) @@ -269,3 +272,43 @@ def create_host(self, type_name="Host", metatype=META_TYPES[0]): 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) From d8fc4d30628c071ea50380703c542bef3194ed67 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 13 Feb 2020 11:16:12 +0100 Subject: [PATCH 487/520] Removed from excluded_fields --- src/niweb/apps/noclook/schema/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index 8ca4f1269..b06326861 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -183,7 +183,6 @@ def resolve_object_id(self, info, **kwargs): class Meta: model = Comment interfaces = (relay.Node, ) - exclude_fields = ('object_pk', ) input_fields_clsnames = {} From d766305396a61855f9f9903638418be59cfeac44 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 14 Feb 2020 11:42:15 +0100 Subject: [PATCH 488/520] If role id is not specified it should be set to employee --- src/niweb/apps/noclook/schema/mutations.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 8439397b4..1c8fbac25 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -8,7 +8,7 @@ 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, Choice as ChoiceModel + 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 @@ -790,7 +790,7 @@ class NIMetaClass: class RoleRelationMutation(relay.ClientIDMutation): class Input: - role_id = graphene.ID(required=True) + role_id = graphene.ID() organization_id = graphene.ID(required=True) relation_id = graphene.Int() @@ -813,7 +813,12 @@ def mutate_and_get_payload(cls, root, info, **input): role_id = input.get('role_id', None) relation_id = input.get('relation_id', None) - role_handle_id = relay.Node.from_global_id(role_id)[1] + 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 From 101d35bb91861e2792d26546ed24118ad1b1736d Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 19 Feb 2020 12:49:12 +0100 Subject: [PATCH 489/520] Remove the harcoded value, every environment will have the envvar set --- src/niweb/niweb/settings/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index 56f88a5c4..37f581e41 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -358,7 +358,7 @@ ########## SESSION_COOKIE_DOMAIN SESSION_COOKIE_HTTPONLY = False CORS_ALLOW_CREDENTIALS = True -COOKIE_DOMAIN = environ.get('COOKIE_DOMAIN', '.ed-integrations.com') +COOKIE_DOMAIN = environ.get('COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = COOKIE_DOMAIN CSRF_COOKIE_DOMAIN = COOKIE_DOMAIN ########## END SESSION_COOKIE_DOMAIN From 5f02d5ae79cd9712e6534a613a57b9504c9fe607 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 21 Feb 2020 13:56:10 +0100 Subject: [PATCH 490/520] Added customers and end users --- .../noclook/management/commands/datafaker.py | 47 +++++++++++++----- .../tests/stressload/data_generator.py | 48 +++++++++++++++++-- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index 8591a021e..b3a34a005 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -10,9 +10,12 @@ class Command(BaseCommand): help = 'Create fake data for the Network module' - generated_types = ['Cable', 'Provider', 'Port', 'Host', 'Router', 'Switch'] + generated_types = ['Customer', 'End User', + 'Cable', 'Provider', 'Port', 'Host', 'Router', 'Switch'] def add_arguments(self, parser): + parser.add_argument("--organizations", + help="Create organization nodes", type=int, default=20) parser.add_argument("--equipmentcables", help="Create equipment and cables nodes", type=int, default=20) parser.add_argument("-d", "--deleteall", action='store_true', @@ -23,11 +26,37 @@ def handle(self, *args, **options): self.delete_network_nodes() return + if options['organizations']: + numnodes = options['organizations'] + self.create_organizations(numnodes) + return + if options['equipmentcables']: numnodes = options['equipmentcables'] 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): + node = create_func() + created_nodes = created_nodes + 1 + self.printProgressBar(created_nodes, total_nodes) + + def create_organizations(self, numnodes): + generator = NetworkFakeDataGenerator() + + create_funcs = [ + generator.create_customer, + generator.create_end_user, + ] + + self.create_entities(numnodes, create_funcs) + def create_equipment_cables(self, numnodes): generator = NetworkFakeDataGenerator() @@ -38,15 +67,7 @@ def create_equipment_cables(self, numnodes): generator.create_switch, ] - 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): - node = create_func() - created_nodes = created_nodes + 1 - self.printProgressBar(created_nodes, total_nodes) + self.create_entities(numnodes, create_funcs) def delete_network_nodes(self): if settings.DEBUG: # guard against accidental deletion on the wrong environment @@ -65,8 +86,12 @@ def delete_network_nodes(self): 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 NodeType.objects.get_or_create(type=type_name, slug=type_name.lower())[0] + return NetworkFakeDataGenerator.get_nodetype(type_name) def get_node_num(self, type_name): node_type = self.get_nodetype(type_name) diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index 725a8a8e4..25f9e39df 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -8,6 +8,7 @@ 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 from norduniclient import META_TYPES import apps.noclook.vakt.utils as sriutils @@ -104,11 +105,12 @@ def add_network_context(self, nh): net_ctx = sriutils.get_network_context() NodeHandleContext(nodehandle=nh, context=net_ctx).save() - def get_nodetype(self, type_name): - return NodeType.objects.get_or_create(type=type_name, slug=type_name.lower())[0] + @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 = self.get_nodetype(type_name) + node_type = NetworkFakeDataGenerator.get_nodetype(type_name) # create object nh = NodeHandle.objects.get_or_create( @@ -124,6 +126,42 @@ def get_or_create_node(self, node_name, type_name, meta_type): 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.fake.company() + customer = self.get_or_create_node( + name, 'Customer', META_TYPES[2]) # Relation + + 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.fake.company() + enduser = self.get_or_create_node( + name, 'End User', META_TYPES[2]) # Relation + + data = { + 'url': self.fake.url(), + 'description': self.fake.paragraph(), + } + + for key, value in data.items(): + enduser.get_node().add_property(key, value) + + return enduser + + ## Equipment and cables + def create_provider(self): provider = self.get_or_create_node( self.fake.company(), 'Provider', META_TYPES[0]) @@ -174,7 +212,7 @@ def create_cable(self): cable_types = self.get_dropdown_keys('cable_types') # check if there's any provider or if we should create one - provider_type = self.get_nodetype('Provider') + provider_type = NetworkFakeDataGenerator.get_nodetype('Provider') providers = NodeHandle.objects.filter(node_type=provider_type) max_providers = self.max_cable_providers @@ -189,7 +227,7 @@ def create_cable(self): port_a = None port_b = None - port_type = self.get_nodetype('Port') + port_type = NetworkFakeDataGenerator.get_nodetype('Port') total_ports = NodeHandle.objects.filter(node_type=port_type).count() if total_ports < self.max_ports_total: From 9e1e6b32b44fee3b8800de1ba9152b1209665370 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 21 Feb 2020 14:36:46 +0100 Subject: [PATCH 491/520] Bugfix on the command line util --- .../apps/noclook/management/commands/datafaker.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index b3a34a005..bd0780cc1 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -15,9 +15,9 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("--organizations", - help="Create organization nodes", type=int, default=20) + help="Create organization nodes", type=int, default=0) parser.add_argument("--equipmentcables", - help="Create equipment and cables nodes", type=int, default=20) + help="Create equipment and cables nodes", type=int, default=0) parser.add_argument("-d", "--deleteall", action='store_true', help="BEWARE: This command deletes information in the database") @@ -28,13 +28,15 @@ def handle(self, *args, **options): if options['organizations']: numnodes = options['organizations'] - self.create_organizations(numnodes) - return + if numnodes > 0: + self.create_organizations(numnodes) if options['equipmentcables']: numnodes = options['equipmentcables'] - self.create_equipment_cables(numnodes) - return + if numnodes > 0: + self.create_equipment_cables(numnodes) + + return def create_entities(self, numnodes, create_funcs): total_nodes = numnodes * len(create_funcs) From 0c799339bace999a82312c0254f46e3eb3aff223 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 21 Feb 2020 14:38:18 +0100 Subject: [PATCH 492/520] Small bugfix on the progress bar --- src/niweb/apps/noclook/management/commands/datafaker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index bd0780cc1..4e66dba2d 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -107,7 +107,9 @@ def delete_type(self, type_name, deleted_nodes, total_nodes): [x.delete() for x in NodeHandle.objects.filter(node_type=node_type)] deleted_nodes = deleted_nodes + node_num - self.printProgressBar(deleted_nodes, total_nodes) + + if node_num > 0: + self.printProgressBar(deleted_nodes, total_nodes) return deleted_nodes From 6f044a763c3b942afd6ba3c06f7ff42bbc68c739 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 21 Feb 2020 15:06:58 +0100 Subject: [PATCH 493/520] Peering partners and groups added --- .../noclook/management/commands/datafaker.py | 11 ++++++++++- .../noclook/tests/stressload/data_generator.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index 4e66dba2d..0679e9dfe 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -29,11 +29,17 @@ def handle(self, *args, **options): if options['organizations']: numnodes = options['organizations'] if numnodes > 0: + self.stdout\ + .write('Forging fake organizations: {} for each subtype:'\ + .format(numnodes)) self.create_organizations(numnodes) if options['equipmentcables']: numnodes = options['equipmentcables'] if numnodes > 0: + self.stdout\ + .write('Forging fake equipement & cables: {} for each subtype:'\ + .format(numnodes)) self.create_equipment_cables(numnodes) return @@ -55,6 +61,8 @@ def create_organizations(self, numnodes): create_funcs = [ generator.create_customer, generator.create_end_user, + generator.create_peering_partner, + generator.create_peering_group, ] self.create_entities(numnodes, create_funcs) @@ -81,6 +89,7 @@ def delete_network_nodes(self): 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) @@ -107,7 +116,7 @@ def delete_type(self, type_name, deleted_nodes, total_nodes): [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) diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index 25f9e39df..b8ad6e5a4 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -160,6 +160,22 @@ def create_end_user(self): return enduser + def create_peering_partner(self): + # create object + name = self.fake.company() + peering_partner = self.get_or_create_node( + name, 'Peering Partner', META_TYPES[2]) # Relation + + return peering_partner + + def create_peering_group(self): + # create object + name = self.fake.company() + peering_group = self.get_or_create_node( + name, 'Peering Group', META_TYPES[1]) # Logical + + return peering_group + ## Equipment and cables def create_provider(self): From b7c7053a76fe35802bbdd7dcaa2bd6dc5cb759ff Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 24 Feb 2020 09:37:34 +0100 Subject: [PATCH 494/520] Add django-vakt to requirements --- requirements/common.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/common.txt b/requirements/common.txt index 7968b61ee..4141b6756 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -17,5 +17,6 @@ configparser graphene-django>=2.0 django-cors-headers vakt +django-vakt django-graphql-jwt Pillow From 5e7c031698c7828d3ee4c519c45797336e560427 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 24 Feb 2020 10:27:16 +0100 Subject: [PATCH 495/520] Refresh jwt token if we have an authenticated session --- src/niweb/apps/noclook/middleware.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/middleware.py b/src/niweb/apps/noclook/middleware.py index 9bd275c7d..a2de33f94 100644 --- a/src/niweb/apps/noclook/middleware.py +++ b/src/niweb/apps/noclook/middleware.py @@ -10,11 +10,24 @@ 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.utils import get_credentials +from graphql_jwt.utils import get_credentials, get_payload, jwt_encode, jwt_payload +from graphql_jwt.exceptions import JSONWebTokenExpired from importlib import import_module import time +def token_is_expired(token): + ret = False + + try: + get_payload(token) + ret = True + except JSONWebTokenExpired: + pass + + return ret + + class SRIJWTCookieMiddleware(object): def __init__(self, get_response): self.get_response = get_response @@ -25,6 +38,11 @@ def __call__(self, request): if user.is_authenticated: token = get_token(user) + + # if token is expired, refresh it + if token_is_expired(token): + token = jwt_encode(jwt_payload(user)) + signals.token_issued.send( sender=SRIJWTCookieMiddleware, request=request, user=user) From 8ac767d9cf01ce6508b64d0c5e3832a242fcda0f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 24 Feb 2020 13:22:19 +0100 Subject: [PATCH 496/520] SRIJWTCookieMiddleware removed. Token refreshed if session is valid --- src/niweb/apps/noclook/middleware.py | 95 ++++++++++++++-------------- src/niweb/niweb/settings/common.py | 2 +- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/src/niweb/apps/noclook/middleware.py b/src/niweb/apps/noclook/middleware.py index a2de33f94..b5bf5b553 100644 --- a/src/niweb/apps/noclook/middleware.py +++ b/src/niweb/apps/noclook/middleware.py @@ -10,7 +10,9 @@ 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.utils import get_credentials, get_payload, jwt_encode, jwt_payload +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 JSONWebTokenExpired from importlib import import_module @@ -21,48 +23,12 @@ def token_is_expired(token): try: get_payload(token) - ret = True except JSONWebTokenExpired: - pass + ret = True return ret -class SRIJWTCookieMiddleware(object): - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - token = None - user = request.user - - if user.is_authenticated: - token = get_token(user) - - # if token is expired, refresh it - if token_is_expired(token): - token = jwt_encode(jwt_payload(user)) - - signals.token_issued.send( - sender=SRIJWTCookieMiddleware, request=request, user=user) - - response = self.get_response(request) - - if token: - expires = datetime.utcnow() + jwt_settings.JWT_EXPIRATION_DELTA - response.set_cookie( - jwt_settings.JWT_COOKIE_NAME, - token, - domain=settings.COOKIE_DOMAIN, - expires=expires, - httponly=False, - secure=jwt_settings.JWT_COOKIE_SECURE, - ) - else: - response.delete_cookie(jwt_settings.JWT_COOKIE_NAME) - - return response - class SRIJWTAuthMiddleware(object): def __init__(self, get_response): self.get_response = get_response @@ -75,38 +41,72 @@ def __call__(self, request): request.user = SimpleLazyObject(lambda: get_user(request)) token = get_credentials(request) - if token is not None and token != '': + 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') or not request.user.is_authenticated: + 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 - # 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 - # process response with inner middleware response = self.get_response(request) + max_age = request.session.get_expiry_age() + expires_time = time.time() + max_age + cookie_expires = cookie_date(expires_time) + + 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, + 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: @@ -140,9 +140,10 @@ def __call__(self, request): response.set_cookie( settings.SESSION_COOKIE_NAME, request.session.session_key, max_age=max_age, - expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, + 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/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index 56f88a5c4..cd56d8a97 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -197,7 +197,6 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', - 'apps.noclook.middleware.SRIJWTCookieMiddleware', ) ########## END MIDDLEWARE CONFIGURATION @@ -241,6 +240,7 @@ 'attachments', 'graphene_django', 'corsheaders', + 'graphql_jwt.refresh_token.apps.RefreshTokenConfig', ) LOCAL_APPS = ( From fc720dd143cdfd61f27c35a465ed5cd696280c93 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 27 Feb 2020 13:41:21 +0100 Subject: [PATCH 497/520] Added context to the Organizations entities generation --- .../apps/noclook/tests/stressload/data_generator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index b8ad6e5a4..54ba2600e 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -134,6 +134,9 @@ def create_customer(self): customer = self.get_or_create_node( name, 'Customer', META_TYPES[2]) # Relation + # add context + self.add_network_context(provider) + data = { 'url': self.fake.url(), 'description': self.fake.paragraph(), @@ -150,6 +153,9 @@ def create_end_user(self): enduser = self.get_or_create_node( name, 'End User', META_TYPES[2]) # Relation + # add context + self.add_network_context(provider) + data = { 'url': self.fake.url(), 'description': self.fake.paragraph(), @@ -166,6 +172,9 @@ def create_peering_partner(self): peering_partner = self.get_or_create_node( name, 'Peering Partner', META_TYPES[2]) # Relation + # add context + self.add_network_context(provider) + return peering_partner def create_peering_group(self): @@ -174,6 +183,9 @@ def create_peering_group(self): peering_group = self.get_or_create_node( name, 'Peering Group', META_TYPES[1]) # Logical + # add context + self.add_network_context(provider) + return peering_group ## Equipment and cables From 19b3bbaa6556a1a661904af875cba3a3b997b250 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 27 Feb 2020 13:53:00 +0100 Subject: [PATCH 498/520] Fixed context setting --- src/niweb/apps/noclook/tests/stressload/data_generator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index 54ba2600e..d4a320ea9 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -135,7 +135,7 @@ def create_customer(self): name, 'Customer', META_TYPES[2]) # Relation # add context - self.add_network_context(provider) + self.add_network_context(customer) data = { 'url': self.fake.url(), @@ -154,7 +154,7 @@ def create_end_user(self): name, 'End User', META_TYPES[2]) # Relation # add context - self.add_network_context(provider) + self.add_network_context(enduser) data = { 'url': self.fake.url(), @@ -173,7 +173,7 @@ def create_peering_partner(self): name, 'Peering Partner', META_TYPES[2]) # Relation # add context - self.add_network_context(provider) + self.add_network_context(peering_partner) return peering_partner @@ -184,7 +184,7 @@ def create_peering_group(self): name, 'Peering Group', META_TYPES[1]) # Logical # add context - self.add_network_context(provider) + self.add_network_context(peering_group) return peering_group From 84808405977bc6f703353ccf2002e6568147db10 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 3 Mar 2020 11:57:19 +0100 Subject: [PATCH 499/520] Added expiration control --- src/niweb/apps/noclook/middleware.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/middleware.py b/src/niweb/apps/noclook/middleware.py index b5bf5b553..da260ebb0 100644 --- a/src/niweb/apps/noclook/middleware.py +++ b/src/niweb/apps/noclook/middleware.py @@ -13,7 +13,7 @@ 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 JSONWebTokenExpired +from graphql_jwt.exceptions import JSONWebTokenError, JSONWebTokenExpired from importlib import import_module import time @@ -23,6 +23,8 @@ def token_is_expired(token): try: get_payload(token) + except JSONWebTokenError: + ret = True except JSONWebTokenExpired: ret = True From bf6e57e5b21587118584f6e46d4ba1fbe5527aea Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 3 Mar 2020 13:35:48 +0100 Subject: [PATCH 500/520] Added peering partners, peering groups and site owners to data generator --- .../noclook/management/commands/datafaker.py | 6 ++-- .../tests/stressload/data_generator.py | 33 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index 0679e9dfe..6d73a0c6b 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -10,8 +10,9 @@ class Command(BaseCommand): help = 'Create fake data for the Network module' - generated_types = ['Customer', 'End User', - 'Cable', 'Provider', 'Port', 'Host', 'Router', 'Switch'] + generated_types = [ + 'Customer', 'End User', 'Site Owner', 'Provider', 'Peering Group', 'Peering Partner' + 'Cable', 'Port', 'Host', 'Router', 'Switch'] def add_arguments(self, parser): parser.add_argument("--organizations", @@ -63,6 +64,7 @@ def create_organizations(self, numnodes): generator.create_end_user, generator.create_peering_partner, generator.create_peering_group, + generator.create_site_owner, ] self.create_entities(numnodes, create_funcs) diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index d4a320ea9..e20cca615 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -127,10 +127,16 @@ def get_dropdown_keys(self, dropdown_name): return [ x[0] for x in Dropdown.get(dropdown_name).as_choices()[1:] ] ## Organizations + def rand_person_or_company_name(self): + person_name = '{} {}'.format(self.fake.first_name(), self.fake.last_name()) + company_name = self.fake.company() + name = random.choice((person_name, company_name)) + + return name def create_customer(self): # create object - name = self.fake.company() + name = self.rand_person_or_company_name() customer = self.get_or_create_node( name, 'Customer', META_TYPES[2]) # Relation @@ -149,7 +155,7 @@ def create_customer(self): def create_end_user(self): # create object - name = self.fake.company() + name = self.rand_person_or_company_name() enduser = self.get_or_create_node( name, 'End User', META_TYPES[2]) # Relation @@ -188,8 +194,6 @@ def create_peering_group(self): return peering_group - ## Equipment and cables - def create_provider(self): provider = self.get_or_create_node( self.fake.company(), 'Provider', META_TYPES[0]) @@ -206,6 +210,27 @@ def create_provider(self): 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( From c2666b9c835c2756d1a21db31db371ec94e2f17f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 3 Mar 2020 13:36:48 +0100 Subject: [PATCH 501/520] Typo in list --- src/niweb/apps/noclook/management/commands/datafaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index 6d73a0c6b..704dc254d 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -11,7 +11,7 @@ class Command(BaseCommand): help = 'Create fake data for the Network module' generated_types = [ - 'Customer', 'End User', 'Site Owner', 'Provider', 'Peering Group', 'Peering Partner' + 'Customer', 'End User', 'Site Owner', 'Provider', 'Peering Group', 'Peering Partner', 'Cable', 'Port', 'Host', 'Router', 'Switch'] def add_arguments(self, parser): From f45fe6db03ffe47bbd370cf3d99ed10c3a99cf7c Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 3 Mar 2020 15:14:28 +0100 Subject: [PATCH 502/520] ASN added to Peering partners --- src/niweb/apps/noclook/tests/stressload/data_generator.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index e20cca615..9d689a557 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -178,6 +178,13 @@ def create_peering_partner(self): peering_partner = self.get_or_create_node( name, 'Peering Partner', META_TYPES[2]) # Relation + data = { + 'as_number' : random.randint(10000, 99999), + } + + for key, value in data.items(): + provider.get_node().add_property(key, value) + # add context self.add_network_context(peering_partner) From 3105fada25cec376aab5aa166fab3a3a52d7b3c6 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 13 Mar 2020 09:38:02 +0100 Subject: [PATCH 503/520] JWT middleware fix when token is expired or not valid --- src/niweb/apps/noclook/middleware.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/niweb/apps/noclook/middleware.py b/src/niweb/apps/noclook/middleware.py index da260ebb0..722d735a3 100644 --- a/src/niweb/apps/noclook/middleware.py +++ b/src/niweb/apps/noclook/middleware.py @@ -4,6 +4,7 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.middleware import get_user +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 @@ -17,6 +18,9 @@ from importlib import import_module import time +import logging + +logger = logging.getLogger(__name__) def token_is_expired(token): ret = False @@ -57,13 +61,33 @@ def __call__(self, request): request.session.save() session_created = True - # process response with inner middleware - response = self.get_response(request) max_age = request.session.get_expiry_age() expires_time = time.time() + max_age + anti_expires_time = time.time() - max_age cookie_expires = cookie_date(expires_time) + if token and token_is_expired(token): + cookie_token = request.COOKIES.get(jwt_settings.JWT_COOKIE_NAME) + + if cookie_token and cookie_token != '""': + response = redirect('/') + 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( From b811d540f2ae2798601d24821e88a58f3fadc500 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 16 Mar 2020 11:01:09 +0100 Subject: [PATCH 504/520] Add user to all groups admin command and test --- .../noclook/management/commands/usrgroups.py | 45 +++++++++++++++++++ .../tests/management/test_usrgroups.py | 38 ++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/niweb/apps/noclook/management/commands/usrgroups.py create mode 100644 src/niweb/apps/noclook/tests/management/test_usrgroups.py diff --git a/src/niweb/apps/noclook/management/commands/usrgroups.py b/src/niweb/apps/noclook/management/commands/usrgroups.py new file mode 100644 index 000000000..db96c4430 --- /dev/null +++ b/src/niweb/apps/noclook/management/commands/usrgroups.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from apps.noclook.models import Context +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User, Group +from django.conf import settings + +import logging + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = 'Add an specific user to the permission groups.' + + def add_arguments(self, parser): + parser.add_argument("--username", + help="Which user should be added to this groups", type=str, default="admin") + + + def handle(self, *args, **options): + if options['username']: + username = options['username'] + self.add_groups(username) + return + + def add_groups(self, username): + user = User.objects.filter(username=username) + + if user: + user = User.objects.get(username=username) + 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()) + + group_names = [gr_name, gw_name, gl_name, ga_name] + + for group_name in group_names: + if Group.objects.filter(name=group_name): + group = Group.objects.get(name=group_name) + group.user_set.add(user) diff --git a/src/niweb/apps/noclook/tests/management/test_usrgroups.py b/src/niweb/apps/noclook/tests/management/test_usrgroups.py new file mode 100644 index 000000000..78139d37d --- /dev/null +++ b/src/niweb/apps/noclook/tests/management/test_usrgroups.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +__author__ = 'ffuentes' + +from django.core.management import call_command +from django.contrib.auth.models import User, Group + +from ..neo4j_base import NeoTestCase + +class UsrGroupTest(NeoTestCase): + cmd_name = 'usrgroups' + username = 'newadmin' + password = 'norduni' + + def test_usrgroups_cmd(self): + # create user + user = User.objects.create_user( + self.username, + password=self.username + ) + + user.is_superuser = True + user.is_staff = False + user.save() + + # check that doesn't have groups assigned + group_lst = list(user.groups.values_list('name', flat=True)) + self.assertEquals(group_lst, []) + + # call command + call_command( + self.cmd_name, + username=self.username, + verbosity=0, + ) + + # check that it has groups assigned + group_lst = list(user.groups.values_list('name', flat=True)) + self.assertNotEquals(group_lst, []) From 019a2a7c375b05cb099e3ae77a614bc460e170d5 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 16 Mar 2020 20:12:12 +0100 Subject: [PATCH 505/520] Instead redirecting, now it gets the user from session and refresh token --- src/niweb/apps/noclook/middleware.py | 51 +++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/niweb/apps/noclook/middleware.py b/src/niweb/apps/noclook/middleware.py index 722d735a3..5cffda442 100644 --- a/src/niweb/apps/noclook/middleware.py +++ b/src/niweb/apps/noclook/middleware.py @@ -4,6 +4,9 @@ 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 @@ -35,6 +38,15 @@ def token_is_expired(token): 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 @@ -69,21 +81,36 @@ def __call__(self, request): 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 != '""': - response = redirect('/') - 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',)) + 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 + return response # process response with inner middleware response = self.get_response(request) From aa7f88f5855e185829c365d5ba57f5b98af66982 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 18 Mar 2020 12:21:32 +0100 Subject: [PATCH 506/520] Bugfixes from graphtypes-split on the datagenerator --- .../noclook/management/commands/datafaker.py | 33 ++++-- .../tests/management/test_datafaker.py | 60 ++++++++++ .../tests/stressload/data_generator.py | 106 +++++++++++++----- 3 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 src/niweb/apps/noclook/tests/management/test_datafaker.py diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index 704dc254d..2746c9d1e 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -14,29 +14,34 @@ class Command(BaseCommand): '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("--organizations", + parser.add_argument("--{}".format(self.option_organizations), help="Create organization nodes", type=int, default=0) - parser.add_argument("--equipmentcables", + parser.add_argument("--{}".format(self.option_equipment), help="Create equipment and cables nodes", type=int, default=0) - parser.add_argument("-d", "--deleteall", action='store_true', + 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['deleteall']: + if options[self.option_deleteall]: self.delete_network_nodes() return - if options['organizations']: - numnodes = options['organizations'] + 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['equipmentcables']: - numnodes = options['equipmentcables'] + if options[self.option_equipment]: + numnodes = options[self.option_equipment] if numnodes > 0: self.stdout\ .write('Forging fake equipement & cables: {} for each subtype:'\ @@ -52,7 +57,17 @@ def create_entities(self, numnodes, create_funcs): for create_func in create_funcs: for i in range(numnodes): - node = create_func() + # 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) 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/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index 9d689a557..d53ee435d 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -9,6 +9,7 @@ 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 @@ -25,6 +26,27 @@ def __init__(self, seed=None): 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 + + +class CommunityFakeDataGenerator(FakeDataGenerator): def create_fake_contact(self): salutations = ['Ms.', 'Mr.', 'Dr.', 'Mrs.', 'Mx.'] contact_types_drop = Dropdown.objects.get(name='contact_type') @@ -33,8 +55,8 @@ def create_fake_contact(self): contact_dict = { 'salutation': random.choice(salutations), - 'first_name': self.fake.first_name(), - 'last_name': self.fake.last_name(), + 'first_name': self.first_name(), + 'last_name': self.last_name(), 'title': '', 'contact_role': self.fake.job(), 'contact_type': random.choice(contact_types), @@ -55,7 +77,7 @@ def create_fake_contact(self): return contact_dict def create_fake_organization(self): - organization_name = self.fake.company() + organization_name = self.company_name() organization_id = organization_name.upper() org_types_drop = Dropdown.objects.get(name='organization_types') @@ -84,18 +106,11 @@ def create_fake_group(self): return group_dict -class NetworkFakeDataGenerator: +class NetworkFakeDataGenerator(FakeDataGenerator): def __init__(self, seed=None): - locales = OrderedDict([ - ('en_GB', 1), - ('sv_SE', 2), - ]) - self.fake = Faker(locales) + super().__init__() - if seed: - self.fake.seed_instance(seed) - - self.user = user = get_user() + self.user = get_user() # set vars self.max_cable_providers = 5 @@ -127,13 +142,6 @@ def get_dropdown_keys(self, dropdown_name): return [ x[0] for x in Dropdown.get(dropdown_name).as_choices()[1:] ] ## Organizations - def rand_person_or_company_name(self): - person_name = '{} {}'.format(self.fake.first_name(), self.fake.last_name()) - company_name = self.fake.company() - name = random.choice((person_name, company_name)) - - return name - def create_customer(self): # create object name = self.rand_person_or_company_name() @@ -174,16 +182,16 @@ def create_end_user(self): def create_peering_partner(self): # create object - name = self.fake.company() + name = self.company_name() peering_partner = self.get_or_create_node( name, 'Peering Partner', META_TYPES[2]) # Relation data = { - 'as_number' : random.randint(10000, 99999), + 'as_number' : str(random.randint(0, 99999)).zfill(5), } for key, value in data.items(): - provider.get_node().add_property(key, value) + peering_partner.get_node().add_property(key, value) # add context self.add_network_context(peering_partner) @@ -192,7 +200,7 @@ def create_peering_partner(self): def create_peering_group(self): # create object - name = self.fake.company() + name = self.company_name() peering_group = self.get_or_create_node( name, 'Peering Group', META_TYPES[1]) # Logical @@ -203,13 +211,14 @@ def create_peering_group(self): def create_provider(self): provider = self.get_or_create_node( - self.fake.company(), 'Provider', META_TYPES[0]) + 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(): @@ -362,7 +371,7 @@ def create_host(self, name=None, type_name="Host", metatype=META_TYPES[0]): 'os': os_choice[0], 'os_version': random.choice(os_choice[1]), 'model': self.fake.license_plate(), - 'vendor': self.fake.company(), + 'vendor': self.company_name(), 'service_tag': self.fake.license_plate(), } @@ -410,3 +419,48 @@ def create_switch(self): 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) From 88103de5619c8ae4ea8ebab4f34ff4e318c90441 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 18 Mar 2020 14:36:55 +0100 Subject: [PATCH 507/520] Removed management command --- .../noclook/management/commands/usrgroups.py | 45 ------------------- .../tests/management/test_usrgroups.py | 38 ---------------- 2 files changed, 83 deletions(-) delete mode 100644 src/niweb/apps/noclook/management/commands/usrgroups.py delete mode 100644 src/niweb/apps/noclook/tests/management/test_usrgroups.py diff --git a/src/niweb/apps/noclook/management/commands/usrgroups.py b/src/niweb/apps/noclook/management/commands/usrgroups.py deleted file mode 100644 index db96c4430..000000000 --- a/src/niweb/apps/noclook/management/commands/usrgroups.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -__author__ = 'ffuentes' - -from apps.noclook.models import Context -from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User, Group -from django.conf import settings - -import logging - -logger = logging.getLogger(__name__) - -class Command(BaseCommand): - help = 'Add an specific user to the permission groups.' - - def add_arguments(self, parser): - parser.add_argument("--username", - help="Which user should be added to this groups", type=str, default="admin") - - - def handle(self, *args, **options): - if options['username']: - username = options['username'] - self.add_groups(username) - return - - def add_groups(self, username): - user = User.objects.filter(username=username) - - if user: - user = User.objects.get(username=username) - 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()) - - group_names = [gr_name, gw_name, gl_name, ga_name] - - for group_name in group_names: - if Group.objects.filter(name=group_name): - group = Group.objects.get(name=group_name) - group.user_set.add(user) diff --git a/src/niweb/apps/noclook/tests/management/test_usrgroups.py b/src/niweb/apps/noclook/tests/management/test_usrgroups.py deleted file mode 100644 index 78139d37d..000000000 --- a/src/niweb/apps/noclook/tests/management/test_usrgroups.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -__author__ = 'ffuentes' - -from django.core.management import call_command -from django.contrib.auth.models import User, Group - -from ..neo4j_base import NeoTestCase - -class UsrGroupTest(NeoTestCase): - cmd_name = 'usrgroups' - username = 'newadmin' - password = 'norduni' - - def test_usrgroups_cmd(self): - # create user - user = User.objects.create_user( - self.username, - password=self.username - ) - - user.is_superuser = True - user.is_staff = False - user.save() - - # check that doesn't have groups assigned - group_lst = list(user.groups.values_list('name', flat=True)) - self.assertEquals(group_lst, []) - - # call command - call_command( - self.cmd_name, - username=self.username, - verbosity=0, - ) - - # check that it has groups assigned - group_lst = list(user.groups.values_list('name', flat=True)) - self.assertNotEquals(group_lst, []) From f8004ff6b66f1353a567c65df0678c01dcf18ec6 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 20 Mar 2020 11:00:48 +0100 Subject: [PATCH 508/520] CORS whitelist is defined in the SRI_FRONTEND_URL envvar --- src/niweb/niweb/settings/common.py | 6 +++++- src/niweb/niweb/settings/prod.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index d1737cdfd..064a78bec 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -357,10 +357,14 @@ ########## SESSION_COOKIE_DOMAIN SESSION_COOKIE_HTTPONLY = False -CORS_ALLOW_CREDENTIALS = True COOKIE_DOMAIN = environ.get('COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = COOKIE_DOMAIN CSRF_COOKIE_DOMAIN = COOKIE_DOMAIN + +CORS_ALLOW_CREDENTIALS = False +CORS_ORIGIN_WHITELIST = [ + environ.get('SRI_FRONTEND_URL', 'https://sri.sunet.se') +] ########## END SESSION_COOKIE_DOMAIN ########## GRAPHQL CONFIGURATION diff --git a/src/niweb/niweb/settings/prod.py b/src/niweb/niweb/settings/prod.py index d3ba84b6f..95356129e 100644 --- a/src/niweb/niweb/settings/prod.py +++ b/src/niweb/niweb/settings/prod.py @@ -116,4 +116,3 @@ ########## END SECRET CONFIGURATION GOOGLE_MAPS_API_KEY = environ.get('GOOGLE_MAPS_API_KEY', 'no-apikey') -CORS_ORIGIN_ALLOW_ALL = False From 8d29247e5d63ea6cf199c8e3b09c8e49e674cd71 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 20 Mar 2020 14:50:05 +0100 Subject: [PATCH 509/520] Added fallback to clean rogue nodetype. --- src/niweb/apps/noclook/management/commands/datafaker.py | 3 +++ src/niweb/apps/noclook/tests/stressload/data_generator.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/niweb/apps/noclook/management/commands/datafaker.py b/src/niweb/apps/noclook/management/commands/datafaker.py index 2746c9d1e..72f27b7ea 100644 --- a/src/niweb/apps/noclook/management/commands/datafaker.py +++ b/src/niweb/apps/noclook/management/commands/datafaker.py @@ -71,6 +71,9 @@ def create_entities(self, numnodes, create_funcs): created_nodes = created_nodes + 1 self.printProgressBar(created_nodes, total_nodes) + NetworkFakeDataGenerator.clean_rogue_nodetype() + + def create_organizations(self, numnodes): generator = NetworkFakeDataGenerator() diff --git a/src/niweb/apps/noclook/tests/stressload/data_generator.py b/src/niweb/apps/noclook/tests/stressload/data_generator.py index d53ee435d..57b78c789 100644 --- a/src/niweb/apps/noclook/tests/stressload/data_generator.py +++ b/src/niweb/apps/noclook/tests/stressload/data_generator.py @@ -45,6 +45,10 @@ def rand_person_or_company_name(self): return name + @staticmethod + def clean_rogue_nodetype(): + NodeType.objects.filter(type="").delete() + class CommunityFakeDataGenerator(FakeDataGenerator): def create_fake_contact(self): From c7f6303bbc374e9db8a652dfef3d6c989c0b7396 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 23 Mar 2020 13:15:36 +0100 Subject: [PATCH 510/520] Corrections on how CORS policy should be run in the different envs --- src/niweb/niweb/settings/common.py | 8 ++++++-- src/niweb/niweb/settings/dev.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index 064a78bec..9794f253f 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -361,9 +361,13 @@ SESSION_COOKIE_DOMAIN = COOKIE_DOMAIN CSRF_COOKIE_DOMAIN = COOKIE_DOMAIN -CORS_ALLOW_CREDENTIALS = False +CORS_ALLOW_CREDENTIALS = True +CORS_ORIGIN_ALLOW_ALL = False CORS_ORIGIN_WHITELIST = [ - environ.get('SRI_FRONTEND_URL', 'https://sri.sunet.se') + '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 diff --git a/src/niweb/niweb/settings/dev.py b/src/niweb/niweb/settings/dev.py index 099c48234..1e8b5b13c 100644 --- a/src/niweb/niweb/settings/dev.py +++ b/src/niweb/niweb/settings/dev.py @@ -38,7 +38,6 @@ ########## SESSION_COOKIE_DOMAIN SESSION_COOKIE_HTTPONLY = False -CORS_ALLOW_CREDENTIALS = True ########## END SESSION_COOKIE_DOMAIN ########## EMAIL CONFIGURATION @@ -82,6 +81,7 @@ 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 From b83490cff4baff6b31d82ca6f048694a7bac2825 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 24 Mar 2020 11:07:57 +0100 Subject: [PATCH 511/520] If the user profile doesn't exists, create it --- src/niweb/apps/userprofile/views.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/niweb/apps/userprofile/views.py b/src/niweb/apps/userprofile/views.py index 4a5af93bc..35217f1f2 100644 --- a/src/niweb/apps/userprofile/views.py +++ b/src/niweb/apps/userprofile/views.py @@ -43,15 +43,21 @@ def userprofile_detail(request, userprofile_id): @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': request.user.profile.display_name, + 'display_name': user_profile.display_name, 'email': request.user.email, - 'landing_page': request.user.profile.landing_page, - 'landing_choices': request.user.profile.LANDING_CHOICES, - 'view_network': request.user.profile.view_network, - 'view_services': request.user.profile.view_services, - 'view_community': request.user.profile.view_community + '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) From 5dd939ab3e36a99dd385672815f14b9c35b32886 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 25 Mar 2020 08:43:05 +0100 Subject: [PATCH 512/520] Change not logged exception when the user doesn't have rigths over nodes --- src/niweb/apps/noclook/schema/core.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index b06326861..e0b938e89 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -536,8 +536,6 @@ def generic_byid_resolver(self, info, **args): 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 GraphQLAuthException() else: raise GraphQLError('A handle_id must be provided') @@ -572,8 +570,6 @@ def generic_list_resolver(self, info, **args): # 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() else: raise GraphQLAuthException() @@ -603,8 +599,6 @@ def generic_count_resolver(self, info, **args): # 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() else: raise GraphQLAuthException() @@ -774,8 +768,6 @@ def generic_list_resolver(self, info, **args): ret = [] return ret - else: - raise GraphQLAuthException() return generic_list_resolver From 8ecfd6d82049a60161fbe22b2d45b31d32be62a2 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 25 Mar 2020 11:34:17 +0100 Subject: [PATCH 513/520] Bugfix: relationship_parent_of field was not using relay id for update --- src/niweb/apps/noclook/schema/mutations.py | 37 +++++++++++++++---- .../apps/noclook/tests/schema/test_complex.py | 7 +++- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/niweb/apps/noclook/schema/mutations.py b/src/niweb/apps/noclook/schema/mutations.py index 1c8fbac25..18a3f6cf6 100644 --- a/src/niweb/apps/noclook/schema/mutations.py +++ b/src/niweb/apps/noclook/schema/mutations.py @@ -18,6 +18,8 @@ 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 * @@ -216,10 +218,21 @@ def do_request(cls, request, **kwargs): for field, roledict in DEFAULT_ROLES.items(): if field in post_data: - contact_id = post_data.get(field) - contact_id = relay.Node.from_global_id(contact_id)[1] + handle_id = post_data.get(field) + handle_id = relay.Node.from_global_id(handle_id)[1] post_data.pop(field) - post_data.update({field: contact_id}) + 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 @@ -330,10 +343,18 @@ def do_request(cls, request, **kwargs): # replace relay ids for handle_id in contacts if present for field, roledict in DEFAULT_ROLES.items(): if field in post_data: - contact_id = post_data.get(field) - contact_id = relay.Node.from_global_id(contact_id)[1] + 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: contact_id}) + post_data.update({field: handle_id}) form = form_class(post_data) form.strict_validation = True @@ -368,10 +389,10 @@ def do_request(cls, request, **kwargs): # Set child organizations if form.cleaned_data['relationship_parent_of']: - organization_nh = NodeHandle.objects.get(pk=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(pk=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 } diff --git a/src/niweb/apps/noclook/tests/schema/test_complex.py b/src/niweb/apps/noclook/tests/schema/test_complex.py index 49afa47fa..8b5a70e9d 100644 --- a/src/niweb/apps/noclook/tests/schema/test_complex.py +++ b/src/niweb/apps/noclook/tests/schema/test_complex.py @@ -747,6 +747,8 @@ def test_composite_organization(self): 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) @@ -760,6 +762,7 @@ def test_composite_organization(self): affiliation_site_owner: false affiliation_partner: true organization_id: "{org_id}" + relationship_parent_of: "{parent_org_id}" website: "{org_web}" organization_number: "{org_num}" }} @@ -981,8 +984,8 @@ def test_composite_organization(self): }} }} '''.format(org_id=organization_id, org_name=org_name, - org_type=org_type, org_web=org_web, - org_num=org_num, c3_first_name=c3_first_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, From d917b9f182fbb2b52198b6a521d8ebff7f14b935 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Wed, 25 Mar 2020 12:57:02 +0100 Subject: [PATCH 514/520] Enabled validators and fixed test --- src/niweb/apps/noclook/forms/common.py | 8 +++++--- src/niweb/apps/noclook/tests/schema/test_mutations.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/niweb/apps/noclook/forms/common.py b/src/niweb/apps/noclook/forms/common.py index a835012d7..a1698e13f 100644 --- a/src/niweb/apps/noclook/forms/common.py +++ b/src/niweb/apps/noclook/forms/common.py @@ -165,9 +165,9 @@ def relationship_field(name, select=False, validators=[]): } label = labels.get(name, name.title()) if select: - return forms.ChoiceField(required=False, label=label, widget=forms.widgets.Select, validators=[]) + 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, validators=[]) + return forms.IntegerField(required=False, label=label, widget=forms.widgets.HiddenInput, validators=validators) class ReserveIdForm(forms.Form): @@ -903,7 +903,7 @@ def __init__(self, *args, **kwargs): self.fields['it_security_contact'].choices = contact_choices self.fields['it_manager_contact'].choices = contact_choices - relationship_parent_of = relationship_field('organization', True, [validate_contact]) + 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]) @@ -931,6 +931,8 @@ def clean(self): 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) diff --git a/src/niweb/apps/noclook/tests/schema/test_mutations.py b/src/niweb/apps/noclook/tests/schema/test_mutations.py index 0df82831c..429126ec2 100644 --- a/src/niweb/apps/noclook/tests/schema/test_mutations.py +++ b/src/niweb/apps/noclook/tests/schema/test_mutations.py @@ -1172,7 +1172,7 @@ def test_node_validation(self): organization2_id = relay.Node.to_global_id('Organization', str(self.organization2.handle_id)) contact_1 = relay.Node.to_global_id('Contact', - str(self.organization2.handle_id)) + str(self.contact1.handle_id)) query = ''' mutation{{ From 8c73669af9a2f7ba7a2d5b434469f917d676dcb3 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 26 Mar 2020 11:22:53 +0100 Subject: [PATCH 515/520] Fix: An empty connection is returned if the user doesn't have rights --- src/niweb/apps/noclook/schema/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/niweb/apps/noclook/schema/core.py b/src/niweb/apps/noclook/schema/core.py index e0b938e89..76fcc5de6 100644 --- a/src/niweb/apps/noclook/schema/core.py +++ b/src/niweb/apps/noclook/schema/core.py @@ -764,10 +764,10 @@ def generic_list_resolver(self, info, **args): ret = list(qs) - if not ret: - ret = [] + if not ret: + ret = [] - return ret + return ret return generic_list_resolver From bb30c5d82179a330f4c8bb0a5e287002efb26506 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Thu, 26 Mar 2020 13:43:23 +0100 Subject: [PATCH 516/520] Fix:The GroupContextAuthzAction objs are created regardless of the users --- .../0017_auth_default_groups_20191023_0823.py | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) 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 index 2671570b5..6a0d7df6d 100644 --- 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 @@ -7,16 +7,12 @@ def forwards_func(apps, schema_editor): # get models - User = apps.get_model('auth', 'User') 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') - # get staff users only - users = User.objects.filter(is_staff=True) - contexts = Context.objects.all() for context in contexts: @@ -31,21 +27,15 @@ def forwards_func(apps, schema_editor): groupl, created = Group.objects.get_or_create(name=gl_name) groupa, created = Group.objects.get_or_create(name=ga_name) - for user in users: - groupr.user_set.add(user) - groupw.user_set.add(user) - groupl.user_set.add(user) - groupa.user_set.add(user) - - 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) + 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 ) + 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): From f4ab6b1eea78f1e084fc6461e4aef0921bd38839 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Fri, 27 Mar 2020 09:42:49 +0100 Subject: [PATCH 517/520] Add migration left out by merge --- .../migrations/0002_auto_20190121_0856.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/niweb/apps/scan/migrations/0002_auto_20190121_0856.py 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), + ), + ] From 37298b7719099f9a2ad8e431a2963b2b5475dd3e Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Mon, 30 Mar 2020 14:08:08 +0200 Subject: [PATCH 518/520] Bugfix and config change: Session expires at browser close --- src/niweb/apps/noclook/middleware.py | 15 ++++++--------- src/niweb/niweb/settings/common.py | 2 ++ src/niweb/niweb/settings/prod.py | 1 - 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/niweb/apps/noclook/middleware.py b/src/niweb/apps/noclook/middleware.py index 5cffda442..82b0b4340 100644 --- a/src/niweb/apps/noclook/middleware.py +++ b/src/niweb/apps/noclook/middleware.py @@ -76,9 +76,13 @@ def __call__(self, request): max_age = request.session.get_expiry_age() expires_time = time.time() + max_age - anti_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) @@ -137,6 +141,7 @@ def __call__(self, request): 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, @@ -171,14 +176,6 @@ def __call__(self, request): SESSION_SAVE_EVERY_REQUEST = None if (modified or SESSION_SAVE_EVERY_REQUEST) and not empty or create_session_cookie: - if request.session.get_expire_at_browser_close(): - max_age = None - expires = None - else: - max_age = request.session.get_expiry_age() - expires_time = time.time() + max_age - expires = cookie_date(expires_time) - # Save the session data and refresh the client cookie. # Skip session save for 500 responses, refs #3881. if response.status_code != 500: diff --git a/src/niweb/niweb/settings/common.py b/src/niweb/niweb/settings/common.py index 9794f253f..00eea6943 100644 --- a/src/niweb/niweb/settings/common.py +++ b/src/niweb/niweb/settings/common.py @@ -356,7 +356,9 @@ ########## 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 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 = { From fc4a3d9d70cdf6725953b5a9977282b699391830 Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 31 Mar 2020 10:24:05 +0200 Subject: [PATCH 519/520] Merge migration --- .../migrations/0020_merge_20200331_0709.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/niweb/apps/noclook/migrations/0020_merge_20200331_0709.py 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 = [ + ] From bba7fb4b42809beb23f7e006e4e560a59297a11f Mon Sep 17 00:00:00 2001 From: Francisco Fuentes Date: Tue, 31 Mar 2020 10:24:25 +0200 Subject: [PATCH 520/520] Fix on userprofile_link tag --- src/niweb/apps/userprofile/templatetags/userprofile_tags.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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