# This script enables the definition of Approvers and Reviewers for specific documents and/or items in the project
# Approvers will be able to Approve and Reject items and enter comments associated to items
# Reviewers will be able to enter comments only
# Approvers and Reviewers are identified via a USER attributes. This attribute can be single- or multi-valued
# The "Reviewers" and "Approvers" attribute can be associated either to documents or to items
# The Visure templates have the Approvers and Reviewers attributes associated to documents, but this can be configured in the scope
# If associated to documents, the user(s) associated to the attribute will be able to Approve and/or Review all items in the document
# If the attribute is empty, then all users will be able to approve and/or review
# If associated to the items, the user(s) associated to the attribute will be able to Approve and/or Review the items the user is assigned to
# If the attribute is empty, then nobody will be able to approve or review
# Alternatively REVIEWER_ROLES and APPROVER_ROLES text attribute can be used to identify user groups instead of individual users to approve and review
# These two role based attributes are disabled and empty in the out-of-the-box script
# This script also defines a "Review Due Date" which identifies the date by which the review has to finish
# After this date, approvals, rejections and comments will not be accepted
# If this value is empty, then it will not be taken into account

"""
Script Name     : VisureReviewProcess.py
Description     : Implements Visure's review and approval process
Author          : Visure Solutions, Inc.
Company         : Visure Solutions, Inc.
Creation Date   : August 22, 2023
Last Modified   : December 1, 2023
Version         : 1.0
"""


import datetime
import Visure


# Visure review process

PROJECTS            = []                    # Projects - Leave empty to apply to any project
SPECIFICATIONS      = []                    # Specifications - Leave empty to apply to any specification

REVIEWERS_ROLES     = []                    # Reviewers roles - Leave empty to let any user to review items
REVIEWERS           = "Reviewers"           # Reviewers attribute - Leave empty to let any user to review items
REVIEW_DUE_DATE     = "Review Due Date"     # Review due date attibute - Leave emtpy to don't take into account any due date

APPROVERS_ROLES     = []                    # Approvers roles - Leave empty to let any user to approve/reject items
APPROVERS           = "Approvers"           # Approvers attribute - Leave empty to let any user to approve/reject items


# Comments and threads
def CheckIfUserIsInValues(bl, userId, attr, item):
    try:
        if len(item.values(attr.id)) > 0:
            for u in item.values(attr.id):
                if u.value == userId:
                    return True

    except Exception as e:
        bl.Trace_ERROR(str(e))

    return False
    
# The following operation will check if the user trying to create a comment has the corresponding reviewer or approver role

def CheckRoleReviewers(bl, item):
    try:
        # Check the role list
        if len(REVIEWERS_ROLES) > 0 and bl.GetUserGroupName() not in REVIEWERS_ROLES:
            raise Exception(f"Your '{bl.GetUserGroupName()}' role does not have permission to perform this operation")
        
        # Check the reviewers attribute
        if len(REVIEWERS) > 0 and bl.GetAttributeID(REVIEWERS) != -1:
            # Reviewers attribute
            reviewersAttr = bl.attribute(REVIEWERS)

            # Check attribute base type, to make sure it´s a USER type of attribute
            if reviewersAttr.type.base_type != 7:
                raise Exception(f"Wrong reviewers attribute type '{reviewersAttr.type.base_type}'")

            # Check if the item has the attribute
            if item.has_attribute(reviewersAttr.id):
                if len(item.values(reviewersAttr.id)) == 0:
                    raise Exception("You are not assigned as a reviewer to this item")

                if len(item.values(reviewersAttr.id)) > 0:
                    if not CheckIfUserIsInValues(bl, bl.GetUserID(), reviewersAttr, item):
                        raise Exception(f"You are not assigned as a reviewer to this item")
            else:
                # Check if item specifications have the attribute
                for spe in item.specifications:
                    if spe.has_attribute(reviewersAttr.id):
                        #if len(spe.values(reviewersAttr.id)) == 0:
                        #   raise Exception(f"*You are not assigned as a reviewer to '{spe.name}'")
                        if len(spe.values(reviewersAttr.id)) > 0:
                            if not CheckIfUserIsInValues(bl, bl.GetUserID(), reviewersAttr, spe):
                                raise Exception(f"You are not assigned as a reviewer to this item")
        else:
            bl.GetLastError()   # Resets error text

        return True;

    except Exception as e:
        #bl.Trace_ERROR(str(e))
        return str(e)

