Skip to content

Commit 38f5633

Browse files
author
Robert
committed
feat(forms): attach submission PDFs to notification emails
Signed-off-by: Robert <robert@local>
1 parent fe58a06 commit 38f5633

19 files changed

+462
-1
lines changed

docs/API_v3.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Returns the full-depth object of the requested form (without submissions).
171171
"submitMultiple": true,
172172
"allowEditSubmissions": false,
173173
"notifyOwnerOnSubmission": false,
174+
"attachSubmissionPdf": false,
174175
"notificationRecipients": ["team@example.com"],
175176
"showExpiration": false,
176177
"canSubmit": true,
@@ -278,6 +279,7 @@ Update a single or multiple properties of a form-object. Concerns **only** the F
278279
- To link a file for submissions, the _keyValuePairs_ need to contain the keys `path` and `fileFormat`
279280
- To unlink a file for submissions, the _keyValuePairs_ need to contain the keys `fileId` and `fileFormat` need to contain the value `null`
280281
- `notifyOwnerOnSubmission` must be a boolean.
282+
- `attachSubmissionPdf` must be a boolean.
281283
- `notificationRecipients` must be an array of valid email addresses.
282284
- Response: **Status-Code OK**, as well as the id of the updated form.
283285

docs/DataStructure.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This document describes the Object-Structure, that is used within the Forms App
2222
| ownerId | String | | The nextcloud userId of the form owner |
2323
| submissionMessage | String | max. 2048 ch. | Optional custom message, with Markdown support, to be shown to users when the form is submitted (default is used if set to null) |
2424
| notifyOwnerOnSubmission | Boolean | | If the form owner should receive an email notification with a response summary for each new submission |
25+
| attachSubmissionPdf | Boolean | | If notification emails should include a PDF attachment with the submitted responses |
2526
| notificationRecipients | Array of Strings | | Additional email recipients to notify for each new submission, independent of `notifyOwnerOnSubmission` |
2627
| created | unix timestamp | | When the form has been created |
2728
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
@@ -70,6 +71,7 @@ This document describes the Object-Structure, that is used within the Forms App
7071
"submissionCount": 0,
7172
"submissionMessage": "string",
7273
"notifyOwnerOnSubmission": false,
74+
"attachSubmissionPdf": false,
7375
"notificationRecipients": ["team@example.com"]
7476
}
7577
```

lib/Controller/ApiController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ public function newForm(?int $fromId = null): DataResponse {
185185
$form->setIsAnonymous(false);
186186
$form->setNotifyOwnerOnSubmission(false);
187187
$form->setState(Constants::FORM_STATE_ACTIVE);
188+
$form->setAttachSubmissionPdf(false);
188189
$form->setNotificationRecipients([]);
189190

190191
$this->formMapper->insert($form);
@@ -216,6 +217,7 @@ public function newForm(?int $fromId = null): DataResponse {
216217
$formData['isAnonymous'] = false;
217218
$formData['notifyOwnerOnSubmission'] = false;
218219
$formData['state'] = Constants::FORM_STATE_ACTIVE;
220+
$formData['attachSubmissionPdf'] = false;
219221
$formData['notificationRecipients'] = [];
220222

221223
$form = Form::fromParams($formData);
@@ -331,6 +333,10 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
331333
throw new OCSBadRequestException('notifyOwnerOnSubmission must be a boolean');
332334
}
333335

336+
if (isset($keyValuePairs['attachSubmissionPdf']) && !is_bool($keyValuePairs['attachSubmissionPdf'])) {
337+
throw new OCSBadRequestException('attachSubmissionPdf must be a boolean');
338+
}
339+
334340
if (array_key_exists('notificationRecipients', $keyValuePairs)) {
335341
$keyValuePairs['notificationRecipients'] = $this->normalizeNotificationRecipients($keyValuePairs['notificationRecipients']);
336342
}

lib/Db/Form.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
* @method void setSubmissionMessage(string|null $value)
4646
* @method bool getNotifyOwnerOnSubmission()
4747
* @method void setNotifyOwnerOnSubmission(bool $value)
48+
* @method bool getAttachSubmissionPdf()
49+
* @method void setAttachSubmissionPdf(bool $value)
4850
* @method string|null getNotificationRecipientsJson()
4951
* @method void setNotificationRecipientsJson(?string $value)
5052
* @method int getState()
@@ -74,6 +76,7 @@ class Form extends Entity {
7476
protected $showExpiration;
7577
protected $submissionMessage;
7678
protected $notifyOwnerOnSubmission;
79+
protected $attachSubmissionPdf;
7780
protected $notificationRecipientsJson;
7881
protected $lastUpdated;
7982
protected $state;
@@ -92,6 +95,7 @@ public function __construct() {
9295
$this->addType('allowEditSubmissions', 'boolean');
9396
$this->addType('showExpiration', 'boolean');
9497
$this->addType('notifyOwnerOnSubmission', 'boolean');
98+
$this->addType('attachSubmissionPdf', 'boolean');
9599
$this->addType('lastUpdated', 'integer');
96100
$this->addType('state', 'integer');
97101
$this->addType('lockedBy', 'string');
@@ -199,6 +203,7 @@ public function setNotificationRecipients(array $recipients): void {
199203
* lastUpdated: int,
200204
* submissionMessage: ?string,
201205
* notifyOwnerOnSubmission: bool,
206+
* attachSubmissionPdf: bool,
202207
* notificationRecipients: list<string>,
203208
* state: 0|1|2,
204209
* lockedBy: ?string,
@@ -225,6 +230,7 @@ public function read() {
225230
'lastUpdated' => (int)$this->getLastUpdated(),
226231
'submissionMessage' => $this->getSubmissionMessage(),
227232
'notifyOwnerOnSubmission' => (bool)$this->getNotifyOwnerOnSubmission(),
233+
'attachSubmissionPdf' => (bool)$this->getAttachSubmissionPdf(),
228234
'notificationRecipients' => $this->getNotificationRecipients(),
229235
'state' => $this->getState(),
230236
'lockedBy' => $this->getLockedBy(),

lib/FormsMigrator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
149149
$form->setAllowEditSubmissions($formData['allowEditSubmissions']);
150150
$form->setShowExpiration($formData['showExpiration']);
151151
$form->setNotifyOwnerOnSubmission($formData['notifyOwnerOnSubmission'] ?? false);
152+
$form->setAttachSubmissionPdf($formData['attachSubmissionPdf'] ?? false);
152153
$form->setNotificationRecipients($formData['notificationRecipients'] ?? []);
153154

154155
$this->formMapper->insert($form);

lib/Listener/OwnerNotificationListener.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public function handle(Event $event): void {
6868
}
6969

7070
$answerSummaries = [];
71+
$pdfAnswerEntries = [];
7172
try {
7273
$answers = $this->answerMapper->findBySubmission($submission->getId());
7374
} catch (DoesNotExistException $e) {
@@ -87,6 +88,12 @@ public function handle(Event $event): void {
8788

8889
$questionType = $question->getType();
8990
$answerText = trim($answer->getText() ?? '');
91+
if ($answerText !== '') {
92+
$pdfAnswerEntries[] = [
93+
'question' => $question->getText(),
94+
'answer' => $answerText,
95+
];
96+
}
9097
if (
9198
$answerText !== ''
9299
&& in_array($questionType, [Constants::ANSWER_TYPE_SHORT, Constants::ANSWER_TYPE_LONG], true)
@@ -103,6 +110,7 @@ public function handle(Event $event): void {
103110
$submission,
104111
array_values($normalizedRecipients),
105112
$answerSummaries,
113+
$pdfAnswerEntries,
106114
);
107115
}
108116
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Forms\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\SimpleMigrationStep;
17+
18+
class Version050300Date20260228190000 extends SimpleMigrationStep {
19+
20+
/**
21+
* @param IOutput $output
22+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
23+
* @param array $options
24+
* @return null|ISchemaWrapper
25+
*/
26+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
27+
/** @var ISchemaWrapper $schema */
28+
$schema = $schemaClosure();
29+
30+
$formsTable = $schema->getTable('forms_v2_forms');
31+
if (!$formsTable->hasColumn('attach_submission_pdf')) {
32+
$formsTable->addColumn('attach_submission_pdf', Types::BOOLEAN, [
33+
'notnull' => false,
34+
'default' => false,
35+
]);
36+
}
37+
38+
return $schema;
39+
}
40+
}

lib/ResponseDefinitions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
* submissionCount?: int,
143143
* submissionMessage: ?string,
144144
* notifyOwnerOnSubmission: bool,
145+
* attachSubmissionPdf: bool,
145146
* notificationRecipients: list<string>,
146147
* }
147148
*

lib/Service/OwnerNotificationMailService.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ public function __construct(
2222
private IMailer $mailer,
2323
private IL10N $l10n,
2424
private IURLGenerator $urlGenerator,
25+
private SubmissionPdfService $submissionPdfService,
2526
private LoggerInterface $logger,
2627
) {
2728
}
2829

2930
/**
3031
* @param list<string> $recipients
3132
* @param array<int, array{question: string, answer: string}> $answerSummaries
33+
* @param array<int, array{question: string, answer: string}> $pdfAnswerEntries
3234
*/
33-
public function send(Form $form, Submission $submission, array $recipients, array $answerSummaries = []): void {
35+
public function send(Form $form, Submission $submission, array $recipients, array $answerSummaries = [], array $pdfAnswerEntries = []): void {
3436
$validRecipients = array_values(array_unique(array_filter($recipients, fn (string $recipient): bool => $this->mailer->validateMailAddress($recipient))));
3537
if ($validRecipients === []) {
3638
return;
@@ -77,6 +79,16 @@ public function send(Form $form, Submission $submission, array $recipients, arra
7779
$message->setSubject($subject);
7880
$message->setTo($validRecipients);
7981
$message->useTemplate($emailTemplate);
82+
if ($form->getAttachSubmissionPdf()) {
83+
$entriesForPdf = $pdfAnswerEntries !== [] ? $pdfAnswerEntries : $answerSummaries;
84+
$message->attach(
85+
$this->mailer->createAttachment(
86+
$this->submissionPdfService->createPdf($form, $submission, $entriesForPdf),
87+
$this->submissionPdfService->createFilename($form, $submission),
88+
'application/pdf',
89+
),
90+
);
91+
}
8092

8193
$this->mailer->send($message);
8294
} catch (\Throwable $e) {

0 commit comments

Comments
 (0)