Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial submissions #705

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c3d0596
Implement partial form submissions feature
chiragchhatrala Feb 18, 2025
a0c5b92
Add status filtering for form submissions
chiragchhatrala Feb 19, 2025
cfcc364
Add Partial Submission in Analytics
chiragchhatrala Feb 20, 2025
00053cd
improve partial submission
chiragchhatrala Feb 20, 2025
1a96a84
fix lint
chiragchhatrala Feb 20, 2025
fb010a6
Add type checking for submission ID in form submission job
chiragchhatrala Feb 20, 2025
8e85ca8
Merge branch 'main' into 627a0-partial-submissions
chiragchhatrala Feb 25, 2025
e4d0f13
on form stats Partial Submissions only if enable
chiragchhatrala Feb 25, 2025
33c1c86
Partial Submissions is PRO Feature
chiragchhatrala Feb 25, 2025
2b5c0fe
Partial Submissions is PRO Feature
chiragchhatrala Feb 25, 2025
e4a3d78
improvement migration
chiragchhatrala Feb 25, 2025
8b01f70
Update form submission status labels to 'Submitted' and 'In Progress'
chiragchhatrala Feb 25, 2025
e196f95
start partial sync when dataFormValue update
chiragchhatrala Feb 27, 2025
7629f3b
badge size xs
chiragchhatrala Feb 27, 2025
a8112e2
Refactor partial submission hash management
chiragchhatrala Mar 3, 2025
232d612
Merge branch 'main' into 627a0-partial-submissions
chiragchhatrala Mar 3, 2025
ca0524c
Refactor partial form submission handling in PublicFormController
chiragchhatrala Mar 3, 2025
f279b87
fix submissiona
chiragchhatrala Mar 3, 2025
2cb9139
Refactor form submission ID handling and metadata processing
JhumanJ Mar 5, 2025
ce1e21d
Enhance form submission settings UI with advanced partial submission …
JhumanJ Mar 6, 2025
d0f73cf
Refactor partial form submission sync mechanism
JhumanJ Mar 6, 2025
52c5812
Merge branch 'main' into 627a0-partial-submissions
chiragchhatrala Mar 6, 2025
d3959f6
Improve partial form submission validation and synchronization
chiragchhatrala Mar 10, 2025
b5914fc
fix lint
chiragchhatrala Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions api/app/Http/Controllers/Forms/FormStatsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Http\Controllers\Controller;
use App\Http\Requests\FormStatsRequest;
use App\Models\Forms\FormSubmission;
use Carbon\CarbonPeriod;
use Carbon\CarbonInterval;
use Illuminate\Http\Request;
Expand All @@ -21,13 +22,14 @@ public function getFormStats(FormStatsRequest $request)
$this->authorize('view', $form);

