<a id="custom-diagnostics"></a>
<a id="id1"></a>
# Custom Diagnostics

In addition to the standard [Diagnostics](../diagnostics.html.md#diagnostics) it is possible to define Custom Diagnostics.

Custom Diagnostics can inspect Draft and Custom Objects.

A Custom Diagnostic has the following properties:

| Property | Description |
| --- | --- |
| **Short Name** | Must be unique within the Rave URL. Can be up to 20 characters in length. Short names comprising 4 digits (0000 - 9999) are reserved for built-in diagnostics. It may be useful to use a standard prefix, for example 'C001', 'C002', etc. |
| **Name** | Must be unique within the Rave URL. |
| **Description** | A description of the custom diagnostic. |
| **Object Types** | The object types which the custom diagnostic will inspect. A diagnostic cannot access object types using `` diagnostic.objects.get("..object name..")` `` if it does not declare that it uses that type of object. |
| **Active** | If active is False the Custom Diagnostic will not be displayed in Project Manage Diagnostics and cannot be run against a Draft. |
| **Code** | The RestrictedPython code that implements the diagnostic logic. |

<a id="copying-custom-diagnostics-between-urls"></a>
## Copying Custom Diagnostics between URLs

Custom Diagnostics can be copied from one URL into another and can be copied from one URL to other URLs. If a Custom Diagnostic has object types which do not exist in the target URL then a warning will be displayed. The Custom Diagnostic will be copied but might not function as expected. There is no check that any Custom Properties referenced in the Custom Diagnostic code exist in the target URL.

<a id="custom-diagnostic-code"></a>
## Custom Diagnostic Code

Custom Diagnostic code is written using a subset of the Python programming language called RestrictedPython. For security, RestrictedPython limits the code which can be used; for example it is not possible to use the 'import', 'exec' or 'eval' statements.

The 'format' and 'format_map' methods of text values cannot be used either. Unlike the statements above, this is reported when the diagnostic runs rather than when you save it. To build a message, use an f-string such as f"Field {field.name} is missing", or add the pieces together with '+'.

A 'diagnostic' object is passed into the Custom Diagnostic Code. The diagnostic object has the following methods which can be called from the Custom Diagnostic code:

- **Method:** **objects.get(plural_name)**

  **Description:** Returns a list of DiagnosticObjects which can be inspected for the given plural_name (the plural name of the Object Definition). See below for the definition of a DiagnosticObject.

  *Parameters:*

  - `plural_name` (str) – (required). The plural name of the Object Definition.

- **Method:** **debug(message)**

  **Description:** Displays the string when testing the Custom Diagnostic. When the Custom Diagnostic has been activated and is run for Drafts no debug messages are displayed.

  *Parameters:*

  - `message` (str) – (required). The message to be displayed.

- **Method:** **report_draft_finding(message)**

  **Description:** Creates a diagnostic finding for the Draft.

  *Parameters:*

  - `message` (str) – (required). The finding text.

- **Method:** **report_object_finding(object_with_finding, property_name, message)**

  **Description:** Creates a diagnostic finding for a specific object and property.

  *Parameters:*

  - `object_with_finding` (DiagnosticObject) – (required). The object associated with the finding.
  - `property_name` (str) – (required). The name of the property associated with the finding.
  - `message` (str) – (required). The finding text.

- **Method:** **is_derived_field(field)**

  **Description:** Returns `True` if the Field is derived, that is, if it is the target of an active Derivation, `False` otherwise. No property of a Field says whether it is derived, so this is the way to find out. The Custom Diagnostic does not need to include "Derivations" in its Object Types to use it.

  *Parameters:*

  - `field` (DiagnosticObject) – (required). An object from `diagnostic.objects.get("Fields")`.

The diagnostic object has the following properties:

| Property | Description |
| --- | --- |
| **draft** | The draft on which the diagnostic is being run. |
| **project** | The project for the draft. |

<a id="project"></a>
## Project

The `diagnostic.project` object provides access to project-level information.

**Methods:**

- **Method/Property:** **property(property_name)**

  **Description:** Returns the value associated with the project property.

  *Parameters:*

  - `property_name` (str) – (required). The name of the project property.

- **Method/Property:** **labels**

  **Description:** Returns a tuple of DiagnosticLabel objects representing the labels assigned to the project.

- **Method/Property:** **has_label(label_name)**

  **Description:** Returns `True` if the project has a label with the given name, `False` otherwise.

  *Parameters:*

  - `label_name` (str) – (required). The name of the label to check for.

- **Method/Property:** **has_tsdv_configuration**

  **Description:** Returns `True` if the project has a TSDV configuration, `False` otherwise.

- **Method/Property:** **custom_tiers**

  **Description:** Returns a tuple of DiagnosticCustomTier objects representing the TSDV custom tiers configured for the project. See [TSDV Configuration](#tsdv-configuration) below for details.

**Available Properties:**

| Property Name | Type | Description |
| --- | --- | --- |
| **id** | int | The unique identifier of the project. |
| **ProjectName** | string | The name of the project. |
| **ProjectDescription** | string | The description of the project. |
| **ProtocolName** | string | The protocol name associated with the project. |
| **?Custom Property Name?** | string | Any additional custom properties defined for the project are accessible by name here |

<a id="draft"></a>
## Draft

The `diagnostic.draft` object provides access to draft-level information.

**Methods:**

- **Method/Property:** **property(property_name)**

  **Description:** Returns the value associated with the draft property.

  *Parameters:*

  - `property_name` (str) – (required). The name of the draft property.

- **Method/Property:** **labels**

  **Description:** Returns a tuple of DiagnosticLabel objects representing the labels assigned to the draft.

- **Method/Property:** **has_label(label_name)**

  **Description:** Returns `True` if the draft has a label with the given name, `False` otherwise.

  *Parameters:*

  - `label_name` (str) – (required). The name of the label to check for.

**Available Properties:**

| Property Name | Type | Description |
| --- | --- | --- |
| **id** | int | The unique identifier of the draft. |
| **DraftName** | string | The name of the draft. |
| **PrimaryForm_id** | int | The ID of the primary form for this draft (foreign key reference). |
| **DefaultMatrix_id** | int | The ID of the default matrix for this draft (foreign key reference). |
| **ConfirmationMessage** | string | The confirmation message displayed to users. |
| **SignaturePrompt** | string | The signature prompt text. |
| **LabStandardGroup** | string | The lab standard group name. |
| **ReferenceLabs** | string | Reference labs configuration. |
| **show_uat_coverage** | bool | Whether UAT coverage is displayed. |
| **approvals** | string | Approval settings for the draft. |
| **is_locked** | bool | Whether the draft is locked. |
| **allow_import** | bool | Whether import is allowed for this draft. |
| **?Custom Property Name?** | string | Any additional custom properties defined for the draft are accessible by name here |

<a id="tsdv-configuration"></a>
<a id="id2"></a>
## TSDV Configuration

The `diagnostic.project.custom_tiers` property provides access to TSDV custom tier configuration. This is only populated if the project has a TSDV configuration (check `diagnostic.project.has_tsdv_configuration`).

**DiagnosticCustomTier Properties:**

| Property | Description |
| --- | --- |
| **tier_name** | The name of the custom tier. |
| **tier_description** | The description of the custom tier. |
| **tier_form_fields** | Tuple of DiagnosticTierFormField objects for this tier. |
| **tier_form_folders** | Tuple of DiagnosticTierFormFolder objects for this tier. |

**DiagnosticTierFormField Properties:**

| Property | Description |
| --- | --- |
| **form_oid** | The OID of the form. |
| **field_oid** | The OID of the field. |

**DiagnosticTierFormFolder Properties:**

| Property | Description |
| --- | --- |
| **form_oid** | The OID of the form. |
| **folder_oid** | The OID of the folder. |
| **ordinal** | The ordinal position (may be None). |

<a id="diagnosticobject"></a>
## DiagnosticObject

A DiagnosticObject (representing Forms, Folders, Edit Checks, Custom Objects etc) has the following properties:

- **Property:** **identifier**

  **Description:** Returns the identifier of the object.

- **Property:** **labels**

  **Description:** Returns a tuple of DiagnosticLabel objects representing the labels assigned to this object. Each DiagnosticLabel has the following properties:

  - `label_id` (int) – The unique identifier of the label.
  - `name` (str) – The name of the label.
  - `color` (str) – The hex color code of the label (e.g., `#66bf39`).
  - `text_color` (str) – The calculated text color for contrast (dark or light).
  - `reserved_for_actions` (bool) – Whether the label is controlled by Actions rather than users.

- **Property:** **user_assignments**

  **Description:** Returns a tuple of DiagnosticUserAssignment objects representing users assigned to this object. Each DiagnosticUserAssignment has the following properties:

  - `id` (int) – The unique identifier of the user.
  - `username` (str) – The username of the assigned user.
  - `email` (str) – The email address of the assigned user.

- **Property:** **role_assignments**

  **Description:** Returns a tuple of DiagnosticRoleAssignment objects representing project roles assigned to this object. Each DiagnosticRoleAssignment has the following properties:

  - `id` (int) – The unique identifier of the project role.
  - `name` (str) – The name of the project role.

A DiagnosticObject has the following methods:

- **Method:** **property(property_name)**

  **Description:** Returns the value associated with the property of the object.

  *Parameters:*

  - `property_name` (str) – (required). The name of the DiagnosticObject property.

- **Method:** **has_label(label_name)**

  **Description:** Returns `True` if the object has a label with the given name, `False` otherwise.

  *Parameters:*

  - `label_name` (str) – (required). The name of the label to check for.

- **Method:** **has_user_assigned(username)**

  **Description:** Returns `True` if the object has a user assignment with the given username, `False` otherwise.

  *Parameters:*

  - `username` (str) – (required). The username to check for.

- **Method:** **has_role_assigned(role_name)**

  **Description:** Returns `True` if the object has a role assignment with the given role name, `False` otherwise.

  *Parameters:*

  - `role_name` (str) – (required). The name of the project role to check for.

<a id="diagnostic-object-properties"></a>
## Diagnostic Object Properties

The list of properties available for the selected object types will be displayed in the "Properties" tab.

Custom Objects will have all defined Custom Properties; the data type will always be 'string'. Custom objects will also have a `parent_model_id` property that is the id of its parent custom object (if it is a child custom object).

Draft Objects will have built-in Medidata Rave properties and any Custom Properties which have been created for that type of Draft Object.

For example, if "Matrices" are selected for inclusion in the Custom Diagnostic the following built-in properties will be available:

> - - **Matrices:**
>
>     - id (int)
>     - MatrixName (string)
>     - OID (string)
>     - Addable (bool)
>     - Maximum (int)

The "id" property is a unique integer identifying each Matrix in TrialGrid.

"Matrix Folder Forms" objects will also be included, with the following properties:

> - - **Matrix Folder Forms:**
>
>     - id (int)
>     - Matrix_id (int)
>     - Folder_id (int)
>     - Form_id (int)

The "id" property is a unique integer identifying each Matrix Folder Form in TrialGrid.

Properties ending with "_id" are "foreign key" references to another object. "Matrix Folder Form" objects have a "Matrix_id" property which references the "Matrix".

The related object can be looked up using this pattern:

```python
matrices_by_id = {matrix.property("id") : matrix for matrix in diagnostic.objects.get('Matrices')}

for mff in diagnostic.objects.get("Matrix Folder Forms"):
    matrix = matrices_by_id[mff.property("Matrix_id")]
```

<a id="examples"></a>
## Examples

```python
# Report a draft finding if the 'ID' property of the 'Specifications' objects is repeated in more than one object

spec_ids = []
all_spec_ids = []
for spec in diagnostic.objects.get('Specifications'):
    spec_id = spec.property('ID')
    if spec_id not in spec_ids:
        spec_ids.append(spec_id)
    all_spec_ids.append(spec_id)

for spec_id in spec_ids:
    spec_count = all_spec_ids.count(spec_id)
    if spec_count > 1:
        msg = f"Specification ID {spec_id} is repeated {spec_count} times"
        diagnostic.report_draft_finding(msg)
```

```python
# Report an object finding if the length of the 'DraftFieldName' property of a 'Fields' object is more than 20

for field in diagnostic.objects.get('Fields'):
    if len(field.property('DraftFieldName')) > 20:
        diagnostic.report_object_finding(field, "DraftFieldName", "is longer than 20 characters")
```

```python
# Report an object finding if the 'Description' property of a 'Specifications' object contains double-spacing

for spec in diagnostic.objects.get('Specifications'):
    if "  " in spec.property('Description'):
        diagnostic.report_object_finding(spec, "Description", "has double-spacing")
```

```python
# Report an object finding for Custom Functions that mix the V1 and V2 Rave APIs.
# 'rave_api_version' is 'v1', 'v2', 'mixed' or 'none' for C# functions, and has no value for SQL functions.
# 'rave_api_conversion' says how much work converting to V2 takes: 'automatic', 'manual' or 'n/a'.

for cf in diagnostic.objects.get('Custom Functions'):
    if cf.property('rave_api_version') == 'mixed':
        diagnostic.report_object_finding(cf, "FunctionName", "mixes the V1 and V2 Rave APIs")
```

```python
# Report an object finding for Fields that are required even though they are derived

for field in diagnostic.objects.get('Fields'):
    if field.property('IsRequired') and diagnostic.is_derived_field(field):
        diagnostic.report_object_finding(field, "IsRequired", "is derived, so should not be required")
```

```python
# Report an object finding for Forms that have the 'Review Required' label

for form in diagnostic.objects.get('Forms'):
    if form.has_label('Review Required'):
        diagnostic.report_object_finding(form, "OID", "is marked for review")
```

```python
# Report an object finding for Forms that have any labels assigned

for form in diagnostic.objects.get('Forms'):
    if len(form.labels) > 0:
        label_names = ", ".join(label.name for label in form.labels)
        diagnostic.report_object_finding(form, "OID", f"has labels: {label_names}")
```

```python
# Check if project has TSDV configuration and iterate tiers

if diagnostic.project.has_tsdv_configuration:
    for tier in diagnostic.project.custom_tiers:
        diagnostic.debug(f"Tier: {tier.tier_name} - {tier.tier_description}")
        for field in tier.tier_form_fields:
            diagnostic.debug(f"  Field: {field.form_oid}.{field.field_oid}")
        for folder in tier.tier_form_folders:
            diagnostic.debug(f"  Folder: {folder.folder_oid}/{folder.form_oid}")
```

```python
# Report an object finding for Forms that are assigned to a specific user

for form in diagnostic.objects.get('Forms'):
    if form.has_user_assigned('jsmith'):
        diagnostic.report_object_finding(form, "OID", "is assigned to jsmith")
```

```python
# Report an object finding for Forms that are assigned to a specific project role

for form in diagnostic.objects.get('Forms'):
    if form.has_role_assigned('Reviewer'):
        diagnostic.report_object_finding(form, "OID", "is assigned to Reviewer role")
```

```python
# Report an object finding for Forms that have any user or role assignments

for form in diagnostic.objects.get('Forms'):
    if len(form.user_assignments) > 0 or len(form.role_assignments) > 0:
        user_names = ", ".join(a.username for a in form.user_assignments)
        role_names = ", ".join(a.name for a in form.role_assignments)
        assignments = []
        if user_names:
            assignments.append(f"users: {user_names}")
        if role_names:
            assignments.append(f"roles: {role_names}")
        diagnostic.report_object_finding(form, "OID", f"has assignments: {'; '.join(assignments)}")
```