def CheckRoleApprovers(bl, item):
    try:
        # Check the role list
        if len(APPROVERS_ROLES) > 0 and bl.GetUserGroupName() not in APPROVERS_ROLES:
            raise Exception(f"Your '{bl.GetUserGroupName()}' role does not have permission to perform this operation")
        
        # Check the approvers attribute
        if len(APPROVERS) > 0 and bl.GetAttributeID(APPROVERS) != -1:
            # Approvers attribute
            approversAttr = bl.attribute(APPROVERS)

            # Check attribute base type, to make sure it´s a USER type of attribute
            if approversAttr.type.base_type != 7:
                raise Exception(f"Wrong approvers attribute type '{approversAttr.type.base_type}'")

            # Check if the item has the attribute
            if item.has_attribute(approversAttr.id):
                if len(item.values(approversAttr.id)) == 0:
                    raise Exception("You are not assigned as an approver to this item")
                elif len(item.values(approversAttr.id)) > 0:
                    if not CheckIfUserIsInValues(bl, bl.GetUserID(), approversAttr, item):
                        raise Exception("You are not assigned as an approver to this item")
            else:
                # Check if item specifications have the attribute
                for spe in item.specifications:
                    if spe.has_attribute(approversAttr.id):
                        if len(spe.values(approversAttr.id)) > 0:
                            if not CheckIfUserIsInValues(bl, bl.GetUserID(), approversAttr, spe):
                                raise Exception("You are not assigned as an approver to this item")
        else:
            bl.GetLastError()   # Resets error text

        return True;

    except Exception as e:
        #bl.Trace_ERROR(str(e))
        return str(e)

def Visure_ReviewProcess(bl, lElementID):
    try:
        # Check the project
        if len(PROJECTS) > 0 and bl.GetProjectName() not in PROJECTS:
            return True

        # The item
        if not bl.ExistsElement(lElementID, 0):
            return True
        item = bl.item(lElementID)

        # Check the specification
        if len(SPECIFICATIONS) > 0:
            belongToAnySpecification = False

            for speName in SPECIFICATIONS:
                spe = bl.GetSpecification(speName)
                if item.belongsToSpecification(spe.id):
                    belongToAnySpecification = True
                    break

            if not belongToAnySpecification:
                return True

        # Check role
        checkRoleReviewersResult = CheckRoleReviewers(bl, item)

        if type(checkRoleReviewersResult) is str or (type(checkRoleReviewersResult) is bool and not checkRoleReviewersResult):
            checkRoleApproversResult = CheckRoleApprovers(bl, item)

            if type(checkRoleApproversResult) is str or (type(checkRoleApproversResult) is bool and not checkRoleApproversResult):
                if type(checkRoleReviewersResult) is str:
                    raise Exception(checkRoleReviewersResult)
                elif type(checkRoleApproversResult) is str:
                    raise Exception(checkRoleApproversResult)
                else:
                    raise Exception("You are not assigned as a reviewer or as an approver to this item")

        # Check the due date
        if len(REVIEW_DUE_DATE) > 0 and bl.GetAttributeID(REVIEW_DUE_DATE) != -1:
            # Due date attribute
            dueDateAttr = bl.attribute(REVIEW_DUE_DATE)

            # Check attribute base type
            if dueDateAttr.type.base_type != 4:
                raise Exception(f"Incorrect Review Due Date attribute type '{dueDateAttr.type.base_type}'")

            now = datetime.datetime.now()

            if item.has_attribute(dueDateAttr.id):
                if len(item.values(dueDateAttr.id)) > 0:
                    itemDueDate = item.value(dueDateAttr.id)

                    if itemDueDate < now:
                        raise Exception("You cannot review an item after the review due date. Please, get in contact with the author of the review")
            else:
                for spe in item.specifications:
                    if spe.has_attribute(dueDateAttr.id):
                        if len(spe.values(dueDateAttr.id)) > 0:
                            speDueDate = spe.value(dueDateAttr.id)

                            if speDueDate < now:
                                raise Exception("You cannot review an item after the review due date. Please, get in contact with the author of the review")
        else:
            bl.GetLastError()   # Resets error text

    except Exception as e:
        bl.Trace_WARNING(str(e))
        return str(e)

    return True
    