$formStats = $form->statistics()->whereBetween('date', [$request->date_from, $request->date_to])->get();
$periodStats = ['views' => [], 'submissions' => []];
$periodStats = ['views' => [], 'submissions' => [], 'partial_submissions' => []];
foreach (CarbonPeriod::create($request->date_from, $request->date_to) as $dateObj) {
$date = $dateObj->format('d-m-Y');

$statisticData = $formStats->where('date', $dateObj->format('Y-m-d'))->first();
$periodStats['views'][$date] = $statisticData->data['views'] ?? 0;
$periodStats['submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->count();
$periodStats['submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_COMPLETED)->count();
$periodStats['partial_submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_PARTIAL)->count();
Comment on lines +31 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

❓ Verification inconclusive

Verify FormSubmission status constants and add error handling.

The code assumes the existence of FormSubmission::STATUS_COMPLETED and FormSubmission::STATUS_PARTIAL constants. Additionally, database queries should include error handling.

Add try-catch blocks for database queries:

-            $periodStats['submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_COMPLETED)->count();
-            $periodStats['partial_submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_PARTIAL)->count();
+            try {
+                $periodStats['submissions'][$date] = $form->submissions()
+                    ->whereDate('created_at', $dateObj)
+                    ->where('status', FormSubmission::STATUS_COMPLETED)
+                    ->count();
+                $periodStats['partial_submissions'][$date] = $form->submissions()
+                    ->whereDate('created_at', $dateObj)
+                    ->where('status', FormSubmission::STATUS_PARTIAL)
+                    ->count();
+            } catch (\Exception $e) {
+                \Log::error('Error fetching form stats: ' . $e->getMessage());
+                $periodStats['submissions'][$date] = 0;
+                $periodStats['partial_submissions'][$date] = 0;
+            }

🏁 Script executed:

#!/bin/bash
# Verify FormSubmission status constants
ast-grep --pattern 'class FormSubmission {
  $$$
  const STATUS_COMPLETED = $_;
  const STATUS_PARTIAL = $_;
  $$$
}'

Length of output: 119


Verify FormSubmission Constants and Add Error Handling

  • Please manually verify that the FormSubmission class defines both STATUS_COMPLETED and STATUS_PARTIAL constants. Our initial AST search did not return output—use a text search (e.g., with rg) to confirm their existence.
  • Wrap the database queries in a try-catch block to handle exceptions and prevent runtime failures during query execution. For example:
-            $periodStats['submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_COMPLETED)->count();
-            $periodStats['partial_submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_PARTIAL)->count();
+            try {
+                $periodStats['submissions'][$date] = $form->submissions()
+                    ->whereDate('created_at', $dateObj)
+                    ->where('status', FormSubmission::STATUS_COMPLETED)
+                    ->count();
+                $periodStats['partial_submissions'][$date] = $form->submissions()
+                    ->whereDate('created_at', $dateObj)
+                    ->where('status', FormSubmission::STATUS_PARTIAL)
+                    ->count();
+            } catch (\Exception $e) {
+                \Log::error('Error fetching form stats: ' . $e->getMessage());
+                $periodStats['submissions'][$date] = 0;
+                $periodStats['partial_submissions'][$date] = 0;
+            }

To further assist verification, you may run:

Please ensure these changes align with your codebase before merging.


🏁 Script executed:

[run_scripts]
#!/bin/bash
# Search for the FormSubmission class and its status constants using rg:
echo "Searching for FormSubmission class definition:"
rg --ignore-case "class\s+FormSubmission" || echo "No FormSubmission class found."

echo "Searching for STATUS_COMPLETED constant:"
rg --ignore-case "const\s+STATUS_COMPLETED" || echo "STATUS_COMPLETED constant not found."

echo "Searching for STATUS_PARTIAL constant:"
rg --ignore-case "const\s+STATUS_PARTIAL" || echo "STATUS_PARTIAL constant not found."

Length of output: 1713


Constants Verified – Please Add Error Handling to Database Queries

The status constants FormSubmission::STATUS_COMPLETED and FormSubmission::STATUS_PARTIAL have been confirmed in api/app/Models/Forms/FormSubmission.php. Since these constants exist, the primary focus should be on enhancing the robustness of the database queries by wrapping them in a try-catch block, as outlined below:

  • Verified:
    • FormSubmission::STATUS_COMPLETED exists.
    • FormSubmission::STATUS_PARTIAL exists.
  • Action Required:
    • Add error handling (a try-catch block) around the database queries to ensure exceptions are caught and logged.

Please update your code as follows:

-            $periodStats['submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_COMPLETED)->count();
-            $periodStats['partial_submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_PARTIAL)->count();
+            try {
+                $periodStats['submissions'][$date] = $form->submissions()
+                    ->whereDate('created_at', $dateObj)
+                    ->where('status', FormSubmission::STATUS_COMPLETED)
+                    ->count();
+                $periodStats['partial_submissions'][$date] = $form->submissions()
+                    ->whereDate('created_at', $dateObj)
+                    ->where('status', FormSubmission::STATUS_PARTIAL)
+                    ->count();
+            } catch (\Exception $e) {
+                \Log::error('Error fetching form stats: ' . $e->getMessage());
+                $periodStats['submissions'][$date] = 0;
+                $periodStats['partial_submissions'][$date] = 0;
+            }

Please verify that the try-catch implementation aligns with your overall error handling strategy before merging.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$periodStats['submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_COMPLETED)->count();
$periodStats['partial_submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->where('status', FormSubmission::STATUS_PARTIAL)->count();
try {
$periodStats['submissions'][$date] = $form->submissions()
->whereDate('created_at', $dateObj)
->where('status', FormSubmission::STATUS_COMPLETED)
->count();
$periodStats['partial_submissions'][$date] = $form->submissions()
->whereDate('created_at', $dateObj)
->where('status', FormSubmission::STATUS_PARTIAL)
->count();
} catch (\Exception $e) {
\Log::error('Error fetching form stats: ' . $e->getMessage());
$periodStats['submissions'][$date] = 0;
$periodStats['partial_submissions'][$date] = 0;
}


if ($dateObj->toDateString() === now()->toDateString()) {
$periodStats['views'][$date] += $form->views()->count();
Expand Down
6 changes: 4 additions & 2 deletions api/app/Http/Controllers/Forms/FormSubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ public function update(AnswerFormRequest $request, $id, $submissionId)
{
$form = $request->form;
$this->authorize('update', $form);
$job = new StoreFormSubmissionJob($request->form, $request->validated());
$job->setSubmissionId($submissionId)->handle();

$submissionData['submission_id'] = $submissionId;
$job = new StoreFormSubmissionJob($request->form, $submissionData);
$job->handle();

$data = new FormSubmissionResource(FormSubmission::findOrFail($submissionId));

Expand Down
105 changes: 96 additions & 9 deletions api/app/Http/Controllers/Forms/PublicFormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Vinkla\Hashids\Facades\Hashids;
use Illuminate\Support\Str;

class PublicFormController extends Controller
{
Expand Down Expand Up @@ -85,38 +86,124 @@ public function showAsset($assetFileName)
return redirect()->to($internal_url);
}

/**
* Handle partial form submissions
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
private function handlePartialSubmissions(Request $request)
{
$form = $request->form;

// Process submission data to extract submission ID
$submissionData = $this->processSubmissionIdentifiers($request, $request->all());

// Validate that at least one field has a value
$hasValue = false;
foreach ($submissionData as $key => $value) {
if (Str::isUuid($key) && !empty($value)) {
$hasValue = true;
break;
}
}
if (!$hasValue) {
return $this->error([
'message' => 'At least one field must have a value for partial submissions.'
], 422);
}

// Explicitly mark this as a partial submission
$submissionData['is_partial'] = true;

// Use the same job as regular submissions to ensure consistent processing
$job = new StoreFormSubmissionJob($form, $submissionData);
$job->handle();

// Get the submission ID
$submissionId = $job->getSubmissionId();

return $this->success([
'message' => 'Partial submission saved',
'submission_hash' => Hashids::encode($submissionId)
]);
}

public function answer(AnswerFormRequest $request, FormSubmissionProcessor $formSubmissionProcessor)
{
$form = $request->form;
$isFirstSubmission = ($form->submissions_count === 0);
$submissionId = false;

// Handle partial submissions
$isPartial = $request->get('is_partial') ?? false;
if ($isPartial && $form->enable_partial_submissions && $form->is_pro) {
return $this->handlePartialSubmissions($request);
}

// Get validated data (includes all metadata)
$submissionData = $request->validated();
$completionTime = $request->get('completion_time') ?? null;
unset($submissionData['completion_time']); // Remove completion_time from the main data array

$job = new StoreFormSubmissionJob($form, $submissionData, $completionTime);
// Process submission hash and ID
$submissionData = $this->processSubmissionIdentifiers($request, $submissionData);

// Create the job with all data (including metadata)
$job = new StoreFormSubmissionJob($form, $submissionData);

// Process the submission
if ($formSubmissionProcessor->shouldProcessSynchronously($form)) {
$job->handle();
$submissionId = Hashids::encode($job->getSubmissionId());
$encodedSubmissionId = Hashids::encode($job->getSubmissionId());
// Update submission data with generated values for redirect URL
$submissionData = $job->getProcessedData();
} else {
StoreFormSubmissionJob::dispatch($form, $submissionData, $completionTime);
$job->handle();
$encodedSubmissionId = Hashids::encode($job->getSubmissionId());
}

// Return the response
return $this->success(array_merge([
'message' => 'Form submission saved.',
'submission_id' => $submissionId,
'submission_id' => $encodedSubmissionId,
'is_first_submission' => $isFirstSubmission,
], $formSubmissionProcessor->getRedirectData($form, $submissionData)));
}

/**
* Process submission hash and ID to ensure consistent format
*
* @param Request $request
* @param array $submissionData
* @return array
*/
private function processSubmissionIdentifiers(Request $request, array $submissionData): array
{
// Handle submission hash if present (convert to numeric submission_id)
$submissionHash = $request->get('submission_hash');
if ($submissionHash) {
$decodedHash = Hashids::decode($submissionHash);
if (!empty($decodedHash)) {
$submissionData['submission_id'] = (int)($decodedHash[0] ?? null);
}
unset($submissionData['submission_hash']);
}

// Handle string submission_id if present (convert to numeric)
if (isset($submissionData['submission_id']) && is_string($submissionData['submission_id']) && !is_numeric($submissionData['submission_id'])) {
$decodedId = Hashids::decode($submissionData['submission_id']);
if (!empty($decodedId)) {
$submissionData['submission_id'] = (int)($decodedId[0] ?? null);
}
}

return $submissionData;
}

public function fetchSubmission(Request $request, string $slug, string $submissionId)
{
$submissionId = ($submissionId) ? Hashids::decode($submissionId) : false;
$submissionId = isset($submissionId[0]) ? $submissionId[0] : false;
// Decode the submission ID using the same approach as in processSubmissionIdentifiers
$decodedId = Hashids::decode($submissionId);
$submissionId = !empty($decodedId) ? (int)($decodedId[0]) : false;

$form = Form::whereSlug($slug)->whereVisibility('public')->firstOrFail();
if ($form->workspace == null || !$form->editable_submissions || !$submissionId) {
return $this->error([
Expand Down
5 changes: 5 additions & 0 deletions api/app/Http/Requests/AnswerFormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public function authorize()
*/
public function rules()
{
// Skip validation if this is a partial submission
if ($this->has('is_partial')) {
return [];
}

$selectionFields = collect($this->form->properties)->filter(function ($pro) {
return in_array($pro['type'], ['select', 'multi_select']);
});
Expand Down
1 change: 1 addition & 0 deletions api/app/Http/Requests/UserFormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public function rules()
'show_progress_bar' => 'boolean',
'auto_save' => 'boolean',
'auto_focus' => 'boolean',
'enable_partial_submissions' => 'boolean',

// Properties
'properties' => 'required|array',
Expand Down
1 change: 1 addition & 0 deletions api/app/Http/Resources/FormSubmissionResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function publiclyAccessed($publiclyAccessed = true)
private function addExtraData()
{
$this->data = array_merge($this->data, [
'status' => $this->status,
'created_at' => $this->created_at->toDateTimeString(),
'id' => $this->id,
]);
Expand Down
Loading
Loading