From 598e952ef748e3b0c106f81e3602f2986a0ea7e7 Mon Sep 17 00:00:00 2001 From: Benn Oshrin Date: Mon, 28 Feb 2022 13:29:35 -0500 Subject: [PATCH] Initial implementation of Address (CFM-12), AdHocAttribute (CFM-13), TelephoneNumber (CFM-16), URL (CFM-17) and associated refactoring --- app/config/schema/schema.json | 130 +++++++++- app/resources/locales/en_US/controller.po | 15 ++ app/resources/locales/en_US/defaultType.po | 18 ++ app/resources/locales/en_US/enumeration.po | 24 ++ app/resources/locales/en_US/error.po | 14 +- app/resources/locales/en_US/field.po | 77 ++++++ app/resources/locales/en_US/operation.po | 2 +- app/src/Command/TransmogrifyCommand.php | 141 ++++++++++- .../Controller/AdHocAttributesController.php | 42 ++++ app/src/Controller/AddressesController.php | 42 ++++ app/src/Controller/ApiV2Controller.php | 4 +- app/src/Controller/AppController.php | 167 +++++++------ .../Controller/EmailAddressesController.php | 22 -- app/src/Controller/IdentifiersController.php | 22 -- app/src/Controller/MVEAController.php | 27 ++ app/src/Controller/PersonRolesController.php | 45 ++++ app/src/Controller/StandardController.php | 23 +- .../Controller/TelephoneNumbersController.php | 63 +++++ app/src/Controller/UrlsController.php | 42 ++++ .../PermittedTelephoneNumberFieldsEnum.php | 41 +++ app/src/Lib/Traits/HistoryTrait.php | 10 +- app/src/Lib/Traits/MVETrait.php | 22 ++ app/src/Lib/Traits/PrimaryLinkTrait.php | 147 ++++++++--- app/src/Lib/Traits/ReadOnlyEntityTrait.php | 58 +++++ app/src/Lib/Traits/ValidationTrait.php | 125 ++++++++-- app/src/Model/Entity/AdHocAttribute.php | 43 ++++ app/src/Model/Entity/Address.php | 43 ++++ app/src/Model/Entity/ApiUser.php | 2 + app/src/Model/Entity/Co.php | 14 ++ app/src/Model/Entity/CoSetting.php | 28 ++- app/src/Model/Entity/Cou.php | 2 + app/src/Model/Entity/Dashboard.php | 2 + app/src/Model/Entity/EmailAddress.php | 1 + app/src/Model/Entity/ExternalIdentity.php | 2 + app/src/Model/Entity/HistoryRecord.php | 2 + app/src/Model/Entity/Identifier.php | 3 +- app/src/Model/Entity/Name.php | 1 + app/src/Model/Entity/Person.php | 2 + app/src/Model/Entity/PersonRole.php | 42 ++++ app/src/Model/Entity/TelephoneNumber.php | 70 ++++++ app/src/Model/Entity/Type.php | 2 + app/src/Model/Entity/Url.php | 43 ++++ app/src/Model/Table/AdHocAttributesTable.php | 128 ++++++++++ app/src/Model/Table/AddressesTable.php | 204 +++++++++++++++ app/src/Model/Table/ApiUsersTable.php | 97 +++----- app/src/Model/Table/CoSettingsTable.php | 111 +++++++-- app/src/Model/Table/CosTable.php | 50 +--- app/src/Model/Table/CousTable.php | 65 ++--- app/src/Model/Table/EmailAddressesTable.php | 73 ++---- app/src/Model/Table/HistoryRecordsTable.php | 54 ++-- app/src/Model/Table/IdentifiersTable.php | 67 ++--- app/src/Model/Table/NamesTable.php | 170 ++----------- app/src/Model/Table/PeopleTable.php | 51 ++-- app/src/Model/Table/PersonRolesTable.php | 233 ++++++++++++++++++ app/src/Model/Table/TelephoneNumbersTable.php | 174 +++++++++++++ app/src/Model/Table/TypesTable.php | 74 +++--- app/src/Model/Table/UrlsTable.php | 156 ++++++++++++ app/templates/AdHocAttributes/columns.inc | 35 +++ app/templates/AdHocAttributes/fields.inc | 33 +++ app/templates/Addresses/columns.inc | 39 +++ app/templates/Addresses/fields.inc | 50 ++++ app/templates/CoSettings/fields.inc | 8 + app/templates/HistoryRecords/fields.inc | 16 ++ app/templates/People/fields.inc | 67 ++++- app/templates/PersonRoles/columns.inc | 49 ++++ app/templates/PersonRoles/fields.inc | 103 ++++++++ app/templates/Standard/add-edit-view.php | 4 +- app/templates/Standard/index.php | 4 +- app/templates/TelephoneNumbers/columns.inc | 35 +++ app/templates/TelephoneNumbers/fields.inc | 42 ++++ app/templates/Urls/columns.inc | 35 +++ app/templates/Urls/fields.inc | 35 +++ app/templates/element/breadcrumbs.php | 17 +- 73 files changed, 3159 insertions(+), 745 deletions(-) create mode 100644 app/src/Controller/AdHocAttributesController.php create mode 100644 app/src/Controller/AddressesController.php create mode 100644 app/src/Controller/PersonRolesController.php create mode 100644 app/src/Controller/TelephoneNumbersController.php create mode 100644 app/src/Controller/UrlsController.php create mode 100644 app/src/Lib/Enum/PermittedTelephoneNumberFieldsEnum.php create mode 100644 app/src/Lib/Traits/ReadOnlyEntityTrait.php create mode 100644 app/src/Model/Entity/AdHocAttribute.php create mode 100644 app/src/Model/Entity/Address.php create mode 100644 app/src/Model/Entity/PersonRole.php create mode 100644 app/src/Model/Entity/TelephoneNumber.php create mode 100644 app/src/Model/Entity/Url.php create mode 100644 app/src/Model/Table/AdHocAttributesTable.php create mode 100644 app/src/Model/Table/AddressesTable.php create mode 100644 app/src/Model/Table/PersonRolesTable.php create mode 100644 app/src/Model/Table/TelephoneNumbersTable.php create mode 100644 app/src/Model/Table/UrlsTable.php create mode 100644 app/templates/AdHocAttributes/columns.inc create mode 100644 app/templates/AdHocAttributes/fields.inc create mode 100644 app/templates/Addresses/columns.inc create mode 100644 app/templates/Addresses/fields.inc create mode 100644 app/templates/PersonRoles/columns.inc create mode 100644 app/templates/PersonRoles/fields.inc create mode 100644 app/templates/TelephoneNumbers/columns.inc create mode 100644 app/templates/TelephoneNumbers/fields.inc create mode 100644 app/templates/Urls/columns.inc create mode 100644 app/templates/Urls/fields.inc diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index 13861a45b..297b7f8fb 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -10,11 +10,14 @@ "columns": { "co_id": { "type": "integer", "foreignkey": { "table": "cos", "column": "id" }, "notnull": true }, + "cou_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" } }, "description": { "type": "string", "size": 128 }, "external_identity_id": { "type": "integer", "foreignkey": { "table": "external_identities", "column": "id" } }, "id": { "type": "integer", "autoincrement": true, "primarykey": true }, + "language": { "type": "string", "size": 16 }, "name": { "type": "string", "size": 128, "notnull": true }, "person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } }, + "person_role_id": { "type": "integer", "foreignkey": { "table": "person_roles", "column": "id" } }, "status": { "type": "string", "size": 2 }, "type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" }, "notnull": true }, "valid_from": { "type": "datetime" }, @@ -62,11 +65,15 @@ "id": {}, "co_id": {}, "address_required_fields": { "type": "string", "size": 160 }, + "address_default_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, "email_address_default_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, "identifier_default_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, "name_default_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, "name_permitted_fields": { "type": "string", "size": 160 }, - "name_required_fields": { "type": "string", "size": 160 } + "name_required_fields": { "type": "string", "size": 160 }, + "telephone_number_default_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, + "telephone_number_permitted_fields": { "type": "string", "size": 160 }, + "url_default_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } } }, "indexes": { "co_settings_i1": { "columns": [ "co_id" ]}, @@ -78,7 +85,10 @@ "columns": [ "name_default_type_id" ] }, "co_settings_i3": { "columns": [ "email_address_default_type_id" ] }, - "co_settings_i4": { "columns": [ "identifier_default_type_id" ] } + "co_settings_i4": { "columns": [ "identifier_default_type_id" ] }, + "co_settings_i5": { "columns": [ "address_default_type_id" ] }, + "co_settings_i6": { "columns": [ "telephone_number_default_type_id" ] }, + "co_settings_i7": { "columns": [ "url_default_type_id" ] } } }, @@ -146,8 +156,37 @@ } }, + "person_roles": { + "columns": { + "id": {}, + "person_id": { "notnull": true }, + "status": {}, + "ordr": { "type": "integer" }, + "cou_id": {}, + "affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, + "title": { "type": "string", "size": 128 }, + "organization": { "type": "string", "size": 128 }, + "department": { "type": "string", "size": 128 }, + "manager_person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } }, + "sponsor_person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } }, + "valid_from": {}, + "valid_through": {} + }, + "indexes": { + "person_roles_i1": { "columns": [ "person_id" ] }, + "person_roles_i2": { "columns": [ "sponsor_person_id" ] }, + "person_roles_i3": { "columns": [ "cou_id" ] }, + "person_roles_i4": { "columns": [ "affiliation_type_id" ] }, + "person_roles_i5": { "columns": [ "manager_person_id" ] } + } + }, + "external_identities": { - "comment": "XXX most of these fields are going to move to person_roles instead", + "comment": [ + "XXX most of these fields are going to move to person_roles instead", + "XXX how is manager_identifier and sponsor_identifier going to work? we can fk from people but not external_identities", + "XXX affiliation should become affiliation_type_id" + ], "columns": { "id": {}, @@ -158,6 +197,8 @@ "title": { "type": "string", "size": 128 }, "organization": { "type": "string", "size": 128 }, "department": { "type": "string", "size": 128 }, + "manager_identifier": { "type": "string", "size": 512 }, + "sponsor_identifier": { "type": "string", "size": 512 }, "valid_from": {}, "valid_through": {} }, @@ -175,7 +216,7 @@ "family": { "type": "string", "size": 128 }, "suffix": { "type": "string", "size": 32 }, "type_id": {}, - "language": { "type": "string", "size": 16 }, + "language": {}, "primary_name": { "type": "boolean" }, "display_name": { "type": "string", "size": 256 } }, @@ -186,6 +227,58 @@ "sourced": true }, + "ad_hoc_attributes": { + "comment": "we use 'tag' instead of 'key' since the latter is reserved by mysql", + + "columns": { + "id": {}, + "tag": { "type": "string", "size": 128 }, + "value": { "type": "string", "size": 256 } + }, + "indexes": { + "ad_hoc_attributes_i1": { "columns": [ "tag" ] } + }, + "mvea": [ "person", "person_role", "external_identity" ], + "sourced": true + }, + + "addresses": { + "columns": { + "id": {}, + "street": { "type": "text" }, + "room": { "type": "string", "size": 64 }, + "locality": { "type": "string", "size": 128 }, + "state": { "type": "string", "size": 128 }, + "postal_code": { "type": "string", "size": 16 }, + "country": { "type": "string", "size": 128 }, + "description": {}, + "type_id": {}, + "language": {} + }, + "indexes": { + "addresses_i1": { "columns": [ "type_id" ] } + }, + "mvea": [ "person", "person_role", "external_identity" ], + "sourced": true + }, + + "email_addresses": { + "columns": { + "id": {}, + "mail": { "type": "string", "size": 256 }, + "description": {}, + "type_id": {}, + "verified": { "type": "boolean" } + }, + "indexes": { + "email_addresses_i1": { "columns": [ "mail", "type_id", "person_id" ] }, + "email_addresses_i2": { "columns": [ "mail", "type_id", "external_identity_id" ] }, + "email_addresses_i3": { "columns": [ "type_id" ] } + }, + "mvea": [ "person", "external_identity" ], + "sourced": true + }, + "identifiers": { "columns": { "id": {}, @@ -203,18 +296,32 @@ "sourced": true }, - "email_addresses": { + "telephone_numbers": { "columns": { "id": {}, - "mail": { "type": "string", "size": 256 }, + "country_code": { "type": "string", "size": 3 }, + "area_code": { "type": "string", "size": 8 }, + "number": { "type": "string", "size": 64 }, + "extension": { "type": "string", "size": 16 }, "description": {}, - "type_id": {}, - "verified": { "type": "boolean" } + "type_id": {} }, "indexes": { - "email_addresses_i1": { "columns": [ "mail", "type_id", "person_id" ] }, - "email_addresses_i2": { "columns": [ "mail", "type_id", "external_identity_id" ] }, - "email_addresses_i3": { "columns": [ "type_id" ] } + "telephone_numbers_i1": { "columns": [ "type_id" ] } + }, + "mvea": [ "person", "person_role", "external_identity" ], + "sourced": true + }, + + "urls": { + "columns": { + "id": {}, + "url": { "type": "string", "size": 256 }, + "description": {}, + "type_id": {} + }, + "indexes": { + "urls_i1": { "columns": [ "type_id" ] } }, "mvea": [ "person", "external_identity" ], "sourced": true @@ -228,6 +335,7 @@ "action": { "type": "string", "size": 4 }, "comment": { "type": "string", "size": 256 }, "person_id": {}, + "person_role_id": {}, "external_identity_id": {}, "actor_person_id": { "type": "integer", "foreignkey": { "table": "people", "column": "id" } } }, diff --git a/app/resources/locales/en_US/controller.po b/app/resources/locales/en_US/controller.po index 095979c1f..8221d2325 100644 --- a/app/resources/locales/en_US/controller.po +++ b/app/resources/locales/en_US/controller.po @@ -24,6 +24,12 @@ # Controllers (Models) +msgid "Addresses" +msgstr "{0,plural,=1{Address} other{Addresses}}" + +msgid "AdHocAttributes" +msgstr "{0,plural,=1{Ad Hoc Attribute} other{Ad Hoc Attributes}}" + msgid "ApiUsers" msgstr "{0,plural,=1{API User} other{API Users}}" @@ -57,5 +63,14 @@ msgstr "{0,plural,=1{Name} other{Names}}" msgid "People" msgstr "{0,plural,=1{Person} other{People}}" +msgid "PersonRoles" +msgstr "{0,plural,=1{Person Role} other{Person Roles}}" + +msgid "TelephoneNumbers" +msgstr "{0,plural,=1{Telephone Number} other{Telephone Numbers}}" + msgid "Types" msgstr "{0,plural,=1{Type} other{Types}}" + +msgid "Urls" +msgstr "{0,plural,=1{URL} other{URLs}}" \ No newline at end of file diff --git a/app/resources/locales/en_US/defaultType.po b/app/resources/locales/en_US/defaultType.po index e143f504e..d2eb532b8 100644 --- a/app/resources/locales/en_US/defaultType.po +++ b/app/resources/locales/en_US/defaultType.po @@ -24,6 +24,18 @@ # Labels for default types when a new CO is created +msgid "Addresses.campus" +msgstr "Campus" + +msgid "Addresses.home" +msgstr "Home" + +msgid "Addresses.office" +msgstr "Office" + +msgid "Addresses.postal" +msgstr "Postal" + msgid "EmailAddresses.delivery" msgstr "Delivery" @@ -113,3 +125,9 @@ msgstr "Official" msgid "Names.preferred" msgstr "Preferred" + +msgid "Urls.official" +msgstr "Official" + +msgid "Urls.personal" +msgstr "Personal" diff --git a/app/resources/locales/en_US/enumeration.po b/app/resources/locales/en_US/enumeration.po index e65ebebd4..6e78f662a 100644 --- a/app/resources/locales/en_US/enumeration.po +++ b/app/resources/locales/en_US/enumeration.po @@ -186,6 +186,30 @@ msgstr "Honorific, Given, Family, Suffix" msgid "PermittedNameFieldsEnum.honorific,given,middle,family,suffix" msgstr "Honorific, Given, Middle, Family, Suffix" +msgid "PermittedTelephoneNumberFieldsEnum.number" +msgstr "Number" + +msgid "PermittedTelephoneNumberFieldsEnum.number,extension" +msgstr "Number, Extension" + +msgid "PermittedTelephoneNumberFieldsEnum.area_code,number" +msgstr "Area Code, Number" + +msgid "PermittedTelephoneNumberFieldsEnum.area_code,number,extension" +msgstr "Area Code, Number, Extension" + +msgid "PermittedTelephoneNumberFieldsEnum.country_code,number" +msgstr "Country Code, Number" + +msgid "PermittedTelephoneNumberFieldsEnum.country_code,number,extension" +msgstr "Country Code, Number, Extension" + +msgid "PermittedTelephoneNumberFieldsEnum.country_code,area_code,number" +msgstr "Country Code, Area Code, Number" + +msgid "PermittedTelephoneNumberFieldsEnum.country_code,area_code,number,extension" +msgstr "Country Code, Area Code, Number, Extension" + msgid "RequiredAddressFieldsEnum.country" msgstr "Country" diff --git a/app/resources/locales/en_US/error.po b/app/resources/locales/en_US/error.po index df939edc8..20eb24700 100644 --- a/app/resources/locales/en_US/error.po +++ b/app/resources/locales/en_US/error.po @@ -102,6 +102,15 @@ msgstr "When this value is selected, {0} cannot be empty" msgid "input.invalid" msgstr "Invalid character found" +msgid "input.invalid.email" +msgstr "The provided value is not a valid email address" + +msgid "input.invalid.url" +msgstr "The provided value is not a valid URL" + +msgid "input.length" +msgstr "The provided value cannot be longer than {0} characters" + msgid "invalid" msgstr "Invalid value \"{0}\"" @@ -130,7 +139,10 @@ msgid "perm" msgstr "Permission Denied" msgid "primary_link" -msgstr "Could not find value for Primary Link {0}" +msgstr "Could not find value for Primary Link" + +msgid "primary_link.mismatch" +msgstr "All records must have the same Primary Link" msgid "rule.ValidateCo.errorField" msgstr "errorField not set in ruleValidateCO" diff --git a/app/resources/locales/en_US/field.po b/app/resources/locales/en_US/field.po index 5b6e295e1..4ecc006a2 100644 --- a/app/resources/locales/en_US/field.po +++ b/app/resources/locales/en_US/field.po @@ -35,12 +35,21 @@ msgstr "Actor" msgid "api_key" msgstr "API Key" +msgid "affiliation" +msgstr "Affiliation" + +msgid "area_code" +msgstr "Area Code" + msgid "attribute" msgstr "Attribute" msgid "comment" msgstr "Comment" +msgid "CoSettings.address_default_type_id" +msgstr "Default Address Type" + msgid "CoSettings.address_required_fields" msgstr "Address Required Fields" @@ -59,12 +68,30 @@ msgstr "Name Permitted Fields" msgid "CoSettings.name_required_fields" msgstr "Name Required Fields" +msgid "CoSettings.telephone_number_default_type_id" +msgstr "Default Telephone Number Type" + +msgid "CoSettings.telephone_number_permitted_fields" +msgstr "Telephone Number Permitted Fields" + +msgid "CoSettings.url_default_type_id" +msgstr "Default URL Type" + +msgid "country" +msgstr "Country" + +msgid "country_code" +msgstr "Country Code" + msgid "created" msgstr "Created" msgid "date_of_birth" msgstr "Date of Birth" +msgid "department" +msgstr "Department" + msgid "description" msgstr "Description" @@ -74,6 +101,9 @@ msgstr "Display Name" msgid "edupersonaffiliation" msgstr "eduPersonAffiliation" +msgid "extension" +msgstr "Extension" + msgid "id" msgstr "ID" @@ -102,21 +132,47 @@ msgstr "Full Name" msgid "language" msgstr "Language" +msgid "locality" +msgstr "Locality" + msgid "login" msgstr "Login" msgid "mail" msgstr "Email Address" +msgid "manager" +msgstr "Manager" + msgid "middle" msgstr "Middle" msgid "name" msgstr "Name" +msgid "TelephoneNumbers.formatted_number" +msgstr "Telephone Number" + +msgid "TelephoneNumbers.number" +msgstr "Telephone Number" + +# This field is for rendering the telephone number into a string (eg: 555 1212 x279) +msgid "TelephoneNumbers.number.ext" +msgstr "x" + +# This field is called "ordr" to avoid conflicts with MySQL +msgid "ordr" +msgstr "Order" + +msgid "organization" +msgstr "Organization" + msgid "parent_id" msgstr "Parent" +msgid "postal_code" +msgstr "Postal Code" + msgid "primary_name" msgstr "Primary Name" @@ -135,18 +191,39 @@ msgstr "If specified, a 'person_id' ] ], + 'person_roles' => [ + 'source' => 'cm_co_person_roles', + 'displayField' => 'id', + 'fieldMap' => [ + 'co_person_id' => 'person_id', + // Rename the changelog key + 'co_person_role_id' => 'person_role_id', + // We need to map affiliation_type_id before we null out affiliation + 'affiliation_type_id' => '&map_affiliation_type', + 'affiliation' => null, + 'manager_co_person_id' => 'manager_person_id', + 'sponsor_co_person_id' => 'sponsor_person_id', + 'o' => 'organization', + 'ou' => 'department', +// XXX temporary until tables are migrated + 'source_org_identity_id' => null + ] + ], 'external_identities' => [ 'source' => 'cm_org_identities', 'displayField' => 'id', @@ -146,6 +164,44 @@ class TransmogrifyCommand extends Command { 'type' => null ] ], + 'ad_hoc_attributes' => [ + 'source' => 'cm_ad_hoc_attributes', + 'displayField' => 'id', + 'fieldMap' => [ + 'co_person_role_id' => 'person_role_id', + 'org_identity_id' => 'external_identity_id', +// XXX temporary until tables are migrated + 'co_department_id' => null, + 'organization_id' => null + ] + ], + 'addresses' => [ + 'source' => 'cm_addresses', + 'displayField' => 'id', + 'fieldMap' => [ + 'co_person_role_id' => 'person_role_id', + 'org_identity_id' => 'external_identity_id', + 'type_id' => '&map_address_type', + 'type' => null, +// XXX temporary until tables are migrated + 'co_department_id' => null, + 'organization_id' => null + ] + ], + 'email_addresses' => [ + 'source' => 'cm_email_addresses', + 'displayField' => 'id', + 'booleans' => [ 'verified' ], + 'fieldMap' => [ + 'co_person_id' => 'person_id', + 'org_identity_id' => 'external_identity_id', + 'type_id' => '&map_email_type', + 'type' => null, +// XXX temporary until tables are migrated + 'co_department_id' => null, + 'organization_id' => null + ] + ], 'identifiers' => [ 'source' => 'cm_identifiers', 'displayField' => 'id', @@ -162,14 +218,26 @@ class TransmogrifyCommand extends Command { 'organization_id' => null ] ], - 'email_addresses' => [ - 'source' => 'cm_email_addresses', + 'telephone_numbers' => [ + 'source' => 'cm_telephone_numbers', + 'displayField' => 'id', + 'fieldMap' => [ + 'co_person_role_id' => 'person_role_id', + 'org_identity_id' => 'external_identity_id', + 'type_id' => '&map_telephone_type', + 'type' => null, +// XXX temporary until tables are migrated + 'co_department_id' => null, + 'organization_id' => null + ] + ], + 'urls' => [ + 'source' => 'cm_urls', 'displayField' => 'id', - 'booleans' => [ 'verified' ], 'fieldMap' => [ 'co_person_id' => 'person_id', 'org_identity_id' => 'external_identity_id', - 'type_id' => '&map_email_type', + 'type_id' => '&map_url_type', 'type' => null, // XXX temporary until tables are migrated 'co_department_id' => null, @@ -183,8 +251,8 @@ class TransmogrifyCommand extends Command { 'co_person_id' => 'person_id', 'org_identity_id' => 'external_identity_id', 'actor_co_person_id' => 'actor_person_id', + 'co_person_role_id' => 'person_role_id', // XXX temporary until tables are migrated - 'co_person_role_id' => null, 'co_group_id' => null, 'co_email_list_id' => null, 'co_service_id' => null @@ -322,7 +390,7 @@ public function execute(Arguments $args, ConsoleIo $io) { while($row = $stmt->fetch()) { if(!empty($row[ $this->tables[$t]['displayField'] ])) { - $io->out($row[ $this->tables[$t]['displayField'] ] . "...", 0); + $io->out("$t " . $row[ $this->tables[$t]['displayField'] ] . "...", 0); } try { @@ -529,11 +597,35 @@ protected function mapFields(string $table, array &$row) { } } + /** + * Map an address type string to a foreign key. + * + * @since COmanage Registry v5.0.0 + * @param array $row Row of table data + * @return int type_id + */ + + protected function map_address_type(array $row) { + return $this->map_type($row, 'Addresses.type', $this->findCoId($row)); + } + + /** + * Map an affiliation type string to a foreign key. + * + * @since COmanage Registry v5.0.0 + * @param array $row Row of table data + * @return int type_id + */ + + protected function map_affiliation_type(array $row) { + return $this->map_type($row, 'PersonRoles.affiliation', $this->findCoId($row), 'affiliation'); + } + /** * Map an email type string to a foreign key. * * @since COmanage Registry v5.0.0 - * @param array $row Row of table data (ignored) + * @param array $row Row of table data * @return int type_id */ @@ -658,23 +750,36 @@ protected function map_org_identity_co_person_id(array $row) { return null; } + /** + * Map a telephone type string to a foreign key. + * + * @since COmanage Registry v5.0.0 + * @param array $row Row of table data + * @return int type_id + */ + + protected function map_telephone_type(array $row) { + return $this->map_type($row, 'TelephoneNumbers.type', $this->findCoId($row)); + } + /** * Map a type string to a foreign key. * * @since COmanage Registry v5.0.0 - * @param array $row Row of table data (ignored) - * @param string $type Type to map (types:attribute) - * @param int $coId CO ID + * @param array $row Row of table data (ignored) + * @param string $type Type to map (types:attribute) + * @param int $coId CO ID + * @param string $attr Row column to use for type value * @return int type_id * @throws InvalidArgumentException */ - protected function map_type(array $row, string $type, $coId) { + protected function map_type(array $row, string $type, $coId, string $attr="type") { if(!$coId) { throw new \InvalidArgumentException("CO ID not provided for $type " . $row['id']); } - $key = $coId . "+" . $type . "+" . $row['type'] . "+"; + $key = $coId . "+" . $type . "+" . $row[$attr] . "+"; if(empty($this->cache['types']['co_id+attribute+value+'][$key])) { throw new \InvalidArgumentException("Type not found for " . $key); @@ -682,4 +787,16 @@ protected function map_type(array $row, string $type, $coId) { return $this->cache['types']['co_id+attribute+value+'][$key]; } + + /** + * Map a URL type string to a foreign key. + * + * @since COmanage Registry v5.0.0 + * @param array $row Row of table data + * @return int type_id + */ + + protected function map_url_type(array $row) { + return $this->map_type($row, 'Urls.type', $this->findCoId($row)); + } } diff --git a/app/src/Controller/AdHocAttributesController.php b/app/src/Controller/AdHocAttributesController.php new file mode 100644 index 000000000..b0bfa35aa --- /dev/null +++ b/app/src/Controller/AdHocAttributesController.php @@ -0,0 +1,42 @@ + [ + 'AdHocAttributes.tag' => 'asc' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Controller/AddressesController.php b/app/src/Controller/AddressesController.php new file mode 100644 index 000000000..abe7d0ac0 --- /dev/null +++ b/app/src/Controller/AddressesController.php @@ -0,0 +1,42 @@ + [ + 'Addresses.street' => 'asc' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Controller/ApiV2Controller.php b/app/src/Controller/ApiV2Controller.php index 0e87b768b..dca6f4f9e 100644 --- a/app/src/Controller/ApiV2Controller.php +++ b/app/src/Controller/ApiV2Controller.php @@ -176,7 +176,7 @@ public function edit($id) { // Pull the current record $obj = $query->firstOrFail(); - if(method_exists($this->$modelsName, "isReadOnly") && $this->$modelsName->isReadOnly($obj)) { + if(method_exists($obj, "isReadOnly") && $obj->isReadOnly()) { throw new BadRequestException(__d('error', 'edit.readonly')); } @@ -278,7 +278,7 @@ public function index() { // We automatically allow API calls to be filtered on primary link if(!empty($link->attr) && !empty($link->value)) { - $query = $query->where([$table->getAlias().'.'.$link->attr => $link->value]); + $query = $query->where([$this->$modelsName->getAlias().'.'.$link->attr => $link->value]); } // This magically makes REST calls paginated... can use eg direction=, diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index 6eeb0c553..7c10bc715 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -194,14 +194,12 @@ public function calculatePermissions(?int $id): array { if($id) { $readOnlyActions = ['view']; - // Does this table have an isReadOnly call? + // Pull the record so we can interrogate it - if(method_exists($table, "isReadOnly")) { - // Pull the record so we can interrogate it - - $obj = $table->get($id); - - $readOnly = $table->isReadOnly($obj); + $obj = $table->get($id); + + if(method_exists($obj, "isReadOnly")) { + $readOnly = $obj->isReadOnly(); if(!empty($permissions['readOnly'])) { // Merge in controller specific actions permitted on read only entities @@ -304,104 +302,127 @@ protected function getPrimaryLink(bool $lookup=false) { $this->cur_pl = new \stdClass(); // PrimaryLinkTrait - if(method_exists($this->$modelsName, "getPrimaryLink") - && $this->$modelsName->getPrimaryLink()) { - $this->cur_pl->attr = $this->$modelsName->getPrimaryLink(); - $this->set('vv_primary_link', $this->cur_pl->attr); + if(method_exists($this->$modelsName, "getPrimaryLinks") + && $this->$modelsName->getPrimaryLinks()) { + // Some models, in particular MVEAs, can have multiple potential primary + // links. In these cases, only one primary link is valid at a time, so we + // have to look through the available primary links and find one. + + $availablePrimaryLinks = $this->$modelsName->getPrimaryLinks(); if($lookup) { - // Try to find a value - - if($this->request->is('get')) { - // If this action allows unkeyed, asserted primary link IDs, check the query - // string (eg: 'add' or 'index' allow matchgrid_id to be passed in) - if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action')) - && $this->request->getQuery($this->cur_pl->attr)) { - $this->cur_pl->value = $this->request->getQuery($this->cur_pl->attr); - } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID - $param = (int)$this->request->getParam('pass.0'); + foreach($availablePrimaryLinks as $potentialPrimaryLink) { + // Try to find a value + + if($this->request->is('get')) { + // If this action allows unkeyed, asserted primary link IDs, check the query + // string (eg: 'add' or 'index' allow matchgrid_id to be passed in) + if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action')) + && $this->request->getQuery()) { + $this->cur_pl->value = $this->request->getQuery($potentialPrimaryLink); + } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { + // Try to map the requested object ID + $param = (int)$this->request->getParam('pass.0'); + + if(!empty($param)) { + $this->cur_pl = $this->$modelsName->findPrimaryLink($param); + // Break the loop here since we also have the link attribute, + // which might not be $potentialPrimaryLink + break; + } + } + } elseif($this->request->is('post') && $this->request->getParam('action') != 'delete') { + // Post = add, where we can have a list of objects and nothing in /objects/{id} + // We don't support different primary links across objects, so we throw an error + // if different parent keys are provided. + + $linkValue = null; - if(!empty($param)) { - $this->cur_pl->value = $this->$modelsName->findPrimaryLinkId($param); + // Data in API format + $reqData = $this->request->getData($modelsName); + + if(!$reqData + // Don't create $reqData if the POST data is also empty + && !empty($this->request->getData())) { + // Data in POST format + $reqData[] = $this->request->getData(); } - } - } elseif($this->request->is('post') && $this->request->getParam('action') != 'delete') { - // Post = add, where we can have a list of objects and nothing in /objects/{id} - // We don't support different primary links across objects, so we throw an error - // if different parent keys are provided. - - $linkValue = null; - - // Data in API format - $reqData = $this->request->getData($modelsName); - - if(!$reqData - // Don't create $reqData if the POST data is also empty - && !empty($this->request->getData())) { - // Data in POST format - $reqData[] = $this->request->getData(); - } - - if(!empty($reqData)) { - foreach($reqData as $rec) { - if(!empty($rec[$this->cur_pl->attr])) { - if(!$linkValue) { - // This is the first record we've seen, use this primary link value - $linkValue = $rec[$this->cur_pl->attr]; - } elseif($linkValue != $rec[$this->cur_pl->attr]) { - // We don't support multiple records with different parents - throw new \InvalidArgumentException('All records must have the same primary link'); // XXX I18n + + if(!empty($reqData)) { + foreach($reqData as $rec) { + if(!empty($rec[$potentialPrimaryLink])) { + if(!$linkValue) { + // This is the first record we've seen, use this primary link value + $linkValue = $rec[$potentialPrimaryLink]; + } elseif($linkValue != $rec[$potentialPrimaryLink]) { + // We don't support multiple records with different parents + throw new \InvalidArgumentException(__d('error', 'primary_link.mismatch')); + } } + + $this->cur_pl->value = $linkValue; } + } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { + // Try to map the requested object ID (this is probably a delete, so no attribute in post body) + $param = (int)$this->request->getParam('pass.0'); - $this->cur_pl->value = $linkValue; + if(!empty($param)) { + $this->cur_pl = $this->$modelsName->findPrimaryLink($param); + // Break the loop here since we also have the link attribute, + // which might not be $potentialPrimaryLink + break; + } } - } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID (this is probably a delete, so no attribute in post body) - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $this->cur_pl->value = $this->$modelsName->findPrimaryLinkId($param); + } elseif($this->request->is('put') || $this->request->getParam('action') == 'delete') { + // Put = edit, so we should look up the parent ID via the object itself + if($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { + // Try to map the requested object ID (this is probably a delete, so no attribute in post body) + $param = (int)$this->request->getParam('pass.0'); + + if(!empty($param)) { + $this->cur_pl = $this->$modelsName->findPrimaryLink($param); + // Break the loop here since we also have the link attribute, + // which might not be $potentialPrimaryLink + break; + } } } - } elseif($this->request->is('put') || $this->request->getParam('action') == 'delete') { - // Put = edit, so we should look up the parent ID via the object itself - if($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID (this is probably a delete, so no attribute in post body) - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $this->cur_pl->value = $this->$modelsName->findPrimaryLinkId($param); - } + + if(!empty($this->cur_pl->value)) { + // We found a populated primary link. Store the attribute and break the loop. + $this->cur_pl->attr = $potentialPrimaryLink; + $this->set('vv_primary_link', $this->cur_pl->attr); + break; } } if(empty($this->cur_pl->value) && !$this->$modelsName->allowEmptyPrimaryLink()) { - throw new \RuntimeException(__d('error', 'primary_link', [ $this->cur_pl->attr ])); + throw new \RuntimeException(__d('error', 'primary_link')); } } if(!empty($this->cur_pl->value)) { // Look up the link value to find the related entity - $linkModelName = $this->$modelsName->getPrimaryLinkTableName(); - $linkModel = TableRegistry::get($linkModelName); + $linkTableName = $this->$modelsName->getPrimaryLinkTableName($this->cur_pl->attr); + $linkTable = TableRegistry::get($linkTableName); - $this->set('vv_primary_link_model', $linkModelName); + $this->set('vv_primary_link_model', $linkTableName); try { - $plObj = $linkModel->findById($this->cur_pl->value)->firstOrFail(); + $plObj = $linkTable->findById($this->cur_pl->value)->firstOrFail(); $this->set('vv_primary_link_obj', $plObj); // While we're here, note the CO since we'll probably need it soon if(!empty($plObj->co_id)) { $this->cur_pl->co_id = $plObj->co_id; + } elseif(method_exists($linkTable, "findCoForRecord")) { + $this->cur_pl->co_id = $linkTable->findCoForRecord((int)$this->cur_pl->value); } } catch(RecordNotFoundException $e) { - $this->llog('error', "Could not find value '" . $this->cur_pl->value . "' for primary link object " . $linkModelName); + $this->llog('error', "Could not find value '" . $this->cur_pl->value . "' for primary link object " . $linkTableName); // Mask this with a generic UnauthorizedException throw new UnauthorizedException(__d('error', 'perm')); } diff --git a/app/src/Controller/EmailAddressesController.php b/app/src/Controller/EmailAddressesController.php index 703bf4009..d57b04fad 100644 --- a/app/src/Controller/EmailAddressesController.php +++ b/app/src/Controller/EmailAddressesController.php @@ -39,26 +39,4 @@ class EmailAddressesController extends MVEAController { 'EmailAddresses.mail' => 'asc' ] ]; - - /** - * Callback run prior to the request render. - * - * @since COmanage Registry v5.0.0 - * @param EventInterface $event Cake Event - * @return \Cake\Http\Response HTTP Response - */ - - public function beforeRender(\Cake\Event\EventInterface $event) { - if(!$this->request->is('restful')) { -// XXX maybe $CoSettings should be available via AppController, like $this->getCOID()? - $CoSettings = TableRegistry::getTableLocator()->get('CoSettings'); - - $settings = $CoSettings->find()->where(['co_id' => $this->getCOID()])->firstOrFail(); - -// XXX move this into MVEAController or a trait - $this->set('vv_default_type', $settings->email_address_default_type_id); - } - - return parent::beforeRender($event); - } } \ No newline at end of file diff --git a/app/src/Controller/IdentifiersController.php b/app/src/Controller/IdentifiersController.php index cfdd8302b..27f087267 100644 --- a/app/src/Controller/IdentifiersController.php +++ b/app/src/Controller/IdentifiersController.php @@ -39,26 +39,4 @@ class IdentifiersController extends MVEAController { 'Identifiers.identifier' => 'asc' ] ]; - - /** - * Callback run prior to the request render. - * - * @since COmanage Registry v5.0.0 - * @param EventInterface $event Cake Event - * @return \Cake\Http\Response HTTP Response - */ - - public function beforeRender(\Cake\Event\EventInterface $event) { - if(!$this->request->is('restful')) { -// XXX maybe $CoSettings should be available via AppController, like $this->getCOID()? - $CoSettings = TableRegistry::getTableLocator()->get('CoSettings'); - - $settings = $CoSettings->find()->where(['co_id' => $this->getCOID()])->firstOrFail(); - -// XXX move this into MVEAController or a trait - $this->set('vv_default_type', $settings->identifier_default_type_id); - } - - return parent::beforeRender($event); - } } \ No newline at end of file diff --git a/app/src/Controller/MVEAController.php b/app/src/Controller/MVEAController.php index 0801431d4..a391fa73f 100644 --- a/app/src/Controller/MVEAController.php +++ b/app/src/Controller/MVEAController.php @@ -32,6 +32,7 @@ // XXX not doing anything with Log yet use Cake\Log\Log; use Cake\ORM\TableRegistry; +use Cake\Utility\Inflector; class MVEAController extends StandardController { /** @@ -44,6 +45,8 @@ class MVEAController extends StandardController { public function beforeRender(\Cake\Event\EventInterface $event) { // $this->name = Models $modelsName = $this->name; + // field = model (or model_name) + $fieldName = Inflector::underscore(Inflector::singularize($modelsName)); if(!$this->request->is('restful')) { // Use the PrimaryLink to set information for breadcrumbs @@ -54,14 +57,38 @@ public function beforeRender(\Cake\Event\EventInterface $event) { $this->set('vv_primary_link_id', $link->value); switch($link->attr) { + case 'person_role_id': + $PersonRoles = TableRegistry::get('PersonRoles'); + $roleEntity = $PersonRoles->findById((int)$link->value)->firstOrFail(); + // Note this is a string, but vv_person_name is an entity + $this->set('vv_person_role', $PersonRoles->generateDisplayField($roleEntity)); + $this->set('vv_person_role_id', $link->value); + + // Also set a name + $Names = TableRegistry::get('Names'); + $this->set('vv_person_name', $Names->primaryName($roleEntity->person_id)); + $this->set('vv_person_id', $roleEntity->person_id); + break; case 'person_id': $Names = TableRegistry::get('Names'); $this->set('vv_person_name', $Names->primaryName((int)$link->value)); + $this->set('vv_person_id', $link->value); break; default; break; } } + + // If there is a default type setting for this model, pass it to the view + if($this->$modelsName->getSchema()->hasColumn('type_id')) { + $defaultTypeField = $fieldName . "_default_type_id"; + + $CoSettings = TableRegistry::getTableLocator()->get('CoSettings'); + + $settings = $CoSettings->find()->where(['co_id' => $this->getCOID()])->firstOrFail(); + + $this->set('vv_default_type', $settings->$defaultTypeField); + } } return parent::beforeRender($event); diff --git a/app/src/Controller/PersonRolesController.php b/app/src/Controller/PersonRolesController.php new file mode 100644 index 000000000..b03d68f46 --- /dev/null +++ b/app/src/Controller/PersonRolesController.php @@ -0,0 +1,45 @@ + [ + 'PersonRoles.ordr' => 'asc', + 'PersonRoles.title' => 'asc' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index e32054eba..04babb9c3 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -130,7 +130,7 @@ public function beforeRender(\Cake\Event\EventInterface $event) { $params = $this->request->getParam('pass'); if(!empty($params[0])) { - if((method_exists($table, "getPrimaryLink") + if((method_exists($table, "allowLookupPrimaryLink") && $table->allowLookupPrimaryLink($this->request->getParam('action'))) || $modelsName == 'Cos') { @@ -246,9 +246,9 @@ public function edit(string $id) { // Pull the current record $obj = $query->firstOrFail(); - if(method_exists($table, "isReadOnly")) { + if(method_exists($obj, "isReadOnly")) { // If this is a read only record, redirect to view - if($table->isReadOnly($obj)) { + if($obj->isReadOnly()) { $redirect = [ 'action' => 'view', $obj->id @@ -484,7 +484,6 @@ protected function populateAutoViewVars(object $obj=null) { // table, inject the current CO along with the requested attribute $avv['model'] = 'Types'; $avv['where'] = [ - 'co_id' => $this->getCOID(), 'attribute' => $avv['attribute'], 'status' => SuspendableStatusEnum::Active ]; @@ -523,13 +522,20 @@ protected function populateAutoViewVars(object $obj=null) { // to PrimaryLinkTrait and call it there? if($v) { - $query = $query->where([$table->getAlias().'.'.$linkFilter => $v]); + $avv['where'][$table->getAlias().'.'.$linkFilter] = $v; + //$query = $query->where([$table->getAlias().'.'.$linkFilter => $v]); } } } else { // Use the specified finder, if configured $query = $query->find($avv['find']); } + } else { +// XXX is this the best logic? maybe some relation to filterPrimaryLink? + // By default, filter everything on CO ID + + $avv['where']['co_id'] = $this->getCOID(); + //$query = $query->where([$table->getAlias().'.co_id' => $this->getCOID()]); } if(!empty($avv['where'])) { @@ -537,6 +543,13 @@ protected function populateAutoViewVars(object $obj=null) { $query = $query->where($avv['where']); } + // Sort the list by display field + if(!empty($avv['model']) && method_exists($this->$avvmodel, "getDisplayField")) { + $query->order([$this->$avvmodel->getDisplayField() => 'ASC']); + } elseif(method_exists($table, "getDisplayField")) { + $query->order([$table->getDisplayField() => 'ASC']); + } + $this->set($vvar, $query->toArray()); break; default: diff --git a/app/src/Controller/TelephoneNumbersController.php b/app/src/Controller/TelephoneNumbersController.php new file mode 100644 index 000000000..d09f804e4 --- /dev/null +++ b/app/src/Controller/TelephoneNumbersController.php @@ -0,0 +1,63 @@ + [ + 'TelephoneNumbers.number' => 'asc' + ] + ]; + + /** + * Callback run prior to the request render. + * + * @since COmanage Registry v5.0.0 + * @param EventInterface $event Cake Event + * @return \Cake\Http\Response HTTP Response + */ + + public function beforeRender(\Cake\Event\EventInterface $event) { + if(!$this->request->is('restful')) { + $CoSettings = TableRegistry::getTableLocator()->get('CoSettings'); + + $settings = $CoSettings->find()->where(['co_id' => $this->getCOID()])->firstOrFail(); + +// XXX move this into MVEAController or a trait + $this->set('vv_permitted_fields', $settings->telephone_number_permitted_fields_array()); + } + + return parent::beforeRender($event); + } +} \ No newline at end of file diff --git a/app/src/Controller/UrlsController.php b/app/src/Controller/UrlsController.php new file mode 100644 index 000000000..6223b8486 --- /dev/null +++ b/app/src/Controller/UrlsController.php @@ -0,0 +1,42 @@ + [ + 'Urls.url' => 'asc' + ] + ]; +} \ No newline at end of file diff --git a/app/src/Lib/Enum/PermittedTelephoneNumberFieldsEnum.php b/app/src/Lib/Enum/PermittedTelephoneNumberFieldsEnum.php new file mode 100644 index 000000000..46956619b --- /dev/null +++ b/app/src/Lib/Enum/PermittedTelephoneNumberFieldsEnum.php @@ -0,0 +1,41 @@ +getTableLocator()->get('HistoryRecords'); + $personId = $this->lookupPersonId($entity); + $personRoleId = $this->lookupPersonRoleId($entity); + return $HistoryRecords->recordForPerson( - $entity->person_id, + $personId, $laction, - $lcomment + $lcomment, + $personRoleId ); } } diff --git a/app/src/Lib/Traits/MVETrait.php b/app/src/Lib/Traits/MVETrait.php index ccd08e034..d7e885341 100644 --- a/app/src/Lib/Traits/MVETrait.php +++ b/app/src/Lib/Traits/MVETrait.php @@ -30,6 +30,28 @@ namespace App\Lib\Traits; trait MVETrait { + /** + * Determine if this entity is Read Only. + * + * @since COmanage Registry v5.0.0 + * @return boolean True if the entity is read only, false otherwise + */ + + public function isMVEReadOnly(): bool { + // Records pipelined from an EIS are read only + + // The class name is something like `\App\Model\Entity\Name', but we just + // want name (lowercased). + $entityName = \Cake\Utility\Inflector::underscore(substr(strrchr(get_class($this), '\\'),1)); + $sourcefk = "source_" . $entityName . "_id"; + + if(isset($entity->$sourcefk)) { + return !empty($entity->$sourcefk); + } + + return false; + } + /** * Generate a where clause suitable for the current entity. * diff --git a/app/src/Lib/Traits/PrimaryLinkTrait.php b/app/src/Lib/Traits/PrimaryLinkTrait.php index b2e8cc01b..06c9aadb1 100644 --- a/app/src/Lib/Traits/PrimaryLinkTrait.php +++ b/app/src/Lib/Traits/PrimaryLinkTrait.php @@ -33,11 +33,11 @@ use \Cake\ORM\TableRegistry; trait PrimaryLinkTrait { - // Primary Link field (eg: model:co_id) - private $primaryLink = null; - - // Primary Link table name (eg: Cos) - private $primaryLinkTable = null; + // Primary Link field (eg: model:co_id). Note some models, in particular + // MVEAs, can have multiple Primary Links. $primaryLinks will be key/value + // pairs, where the key is the field (eg: co_id) and the value is the table + // (eg: Cos). + private $primaryLinks = []; // Allow empty primary link? private $allowEmpty = false; @@ -112,18 +112,22 @@ public function allowUnkeyedPrimaryLink(string $action) { */ public function calculateCoForRecord(EntityInterface $entity): ?int { - if($this->primaryLink == 'co_id') { + if(isset($this->primaryLinks['co_id'])) { if(!empty($entity->co_id)) { return $entity->co_id; } - - return null; } else { - // Recursively ask the primaryLink until we get an answer - $LinkTable = $this->getPrimaryLinkTable(); - - return $LinkTable->findCoForRecord($entity->{$this->primaryLink}); + foreach($this->primaryLinks as $linkField => $linkTable) { + if(!empty($entity->$linkField)) { + // Use this field. Recursively ask the primaryLink until we get an answer. + $LinkTable = TableRegistry::getTableLocator()->get($linkTable); + + return $LinkTable->findCoForRecord($entity->$linkField); + } + } } + + return null; } /** @@ -135,7 +139,7 @@ public function calculateCoForRecord(EntityInterface $entity): ?int { */ public function findCoForRecord(int $id): ?int { - // Pull tho object to examine the primary links + // Pull the object to examine the primary links $query = $this->findById($id); // This will throw an error on failure @@ -156,51 +160,70 @@ public function findFilterPrimaryLink(\Cake\ORM\Query $query, array $options) { } /** - * Calculate the Primary Link ID associated with the requested object ID. + * Find the Primary Link for an entity. * * @since COmanage Registry v5.0.0 - * @param int $id Object ID - * @return int Primary Link ID - * @throws Cake\Datasource\Exception\RecordNotFoundException + * @param Entity $entity Entity + * @return Entity Primary Link (as an Entity) + * @throws \InvalidArgumentException */ - public function findPrimaryLinkId(int $id) { - $obj = $this->findById($id)->firstOrFail(); + public function findPrimaryLinkEntity($entity) { + foreach(array_keys($this->primaryLinks) as $plKey) { + if(!empty($entity->$plKey)) { + $LinkTable = TableRegistry::getTableLocator()->get($this->primaryLinks[$plKey]); + + return $LinkTable->findById($entity->$plKey)->firstOrFail(); + } + } - return $obj->{$this->primaryLink}; + throw new \InvalidArgumentException(__d('error', 'primary_link')); } /** - * Obtain the primary link field. + * Find the Primary Link associated with the requested object ID. * * @since COmanage Registry v5.0.0 - * @return string Primary link attribute + * @param int $id Object ID + * @return Entity Primary Link (as an Entity) + * @throws \InvalidArgumentException */ - public function getPrimaryLink() { - return $this->primaryLink; + public function findPrimaryLink(int $id) { + $obj = $this->findById($id)->firstOrFail(); + + // We might have multiple primary link keys (eg for MVEAs), but only one + // should be set. Return the first one we find. + foreach(array_keys($this->primaryLinks) as $plKey) { + if(!empty($obj->$plKey)) { + return (object)['attr' => $plKey, 'value' => $obj->$plKey]; + } + } + + throw new \InvalidArgumentException(__d('error', 'primary_link')); } /** - * Obtain the primary link's table. + * Obtain the primary link fields. * * @since COmanage Registry v5.0.0 - * @return Table Cake Table object + * @return array Primary link attributes */ - public function getPrimaryLinkTable() { - return TableRegistry::getTableLocator()->get($this->primaryLinkTable); + public function getPrimaryLinks(): array { + return array_keys($this->primaryLinks); } /** * Obtain the primary link's table name. * * @since COmanage Registry v5.0.0 - * @return string Primary link table name + * @param string $primaryLink Primary Link field + * @return string Primary link table name */ - public function getPrimaryLinkTableName(): string { - return $this->primaryLinkTable; + public function getPrimaryLinkTableName(string $primaryLink): string { + return $this->primaryLinks[$primaryLink]; } /** @@ -214,6 +237,48 @@ public function getRedirectGoal(): string { return $this->redirectGoal; } + /** + * Determine the Person ID associated with an entity. + * + * @since COmanage Registry v5.0.0 + * @param Entity $entity Entity + * @return ?int Person ID + */ + + public function lookupPersonId($entity): ?int { + if(!empty($entity->person_id)) { + return $entity->person_id; + } elseif($entity->getSource() == 'People') { + return $entity->id; + } else { + $linkEntity = $this->findPrimaryLinkEntity($entity); + + if(!empty($linkEntity->person_id)) { + return $linkEntity->person_id; + } + } + + return null; + } + + /** + * Determine the Person Role ID associated with an entity. + * + * @since COmanage Registry v5.0.0 + * @param Entity $entity Entity + * @return int Person Role ID + */ + + public function lookupPersonRoleId($entity): ?int { + if(!empty($entity->person_role_id)) { + return $entity->person_role_id; + } elseif($entity->getSource() == 'PersonRoles') { + return $entity->id; + } + + return null; + } + /** * Set whether this table accepts a CO ID, set by AppController. In general, * tables should NOT use this unless there is no other way to get the CO ID. @@ -280,15 +345,23 @@ public function setCurCoId(int $coId) { * Set the primary link attribute. * * @since COmanage Registry v5.0.0 - * @param string $field Primary link attribute + * @param mixed $field Primary link attribute, or an array of primary links */ - public function setPrimaryLink($field) { - $this->primaryLink = $field; + public function setPrimaryLink($fields) { + if(is_string($fields)) { + $fields = [$fields]; + } - // Calculate the table name for future reference - if(preg_match('/^(.*?)_id$/', $field, $f)) { - $this->primaryLinkTable = \Cake\Utility\Inflector::camelize(\Cake\Utility\Inflector::pluralize($f[1])); + foreach($fields as $field) { + $t = null; + + // Calculate the table name for future reference + if(preg_match('/^(.*?)_id$/', $field, $f)) { + $t = \Cake\Utility\Inflector::camelize(\Cake\Utility\Inflector::pluralize($f[1])); + } + + $this->primaryLinks[$field] = $t; } } diff --git a/app/src/Lib/Traits/ReadOnlyEntityTrait.php b/app/src/Lib/Traits/ReadOnlyEntityTrait.php new file mode 100644 index 000000000..592e07885 --- /dev/null +++ b/app/src/Lib/Traits/ReadOnlyEntityTrait.php @@ -0,0 +1,58 @@ +isMVEReadOnly()) { + return true; + } + + // Records flagged as deleted or with a parent foreign key are read only + + // The class name is something like `\App\Model\Entity\PersonRole', but we just + // want person_role (lowercased). + $entityName = \Cake\Utility\Inflector::underscore(substr(strrchr(get_class($this), '\\'),1)); + $parentfk = $entityName . "_id"; + + return (isset($entity->deleted) && $entity->deleted) + || !empty($entity->$parentfk); + } +} diff --git a/app/src/Lib/Traits/ValidationTrait.php b/app/src/Lib/Traits/ValidationTrait.php index c6246e88f..e31881c50 100644 --- a/app/src/Lib/Traits/ValidationTrait.php +++ b/app/src/Lib/Traits/ValidationTrait.php @@ -31,8 +31,66 @@ use Cake\Core\Configure; use Cake\ORM\TableRegistry; +use Cake\Validation\Validator; trait ValidationTrait { + /** + * Register validation rules for the primary link key(s) associated with this table. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Cake Validator + * @param array $primaryKeys Array of primary link key(s) for this table + * @return Validator Cake Validator + */ + + public function registerPrimaryKeyValidation(Validator $validator, array $primaryKeys): Validator { + foreach($primaryKeys as $pk) { + $validator->add($pk, [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString($pk, null, function($context) use ($pk, $primaryKeys) { + // This primary key must be populated if all other primary keys are empty + $othersEmpty = true; + + foreach(array_diff($primaryKeys, [$pk]) as $opk) { + $othersEmpty &= empty($context['data'][$opk]); + } + + return !$othersEmpty; + }); + } + + return $validator; + } + + /** + * Register validation rules for the provided field, as a string. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Cake Validator + * @param Schema $schema Cake Schema + * @param string $field Field name + * @param bool $required Whether this field is required + * @return Validator Cake Validator + */ + + public function registerStringValidation(Validator $validator, $schema, string $field, bool $required): Validator { + $validator->add($field, [ + 'size' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn($field)]], + 'provider' => 'table'], + 'filter' => ['rule' => ['validateInput'], + 'provider' => 'table'] + ]); + + if($required) { + $validator->notEmptyString($field); + } else { + $validator->allowEmptyString($field); + } + + return $validator; + } + /** * Verify that $value is a valid * @@ -126,9 +184,6 @@ public function validateCO($value, array $context) { */ public function validateConditionalRequire($value, array $context) { - // What component are we? - $COmponent = __('product.code'); - if(!empty($value) && in_array($value, $context['providers']['conditionalRequire']['inArray']) && empty($context['data'][ $context['providers']['conditionalRequire']['require'] ])) { @@ -153,25 +208,58 @@ public function validateInput($value, array $context) { // as an extra "line of defense" against unsanitized HTML output, since there are // currently no known cases where user-entered input should permit angle brackets. -// XXX we previously supported 'filter'. 'flags', and 'invalidchars' as arguments, do we still need to? - - // What component are we? - $COmponent = __('product.code'); +// XXX we previously supported 'flags' and 'invalidchars' as arguments, do we still need to? +// CFM-152 review the logic here + + if(!empty($context['filter'])) { + // We use filter_var for consistency with the views, and simply check + // that we end up with the same string we started with. + + $filtered = filter_var($value, $context['filter']); + + if($value != $filtered) { + // Mismatch, implying bad input + return __d('error', 'input.invalid'); + } + } else { + // Perform a basic string search. + + $invalid = "<>"; + + if(strlen($value) != strcspn($value, $invalid)) { + // Mismatch, implying bad input + return __d('error', 'input.invalid'); + } + + // We require at least one non-whitespace character (CO-1551) + if(!preg_match('/\S/', $value)) { + return __d('error', 'input.blank'); + } + } - // Perform a basic string search. + return true; + } + + /** + * Validate the maximum length of a field. + * + * @since COmanage Registry v5.0.0 + * @param string $value Value to validate + * @param array $context Validation context, which must include the schema definition + * @return mixed True if $value validates, or an error string otherwise + */ + + public function validateMaxLength($value, array $context) { + // We use our own so we can introspect the field's max length from the + // provided table schema object, and use our own error message (without + // having to copy it to every table definition). - $invalid = "<>"; + $maxLength = $context['column']['length']; - if(strlen($value) != strcspn($value, $invalid)) { - // Mismatch, implying bad input - return __d('error', 'input.invalid'); + if(!empty($value) && strlen($value) > $maxLength) { + return __d('error', 'input.length', [$maxLength]); } - // We require at least one non-whitespace character (CO-1551) - if(!preg_match('/\S/', $value)) { - return __d('error', 'input.blank'); - } - return true; } @@ -185,9 +273,6 @@ public function validateInput($value, array $context) { */ public function validateSqlIdentifier($value, array $context) { - // What component are we? - $COmponent = __('product.code'); - // Valid (portable) SQL identifiers begin with a letter or underscore, and // subsequent characters can also include digits. We'll be a little stricter // than we need to be for now by only accepting A-Z, when in fact certain diff --git a/app/src/Model/Entity/AdHocAttribute.php b/app/src/Model/Entity/AdHocAttribute.php new file mode 100644 index 000000000..e93f97d3f --- /dev/null +++ b/app/src/Model/Entity/AdHocAttribute.php @@ -0,0 +1,43 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Entity/Address.php b/app/src/Model/Entity/Address.php new file mode 100644 index 000000000..d990e8c0d --- /dev/null +++ b/app/src/Model/Entity/Address.php @@ -0,0 +1,43 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Entity/ApiUser.php b/app/src/Model/Entity/ApiUser.php index 832ca40de..63abe5ab3 100644 --- a/app/src/Model/Entity/ApiUser.php +++ b/app/src/Model/Entity/ApiUser.php @@ -33,6 +33,8 @@ use Cake\ORM\Entity; class ApiUser extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, diff --git a/app/src/Model/Entity/Co.php b/app/src/Model/Entity/Co.php index e94862e2d..d1e5fcede 100644 --- a/app/src/Model/Entity/Co.php +++ b/app/src/Model/Entity/Co.php @@ -48,4 +48,18 @@ class Co extends Entity { public function isCOmanageCO(): bool { return (strtolower($this->name) == 'comanage'); } + + /** + * Determine if this entity is Read Only. + * + * @since COmanage Registry v5.0.0 + * @param Entity $entity Cake Entity + * @return boolean true if the entity is read only, false otherwise + */ + + public function isReadOnly(): bool { + // The COmanage CO is read only + + return $this->isCOmanageCO(); + } } \ No newline at end of file diff --git a/app/src/Model/Entity/CoSetting.php b/app/src/Model/Entity/CoSetting.php index ad97f58f9..a59e1434a 100644 --- a/app/src/Model/Entity/CoSetting.php +++ b/app/src/Model/Entity/CoSetting.php @@ -35,17 +35,30 @@ // collection of settings for a given CO, but it's easier not to fight // Cake's inflection. class CoSetting extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, 'slug' => false, ]; + /** + * Obtain the set of fields required for addresses, as an array. + * + * @since COmanage Registry v5.0.0 + * @return array Array of required addresses fields + */ + + public function address_required_fields_array(): array { + return explode(",", $this->address_required_fields); + } + /** * Obtain the set of fields permitted for names, as an array. * * @since COmanage Registry v5.0.0 - * @return array Arroy of permitted name fields + * @return array Array of permitted name fields */ public function name_permitted_fields_array(): array { @@ -56,10 +69,21 @@ public function name_permitted_fields_array(): array { * Obtain the set of fields required for names, as an array. * * @since COmanage Registry v5.0.0 - * @return array Arroy of required name fields + * @return array Array of required name fields */ public function name_required_fields_array(): array { return explode(",", $this->name_required_fields); } + + /** + * Obtain the set of fields permitted for telephone numbers, as an array. + * + * @since COmanage Registry v5.0.0 + * @return array Array of permitted telephone number fields + */ + + public function telephone_number_permitted_fields_array(): array { + return explode(",", $this->telephone_number_permitted_fields); + } } \ No newline at end of file diff --git a/app/src/Model/Entity/Cou.php b/app/src/Model/Entity/Cou.php index 32dcdb36d..a494edb1f 100644 --- a/app/src/Model/Entity/Cou.php +++ b/app/src/Model/Entity/Cou.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class Cou extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, diff --git a/app/src/Model/Entity/Dashboard.php b/app/src/Model/Entity/Dashboard.php index b5517f5a0..1f68e2dc7 100644 --- a/app/src/Model/Entity/Dashboard.php +++ b/app/src/Model/Entity/Dashboard.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class Dashboard extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, diff --git a/app/src/Model/Entity/EmailAddress.php b/app/src/Model/Entity/EmailAddress.php index 4a3911f2c..85c03fd99 100644 --- a/app/src/Model/Entity/EmailAddress.php +++ b/app/src/Model/Entity/EmailAddress.php @@ -32,6 +32,7 @@ use Cake\ORM\Entity; class EmailAddress extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; use \App\Lib\Traits\MVETrait; protected $_accessible = [ diff --git a/app/src/Model/Entity/ExternalIdentity.php b/app/src/Model/Entity/ExternalIdentity.php index f0beb80f7..b9b01a340 100644 --- a/app/src/Model/Entity/ExternalIdentity.php +++ b/app/src/Model/Entity/ExternalIdentity.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class ExternalIdentity extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, diff --git a/app/src/Model/Entity/HistoryRecord.php b/app/src/Model/Entity/HistoryRecord.php index d6c2b218d..71df1779f 100644 --- a/app/src/Model/Entity/HistoryRecord.php +++ b/app/src/Model/Entity/HistoryRecord.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class HistoryRecord extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, diff --git a/app/src/Model/Entity/Identifier.php b/app/src/Model/Entity/Identifier.php index e2da65b54..df9af491d 100644 --- a/app/src/Model/Entity/Identifier.php +++ b/app/src/Model/Entity/Identifier.php @@ -31,7 +31,8 @@ use Cake\ORM\Entity; -class identifier extends Entity { +class Identifier extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; use \App\Lib\Traits\MVETrait; protected $_accessible = [ diff --git a/app/src/Model/Entity/Name.php b/app/src/Model/Entity/Name.php index 8058995ca..5a96985e9 100644 --- a/app/src/Model/Entity/Name.php +++ b/app/src/Model/Entity/Name.php @@ -32,6 +32,7 @@ use Cake\ORM\Entity; class Name extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; use \App\Lib\Traits\MVETrait; protected $_accessible = [ diff --git a/app/src/Model/Entity/Person.php b/app/src/Model/Entity/Person.php index be8001256..3721f3f05 100644 --- a/app/src/Model/Entity/Person.php +++ b/app/src/Model/Entity/Person.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class Person extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, diff --git a/app/src/Model/Entity/PersonRole.php b/app/src/Model/Entity/PersonRole.php new file mode 100644 index 000000000..f45cab4e3 --- /dev/null +++ b/app/src/Model/Entity/PersonRole.php @@ -0,0 +1,42 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Entity/TelephoneNumber.php b/app/src/Model/Entity/TelephoneNumber.php new file mode 100644 index 000000000..be254563a --- /dev/null +++ b/app/src/Model/Entity/TelephoneNumber.php @@ -0,0 +1,70 @@ + true, + 'id' => false, + 'slug' => false, + ]; + + /** + * Generate a formatted number + * + * @since COmanage Registry v5.0.0 + * @return string Formatted telephone number + */ + + protected function _getFormattedNumber() { + // Start with number since it's always required, then prepend and/or append + $n = $this->number; + + if(!empty($this->country_code)) { + // We'll only output + style if a country code was provided + $n = "+" . $this->country_code . " " . $n; + } + + if(!empty($this->area_code)) { + $n = $this->area_code . " " . $n; + } + + if(!empty($this->extension)) { + $n .= " " . __d('field', 'number.ext') . $this->extension; + } + + return $n; + } +} \ No newline at end of file diff --git a/app/src/Model/Entity/Type.php b/app/src/Model/Entity/Type.php index 0c862f615..1cee64e8b 100644 --- a/app/src/Model/Entity/Type.php +++ b/app/src/Model/Entity/Type.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class Type extends Entity { + use \App\Lib\Traits\ReadOnlyEntityTrait; + protected $_accessible = [ '*' => true, 'id' => false, diff --git a/app/src/Model/Entity/Url.php b/app/src/Model/Entity/Url.php new file mode 100644 index 000000000..119f40442 --- /dev/null +++ b/app/src/Model/Entity/Url.php @@ -0,0 +1,43 @@ + true, + 'id' => false, + 'slug' => false, + ]; +} \ No newline at end of file diff --git a/app/src/Model/Table/AdHocAttributesTable.php b/app/src/Model/Table/AdHocAttributesTable.php new file mode 100644 index 000000000..d9542a696 --- /dev/null +++ b/app/src/Model/Table/AdHocAttributesTable.php @@ -0,0 +1,128 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // Ad Hoc Attributes are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('People'); + $this->belongsTo('PersonRoles'); + $this->belongsTo('ExternalIdentities'); + + $this->setDisplayField('tag'); + +// XXX note primary link is external_identity_id when set... + $this->setPrimaryLink(['person_id', 'person_role_id']); + $this->setRequiresCO(true); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'primary' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Callback after model save. + * + * @since COmanage Registry v5.0.0 + * @param EventInterface $event Event + * @param EntityInterface $entity Entity (ie: Co) + * @param ArrayObject $options Save options + * @return bool True on success + */ + + public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool { + $this->recordHistory($entity); + + return true; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + * @throws InvalidArgumentException + * @throws RecordNotFoundException + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); + + $this->registerStringValidation($validator, $schema, 'tag', true); + + $this->registerStringValidation($validator, $schema, 'value', false); + + $validator->add('source_ad_hoc_attribute_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('source_ad_hoc_attribute_id'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/AddressesTable.php b/app/src/Model/Table/AddressesTable.php new file mode 100644 index 000000000..f8b67c503 --- /dev/null +++ b/app/src/Model/Table/AddressesTable.php @@ -0,0 +1,204 @@ + [ + 'campus', + 'home', + 'office', + 'postal' + ] + ]; + + /** + * Perform Cake Model initialization. + * + * @since COmanage Registry v5.0.0 + * @param array $config Configuration options passed to constructor + */ + + public function initialize(array $config): void { + // Timestamp behavior handles created/modified updates + $this->addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // Addesses are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('People'); + $this->belongsTo('PersonRoles'); + $this->belongsTo('ExternalIdentities'); + $this->belongsTo('Types'); + + $this->setDisplayField('street'); + +// XXX note primary link is external_identity_id when set... + $this->setPrimaryLink(['person_id', 'person_role_id']); + $this->setRequiresCO(true); + $this->setAcceptsCoId(true); + + $this->setAutoViewVars([ + 'languages' => [ + 'type' => 'enum', + 'class' => 'LanguageEnum' + ], + 'types' => [ + 'type' => 'type', + 'attribute' => 'Addresses.type' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'primary' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Callback after model save. + * + * @since COmanage Registry v5.0.0 + * @param EventInterface $event Event + * @param EntityInterface $entity Entity (ie: Co) + * @param ArrayObject $options Save options + * @return bool True on success + */ + + public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool { + $this->recordHistory($entity); + + return true; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + * @throws InvalidArgumentException + * @throws RecordNotFoundException + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + // We need the current CO ID to dynamically set validation rules according + // to CoSettings. + + if(!$this->curCoId) { + throw new \InvalidArgumentException(__d('error', 'coid')); + } + + $CoSettings = TableRegistry::getTableLocator()->get('CoSettings'); + + $settings = $CoSettings->find()->where(['co_id' => $this->curCoId])->firstOrFail(); + + $requiredFields = $settings->address_required_fields_array(); + + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); + + // CO Settings determines if these fields are required + $validator->add('street', [ + 'filter' => ['rule' => ['validateInput'], + 'provider' => 'table'] + ]); + if(in_array('street', $requiredFields)) { + $validator->notEmptyString('street'); + } else { + $validator->allowEmptyString('street'); + } + + foreach(['locality', 'state', 'postal_code', 'country'] as $f) { + $validator->add($f, [ + 'size' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn($f)]], + 'provider' => 'table'], + 'filter' => ['rule' => ['validateInput'], + 'provider' => 'table'] + ]); + if(in_array($f, $requiredFields)) { + $validator->notEmptyString($f); + } else { + $validator->allowEmptyString($f); + } + } + + $this->registerStringValidation($validator, $schema, 'room', false); + + $this->registerStringValidation($validator, $schema, 'description', false); + + $validator->add('language', [ + 'content' => ['rule' => ['inList', LanguageEnum::getConstValues()]] + ]); + $validator->allowEmptyString('language'); + + $validator->add('type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('type_id'); + + $validator->add('source_address_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('source_address_id'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/ApiUsersTable.php b/app/src/Model/Table/ApiUsersTable.php index ffb4bf2dd..6c364d3e8 100644 --- a/app/src/Model/Table/ApiUsersTable.php +++ b/app/src/Model/Table/ApiUsersTable.php @@ -302,67 +302,46 @@ public function validateKey(string $username, string $apiKey, string $remoteIp) */ public function validationDefault(Validator $validator): Validator { - $validator->add( - 'co_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $schema = $this->getSchema(); + + $validator->add('co_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->notEmpty('co_id'); - $validator->add( - 'username', - 'length', - [ 'rule' => [ 'maxLength', 64 ] ] - ); - $validator->add( - 'username', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->notEmpty('username'); - - $validator->add( - 'api_key', - 'length', - [ 'rule' => [ 'maxLength', 256 ] ] - ); - $validator->allowEmpty('api_key'); - - $validator->add( - 'status', - 'content', - [ 'rule' => [ 'inList', SuspendableStatusEnum::getConstValues() ] ] - ); - $validator->notEmpty('status'); - - $validator->add( - 'privileged', - 'content', - [ 'rule' => [ 'boolean' ] ] - ); - $validator->allowEmpty('privileged'); - - $validator->add( - 'valid_from', - 'content', - [ 'rule' => [ 'datetime' ] ] - ); - $validator->allowEmpty('valid_from'); - - $validator->add( - 'valid_through', - 'content', - [ 'rule' => [ 'datetime' ] ] - ); - $validator->allowEmpty('valid_through'); - - $validator->add( - 'remote_ip', - 'length', - [ 'rule' => [ 'maxLength', 80 ] ] - ); - $validator->allowEmpty('remote_ip'); + $this->registerStringValidation($validator, $schema, 'username', true); + + $validator->add('api_key', [ + 'length' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn('api_key')]], + 'provider' => 'table'], + ]); + $validator->allowEmptyString('api_key'); + + $validator->add('status', [ + 'content' => ['rule' => ['inList', SuspendableStatusEnum::getConstValues()]] + ]); + $validator->notEmptyString('status'); + + $validator->add('privileged', [ + 'content' => ['rule' => ['boolean']] + ]); + $validator->allowEmptyString('privileged'); + + $validator->add('valid_from', [ + 'content' => ['rule' => ['datetime']] + ]); + $validator->allowEmptyString('valid_from'); + + $validator->add('valid_through', [ + 'content' => ['rule' => ['datetime']] + ]); + $validator->allowEmptyString('valid_through'); + + $validator->add('remote_ip', [ + 'length' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn('remote_ip')]], + 'provider' => 'table'], + ]); + $validator->allowEmptyString('remote_ip'); return $validator; } diff --git a/app/src/Model/Table/CoSettingsTable.php b/app/src/Model/Table/CoSettingsTable.php index 5bf8503bb..9ed048d5f 100644 --- a/app/src/Model/Table/CoSettingsTable.php +++ b/app/src/Model/Table/CoSettingsTable.php @@ -45,6 +45,7 @@ use \Cake\ORM\Table; use \Cake\Validation\Validator; use \App\Lib\Enum\PermittedNameFieldsEnum; +use \App\Lib\Enum\PermittedTelephoneNumberFieldsEnum; use \App\Lib\Enum\RequiredAddressFieldsEnum; use \App\Lib\Enum\RequiredNameFieldsEnum; @@ -72,11 +73,15 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('Cos'); - $this->belongsTo('EmailAddressDefaultTypes') + $this->belongsTo('AddressDefaultTypes') ->setClassName('Types') - ->setForeignKey('email_address_default_type_id') + ->setForeignKey('address_default_type_id') // Property is set so ruleValidateCO can find it. We don't use the // _id suffix to match Cake's default pattern. + ->setProperty('address_default_type'); + $this->belongsTo('EmailAddressDefaultTypes') + ->setClassName('Types') + ->setForeignKey('email_address_default_type_id') ->setProperty('email_address_default_type'); $this->belongsTo('IdentifierDefaultTypes') ->setClassName('Types') @@ -86,6 +91,14 @@ public function initialize(array $config): void { ->setClassName('Types') ->setForeignKey('name_default_type_id') ->setProperty('name_default_type'); + $this->belongsTo('TelephoneNumberDefaultTypes') + ->setClassName('Types') + ->setForeignKey('telephone_number_default_type_id') + ->setProperty('telephone_number_default_type'); + $this->belongsTo('UrlDefaultTypes') + ->setClassName('Types') + ->setForeignKey('url_default_type_id') + ->setProperty('url_default_type'); $this->setDisplayField('co_id'); @@ -95,6 +108,10 @@ public function initialize(array $config): void { $this->setRedirectGoal('self'); $this->setAutoViewVars([ + 'addressDefaultTypes' => [ + 'type' => 'type', + 'attribute' => 'Addresses.type' + ], 'addressRequiredFields' => [ 'type' => 'enum', 'class' => 'RequiredAddressFieldsEnum' @@ -118,6 +135,18 @@ public function initialize(array $config): void { 'nameRequiredFields' => [ 'type' => 'enum', 'class' => 'RequiredNameFieldsEnum' + ], + 'telephoneNumberDefaultTypes' => [ + 'type' => 'type', + 'attribute' => 'TelephoneNumbers.type' + ], + 'telephoneNumberPermittedFields' => [ + 'type' => 'enum', + 'class' => 'PermittedTelephoneNumberFieldsEnum' + ], + 'urlDefaultTypes' => [ + 'type' => 'type', + 'attribute' => 'Urls.type' ] ]); @@ -150,11 +179,17 @@ public function addDefaults(int $coId): int { // Default values for each setting $defaultSettings = [ - 'co_id' => $coId, - 'address_required_fields' => RequiredAddressFieldsEnum::Street, - 'name_default_type_id' => null, - 'name_permitted_fields' => PermittedNameFieldsEnum::HGMFS, - 'name_required_fields' => RequiredNameFieldsEnum::Given + 'co_id' => $coId, + 'address_default_type_id' => null, + 'address_required_fields' => RequiredAddressFieldsEnum::Street, + 'email_address_default_type_id' => null, + 'identifier_default_type_id' => null, + 'name_default_type_id' => null, + 'name_permitted_fields' => PermittedNameFieldsEnum::HGMFS, + 'name_required_fields' => RequiredNameFieldsEnum::Given, + 'telephone_number_default_type_id' => null, + 'telephone_number_permitted_fields' => PermittedTelephoneNumberFieldsEnum::CANE, + 'url_default_type_id' => null // XXX to add new settings, set a default here, then add a validation rule below // also update data model documentation // 'disable_expiration' => false, @@ -230,34 +265,56 @@ public function typeIsDefault(int $id): bool { */ public function validationDefault(Validator $validator): Validator { - $validator->add( - 'address_required_fields', - 'content', - [ 'rule' => [ 'inList', RequiredAddressFieldsEnum::getConstValues() ] ] - ); + $validator->add('address_default_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('address_default_type_id'); + + $validator->add('address_required_fields', [ + 'content' => ['rule' => ['inList', RequiredAddressFieldsEnum::getConstValues()]] + ]); $validator->notEmptyString('address_required_fields'); - $validator->add( - 'name_default_type_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('email_address_default_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('email_address_default_type_id'); + + $validator->add('identifier_default_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('identifier_default_type_id'); + + $validator->add('name_default_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->allowEmptyString('name_default_type_id'); - $validator->add( - 'name_permitted_fields', - 'content', - [ 'rule' => [ 'inList', PermittedNameFieldsEnum::getConstValues() ] ] - ); + $validator->add('name_permitted_fields', [ + 'content' => ['rule' => ['inList', PermittedNameFieldsEnum::getConstValues()]] + ]); $validator->notEmptyString('name_permitted_fields'); - $validator->add( - 'name_required_fields', - 'content', - [ 'rule' => [ 'inList', RequiredNameFieldsEnum::getConstValues() ] ] - ); + $validator->add('name_required_fields', [ + 'content' => ['rule' => ['inList', RequiredNameFieldsEnum::getConstValues()]] + ]); $validator->notEmptyString('name_required_fields'); + $validator->add('telephone_number_default_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('telephone_number_default_type_id'); + + $validator->add('telephone_number_permitted_fields', [ + 'content' => ['rule' => ['inList', PermittedTelephoneNumberFieldsEnum::getConstValues()]] + ]); + $validator->notEmptyString('telephone_number_permitted_fields'); + + $validator->add('url_default_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('url_default_type_id'); + return $validator; } } \ No newline at end of file diff --git a/app/src/Model/Table/CosTable.php b/app/src/Model/Table/CosTable.php index 816130336..245024f22 100644 --- a/app/src/Model/Table/CosTable.php +++ b/app/src/Model/Table/CosTable.php @@ -176,20 +176,6 @@ public function findCOmanageCO(Query $query): Query { return $query->where(['lower(name)' => 'comanage']); } - /** - * Determine if this is a Read Only record. - * - * @since COmanage Registry v5.0.0 - * @param Entity $entity Cake Entity - * @return boolean true if the entity is read only, false otherwise - */ - - public function isReadOnly($entity): bool { - // The COmanage CO is read only - - return $entity->isCOmanageCO(); - } - /** * Application Rule to determine if the current entity is the COmanage CO. * @@ -260,37 +246,15 @@ public function setup(int $id): bool { */ public function validationDefault(Validator $validator): Validator { - $validator->add( - 'name', - 'length', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'name', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->notEmptyString('name'); + $schema = $this->getSchema(); - $validator->add( - 'description', - 'length', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'description', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->allowEmptyString('description'); + $this->registerStringValidation($validator, $schema, 'name', true); - $validator->add( - 'status', - 'content', - [ 'rule' => [ 'inList', TemplateableStatusEnum::getConstValues() ] ] - ); + $this->registerStringValidation($validator, $schema, 'description', false); + + $validator->add('status', [ + 'content' => ['rule' => ['inList', TemplateableStatusEnum::getConstValues()]] + ]); $validator->notEmptyString('status'); return $validator; diff --git a/app/src/Model/Table/CousTable.php b/app/src/Model/Table/CousTable.php index 77a33fb6d..055fbf5c9 100644 --- a/app/src/Model/Table/CousTable.php +++ b/app/src/Model/Table/CousTable.php @@ -33,7 +33,6 @@ use Cake\ORM\RulesChecker; use Cake\ORM\Table; use Cake\Validation\Validator; -use \App\Lib\Enum\TemplateableStatusEnum; class CousTable extends Table { use \App\Lib\Traits\AutoViewVarsTrait; @@ -174,58 +173,30 @@ public function rulePotentialParent($entity, $options) { */ public function validationDefault(Validator $validator): Validator { - $validator->add( - 'co_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $schema = $this->getSchema(); + + $validator->add('co_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->notEmptyString('co_id'); - $validator->add( - 'name', - 'length', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'name', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->notEmptyString('name'); - - $validator->add( - 'description', - 'length', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'description', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->allowEmptyString('description'); - - $validator->add( - 'parent_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $this->registerStringValidation($validator, $schema, 'name', true); + + $this->registerStringValidation($validator, $schema, 'description', false); + + $validator->add('parent_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->allowEmptyString('parent_id'); - $validator->add( - 'lft', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('lft', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->allowEmptyString('lft'); - $validator->add( - 'rght', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('rght', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->allowEmptyString('rght'); return $validator; diff --git a/app/src/Model/Table/EmailAddressesTable.php b/app/src/Model/Table/EmailAddressesTable.php index fe01241cc..9da2c1563 100644 --- a/app/src/Model/Table/EmailAddressesTable.php +++ b/app/src/Model/Table/EmailAddressesTable.php @@ -69,7 +69,7 @@ public function initialize(array $config): void { $this->addBehavior('Log'); $this->addBehavior('Timestamp'); - // Identifiers are not configuration + // Email Addresses are not configuration $this->setIsConfigurationTable(false); // Define associations @@ -134,69 +134,32 @@ public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\En */ public function validationDefault(Validator $validator): Validator { - // One of Person ID or External Identity ID is required - $validator->add( - 'person_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('person_id', null, function($context) { - return empty($context['data']['external_identity_id']); - }); + $schema = $this->getSchema(); - $validator->add( - 'external_identity_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('external_identity_id', null, function($context) { - return empty($context['data']['person_id']); - }); + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); - $validator->add( - 'mail', - 'length', - [ 'rule' => [ 'maxLength', 256 ] ] - ); - $validator->add( - 'mail', - 'content', - [ 'rule' => [ 'email' ] ] - ); + $this->registerStringValidation($validator, $schema, 'mail', true); + $validator->add('mail', [ + 'content' => ['rule' => ['email'], + 'message' => __d('error', 'input.invalid.email')] + ]); $validator->notEmptyString('mail'); - $validator->add( - 'type_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->notEmptyString('type_id'); - $validator->add( - 'verified', - 'content', - [ 'rule' => [ 'boolean' ] ] - ); + $validator->add('verified', [ + 'content' => ['rule' => ['boolean']] + ]); $validator->allowEmptyString('verified'); - $validator->add( - 'description', - 'content', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'description', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->allowEmptyString('description'); + $this->registerStringValidation($validator, $schema, 'description', false); - $validator->add( - 'source_email_address_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('source_email_address_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->allowEmptyString('source_email_address_id'); return $validator; diff --git a/app/src/Model/Table/HistoryRecordsTable.php b/app/src/Model/Table/HistoryRecordsTable.php index e5712cfa0..75e32d632 100644 --- a/app/src/Model/Table/HistoryRecordsTable.php +++ b/app/src/Model/Table/HistoryRecordsTable.php @@ -64,6 +64,7 @@ public function initialize(array $config): void { // _id suffix to match Cake's default pattern. ->setProperty('actor_person'); $this->belongsTo('People'); + $this->belongsTo('PersonRoles'); $this->belongsTo('ExternalIdentities'); $this->setDisplayField('comment'); @@ -125,19 +126,24 @@ public function generateDisplayField(\App\Model\Entity\HistoryRecord $entity): s * Record a History Record entry for a Person. * * @since COmanage Registry v5.0.0 - * @param int $personId Person ID - * @param string $action Action - * @param string $comment Comment - * @return int History Record ID + * @param int $personId Person ID + * @param string $action Action + * @param string $comment Comment + * @param int $personRoleId Person Role ID + * @return int History Record ID */ - public function recordForPerson(int $personId, string $action, string $comment): int { + public function recordForPerson(int $personId, string $action, string $comment, ?int $personRoleId=null): int { $record = [ 'person_id' => $personId, 'action' => $action, 'comment' => $comment ]; + if($personRoleId) { + $record['person_role_id'] = $personRoleId; + } + $obj = $this->newEntity($record); $this->save($obj); @@ -156,38 +162,20 @@ public function recordForPerson(int $personId, string $action, string $comment): */ public function validationDefault(Validator $validator): Validator { - // One of Person ID or External Identity ID is required -// XXX or the other fields as we add them - $validator->add( - 'person_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('person_id', null, function($context) { - return empty($context['data']['external_identity_id']); - }); + $schema = $this->getSchema(); - $validator->add( - 'external_identity_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('external_identity_id', null, function($context) { - return empty($context['data']['person_id']); - }); + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); - $validator->add( - 'action', - 'length', - [ 'rule' => [ 'maxLength', 4 ] ] - ); + $validator->add('action', [ + 'length' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn('action')]], + 'provider' => 'table'], + ]); $validator->notEmptyString('action'); - $validator->add( - 'comment', - 'length', - [ 'rule' => [ 'maxLength', 256 ] ] - ); + $validator->add('comment', [ + 'length' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn('comment')]], + 'provider' => 'table'], + ]); $validator->notEmptyString('comment'); return $validator; diff --git a/app/src/Model/Table/IdentifiersTable.php b/app/src/Model/Table/IdentifiersTable.php index e7a1ffdc8..ca5f91b72 100644 --- a/app/src/Model/Table/IdentifiersTable.php +++ b/app/src/Model/Table/IdentifiersTable.php @@ -150,65 +150,36 @@ public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\En */ public function validationDefault(Validator $validator): Validator { - // One of Person ID or External Identity ID is required - $validator->add( - 'person_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('person_id', null, function($context) { - return empty($context['data']['external_identity_id']); - }); + $schema = $this->getSchema(); - $validator->add( - 'external_identity_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('external_identity_id', null, function($context) { - return empty($context['data']['person_id']); - }); + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); - $validator->add( - 'identifier', - 'length', - [ 'rule' => [ 'maxLength', 512 ] ] - ); - $validator->add( - 'identifier', - 'content', + $this->registerStringValidation($validator, $schema, 'identifier', true); + $validator->add('identifier', [ // Identifier must have at least one non-space character in order to avoid // errors (eg: with provisioning ldap) - [ 'rule' => [ 'notBlank' ] ] - ); - $validator->notEmptyString('identifier'); + 'content' => ['rule' => ['notBlank'], + 'message' => __d('error', 'input.blank')] + ]); - $validator->add( - 'type_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->notEmptyString('type_id'); - $validator->add( - 'login', - 'content', - [ 'rule' => [ 'boolean' ] ] - ); + $validator->add('login', [ + 'content' => ['rule' => ['boolean']] + ]); $validator->allowEmptyString('login'); - $validator->add( - 'status', - 'content', - [ 'rule' => [ 'inList', SuspendableStatusEnum::getConstValues() ] ] - ); + $validator->add('status', [ + 'content' => ['rule' => ['inList', SuspendableStatusEnum::getConstValues()]] + ]); $validator->notEmptyString('status'); - $validator->add( - 'source_identifier_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('source_identifier_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->allowEmptyString('source_identifier_id'); return $validator; diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php index b4c8c865f..8ad142291 100644 --- a/app/src/Model/Table/NamesTable.php +++ b/app/src/Model/Table/NamesTable.php @@ -190,21 +190,6 @@ public function buildRules(RulesChecker $rules): RulesChecker { return $rules; } - /** - * Determine if this is a Read Only record. - * - * @since COmanage Registry v5.0.0 - * @param Entity $entity Cake Entity - * @return boolean true if the entity is read only, false otherwise - */ -// XXX this should move to the entity directly - - public function isReadOnly($entity) { - // Names pipelined from an EIS are read only - - return !empty($entity->source_name_id); - } - /** * Obtain the primary name entity for a person. * @@ -251,6 +236,8 @@ public function ruleMinimumOneName($entity, $options) { */ public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + // We need the current CO ID to dynamically set validation rules according // to CoSettings. @@ -265,147 +252,42 @@ public function validationDefault(Validator $validator): Validator { $permittedFields = $settings->name_permitted_fields_array(); $requiredFields = $settings->name_required_fields_array(); - // One of Person ID or External Identity ID is required - $validator->add( - 'person_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('person_id', null, function($context) { - return empty($context['data']['external_identity_id']); - }); - - $validator->add( - 'external_identity_id', - 'content', - [ 'rule' => 'isInteger' ] - ); - $validator->notEmptyString('external_identity_id', null, function($context) { - return empty($context['data']['person_id']); - }); - - if(in_array('honorific', $permittedFields)) { - $validator->add( - 'honorific', - 'length', - [ 'rule' => [ 'maxLength', 32 ] ] - ); - $validator->add( - 'honorific', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->allowEmptyString('honorific'); - } - - if(in_array('given', $permittedFields)) { - $validator->add( - 'given', - 'length', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'given', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - if(in_array('given', $requiredFields)) { - $validator->notEmptyString('given'); - } else { - $validator->allowEmptyString('given'); - } - } - - if(in_array('middle', $permittedFields)) { - $validator->add( - 'middle', - 'length', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'middle', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->allowEmptyString('middle'); - } + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); - if(in_array('family', $permittedFields)) { - $validator->add( - 'family', - 'length', - [ 'rule' => [ 'maxLength', 128 ] ] - ); - $validator->add( - 'family', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - if(in_array('family', $requiredFields)) { - $validator->notEmptyString('family'); + foreach(['honorific', 'given', 'middle', 'family', 'suffix'] as $f) { + $validator->add($f, [ + 'size' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn($f)]], + 'provider' => 'table'], + 'filter' => ['rule' => ['validateInput'], + 'provider' => 'table'] + ]); + if(in_array($f, $requiredFields)) { + $validator->notEmptyString($f); } else { - $validator->allowEmptyString('family'); + $validator->allowEmptyString($f); } } - if(in_array('suffix', $permittedFields)) { - $validator->add( - 'suffix', - 'length', - [ 'rule' => [ 'maxLength', 32 ] ] - ); - $validator->add( - 'suffix', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->allowEmptyString('suffix'); - } - - $validator->add( - 'type_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->notEmptyString('type_id'); - $validator->add( - 'language', - 'content', - [ 'rule' => [ 'inList', LanguageEnum::getConstValues() ] ] - ); + $validator->add('language', [ + 'content' => ['rule' => ['inList', LanguageEnum::getConstValues()]] + ]); $validator->allowEmptyString('language'); - $validator->add( - 'primary_name', - 'content', - [ 'rule' => [ 'boolean' ] ] - ); + $validator->add('primary_name', [ + 'content' => ['rule' => ['boolean']] + ]); $validator->allowEmptyString('primary_name'); - $validator->add( - 'display_name', - 'length', - [ 'rule' => [ 'maxLength', 256 ] ] - ); - $validator->add( - 'display_name', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->allowEmptyString('display_name'); + $this->registerStringValidation($validator, $schema, 'display_name', false); - $validator->add( - 'source_name_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('source_name_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->allowEmptyString('source_name_id'); return $validator; diff --git a/app/src/Model/Table/PeopleTable.php b/app/src/Model/Table/PeopleTable.php index 6f65cf58d..e1b649050 100644 --- a/app/src/Model/Table/PeopleTable.php +++ b/app/src/Model/Table/PeopleTable.php @@ -68,10 +68,20 @@ public function initialize(array $config): void { ->setConditions(['PrimaryName.primary_name' => true]); $this->hasMany('Names') ->setDependent(true); + $this->hasMany('Addresses') + ->setDependent(true); + $this->hasMany('AdHocAttributes') + ->setDependent(true); $this->hasMany('EmailAddresses') ->setDependent(true); $this->hasMany('Identifiers') ->setDependent(true); + $this->hasMany('PersonRoles') + ->setDependent(true); + $this->hasMany('TelephoneNumbers') + ->setDependent(true); + $this->hasMany('Urls') + ->setDependent(true); // XXX can we change this to Name? $this->setDisplayField('id'); @@ -84,9 +94,14 @@ public function initialize(array $config): void { // XXX does some of this stuff really belong in the controller? $this->setEditContains([ 'PrimaryName', + 'Addresses', + 'AdHocAttributes', 'EmailAddresses', 'Identifiers', - 'Names' + 'Names', + 'PersonRoles', + 'TelephoneNumbers', + 'Urls' ]); $this->setIndexContains(['PrimaryName']); @@ -143,33 +158,25 @@ public function generateDisplayField(\App\Model\Entity\Person $entity): string { */ public function validationDefault(Validator $validator): Validator { - $validator->add( - 'co_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $validator->add('co_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->notEmptyString('co_id'); - $validator->add( - 'status', - 'content', - [ 'rule' => [ 'inList', StatusEnum::getConstValues() ]] - ); + $validator->add('status', [ + 'content' => ['rule' => ['inList', StatusEnum::getConstValues()]] + ]); $validator->notEmptyString('status'); - $validator->add( - 'timezone', - 'content', - [ 'rule' => [ 'validateTimeZone' ], - 'provider' => 'table' ] - ); + $validator->add('timezone', [ + 'content' => ['rule' => ['validateTimeZone'], + 'provider' => 'table' ] + ]); $validator->allowEmptyString('timezone'); - $validator->add( - 'date_of_birth', - 'content', - [ 'rule' => 'date' ] - ); + $validator->add('date_of_birth', [ + 'content' => ['rule' => 'date'] + ]); $validator->allowEmptyString('date_of_birth'); return $validator; diff --git a/app/src/Model/Table/PersonRolesTable.php b/app/src/Model/Table/PersonRolesTable.php new file mode 100644 index 000000000..bc86418e6 --- /dev/null +++ b/app/src/Model/Table/PersonRolesTable.php @@ -0,0 +1,233 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // Person Roles are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('Cous'); + $this->belongsTo('People'); + $this->belongsTo('ManagerPeople') + ->setClassName('People') + ->setForeignKey('manager_person_id') + // Property is set so ruleValidateCO can find it. We don't use the + // _id suffix to match Cake's default pattern. + ->setProperty('manager_person'); + $this->belongsTo('SponsorPeople') + ->setClassName('People') + ->setForeignKey('sponsor_person_id') + ->setProperty('sponsor_person'); + $this->belongsTo('Types') + ->setForeignKey('affiliation_type_id'); + + $this->hasMany('Addresses') + ->setDependent(true); + $this->hasMany('AdHocAttributes') + ->setDependent(true); + $this->hasMany('TelephoneNumbers') + ->setDependent(true); + $this->hasMany('HistoryRecords') + ->setDependent(true); + + $this->setDisplayField('id'); + + $this->setPrimaryLink('person_id'); + $this->setRequiresCO(true); + $this->setRedirectGoal('self'); + + $this->setEditContains([ + 'Addresses', + 'AdHocAttributes', + 'TelephoneNumbers', + // contain results in a join when the relation is belongsTo (or hasOne), + // and joining the same table twice makes the database unhappy, so we + // force these to use multiple queries. + 'ManagerPeople' => ['Names' => ['queryBuilder' => function ($q) { + return $q->where(['primary_name' => true]); + }]], + 'SponsorPeople' => ['Names' => ['queryBuilder' => function ($q) { + return $q->where(['primary_name' => true]); + }]] + ]); + + $this->setAutoViewVars([ + 'statuses' => [ + 'type' => 'enum', + 'class' => 'StatusEnum' + ], + 'affiliationTypes' => [ + 'type' => 'type', + 'attribute' => 'PersonRoles.affiliation' + ], + 'cous' => [ + 'type' => 'select', + 'model' => 'Cous' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) +// See also CFM-126 +// XXX need to add couAdmin, eventually + 'entity' => [ + 'canvas' => ['platformAdmin', 'coAdmin'], + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.0.0 + * @param Person $entity Entity to generate display field for + * @return string Display field + */ + + public function generateDisplayField(\App\Model\Entity\PersonRole $entity): string { + // Try to find something renderable + + if(!empty($entity->title)) { + return $entity->title; + } + +// XXX else affiliation type if set, else cou name, else organization, else department + + return (string)$entity->id; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $validator->add('person_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('co_id'); + + $validator->add('cou_id', [ + 'content' => ['rule' => 'isInteger'] + ]); +// XXX this should be dynamically set based on CO Settings + $validator->allowEmptyString('cou_id'); + + $validator->add('affiliation_type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('affiliation_type_id'); + + $validator->add('sponsor_person_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('sponsor_person_id'); + + $validator->add('manager_person_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('manager_person_id'); + + $this->registerStringValidation($validator, $schema, 'title', false); + + $this->registerStringValidation($validator, $schema, 'organization', false); + + $this->registerStringValidation($validator, $schema, 'department', false); + + $validator->add('valid_from', [ + 'content' => ['rule' => 'dateTime'] + ]); + $validator->allowEmptyString('valid_from'); + + $validator->add('valid_through', [ + 'content' => ['rule' => 'dateTime'] + ]); + $validator->allowEmptyString('valid_through'); + + $validator->add('status', [ + 'content' => ['rule' => ['inList', StatusEnum::getConstValues()]] + ]); + $validator->notEmptyString('status'); + + $validator->add('timezone', [ + 'content' => ['rule' => ['validateTimeZone'], + 'provider' => 'table'] + ]); + $validator->allowEmptyString('timezone'); + + $validator->add('ordr', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('ordr'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/TelephoneNumbersTable.php b/app/src/Model/Table/TelephoneNumbersTable.php new file mode 100644 index 000000000..ea26713e6 --- /dev/null +++ b/app/src/Model/Table/TelephoneNumbersTable.php @@ -0,0 +1,174 @@ + [ + 'campus', + 'fax', + 'home', + 'mobile', + 'office' + ] + ]; + + /** + * Perform Cake Model initialization. + * + * @since COmanage Registry v5.0.0 + * @param array $config Configuration options passed to constructor + */ + + public function initialize(array $config): void { + // Timestamp behavior handles created/modified updates + $this->addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // Telephone Numbers are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('People'); + $this->belongsTo('PersonRoles'); + $this->belongsTo('ExternalIdentities'); + $this->belongsTo('Types'); + + $this->setDisplayField('number'); + +// XXX note primary link is external_identity_id when set... + $this->setPrimaryLink(['person_id', 'person_role_id']); + $this->setRequiresCO(true); + $this->setAcceptsCoId(true); + + $this->setAutoViewVars([ + 'types' => [ + 'type' => 'type', + 'attribute' => 'TelephoneNumbers.type' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'primary' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Callback after model save. + * + * @since COmanage Registry v5.0.0 + * @param EventInterface $event Event + * @param EntityInterface $entity Entity (ie: Co) + * @param ArrayObject $options Save options + * @return bool True on success + */ + + public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool { + $this->recordHistory($entity); + + return true; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + * @throws InvalidArgumentException + * @throws RecordNotFoundException + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + // We need the current CO ID to dynamically set validation rules according + // to CoSettings. + + if(!$this->curCoId) { + throw new \InvalidArgumentException(__d('error', 'coid')); + } + + $CoSettings = TableRegistry::getTableLocator()->get('CoSettings'); + + $settings = $CoSettings->find()->where(['co_id' => $this->curCoId])->firstOrFail(); + + $permittedFields = $settings->telephone_number_permitted_fields_array(); + + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); + + foreach(['country_code', 'area_code', 'number', 'extension'] as $f) { + if(in_array($f, $permittedFields)) { + $this->registerStringValidation($validator, $schema, $f, ($f == 'number')); + } + } + + $this->registerStringValidation($validator, $schema, 'description', false); + + $validator->add('type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('type_id'); + + $validator->add('source_telephone_number_id', [ + 'content' => [ 'rule' => 'isInteger' ] + ]); + $validator->allowEmptyString('source_telephone_number_id'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/src/Model/Table/TypesTable.php b/app/src/Model/Table/TypesTable.php index 052128a55..54b6952a0 100644 --- a/app/src/Model/Table/TypesTable.php +++ b/app/src/Model/Table/TypesTable.php @@ -53,15 +53,15 @@ class TypesTable extends Table { protected $testVar = "123"; protected $supportedAttributes = [ -// 'Addresses.type', + 'Addresses.type', // 'Departments.type', -// 'PersonRoles.affiliation', + 'PersonRoles.affiliation', 'EmailAddresses.type', 'Identifiers.type', 'Names.type', // 'Organizations.type', -// 'TelephoneNumbers.type', -// 'Urls.type' + 'TelephoneNumbers.type', + 'Urls.type' ]; /** @@ -84,9 +84,12 @@ public function initialize(array $config): void { $this->belongsTo('Cos'); $this->hasMany('CoSettings') ->setForeignKey('name_default_type_id'); + $this->hasMany('Addresses'); $this->hasMany('EmailAddresses'); $this->hasMany('Identifiers'); $this->hasMany('Names'); + $this->hasMany('TelephoneNumbers'); + $this->hasMany('Urls'); // XXX add other MVEA models $this->setDisplayField('display_name'); @@ -309,57 +312,36 @@ public function ruleTypeInUse($entity, $options) { */ public function validationDefault(Validator $validator): Validator { - $validator->add( - 'co_id', - 'content', - [ 'rule' => 'isInteger' ] - ); + $schema = $this->getSchema(); + + $validator->add('co_id', [ + 'content' => ['rule' => 'isInteger'] + ]); $validator->notEmptyString('co_id'); - $validator->add( - 'attribute', - 'content', - [ 'rule' => [ 'inList', $this->supportedAttributes ] ] - ); + $validator->add('attribute', [ + 'content' => ['rule' => ['inList', $this->supportedAttributes]] + ]); $validator->notEmptyString('attribute'); - $validator->add( - 'value', - 'length', - [ 'rule' => [ 'maxLength', 32 ] ] - ); - $validator->add( - 'value', - 'content', - [ 'rule' => [ 'custom', '/^[a-zA-Z0-9\-\.]+$/' ] ] - ); + $validator->add('value', [ + 'length' => ['rule' => ['validateMaxLength', ['column' => $schema->getColumn('value')]], + 'provider' => 'table'], + 'value' => ['rule' => ['custom', '/^[a-zA-Z0-9\-\.]+$/'], + 'message' => __d('error', 'input.invalid.url')] + ]); $validator->notEmptyString('value'); - $validator->add( - 'display_name', - 'length', - [ 'rule' => [ 'maxLength', 64 ] ] - ); - $validator->add( - 'display_name', - 'content', - [ 'rule' => [ 'validateInput' ], - 'provider' => 'table' ] - ); - $validator->notEmptyString('display_name'); + $this->registerStringValidation($validator, $schema, 'display_name', true); - $validator->add( - 'edupersonaffiliation', - 'content', - [ 'rule' => [ 'inList', EduPersonAffiliationEnum::getConstValues() ] ] - ); + $validator->add('edupersonaffiliation', [ + 'content' => ['rule' => ['inList', EduPersonAffiliationEnum::getConstValues()]] + ]); $validator->allowEmptyString('edupersonaffiliation'); - $validator->add( - 'status', - 'content', - [ 'rule' => [ 'inList', SuspendableStatusEnum::getConstValues() ] ] - ); + $validator->add('status', [ + 'content' => ['rule' => ['inList', SuspendableStatusEnum::getConstValues()]] + ]); $validator->notEmptyString('status'); return $validator; diff --git a/app/src/Model/Table/UrlsTable.php b/app/src/Model/Table/UrlsTable.php new file mode 100644 index 000000000..009310f9a --- /dev/null +++ b/app/src/Model/Table/UrlsTable.php @@ -0,0 +1,156 @@ + [ + 'official', + 'personal' + ] + ]; + + /** + * Perform Cake Model initialization. + * + * @since COmanage Registry v5.0.0 + * @param array $config Configuration options passed to constructor + */ + + public function initialize(array $config): void { + // Timestamp behavior handles created/modified updates + $this->addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + // URLs are not configuration + $this->setIsConfigurationTable(false); + + // Define associations + $this->belongsTo('People'); + $this->belongsTo('ExternalIdentities'); + $this->belongsTo('Types'); + + $this->setDisplayField('url'); + +// XXX note primary link is external_identity_id when set... + $this->setPrimaryLink(['person_id']); + $this->setRequiresCO(true); + + $this->setAutoViewVars([ + 'types' => [ + 'type' => 'type', + 'attribute' => 'Urls.type' + ] + ]); + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => ['platformAdmin', 'coAdmin'], + 'edit' => ['platformAdmin', 'coAdmin'], + 'primary' => ['platformAdmin', 'coAdmin'], + 'view' => ['platformAdmin', 'coAdmin'] + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => ['platformAdmin', 'coAdmin'], + 'index' => ['platformAdmin', 'coAdmin'] + ] + ]); + } + + /** + * Callback after model save. + * + * @since COmanage Registry v5.0.0 + * @param EventInterface $event Event + * @param EntityInterface $entity Entity (ie: Co) + * @param ArrayObject $options Save options + * @return bool True on success + */ + + public function afterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options): bool { + $this->recordHistory($entity); + + return true; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.0.0 + * @param Validator $validator Validator + * @return Validator Validator + * @throws InvalidArgumentException + * @throws RecordNotFoundException + */ + + public function validationDefault(Validator $validator): Validator { + $schema = $this->getSchema(); + + $this->registerPrimaryKeyValidation($validator, $this->getPrimaryLinks()); + + $validator->add('url', [ + 'content' => ['rule' => ['url'], + 'message' => __d('error', 'input.invalid.url')] + ]); + + $this->registerStringValidation($validator, $schema, 'url', true); + + $this->registerStringValidation($validator, $schema, 'description', false); + + $validator->add('type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('type_id'); + + $validator->add('source_url_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->allowEmptyString('source_url_id'); + + return $validator; + } +} \ No newline at end of file diff --git a/app/templates/AdHocAttributes/columns.inc b/app/templates/AdHocAttributes/columns.inc new file mode 100644 index 000000000..4fee411ec --- /dev/null +++ b/app/templates/AdHocAttributes/columns.inc @@ -0,0 +1,35 @@ + [ + 'type' => 'link' + ], + 'value' => [ + 'type' => 'echo' + ] +]; diff --git a/app/templates/AdHocAttributes/fields.inc b/app/templates/AdHocAttributes/fields.inc new file mode 100644 index 000000000..6a66210f0 --- /dev/null +++ b/app/templates/AdHocAttributes/fields.inc @@ -0,0 +1,33 @@ +Field->control('tag'); + + print $this->Field->control('value'); +} diff --git a/app/templates/Addresses/columns.inc b/app/templates/Addresses/columns.inc new file mode 100644 index 000000000..21eee18bf --- /dev/null +++ b/app/templates/Addresses/columns.inc @@ -0,0 +1,39 @@ + [ + 'type' => 'link' + ], + 'type_id' => [ + 'type' => 'fk' + ], + 'language' => [ + 'type' => 'enum', + 'class' => 'LanguageEnum' + ] +]; diff --git a/app/templates/Addresses/fields.inc b/app/templates/Addresses/fields.inc new file mode 100644 index 000000000..63c9a8703 --- /dev/null +++ b/app/templates/Addresses/fields.inc @@ -0,0 +1,50 @@ +Field->control('street'); + + print $this->Field->control('room'); + + print $this->Field->control('locality'); + + print $this->Field->control('state'); + + print $this->Field->control('postal_code'); + + print $this->Field->control('country'); + + print $this->Field->control('type_id', ['default' => $vv_default_type]); + + print $this->Field->control('language'); + + print $this->Field->control('description'); +} diff --git a/app/templates/CoSettings/fields.inc b/app/templates/CoSettings/fields.inc index 3241ad571..ddc22f67f 100644 --- a/app/templates/CoSettings/fields.inc +++ b/app/templates/CoSettings/fields.inc @@ -30,6 +30,8 @@ if($vv_action == 'edit') { print $this->Field->control('address_required_fields', ['suppressBlank' => true]); + print $this->Field->control('address_default_type_id'); + print $this->Field->control('email_address_default_type_id'); print $this->Field->control('identifier_default_type_id'); @@ -39,4 +41,10 @@ if($vv_action == 'edit') { print $this->Field->control('name_permitted_fields', ['suppressBlank' => true]); print $this->Field->control('name_required_fields', ['suppressBlank' => true]); + + print $this->Field->control('telephone_number_default_type_id'); + + print $this->Field->control('telephone_number_permitted_fields', ['suppressBlank' => true]); + + print $this->Field->control('url_default_type_id'); } diff --git a/app/templates/HistoryRecords/fields.inc b/app/templates/HistoryRecords/fields.inc index 360a8def9..95ff683ef 100644 --- a/app/templates/HistoryRecords/fields.inc +++ b/app/templates/HistoryRecords/fields.inc @@ -57,6 +57,22 @@ if($vv_action == 'add' || $vv_action == 'view') { ); } + if(!empty($vv_obj->person_role_id)) { + $viewLink = [ + 'url' => [ + 'controller' => 'person_roles', + 'action' => 'edit', + $vv_obj->person_role_id + ], + ]; + + print $this->Field->statusControl( + 'person_role_id', + $vv_obj->person_role_id, + $viewLink + ); + } + if(!empty($vv_obj->actor_person->names)) { $viewLink = [ 'url' => [ diff --git a/app/templates/People/fields.inc b/app/templates/People/fields.inc index 266629634..6bcaf1606 100644 --- a/app/templates/People/fields.inc +++ b/app/templates/People/fields.inc @@ -36,12 +36,8 @@ if($vv_action == 'add') { print $this->Field->control('names.0.'.$f, ['required' => in_array($f, $vv_required_name_fields)]); } } - - print $this->Field->control('names.0.type_id', ['empty' => false, 'default' => $vv_default_name_type]); - - print $this->Field->control('status', ['empty' => false]); - print $this->Field->control('date_of_birth'); + print $this->Field->control('names.0.type_id', ['empty' => false, 'default' => $vv_default_name_type]); // AR-Name-1 Since this is the first name for this Person, it must be // designated primary @@ -86,6 +82,17 @@ if($vv_action == 'add') { ['class' => 'linkbutton'] ); + print $this->Html->link( + __d('controller', 'PersonRoles', [99]), + [ 'controller' => 'person_roles', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] + ); + print $this->Html->link( __d('controller', 'HistoryRecords', [99]), [ 'controller' => 'history_records', @@ -97,7 +104,55 @@ if($vv_action == 'add') { ['class' => 'linkbutton'] ); - // XXX add other MVEAs here + print $this->Html->link( + __d('controller', 'AdHocAttributes', [99]), + [ 'controller' => 'ad_hoc_attributes', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] + ); + + print $this->Html->link( + __d('controller', 'Addresses', [99]), + [ 'controller' => 'addresses', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] + ); + + print $this->Html->link( + __d('controller', 'TelephoneNumbers', [99]), + [ 'controller' => 'telephone_numbers', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] + ); + + print $this->Html->link( + __d('controller', 'Urls', [99]), + [ 'controller' => 'urls', + 'action' => 'index', + '?' => [ + 'person_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] + ); debug($vv_obj); +} + +if($vv_action == 'add' || $vv_action == 'canvas') { + print $this->Field->control('status', ['empty' => false]); + + print $this->Field->control('date_of_birth'); } \ No newline at end of file diff --git a/app/templates/PersonRoles/columns.inc b/app/templates/PersonRoles/columns.inc new file mode 100644 index 000000000..bccd329ad --- /dev/null +++ b/app/templates/PersonRoles/columns.inc @@ -0,0 +1,49 @@ + [ + 'type' => 'fk', + 'sortable' => true + ], + 'title' => [ + 'type' => 'link', + 'sortable' => true + ], + 'affiliation_type_id' => [ + 'type' => 'fk', + 'label' => __d('field', 'affiliation'), + 'sortable' => true + ], + 'status' => [ + 'type' => 'enum', + 'class' => 'StatusEnum', + 'sortable' => true + ] +]; diff --git a/app/templates/PersonRoles/fields.inc b/app/templates/PersonRoles/fields.inc new file mode 100644 index 000000000..f73e014c0 --- /dev/null +++ b/app/templates/PersonRoles/fields.inc @@ -0,0 +1,103 @@ +Field->control('cou_id'); + + print $this->Field->control('affiliation_type_id', [], __d('field', 'affiliation')); + + print $this->Field->control('status', ['empty' => false]); + + print $this->Field->control('ordr'); + + print $this->Field->control('title'); + + print $this->Field->control('organization'); + + print $this->Field->control('department'); + + // For now, we render sponsor and manager as read only. + // XXX Need People Picker (CFM-150) + foreach(['sponsor', 'manager'] as $f) { + $fp = $f."_person"; + + $fname = ""; + $flink = []; + + if(!empty($vv_obj->$fp->names[0])) { + $fname = $vv_obj->$fp->names[0]->full_name; + $flink = ['url' => ['controller' => 'people', 'action' => 'canvas', $vv_obj->$fp->id]]; + } + + print $this->Field->statusControl($f.'_person_id', $fname, $flink, __d('field', $f)); + } + +// XXX these need to render date pickers +// - we specifically have code in FieldHelper that checks for these two, but not date_of_birth +// - can FieldHelper introspect the date type rather than require a hard coded list of fields? +// though note valid_from/through uses special logic for 00:00:00 and 23:59:59 + print $this->Field->control('valid_from'); + + print $this->Field->control('valid_through'); +} + +// XXX This is a placeholder for canvas... maybe it should become a separate page +// rather than overload fields.inc? + +print $this->Html->link( + __d('controller', 'AdHocAttributes', [99]), + [ 'controller' => 'ad_hoc_attributes', + 'action' => 'index', + '?' => [ + 'person_role_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'Addresses', [99]), + [ 'controller' => 'addresses', + 'action' => 'index', + '?' => [ + 'person_role_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); + +print $this->Html->link( + __d('controller', 'TelephoneNumbers', [99]), + [ 'controller' => 'telephone_numbers', + 'action' => 'index', + '?' => [ + 'person_role_id' => $vv_obj->id + ] + ], + ['class' => 'linkbutton'] +); diff --git a/app/templates/Standard/add-edit-view.php b/app/templates/Standard/add-edit-view.php index ecdf6f547..008d0a495 100644 --- a/app/templates/Standard/add-edit-view.php +++ b/app/templates/Standard/add-edit-view.php @@ -108,7 +108,9 @@ print $this->Field->startControlSet($this->name, $vv_action, - ($vv_action == 'add' || $vv_action == 'edit'), + // XXX We need a model specific mechanism to disable read-only + // (eg: canvas should be declared by People) + ($vv_action == 'add' || $vv_action == 'canvas' || $vv_action == 'edit'), $vv_required_fields); // We allow the fields.inc file to be specified for Controllers that have more diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index 86fbf5d75..01fa7eec3 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -61,9 +61,9 @@ function _column_key($modelsName, $c, $tz=null) { if(strpos($c, "_id", strlen($c)-3)) { // Key is of the form field_id, use .ct label instead - $k = Inflector::classify(Inflector::pluralize(substr($c, 0, strlen($c)-3))); + $k = Inflector::camelize(Inflector::pluralize(substr($c, 0, strlen($c)-3))); - return __d('controller' ,$k, [1]); + return __d('controller', $k, [1]); } // Look for a model specific key first diff --git a/app/templates/TelephoneNumbers/columns.inc b/app/templates/TelephoneNumbers/columns.inc new file mode 100644 index 000000000..6b2eff199 --- /dev/null +++ b/app/templates/TelephoneNumbers/columns.inc @@ -0,0 +1,35 @@ + [ + 'type' => 'link' + ], + 'type_id' => [ + 'type' => 'fk' + ] +]; diff --git a/app/templates/TelephoneNumbers/fields.inc b/app/templates/TelephoneNumbers/fields.inc new file mode 100644 index 000000000..058b2172a --- /dev/null +++ b/app/templates/TelephoneNumbers/fields.inc @@ -0,0 +1,42 @@ +Field->control($f); + } + } + + print $this->Field->control('type_id', ['default' => $vv_default_type]); + + print $this->Field->control('description'); +} diff --git a/app/templates/Urls/columns.inc b/app/templates/Urls/columns.inc new file mode 100644 index 000000000..dfcedffe4 --- /dev/null +++ b/app/templates/Urls/columns.inc @@ -0,0 +1,35 @@ + [ + 'type' => 'link' + ], + 'type_id' => [ + 'type' => 'fk' + ] +]; diff --git a/app/templates/Urls/fields.inc b/app/templates/Urls/fields.inc new file mode 100644 index 000000000..f3d442389 --- /dev/null +++ b/app/templates/Urls/fields.inc @@ -0,0 +1,35 @@ +Field->control('url'); + + print $this->Field->control('type_id', ['default' => $vv_default_type]); + + print $this->Field->control('description'); +} diff --git a/app/templates/element/breadcrumbs.php b/app/templates/element/breadcrumbs.php index e646e49c3..edee2b1c9 100644 --- a/app/templates/element/breadcrumbs.php +++ b/app/templates/element/breadcrumbs.php @@ -81,7 +81,22 @@ $vv_person_name->full_name, ['controller' => 'people', 'action' => 'canvas', - $vv_primary_link_id] + $vv_person_id] + ); + } + + if(!empty($vv_person_role)) { + $this->Breadcrumbs->add( + __d('controller', 'PersonRoles', [99]), + ['controller' => 'person_roles', + '?' => ['person_id' => $vv_person_role_id]] + ); + + $this->Breadcrumbs->add( + $vv_person_role, + ['controller' => 'person_roles', + 'action' => 'edit', + $vv_person_role_id] ); } }