def Visure_ApprovalProcess(bl, lElementID):
    try:
        # Check the project
        if len(PROJECTS) > 0 and bl.GetProjectName() not in PROJECTS:
            return True

        # The item
        if not bl.ExistsElement(lElementID, 0):
            return True
        item = bl.item(lElementID)

        # Check the specification
        if len(SPECIFICATIONS) > 0:
            belongToAnySpecification = False

            for speName in SPECIFICATIONS:
                spe = bl.GetSpecification(speName)
                if item.belongsToSpecification(spe.id):
                    belongToAnySpecification = True
                    break

            if not belongToAnySpecification:
                return True

        # Check the role list
        checkRoleApproversResult = CheckRoleApprovers(bl, item)

        if type(checkRoleApproversResult) is str:
            raise Exception(checkRoleApproversResult)
        elif type(checkRoleApproversResult) is bool and not checkRoleApproversResult:
            raise Exception("You are not assigned as an approver to this item")

        # Check the due date
        if len(REVIEW_DUE_DATE) > 0 and bl.GetAttributeID(REVIEW_DUE_DATE) != -1:
            # Due date attribute
            dueDateAttr = bl.attribute(REVIEW_DUE_DATE)

            # Check attribute base type
            if dueDateAttr.type.base_type != 4:
                raise Exception(f"Wrong due date attribute type '{dueDateAttr.type.base_type}'")

            now = datetime.datetime.now()

            if item.has_attribute(dueDateAttr.id):
                if len(item.values(dueDateAttr.id)) > 0:
                    itemDueDate = item.value(dueDateAttr.id)

                    if itemDueDate < now:
                        raise Exception("You cannot approve or reject an item after the review due date. Please, get in contact with the author of the review")
            else:
                for spe in item.specifications:
                    if spe.has_attribute(dueDateAttr.id):
                        if len(spe.values(dueDateAttr.id)) > 0:
                            speDueDate = spe.value(dueDateAttr.id)

                            if speDueDate < now:
                                raise Exception("You cannot approve or reject an item after the review due date. Please, get in contact with the author of the review")
        else:
            bl.GetLastError()   # Resets error text

    except Exception as e:
        bl.Trace_WARNING(str(e))
        return str(e)

    return True    


def Visure_beforeCreateCommentThread(bl, lCommentID, lElementID, strComment):
    return Visure_ReviewProcess(bl, lElementID)

def Visure_beforeDeleteCommentThread(bl, lThreadID, lElementID):
    return Visure_ReviewProcess(bl, lElementID)

def Visure_beforeChangeCommentInThread(bl, lCommentID, lElementID, strComment):
    return Visure_ReviewProcess(bl, lElementID)

def Visure_beforeDeleteCommentInThread(bl, lCommentID, lElementID):
    return Visure_ReviewProcess(bl, lElementID)

def Visure_beforeChangeCommentThreadType(bl, lThreadID, lTypeID, lElementID):
    return Visure_ReviewProcess(bl, lElementID)

def Visure_beforeChangeCommentThreadStatus(bl, lThreadID, statusEvID, lElementID):
    return Visure_ReviewProcess(bl, lElementID)

def Visure_beforeChangeCommentThreadCategory(bl, lThreadID, lCategoryID, lElementID):
    return Visure_ReviewProcess(bl, lElementID)


# Approvals

def Visure_beforeApproveRequirement(bl, lElementID, strComment):
    return Visure_ApprovalProcess(bl, lElementID)

def Visure_beforeRejectRequirement(bl, lElementID, strComment):
    return Visure_ApprovalProcess(bl, lElementID)

def Visure_beforeClearApproval(bl, lElementID):
    return Visure_ApprovalProcess(bl, lElementID)

def Visure_beforeClearRejection(bl, lElementID):
    return Visure_ApprovalProcess(bl, lElementID)
