From cf2681c2ee6bf25ee70045365f53ef67e913a62f Mon Sep 17 00:00:00 2001 From: Dominic Tubach Date: Thu, 22 Feb 2024 11:40:05 +0100 Subject: [PATCH] Allow to use selected CiviOffice documents for applications via remote API --- .../BAO/ApplicationCiviOfficeTemplate.php | 35 ++ .../DAO/ApplicationCiviOfficeTemplate.php | 302 ++++++++++++++++++ .../Page/RemoteApplicationTemplateRender.php | 17 + .../FundingApplicationCiviOfficeTemplate.php | 22 ++ Civi/Api4/RemoteFundingApplicationProcess.php | 10 + .../GetTemplateRenderUriAction.php | 44 +++ .../ApplicationProcess/GetTemplatesAction.php | 34 ++ .../GetTemplateRenderUriActionHandler.php | 54 ++++ .../GetTemplatesActionHandler.php | 74 +++++ .../ApplicationTemplateRenderController.php | 122 +++++++ .../CiviOffice/CiviOfficeDocumentRenderer.php | 55 +--- .../CiviOffice/CiviOfficePseudoConstants.php | 44 +++ .../CiviOffice/CiviOfficeRenderer.php | 96 ++++++ ang/afformApplicationTemplates.aff.html | 16 + ang/afformApplicationTemplates.aff.json | 28 ++ ang/afsearchFundingCaseTypes.aff.html | 3 + ang/afsearchFundingCaseTypes.aff.json | 12 + .../afform/afSubmitReload.directive.js | 34 ++ managed/NavigationFunding.mgd.php | 25 ++ managed/SavedSearchFundingCaseTypes.mgd.php | 135 ++++++++ services/document-render.php | 2 + sql/auto_install.sql | 19 ++ sql/auto_uninstall.sql | 1 + sql/upgrade/0001.sql | 11 + .../GetTemplatesActionHandlerTest.php | 107 +++++++ ...pplicationTemplateRenderControllerTest.php | 186 +++++++++++ .../CiviOfficeDocumentRendererTest.php | 74 ++--- .../CiviOffice/CiviOfficeRendererTest.php | 94 ++++++ xml/Menu/funding.xml | 6 + ...plicationCiviOfficeTemplate.entityType.php | 10 + .../FundingApplicationCiviOfficeTemplate.xml | 82 +++++ 31 files changed, 1648 insertions(+), 106 deletions(-) create mode 100644 CRM/Funding/BAO/ApplicationCiviOfficeTemplate.php create mode 100644 CRM/Funding/DAO/ApplicationCiviOfficeTemplate.php create mode 100644 CRM/Funding/Page/RemoteApplicationTemplateRender.php create mode 100644 Civi/Api4/FundingApplicationCiviOfficeTemplate.php create mode 100644 Civi/Funding/Api4/Action/Remote/ApplicationProcess/GetTemplateRenderUriAction.php create mode 100644 Civi/Funding/Api4/Action/Remote/ApplicationProcess/GetTemplatesAction.php create mode 100644 Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplateRenderUriActionHandler.php create mode 100644 Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandler.php create mode 100644 Civi/Funding/Controller/ApplicationTemplateRenderController.php create mode 100644 Civi/Funding/DocumentRender/CiviOffice/CiviOfficePseudoConstants.php create mode 100644 Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRenderer.php create mode 100644 ang/afformApplicationTemplates.aff.html create mode 100644 ang/afformApplicationTemplates.aff.json create mode 100644 ang/afsearchFundingCaseTypes.aff.html create mode 100644 ang/afsearchFundingCaseTypes.aff.json create mode 100644 ang/crmFunding/afform/afSubmitReload.directive.js create mode 100644 managed/SavedSearchFundingCaseTypes.mgd.php create mode 100644 tests/phpunit/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandlerTest.php create mode 100644 tests/phpunit/Civi/Funding/Controller/ApplicationTemplateRenderControllerTest.php create mode 100644 tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRendererTest.php create mode 100644 xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.entityType.php create mode 100644 xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.xml diff --git a/CRM/Funding/BAO/ApplicationCiviOfficeTemplate.php b/CRM/Funding/BAO/ApplicationCiviOfficeTemplate.php new file mode 100644 index 000000000..3dc0b2702 --- /dev/null +++ b/CRM/Funding/BAO/ApplicationCiviOfficeTemplate.php @@ -0,0 +1,35 @@ + 'onAfformAdminMetadata', + ]; + } + + /** + * Provides Afform metadata about this entity. + * + * @see \Civi\AfformAdmin\AfformAdminMeta::getMetadata() + */ + public static function onAfformAdminMetadata(GenericHookEvent $event): void { + $entity = 'Funding' . pathinfo(__FILE__, PATHINFO_FILENAME); + $event->entities[$entity] = [ + 'entity' => $entity, + 'label' => $entity, + 'type' => 'primary', + 'defaults' => '{}', + ]; + } + +} diff --git a/CRM/Funding/DAO/ApplicationCiviOfficeTemplate.php b/CRM/Funding/DAO/ApplicationCiviOfficeTemplate.php new file mode 100644 index 000000000..496ea0dd9 --- /dev/null +++ b/CRM/Funding/DAO/ApplicationCiviOfficeTemplate.php @@ -0,0 +1,302 @@ +__table = 'civicrm_funding_application_civioffice_template'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Application Templates') : E::ts('Application Template'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'case_type_id', 'civicrm_funding_case_type', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); + } + return Civi::$statics[__CLASS__]['links']; + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('ID'), + 'description' => E::ts('Unique FundingApplicationCiviOfficeTemplate ID'), + 'required' => TRUE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_funding_application_civioffice_template.id', + 'table_name' => 'civicrm_funding_application_civioffice_template', + 'entity' => 'ApplicationCiviOfficeTemplate', + 'bao' => 'CRM_Funding_DAO_ApplicationCiviOfficeTemplate', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'case_type_id' => [ + 'name' => 'case_type_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('Case Type ID'), + 'description' => E::ts('FK to FundingCaseType'), + 'required' => TRUE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_funding_application_civioffice_template.case_type_id', + 'table_name' => 'civicrm_funding_application_civioffice_template', + 'entity' => 'ApplicationCiviOfficeTemplate', + 'bao' => 'CRM_Funding_DAO_ApplicationCiviOfficeTemplate', + 'localizable' => 0, + 'FKClassName' => 'CRM_Funding_DAO_FundingCaseType', + 'html' => [ + 'type' => 'EntityRef', + ], + 'pseudoconstant' => [ + 'table' => 'civicrm_funding_case_type', + 'keyColumn' => 'id', + 'labelColumn' => 'title', + 'prefetch' => 'false', + ], + 'add' => NULL, + ], + 'document_uri' => [ + 'name' => 'document_uri', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('Document Uri'), + 'description' => E::ts('CiviOffice document URI'), + 'required' => TRUE, + 'maxlength' => 255, + 'size' => CRM_Utils_Type::HUGE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_funding_application_civioffice_template.document_uri', + 'table_name' => 'civicrm_funding_application_civioffice_template', + 'entity' => 'ApplicationCiviOfficeTemplate', + 'bao' => 'CRM_Funding_DAO_ApplicationCiviOfficeTemplate', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + ], + 'pseudoconstant' => [ + 'callback' => 'Civi\Funding\DocumentRender\CiviOffice\CiviOfficePseudoConstants::getSharedDocumentUris', + ], + 'add' => NULL, + ], + 'label' => [ + 'name' => 'label', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('Label'), + 'required' => TRUE, + 'maxlength' => 255, + 'size' => CRM_Utils_Type::HUGE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_funding_application_civioffice_template.label', + 'table_name' => 'civicrm_funding_application_civioffice_template', + 'entity' => 'ApplicationCiviOfficeTemplate', + 'bao' => 'CRM_Funding_DAO_ApplicationCiviOfficeTemplate', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + ], + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'funding_application_civioffice_template', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'funding_application_civioffice_template', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = [ + 'UI_case_type_id_label' => [ + 'name' => 'UI_case_type_id_label', + 'field' => [ + 0 => 'case_type_id', + 1 => 'label', + ], + 'localizable' => FALSE, + 'unique' => TRUE, + 'sig' => 'civicrm_funding_application_civioffice_template::1::case_type_id::label', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/CRM/Funding/Page/RemoteApplicationTemplateRender.php b/CRM/Funding/Page/RemoteApplicationTemplateRender.php new file mode 100644 index 000000000..9aefd5e92 --- /dev/null +++ b/CRM/Funding/Page/RemoteApplicationTemplateRender.php @@ -0,0 +1,17 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\Api4\Action\Remote\ApplicationProcess; + +use Civi\Api4\RemoteFundingApplicationProcess; +use Civi\Funding\Api4\Action\Remote\AbstractRemoteFundingAction; +use Civi\Funding\Api4\Action\Traits\ApplicationProcessIdParameterTrait; + +/** + * @method int getTemplateId() + * @method $this setTemplateId(int $templateId) + */ +class GetTemplateRenderUriAction extends AbstractRemoteFundingAction { + + use ApplicationProcessIdParameterTrait; + + /** + * @var int + * @required + */ + protected ?int $templateId = NULL; + + public function __construct() { + parent::__construct(RemoteFundingApplicationProcess::getEntityName(), 'getTemplateRenderUri'); + } + +} diff --git a/Civi/Funding/Api4/Action/Remote/ApplicationProcess/GetTemplatesAction.php b/Civi/Funding/Api4/Action/Remote/ApplicationProcess/GetTemplatesAction.php new file mode 100644 index 000000000..d14b076d4 --- /dev/null +++ b/Civi/Funding/Api4/Action/Remote/ApplicationProcess/GetTemplatesAction.php @@ -0,0 +1,34 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\Api4\Action\Remote\ApplicationProcess; + +use Civi\Api4\RemoteFundingApplicationProcess; +use Civi\Funding\Api4\Action\Remote\AbstractRemoteFundingAction; +use Civi\Funding\Api4\Action\Traits\ApplicationProcessIdParameterTrait; + +class GetTemplatesAction extends AbstractRemoteFundingAction { + + use ApplicationProcessIdParameterTrait; + + public function __construct() { + parent::__construct(RemoteFundingApplicationProcess::getEntityName(), 'getTemplates'); + } + +} diff --git a/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplateRenderUriActionHandler.php b/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplateRenderUriActionHandler.php new file mode 100644 index 000000000..cee33b17d --- /dev/null +++ b/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplateRenderUriActionHandler.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\ApplicationProcess\Remote\Api4\ActionHandler; + +use Civi\Funding\Api4\Action\Remote\ApplicationProcess\GetTemplateRenderUriAction; +use Civi\Funding\Util\UrlGenerator; +use Civi\RemoteTools\ActionHandler\ActionHandlerInterface; + +/** + * @codeCoverageIgnore + */ +final class GetTemplateRenderUriActionHandler implements ActionHandlerInterface { + + public const ENTITY_NAME = 'RemoteFundingApplicationProcess'; + + private UrlGenerator $urlGenerator; + + public function __construct(UrlGenerator $urlGenerator) { + $this->urlGenerator = $urlGenerator; + } + + /** + * @phpstan-return array{renderUri: string} + */ + public function getTemplateRenderUri(GetTemplateRenderUriAction $action): array { + $renderUri = $this->urlGenerator->generate( + 'civicrm/funding/remote/application/render', + [ + 'applicationProcessId' => (string) $action->getApplicationProcessId(), + 'templateId' => (string) $action->getTemplateId(), + ] + ); + + return ['renderUri' => $renderUri]; + } + +} diff --git a/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandler.php b/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandler.php new file mode 100644 index 000000000..ff96e4d8e --- /dev/null +++ b/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandler.php @@ -0,0 +1,74 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\ApplicationProcess\Remote\Api4\ActionHandler; + +use Civi\Api4\FundingApplicationCiviOfficeTemplate; +use Civi\Funding\Api4\Action\Remote\ApplicationProcess\GetTemplatesAction; +use Civi\Funding\ApplicationProcess\ApplicationProcessManager; +use Civi\Funding\FundingCase\FundingCaseManager; +use Civi\RemoteTools\ActionHandler\ActionHandlerInterface; +use Civi\RemoteTools\Api4\Api4Interface; +use Webmozart\Assert\Assert; + +final class GetTemplatesActionHandler implements ActionHandlerInterface { + + public const ENTITY_NAME = 'RemoteFundingApplicationProcess'; + + private Api4Interface $api4; + + private ApplicationProcessManager $applicationProcessManager; + + private FundingCaseManager $fundingCaseManager; + + public function __construct( + Api4Interface $api4, + ApplicationProcessManager $applicationProcessManager, + FundingCaseManager $fundingCaseManager + ) { + $this->api4 = $api4; + $this->applicationProcessManager = $applicationProcessManager; + $this->fundingCaseManager = $fundingCaseManager; + } + + /** + * @phpstan-return list + * + * @throws \CRM_Core_Exception + */ + public function getTemplates(GetTemplatesAction $action): array { + $applicationProcess = $this->applicationProcessManager->get($action->getApplicationProcessId()); + if (NULL === $applicationProcess) { + return []; + } + + $fundingCase = $this->fundingCaseManager->get($applicationProcess->getFundingCaseId()); + Assert::notNull($fundingCase); + + /** @phpstan-var list $templates */ + $templates = $this->api4->execute(FundingApplicationCiviOfficeTemplate::getEntityName(), 'get', [ + 'select' => ['id', 'label'], + 'where' => [['case_type_id', '=', $fundingCase->getFundingCaseTypeId()]], + 'orderBy' => ['label' => 'ASC'], + ])->getArrayCopy(); + + return $templates; + } + +} diff --git a/Civi/Funding/Controller/ApplicationTemplateRenderController.php b/Civi/Funding/Controller/ApplicationTemplateRenderController.php new file mode 100644 index 000000000..2da1c6781 --- /dev/null +++ b/Civi/Funding/Controller/ApplicationTemplateRenderController.php @@ -0,0 +1,122 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\Controller; + +use Civi\Api4\FundingApplicationCiviOfficeTemplate; +use Civi\Api4\FundingApplicationProcess; +use Civi\Funding\ApplicationProcess\ApplicationProcessManager; +use Civi\Funding\DocumentRender\CiviOffice\CiviOfficeRenderer; +use Civi\Funding\FundingCase\FundingCaseManager; +use Civi\RemoteTools\Api4\Api4Interface; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Webmozart\Assert\Assert; + +/** + * @phpstan-import-type applicationCiviOfficeTemplateT from \Civi\Api4\FundingApplicationCiviOfficeTemplate + */ +final class ApplicationTemplateRenderController implements PageControllerInterface { + + private Api4Interface $api4; + + private ApplicationProcessManager $applicationProcessManager; + + private FundingCaseManager $fundingCaseManager; + + private CiviOfficeRenderer $renderer; + + public function __construct( + Api4Interface $api4, + ApplicationProcessManager $applicationProcessManager, + FundingCaseManager $fundingCaseManager, + CiviOfficeRenderer $renderer + ) { + $this->api4 = $api4; + $this->applicationProcessManager = $applicationProcessManager; + $this->fundingCaseManager = $fundingCaseManager; + $this->renderer = $renderer; + } + + /** + * @inheritDoc + * @throws \CRM_Core_Exception + */ + public function handle(Request $request): Response { + $applicationProcessId = $request->query->get('applicationProcessId'); + if (!is_numeric($applicationProcessId)) { + throw new BadRequestHttpException('Invalid application process ID'); + } + + $templateId = $request->query->get('templateId'); + if (!is_numeric($templateId)) { + throw new BadRequestHttpException('Invalid template ID'); + } + + return $this->render((int) $applicationProcessId, (int) $templateId); + } + + /** + * @throws \CRM_Core_Exception + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + private function render(int $applicationProcessId, int $templateId): Response { + $applicationProcess = $this->applicationProcessManager->get($applicationProcessId); + if (NULL === $applicationProcess) { + throw new NotFoundHttpException('Application process not found'); + } + + $fundingCase = $this->fundingCaseManager->get($applicationProcess->getFundingCaseId()); + Assert::notNull($fundingCase); + + /** @phpstan-var applicationCiviOfficeTemplateT $template */ + $template = $this->api4->getEntity(FundingApplicationCiviOfficeTemplate::getEntityName(), $templateId); + if (NULL === $template) { + throw new NotFoundHttpException('Template not found'); + } + + if ($fundingCase->getFundingCaseTypeId() !== $template['case_type_id']) { + throw new BadRequestHttpException('Funding case type of application process and template do not match'); + } + + $renderedFile = $this->renderer->render( + $template['document_uri'], + FundingApplicationProcess::getEntityName(), + $applicationProcess->getId() + ); + + $headers = [ + 'Content-Type' => $this->renderer->getMimeType(), + ]; + $filename = $template['label'] . '.' . pathinfo($renderedFile, PATHINFO_EXTENSION); + + return (new BinaryFileResponse( + $renderedFile, + Response::HTTP_OK, + $headers, + FALSE, + ))->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $filename) + ->deleteFileAfterSend(); + } + +} diff --git a/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRenderer.php b/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRenderer.php index 8e28a482c..18d5a3a38 100644 --- a/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRenderer.php +++ b/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRenderer.php @@ -20,33 +20,17 @@ namespace Civi\Funding\DocumentRender\CiviOffice; use Civi\Funding\DocumentRender\DocumentRendererInterface; -use Civi\RemoteTools\Api3\Api3Interface; -use Webmozart\Assert\Assert; class CiviOfficeDocumentRenderer implements DocumentRendererInterface { - private Api3Interface $api3; + private CiviOfficeRenderer $renderer; - private CiviOfficeContextDataHolder $contextDataHolder; - - private string $mimeType; - - private string $rendererUri; - - public function __construct( - Api3Interface $api3, - CiviOfficeContextDataHolder $contextDataHolder, - string $rendererUri = 'unoconv-local', - string $mimeType = 'application/pdf' - ) { - $this->api3 = $api3; - $this->contextDataHolder = $contextDataHolder; - $this->rendererUri = $rendererUri; - $this->mimeType = $mimeType; + public function __construct(CiviOfficeRenderer $renderer) { + $this->renderer = $renderer; } public function getMimeType(): string { - return $this->mimeType; + return $this->renderer->getMimeType(); } public function render( @@ -55,36 +39,7 @@ public function render( int $entityId, array $data = [] ): string { - $this->contextDataHolder->addEntityData($entityName, $entityId, $data); - - try { - /** @phpstan-var array{values: array{string}} $result */ - $result = $this->api3->execute('CiviOffice', 'convert', [ - 'document_uri' => 'file://' . $templateFile, - 'entity_type' => $entityName, - 'entity_ids' => [$entityId], - 'target_mime_type' => $this->mimeType, - 'renderer_uri' => $this->rendererUri, - ]); - } - finally { - $this->contextDataHolder->removeEntityData($entityName, $entityId); - } - - $documentStoreUri = $result['values'][0]; - $documentStore = \CRM_Civioffice_Configuration::getDocumentStore($documentStoreUri); - Assert::notNull($documentStore, sprintf('No CiviOffice document store with URI "%s" found.', $documentStoreUri)); - /** @var \CRM_Civioffice_Document $document */ - foreach ($documentStore->getDocuments() as $document) { - // Avoid unnecessary file copy. - if (method_exists($document, 'getAbsolutePath')) { - return $document->getAbsolutePath(); - } - - return $document->getLocalTempCopy(); - } - - throw new \RuntimeException('No rendered file found.'); + return $this->renderer->render('file://' . $templateFile, $entityName, $entityId, $data); } } diff --git a/Civi/Funding/DocumentRender/CiviOffice/CiviOfficePseudoConstants.php b/Civi/Funding/DocumentRender/CiviOffice/CiviOfficePseudoConstants.php new file mode 100644 index 000000000..920a8af0c --- /dev/null +++ b/Civi/Funding/DocumentRender/CiviOffice/CiviOfficePseudoConstants.php @@ -0,0 +1,44 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\DocumentRender\CiviOffice; + +use Civi\Api4\CiviofficeDocument; + +final class CiviOfficePseudoConstants { + + /** + * @phpstan-return array + * Document URIs mapped to document names. + * + * @throws \CRM_Core_Exception + */ + public static function getSharedDocumentUris(): array { + static $documents; + + return $documents ??= CiviofficeDocument::get(FALSE) + ->addSelect('name', 'uri') + // Exclude contact-specific templates. + ->addWhere('document_store_uri', 'NOT REGEXP', '^upload::.+[0-9]+$') + ->execute() + ->indexBy('uri') + ->column('name'); + } + +} diff --git a/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRenderer.php b/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRenderer.php new file mode 100644 index 000000000..cbbd6dc59 --- /dev/null +++ b/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRenderer.php @@ -0,0 +1,96 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\DocumentRender\CiviOffice; + +use Civi\RemoteTools\Api3\Api3Interface; +use Webmozart\Assert\Assert; + +class CiviOfficeRenderer { + + private Api3Interface $api3; + + private CiviOfficeContextDataHolder $contextDataHolder; + + private string $mimeType; + + private string $rendererUri; + + public function __construct( + Api3Interface $api3, + CiviOfficeContextDataHolder $contextDataHolder, + string $rendererUri = 'unoconv-local', + string $mimeType = 'application/pdf' + ) { + $this->api3 = $api3; + $this->contextDataHolder = $contextDataHolder; + $this->rendererUri = $rendererUri; + $this->mimeType = $mimeType; + } + + public function getMimeType(): string { + return $this->mimeType; + } + + /** + * @phpstan-param array $contextData + * + * @return string Path to rendered file. + * + * @throws \CRM_Core_Exception + */ + public function render( + string $documentUri, + string $entityName, + int $entityId, + array $contextData = [] + ): string { + $this->contextDataHolder->addEntityData($entityName, $entityId, $contextData); + + try { + /** @phpstan-var array{values: array{string}} $result */ + $result = $this->api3->execute('CiviOffice', 'convert', [ + 'document_uri' => $documentUri, + 'entity_type' => $entityName, + 'entity_ids' => [$entityId], + 'target_mime_type' => $this->mimeType, + 'renderer_uri' => $this->rendererUri, + ]); + } + finally { + $this->contextDataHolder->removeEntityData($entityName, $entityId); + } + + $documentStoreUri = $result['values'][0]; + $documentStore = \CRM_Civioffice_Configuration::getDocumentStore($documentStoreUri); + Assert::notNull($documentStore, sprintf('No CiviOffice document store with URI "%s" found.', $documentStoreUri)); + /** @var \CRM_Civioffice_Document $document */ + foreach ($documentStore->getDocuments() as $document) { + // Avoid unnecessary file copy. + if (method_exists($document, 'getAbsolutePath')) { + return $document->getAbsolutePath(); + } + + return $document->getLocalTempCopy(); + } + + throw new \RuntimeException('No rendered file found.'); + } + +} diff --git a/ang/afformApplicationTemplates.aff.html b/ang/afformApplicationTemplates.aff.html new file mode 100644 index 000000000..a33be4082 --- /dev/null +++ b/ang/afformApplicationTemplates.aff.html @@ -0,0 +1,16 @@ +

Application templates available for applicants.

+ +
+ + +
+ + + +
+ + + +
+ +
diff --git a/ang/afformApplicationTemplates.aff.json b/ang/afformApplicationTemplates.aff.json new file mode 100644 index 000000000..84f234c5c --- /dev/null +++ b/ang/afformApplicationTemplates.aff.json @@ -0,0 +1,28 @@ +{ + "type": "form", + "requires": ["crmFunding"], + "entity_type": null, + "join_entity": null, + "title": "Application Templates", + "description": null, + "placement": [], + "summary_contact_type": null, + "summary_weight": null, + "icon": "fa-file-text-o", + "server_route": "civicrm/funding/application-templates", + "is_public": false, + "permission": [ + "access CiviCRM", + "administer Funding" + ], + "permission_operator": "AND", + "redirect": null, + "submit_enabled": true, + "submit_limit": null, + "create_submission": true, + "manual_processing": false, + "allow_verification_by_email": false, + "email_confirmation_template_id": null, + "navigation": null, + "modified_date": null +} diff --git a/ang/afsearchFundingCaseTypes.aff.html b/ang/afsearchFundingCaseTypes.aff.html new file mode 100644 index 000000000..e1f8d6945 --- /dev/null +++ b/ang/afsearchFundingCaseTypes.aff.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ang/afsearchFundingCaseTypes.aff.json b/ang/afsearchFundingCaseTypes.aff.json new file mode 100644 index 000000000..47e242067 --- /dev/null +++ b/ang/afsearchFundingCaseTypes.aff.json @@ -0,0 +1,12 @@ +{ + "type": "search", + "title": "Funding Case Types", + "placement": [], + "icon": "fa-list-alt", + "server_route": "civicrm/funding/case-type/list", + "permission": [ + "access Funding", + "administer Funding" + ], + "permission_operator": "OR" +} diff --git a/ang/crmFunding/afform/afSubmitReload.directive.js b/ang/crmFunding/afform/afSubmitReload.directive.js new file mode 100644 index 000000000..790d1cb72 --- /dev/null +++ b/ang/crmFunding/afform/afSubmitReload.directive.js @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 SYSTOPIA GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation in version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict'; + +/** + * Reloads page after successful afform submit. + * Usage: + */ +fundingModule.directive('fundingAfSubmitReload', function() { + return { + restrict: 'A', + scope: false, + template: '', + controller: ['$element', function($element) { + $element.on('crmFormSuccess', function () { + window.location.reload(); + }); + }], + }; +}); diff --git a/managed/NavigationFunding.mgd.php b/managed/NavigationFunding.mgd.php index de054ba6c..2bb7cf783 100644 --- a/managed/NavigationFunding.mgd.php +++ b/managed/NavigationFunding.mgd.php @@ -196,4 +196,29 @@ ], ], ], + [ + 'name' => 'Navigation_Funding.FundingCaseTypes', + 'entity' => 'Navigation', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'domain_id' => 'current_domain', + 'label' => E::ts('Funding Case Types'), + 'name' => 'afsearchFundingCaseTypes', + 'url' => 'civicrm/funding/case-type/list', + 'icon' => 'crm-i fa-list-alt', + 'permission' => [ + 'administer Funding', + 'access Funding', + ], + 'permission_operator' => 'OR', + 'parent_id.name' => 'funding', + 'is_active' => TRUE, + 'has_separator' => 0, + 'weight' => ++$weight, + ], + ], + ], ]; diff --git a/managed/SavedSearchFundingCaseTypes.mgd.php b/managed/SavedSearchFundingCaseTypes.mgd.php new file mode 100644 index 000000000..a6724dd59 --- /dev/null +++ b/managed/SavedSearchFundingCaseTypes.mgd.php @@ -0,0 +1,135 @@ +. + */ + +declare(strict_types = 1); + +use CRM_Funding_ExtensionUtil as E; + +return [ + [ + 'name' => 'SavedSearch_funding_case_types', + 'entity' => 'SavedSearch', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'funding_case_types', + 'label' => E::ts('Funding Case Types'), + 'api_entity' => 'FundingCaseType', + 'api_params' => [ + 'version' => 4, + 'select' => [ + 'id', + 'title', + 'abbreviation', + 'is_combined_application', + ], + 'orderBy' => [], + 'where' => [], + 'groupBy' => [], + 'join' => [], + 'having' => [], + ], + ], + 'match' => [ + 'name', + ], + ], + ], + [ + 'name' => 'SavedSearch_funding_case_types_SearchDisplay_table', + 'entity' => 'SearchDisplay', + 'cleanup' => 'always', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'table', + 'label' => E::ts('table'), + 'saved_search_id.name' => 'funding_case_types', + 'type' => 'table', + 'settings' => [ + 'description' => NULL, + 'sort' => [], + 'limit' => 10, + 'pager' => [ + 'expose_limit' => TRUE, + ], + 'placeholder' => 3, + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'title', + 'dataType' => 'String', + 'label' => E::ts('Title'), + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'abbreviation', + 'dataType' => 'String', + 'label' => E::ts('Abbreviation'), + 'sortable' => TRUE, + ], + [ + 'type' => 'field', + 'key' => 'is_combined_application', + 'dataType' => 'Boolean', + 'label' => E::ts('Is Combined Application'), + 'sortable' => TRUE, + ], + [ + 'size' => 'btn-xs', + 'links' => [ + [ + 'path' => 'civicrm/funding/application-templates#?case_type_id=[id]', + 'icon' => 'fa-external-link', + 'text' => E::ts('Manage external application templates'), + 'style' => 'default', + 'condition' => [ + 'check user permission', + '=', + [ + 'administer Funding', + ], + ], + 'task' => '', + 'entity' => '', + 'action' => '', + 'join' => '', + 'target' => '_blank', + ], + ], + 'type' => 'buttons', + 'alignment' => 'text-right', + ], + ], + 'actions' => FALSE, + 'classes' => [ + 'table', + 'table-striped', + ], + ], + ], + 'match' => [ + 'saved_search_id', + 'name', + ], + ], + ], +]; diff --git a/services/document-render.php b/services/document-render.php index a3beddec7..e2d53c1b7 100644 --- a/services/document-render.php +++ b/services/document-render.php @@ -24,6 +24,7 @@ use Civi\Funding\DocumentRender\CiviOffice\CiviOfficeContextDataHolder; use Civi\Funding\DocumentRender\CiviOffice\CiviOfficeDocumentRenderer; use Civi\Funding\DocumentRender\CiviOffice\CiviOfficeDocumentStore; +use Civi\Funding\DocumentRender\CiviOffice\CiviOfficeRenderer; use Civi\Funding\DocumentRender\DocumentRendererInterface; use Civi\Funding\DocumentRender\Token\TokenNameExtractor; use Civi\Funding\DocumentRender\Token\TokenNameExtractorCacheDecorator; @@ -48,6 +49,7 @@ } $container->autowire(CiviOfficeContextDataHolder::class); +$container->autowire(CiviOfficeRenderer::class); $container->autowire(DocumentRendererInterface::class, CiviOfficeDocumentRenderer::class); $container->autowire(TokenNameExtractorInterface::class, TokenNameExtractor::class); diff --git a/sql/auto_install.sql b/sql/auto_install.sql index 54e031790..0b8832c2e 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -28,6 +28,7 @@ DROP TABLE IF EXISTS `civicrm_funding_case_type_program`; DROP TABLE IF EXISTS `civicrm_funding_case_permissions_cache`; DROP TABLE IF EXISTS `civicrm_funding_case_contact_relation`; DROP TABLE IF EXISTS `civicrm_funding_case`; +DROP TABLE IF EXISTS `civicrm_funding_application_civioffice_template`; DROP TABLE IF EXISTS `civicrm_funding_recipient_contact_relation`; DROP TABLE IF EXISTS `civicrm_funding_program_relationship`; DROP TABLE IF EXISTS `civicrm_funding_program_contact_relation`; @@ -135,6 +136,24 @@ CREATE TABLE `civicrm_funding_recipient_contact_relation` ( ) ENGINE=InnoDB; +-- /******************************************************* +-- * +-- * civicrm_funding_application_civioffice_template +-- * +-- * Templates for use in application portal +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_funding_application_civioffice_template` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique FundingApplicationCiviOfficeTemplate ID', + `case_type_id` int unsigned NOT NULL COMMENT 'FK to FundingCaseType', + `document_uri` varchar(255) NOT NULL COMMENT 'CiviOffice document URI', + `label` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `UI_case_type_id_label`(case_type_id, label), + CONSTRAINT FK_civicrm_funding_application_civioffice_template_case_type_id FOREIGN KEY (`case_type_id`) REFERENCES `civicrm_funding_case_type`(`id`) ON DELETE CASCADE +) +ENGINE=InnoDB; + -- /******************************************************* -- * -- * civicrm_funding_case diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index a1a2f8a85..fc212d864 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -26,6 +26,7 @@ DROP TABLE IF EXISTS `civicrm_funding_case_type_program`; DROP TABLE IF EXISTS `civicrm_funding_case_permissions_cache`; DROP TABLE IF EXISTS `civicrm_funding_case_contact_relation`; DROP TABLE IF EXISTS `civicrm_funding_case`; +DROP TABLE IF EXISTS `civicrm_funding_application_civioffice_template`; DROP TABLE IF EXISTS `civicrm_funding_recipient_contact_relation`; DROP TABLE IF EXISTS `civicrm_funding_program_relationship`; DROP TABLE IF EXISTS `civicrm_funding_program_contact_relation`; diff --git a/sql/upgrade/0001.sql b/sql/upgrade/0001.sql index a5978e1b0..ccb074bd4 100644 --- a/sql/upgrade/0001.sql +++ b/sql/upgrade/0001.sql @@ -13,3 +13,14 @@ ALTER TABLE civicrm_funding_app_resources_item ADD data_pointer varchar(255) NOT NULL COMMENT 'JSON pointer to data in application data'; UPDATE civicrm_funding_app_resources_item SET identifier = REPLACE(identifier, '/', '.'); + +CREATE TABLE `civicrm_funding_application_civioffice_template` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique FundingApplicationCiviOfficeTemplate ID', + `case_type_id` int unsigned NOT NULL COMMENT 'FK to FundingCaseType', + `document_uri` varchar(255) NOT NULL COMMENT 'CiviOffice document URI', + `label` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `UI_case_type_id_label`(case_type_id, label), + CONSTRAINT FK_civicrm_funding_application_civioffice_template_case_type_id FOREIGN KEY (`case_type_id`) REFERENCES `civicrm_funding_case_type`(`id`) ON DELETE CASCADE +) +ENGINE=InnoDB; diff --git a/tests/phpunit/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandlerTest.php b/tests/phpunit/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandlerTest.php new file mode 100644 index 000000000..49c16ff3a --- /dev/null +++ b/tests/phpunit/Civi/Funding/ApplicationProcess/Remote/Api4/ActionHandler/GetTemplatesActionHandlerTest.php @@ -0,0 +1,107 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\ApplicationProcess\Remote\Api4\ActionHandler; + +use Civi\Api4\FundingApplicationCiviOfficeTemplate; +use Civi\Api4\Generic\Result; +use Civi\Funding\Api4\Action\Remote\ApplicationProcess\GetTemplatesAction; +use Civi\Funding\ApplicationProcess\ApplicationProcessManager; +use Civi\Funding\EntityFactory\ApplicationProcessFactory; +use Civi\Funding\EntityFactory\FundingCaseFactory; +use Civi\Funding\FundingCase\FundingCaseManager; +use Civi\Funding\Traits\CreateMockTrait; +use Civi\RemoteTools\Api4\Api4Interface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @covers \Civi\Funding\ApplicationProcess\Remote\Api4\ActionHandler\GetTemplatesActionHandler + * + * @phpstan-import-type applicationCiviOfficeTemplateT from \Civi\Api4\FundingApplicationCiviOfficeTemplate + */ +final class GetTemplatesActionHandlerTest extends TestCase { + + use CreateMockTrait; + + private GetTemplatesActionHandler $actionHandler; + + /** + * @var \Civi\RemoteTools\Api4\Api4Interface&\PHPUnit\Framework\MockObject\MockObject + */ + private MockObject $api4Mock; + + /** + * @var \Civi\Funding\ApplicationProcess\ApplicationProcessManager&\PHPUnit\Framework\MockObject\MockObject + */ + private MockObject $applicationProcessManagerMock; + + /** + * @var \Civi\Funding\FundingCase\FundingCaseManager&\PHPUnit\Framework\MockObject\MockObject + */ + private MockObject $fundingCaseManagerMock; + + protected function setUp(): void { + parent::setUp(); + $this->api4Mock = $this->createMock(Api4Interface::class); + $this->applicationProcessManagerMock = $this->createMock(ApplicationProcessManager::class); + $this->fundingCaseManagerMock = $this->createMock(FundingCaseManager::class); + $this->actionHandler = new GetTemplatesActionHandler( + $this->api4Mock, + $this->applicationProcessManagerMock, + $this->fundingCaseManagerMock + ); + } + + public function testGetTemplates(): void { + $applicationProcess = ApplicationProcessFactory::createApplicationProcess(['id' => 2]); + $this->applicationProcessManagerMock->method('get') + ->with(2) + ->willReturn($applicationProcess); + $fundingCase = FundingCaseFactory::createFundingCase(); + $this->fundingCaseManagerMock->method('get') + ->with($fundingCase->getId()) + ->willReturn($fundingCase); + + $this->api4Mock->method('execute') + ->with(FundingApplicationCiviOfficeTemplate::getEntityName(), 'get', [ + 'select' => ['id', 'label'], + 'where' => [['case_type_id', '=', $fundingCase->getFundingCaseTypeId()]], + 'orderBy' => ['label' => 'ASC'], + ]) + ->willReturn(new Result([['id' => 3, 'label' => 'test']])); + + $action = $this->createApi4ActionMock(GetTemplatesAction::class) + ->setApplicationProcessId(2); + + static::assertSame([['id' => 3, 'label' => 'test']], $this->actionHandler->getTemplates($action)); + } + + public function testGetTemplatesNoApplicationProcess(): void { + $this->applicationProcessManagerMock->method('get') + ->with(2) + ->willReturn(NULL); + + $action = $this->createApi4ActionMock(GetTemplatesAction::class) + ->setApplicationProcessId(2); + + static::assertSame([], $this->actionHandler->getTemplates($action)); + } + +} diff --git a/tests/phpunit/Civi/Funding/Controller/ApplicationTemplateRenderControllerTest.php b/tests/phpunit/Civi/Funding/Controller/ApplicationTemplateRenderControllerTest.php new file mode 100644 index 000000000..8ab5671d5 --- /dev/null +++ b/tests/phpunit/Civi/Funding/Controller/ApplicationTemplateRenderControllerTest.php @@ -0,0 +1,186 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\Controller; + +use Civi\Api4\FundingApplicationCiviOfficeTemplate; +use Civi\Api4\FundingApplicationProcess; +use Civi\Funding\ApplicationProcess\ApplicationProcessManager; +use Civi\Funding\DocumentRender\CiviOffice\CiviOfficeRenderer; +use Civi\Funding\EntityFactory\ApplicationProcessFactory; +use Civi\Funding\EntityFactory\FundingCaseFactory; +use Civi\Funding\FundingCase\FundingCaseManager; +use Civi\Funding\Util\TestFileUtil; +use Civi\RemoteTools\Api4\Api4Interface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\HeaderUtils; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * @covers \Civi\Funding\Controller\ApplicationTemplateRenderController + * + * @phpstan-import-type applicationCiviOfficeTemplateT from \Civi\Api4\FundingApplicationCiviOfficeTemplate + */ +final class ApplicationTemplateRenderControllerTest extends TestCase { + + /** + * @var \Civi\RemoteTools\Api4\Api4Interface&\PHPUnit\Framework\MockObject\MockObject + */ + private MockObject $api4Mock; + + /** + * @var \Civi\Funding\ApplicationProcess\ApplicationProcessManager&\PHPUnit\Framework\MockObject\MockObject + */ + private MockObject $applicationProcessManagerMock; + + private ApplicationTemplateRenderController $controller; + + /** + * @var \Civi\Funding\FundingCase\FundingCaseManager&\PHPUnit\Framework\MockObject\MockObject + */ + private MockObject $fundingCaseManagerMock; + + /** + * @var \Civi\Funding\DocumentRender\CiviOffice\CiviOfficeRenderer&\PHPUnit\Framework\MockObject\MockObject + */ + private $rendererMock; + + protected function setUp(): void { + parent::setUp(); + $this->api4Mock = $this->createMock(Api4Interface::class); + $this->applicationProcessManagerMock = $this->createMock(ApplicationProcessManager::class); + $this->fundingCaseManagerMock = $this->createMock(FundingCaseManager::class); + $this->rendererMock = $this->createMock(CiviOfficeRenderer::class); + $this->controller = new ApplicationTemplateRenderController( + $this->api4Mock, + $this->applicationProcessManagerMock, + $this->fundingCaseManagerMock, + $this->rendererMock + ); + } + + public function test(): void { + $request = new Request(['applicationProcessId' => 2, 'templateId' => 3]); + + $applicationProcess = ApplicationProcessFactory::createApplicationProcess(['id' => 2]); + $this->applicationProcessManagerMock->method('get') + ->with(2) + ->willReturn($applicationProcess); + $fundingCase = FundingCaseFactory::createFundingCase(); + $this->fundingCaseManagerMock->method('get') + ->with($fundingCase->getId()) + ->willReturn($fundingCase); + + $this->api4Mock->method('getEntity') + ->with(FundingApplicationCiviOfficeTemplate::getEntityName(), 3) + ->willReturn($this->createTemplate($fundingCase->getFundingCaseTypeId())); + + $tempDir = TestFileUtil::createTempDir(basename(__FILE__, '.php')); + $resultFile = $tempDir . '/result.txt'; + file_put_contents($resultFile, 'test'); + + $this->rendererMock->method('render') + ->with('local::abc', FundingApplicationProcess::getEntityName(), 2) + ->willReturn($resultFile); + + $this->rendererMock->method('getMimeType') + ->willReturn('text/plain'); + + $response = $this->controller->handle($request); + static::assertInstanceOf(BinaryFileResponse::class, $response); + /** @var \Symfony\Component\HttpFoundation\BinaryFileResponse $response */ + static::assertSame($resultFile, $response->getFile()->getRealPath()); + static::assertSame( + HeaderUtils::makeDisposition('inline', 'Test.txt'), + $response->headers->get('Content-Disposition') + ); + static::assertSame('text/plain', $response->headers->get('Content-Type')); + } + + public function testApplicationProcessNotFound(): void { + $request = new Request(['applicationProcessId' => 2, 'templateId' => 3]); + + $this->applicationProcessManagerMock->method('get') + ->with(2) + ->willReturn(NULL); + + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('Application process not found'); + $this->controller->handle($request); + } + + public function testTemplateNotFound(): void { + $request = new Request(['applicationProcessId' => 2, 'templateId' => 3]); + + $applicationProcess = ApplicationProcessFactory::createApplicationProcess(['id' => 2]); + $this->applicationProcessManagerMock->method('get') + ->with(2) + ->willReturn($applicationProcess); + $fundingCase = FundingCaseFactory::createFundingCase(); + $this->fundingCaseManagerMock->method('get') + ->with($fundingCase->getId()) + ->willReturn($fundingCase); + + $this->api4Mock->method('getEntity') + ->with(FundingApplicationCiviOfficeTemplate::getEntityName(), 3) + ->willReturn(NULL); + + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('Template not found'); + $this->controller->handle($request); + } + + public function testFundingCaseTypeIdMismatch(): void { + $request = new Request(['applicationProcessId' => 2, 'templateId' => 3]); + + $applicationProcess = ApplicationProcessFactory::createApplicationProcess(['id' => 2]); + $this->applicationProcessManagerMock->method('get') + ->with(2) + ->willReturn($applicationProcess); + $fundingCase = FundingCaseFactory::createFundingCase(); + $this->fundingCaseManagerMock->method('get') + ->with($fundingCase->getId()) + ->willReturn($fundingCase); + + $this->api4Mock->method('getEntity') + ->with(FundingApplicationCiviOfficeTemplate::getEntityName(), 3) + ->willReturn($this->createTemplate($fundingCase->getFundingCaseTypeId() + 1)); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('Funding case type of application process and template do not match'); + $this->controller->handle($request); + } + + /** + * @phpstan-return applicationCiviOfficeTemplateT + */ + private function createTemplate(int $fundingCaseTypeId): array { + return [ + 'id' => 3, + 'case_type_id' => $fundingCaseTypeId, + 'document_uri' => 'local::abc', + 'label' => 'Test', + ]; + } + +} diff --git a/tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRendererTest.php b/tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRendererTest.php index f14781438..15c47e9a9 100644 --- a/tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRendererTest.php +++ b/tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeDocumentRendererTest.php @@ -19,80 +19,42 @@ namespace Civi\Funding\DocumentRender\CiviOffice; -use Civi\Funding\AbstractFundingHeadlessTestCase; -use Civi\Funding\Fixtures\ApplicationProcessFixture; -use Civi\Funding\Fixtures\ContactFixture; -use Civi\Funding\Fixtures\FundingCaseContactRelationFixture; -use Civi\Funding\Fixtures\FundingCaseFixture; -use Civi\Funding\Fixtures\FundingCaseTypeFixture; -use Civi\Funding\Fixtures\FundingProgramFixture; -use Civi\Funding\Util\RequestTestUtil; -use Civi\RemoteTools\Api3\Api3; -use CRM_Funding_ExtensionUtil as E; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * @covers \Civi\Funding\DocumentRender\CiviOffice\CiviOfficeDocumentRenderer - * - * @group headless */ -final class CiviOfficeDocumentRendererTest extends AbstractFundingHeadlessTestCase { +final class CiviOfficeDocumentRendererTest extends TestCase { private CiviOfficeDocumentRenderer $documentRenderer; - public static function setUpBeforeClass(): void { - parent::setUpBeforeClass(); - } + /** + * @var \Civi\Funding\DocumentRender\CiviOffice\CiviOfficeRenderer&\PHPUnit\Framework\MockObject\MockObject + */ + private MockObject $rendererMock; protected function setUp(): void { - if (!UnoconvLocalTestConfigurator::isAvailable()) { - static::markTestSkipped(<<documentRenderer = new CiviOfficeDocumentRenderer( - new Api3(), $contextDataHolder, - ); + $this->rendererMock = $this->createMock(CiviOfficeRenderer::class); + $this->documentRenderer = new CiviOfficeDocumentRenderer($this->rendererMock); } public function testGetMimeType(): void { - static::assertSame('application/pdf', $this->documentRenderer->getMimeType()); + $this->rendererMock->method('getMimeType') + ->willReturn('application/test'); + static::assertSame('application/test', $this->documentRenderer->getMimeType()); } public function testRender(): void { - $creationContact = ContactFixture::addIndividual(); - $recipientContact = ContactFixture::addOrganization(['display_name' => 'Some Org']); - $fundingProgram = FundingProgramFixture::addFixture(); - $fundingCaseType = FundingCaseTypeFixture::addFixture(); - $fundingCase = FundingCaseFixture::addFixture( - $fundingProgram->getId(), - $fundingCaseType->getId(), - $recipientContact['id'], - $creationContact['id'], - ); - $applicationProcess = ApplicationProcessFixture::addFixture($fundingCase->getId()); - FundingCaseContactRelationFixture::addContact($creationContact['id'], $fundingCase->getId(), ['view']); - - UnoconvLocalTestConfigurator::configure(); + $this->rendererMock->method('render') + ->with('file://template.abc', 'TestEntity', 2, ['foo' => 'bar']) + ->willReturn('/result/file.test'); - RequestTestUtil::mockInternalRequest($creationContact['id']); - $filename = $this->documentRenderer->render( - E::path('tests/phpunit/resources/FundingCaseDocumentTemplate.docx'), - 'TransferContract', - $fundingCase->getId(), - [ - 'fundingCase' => $fundingCase, - 'eligibleApplicationProcesses' => [$applicationProcess], - 'fundingCaseType' => $fundingCaseType, - 'fundingProgram' => $fundingProgram, - ], + static::assertSame( + '/result/file.test', + $this->documentRenderer->render('template.abc', 'TestEntity', 2, ['foo' => 'bar']) ); - static::assertFileExists($filename); - static::assertSame('application/pdf', mime_content_type($filename)); - unlink($filename); } } diff --git a/tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRendererTest.php b/tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRendererTest.php new file mode 100644 index 000000000..dce0564a8 --- /dev/null +++ b/tests/phpunit/Civi/Funding/DocumentRender/CiviOffice/CiviOfficeRendererTest.php @@ -0,0 +1,94 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Funding\DocumentRender\CiviOffice; + +use Civi\Funding\AbstractFundingHeadlessTestCase; +use Civi\Funding\Fixtures\ApplicationProcessFixture; +use Civi\Funding\Fixtures\ContactFixture; +use Civi\Funding\Fixtures\FundingCaseContactRelationFixture; +use Civi\Funding\Fixtures\FundingCaseFixture; +use Civi\Funding\Fixtures\FundingCaseTypeFixture; +use Civi\Funding\Fixtures\FundingProgramFixture; +use Civi\Funding\Util\RequestTestUtil; +use Civi\RemoteTools\Api3\Api3; +use CRM_Funding_ExtensionUtil as E; + +/** + * @covers \Civi\Funding\DocumentRender\CiviOffice\CiviOfficeRenderer + * + * @group headless + */ +final class CiviOfficeRendererTest extends AbstractFundingHeadlessTestCase { + + private CiviOfficeRenderer $renderer; + + protected function setUp(): void { + if (!UnoconvLocalTestConfigurator::isAvailable()) { + static::markTestSkipped(<<renderer = new CiviOfficeRenderer( + new Api3(), $contextDataHolder, + ); + } + + public function testGetMimeType(): void { + static::assertSame('application/pdf', $this->renderer->getMimeType()); + } + + public function testRender(): void { + $creationContact = ContactFixture::addIndividual(); + $recipientContact = ContactFixture::addOrganization(['display_name' => 'Some Org']); + $fundingProgram = FundingProgramFixture::addFixture(); + $fundingCaseType = FundingCaseTypeFixture::addFixture(); + $fundingCase = FundingCaseFixture::addFixture( + $fundingProgram->getId(), + $fundingCaseType->getId(), + $recipientContact['id'], + $creationContact['id'], + ); + $applicationProcess = ApplicationProcessFixture::addFixture($fundingCase->getId()); + FundingCaseContactRelationFixture::addContact($creationContact['id'], $fundingCase->getId(), ['view']); + + UnoconvLocalTestConfigurator::configure(); + + RequestTestUtil::mockInternalRequest($creationContact['id']); + $filename = $this->renderer->render( + 'file://' . E::path('tests/phpunit/resources/FundingCaseDocumentTemplate.docx'), + 'TransferContract', + $fundingCase->getId(), + [ + 'fundingCase' => $fundingCase, + 'eligibleApplicationProcesses' => [$applicationProcess], + 'fundingCaseType' => $fundingCaseType, + 'fundingProgram' => $fundingProgram, + ], + ); + static::assertFileExists($filename); + static::assertSame('application/pdf', mime_content_type($filename)); + unlink($filename); + } + +} diff --git a/xml/Menu/funding.xml b/xml/Menu/funding.xml index 3a35e006c..a1518a2b0 100644 --- a/xml/Menu/funding.xml +++ b/xml/Menu/funding.xml @@ -1,5 +1,11 @@ + + civicrm/funding/remote/application/render + CRM_Funding_Page_RemoteApplicationTemplateRender + RemoteApplicationTemplateRender + access Remote Funding + civicrm/funding/transfer-contract/download CRM_Funding_Page_TransferContractDownload diff --git a/xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.entityType.php b/xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.entityType.php new file mode 100644 index 000000000..23c489164 --- /dev/null +++ b/xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.entityType.php @@ -0,0 +1,10 @@ + 'FundingApplicationCiviOfficeTemplate', + 'class' => 'CRM_Funding_DAO_ApplicationCiviOfficeTemplate', + 'table' => 'civicrm_funding_application_civioffice_template', + ], +]; diff --git a/xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.xml b/xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.xml new file mode 100644 index 000000000..2a9598b5a --- /dev/null +++ b/xml/schema/CRM/Funding/FundingApplicationCiviOfficeTemplate.xml @@ -0,0 +1,82 @@ + + + + CRM/Funding + ApplicationCiviOfficeTemplate + civicrm_funding_application_civioffice_template + Application Template + Application Templates + Templates for use in application portal + true + + + id + int unsigned + true + Unique FundingApplicationCiviOfficeTemplate ID + + Number + + + + id + true + + + + + case_type_id + int unsigned + true + FK to FundingCaseType + +
civicrm_funding_case_type
+ id + title + false + + + EntityRef + + + + case_type_id + civicrm_funding_case_type
+ id + CASCADE +
+ + + document_uri + varchar + 255 + true + CiviOffice document URI + + Civi\Funding\DocumentRender\CiviOffice\CiviOfficePseudoConstants::getSharedDocumentUris + + + Select + + + + + label + varchar + 255 + true + + Text + + + + + UI_case_type_id_label + case_type_id + label + true + +