diff --git a/config/stache.php b/config/stache.php index e0db82e05c..71044d5ab3 100644 --- a/config/stache.php +++ b/config/stache.php @@ -89,6 +89,11 @@ 'directory' => base_path('users'), ], + 'form-submissions' => [ + 'class' => Stores\SubmissionsStore::class, + 'directory' => storage_path('forms'), + ], + ], /* diff --git a/src/Contracts/Forms/SubmissionQueryBuilder.php b/src/Contracts/Forms/SubmissionQueryBuilder.php new file mode 100644 index 0000000000..08e1fcfbd1 --- /dev/null +++ b/src/Contracts/Forms/SubmissionQueryBuilder.php @@ -0,0 +1,8 @@ +handle(); - - return collect(Folder::getFilesByType($path, 'yaml'))->map(function ($file) { - try { - $data = YAML::parse(File::get($file)); - } catch (ParseException $e) { - $data = []; - Log::warning('Could not parse form submission file: '.$file); - } - - return $this->makeSubmission() - ->id(pathinfo($file)['filename']) - ->data($data); - }); + return FormSubmission::whereForm($this->handle()); } /** @@ -319,9 +304,7 @@ public function submissions() */ public function submission($id) { - return $this->submissions()->filter(function ($submission) use ($id) { - return $submission->id() === $id; - })->first(); + return FormSubmission::find($id); } /** @@ -331,7 +314,7 @@ public function submission($id) */ public function makeSubmission() { - $submission = app(Submission::class); + $submission = FormSubmission::make(); $submission->form($this); diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index d3088689c2..1637dc712e 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -6,7 +6,10 @@ use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Forms\Submission as SubmissionContract; use Statamic\Data\ContainsData; +use Statamic\Data\ExistsAsFile; use Statamic\Data\HasAugmentedData; +use Statamic\Data\TracksQueriedColumns; +use Statamic\Data\TracksQueriedRelations; use Statamic\Events\SubmissionCreated; use Statamic\Events\SubmissionCreating; use Statamic\Events\SubmissionDeleted; @@ -14,15 +17,15 @@ use Statamic\Events\SubmissionSaving; use Statamic\Facades\Asset; use Statamic\Facades\File; -use Statamic\Facades\YAML; +use Statamic\Facades\FormSubmission; +use Statamic\Facades\Stache; use Statamic\Forms\Uploaders\AssetsUploader; use Statamic\Forms\Uploaders\FilesUploader; -use Statamic\Support\Arr; use Statamic\Support\Traits\FluentlyGetsAndSets; class Submission implements Augmentable, SubmissionContract { - use ContainsData, FluentlyGetsAndSets, HasAugmentedData; + use ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedData, TracksQueriedColumns, TracksQueriedRelations; /** * @var string @@ -167,7 +170,7 @@ public function save() } } - File::put($this->getPath(), YAML::dump(Arr::removeNullValues($this->data()->all()))); + FormSubmission::save($this); foreach ($afterSaveCallbacks as $callback) { $callback($this); @@ -187,7 +190,7 @@ public function save() */ public function delete() { - File::delete($this->getPath()); + FormSubmission::delete($this); SubmissionDeleted::dispatch($this); } @@ -199,7 +202,16 @@ public function delete() */ public function getPath() { - return config('statamic.forms.submissions').'/'.$this->form()->handle().'/'.$this->id().'.yaml'; + return $this->path(); + } + + public function path() + { + return vsprintf('%s/%s/%s.yaml', [ + rtrim(Stache::store('form-submissions')->directory(), '/'), + $this->form()->handle(), + $this->id(), + ]); } /** @@ -237,6 +249,11 @@ public function blueprint() return $this->form->blueprint(); } + public function fileData() + { + return $this->data()->all(); + } + public function __get($key) { return $this->get($key); diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index a91254ae64..08fa4494f9 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -121,6 +121,7 @@ public function register() \Statamic\Contracts\Structures\NavigationRepository::class => \Statamic\Stache\Repositories\NavigationRepository::class, \Statamic\Contracts\Assets\AssetRepository::class => \Statamic\Assets\AssetRepository::class, \Statamic\Contracts\Forms\FormRepository::class => \Statamic\Forms\FormRepository::class, + \Statamic\Contracts\Forms\SubmissionRepository::class => \Statamic\Stache\Repositories\SubmissionRepository::class, ])->each(function ($concrete, $abstract) { if (! $this->app->bound($abstract)) { Statamic::repository($abstract, $concrete); @@ -155,6 +156,7 @@ public function register() collect([ 'entries' => fn () => Facades\Entry::query(), + 'form-submissions' => fn () => Facades\FormSubmission::query(), 'terms' => fn () => Facades\Term::query(), 'assets' => fn () => Facades\Asset::query(), 'users' => fn () => Facades\User::query(), diff --git a/src/Stache/Query/SubmissionQueryBuilder.php b/src/Stache/Query/SubmissionQueryBuilder.php new file mode 100644 index 0000000000..4a60afcc66 --- /dev/null +++ b/src/Stache/Query/SubmissionQueryBuilder.php @@ -0,0 +1,125 @@ +forms[] = $operator; + + return $this; + } + + return parent::where($column, $operator, $value, $boolean); + } + + public function whereIn($column, $values, $boolean = 'and') + { + if (in_array($column, ['form', 'forms'])) { + $this->forms = array_merge($this->forms ?? [], $values); + + return $this; + } + + return parent::whereIn($column, $values, $boolean); + } + + protected function collect($items = []) + { + return Collection::make($items); + } + + protected function getFilteredKeys() + { + $forms = empty($this->forms) + ? Facades\Form::all()->map->handle() + : $this->forms; + + return empty($this->wheres) + ? $this->getKeysFromForms($forms) + : $this->getKeysFromFormsWithWheres($forms, $this->wheres); + } + + protected function getKeysFromForms($forms) + { + return collect($forms)->flatMap(function ($form) { + $keys = $this->store->store($form)->paths()->keys(); + + return collect($keys)->map(function ($key) use ($form) { + return "{$form}::{$key}"; + }); + }); + } + + protected function getKeysFromFormsWithWheres($forms, $wheres) + { + return collect($wheres)->reduce(function ($ids, $where) use ($forms) { + $keys = $where['type'] == 'Nested' + ? $this->getKeysFromFormsWithWheres($forms, $where['query']->wheres) + : $this->getKeysFromFormsWithWhere($forms, $where); + + return $this->intersectKeysFromWhereClause($ids, $keys, $where); + }); + } + + protected function getKeysFromFormsWithWhere($forms, $where) + { + $items = collect($forms)->flatMap(function ($form) use ($where) { + return $this->getWhereColumnKeysFromStore($form, $where); + }); + + $method = 'filterWhere'.$where['type']; + + return $this->{$method}($items, $where)->keys(); + } + + protected function getOrderKeyValuesByIndex() + { + $forms = empty($this->forms) + ? Facades\Form::all()->map->handle() + : $this->forms; + + // First, we'll get the values from each index grouped by form + $keys = collect($forms)->map(function ($form) { + $store = $this->store->store($form); + + return collect($this->orderBys)->mapWithKeys(function ($orderBy) use ($form, $store) { + $items = $store->index($orderBy->sort) + ->items() + ->mapWithKeys(function ($item, $key) use ($form) { + return ["{$form}::{$key}" => $item]; + })->all(); + + return [$orderBy->sort => $items]; + }); + }); + + // Then, we'll merge all the corresponding index values together from each form. + return $keys->reduce(function ($carry, $form) { + foreach ($form as $sort => $values) { + $carry[$sort] = array_merge($carry[$sort] ?? [], $values); + } + + return $carry; + }, collect()); + } + + protected function getWhereColumnKeyValuesByIndex($column) + { + $forms = empty($this->forms) + ? Facades\Form::all()->map->handle() + : $this->forms; + + return collect($forms)->flatMap(function ($form) use ($column) { + return $this->getWhereColumnKeysFromStore($form, ['column' => $column]); + }); + } +} diff --git a/src/Stache/Repositories/SubmissionRepository.php b/src/Stache/Repositories/SubmissionRepository.php new file mode 100644 index 0000000000..f10396aaef --- /dev/null +++ b/src/Stache/Repositories/SubmissionRepository.php @@ -0,0 +1,73 @@ +stache = $stache; + $this->store = $stache->store('form-submissions'); + } + + public function all(): Collection + { + return $this->query()->get(); + } + + public function whereForm(string $handle): Collection + { + return $this->query()->where('form', $handle)->get(); + } + + public function whereInForm(array $handles): Collection + { + return $this->query()->whereIn('form', $handles)->get(); + } + + public function find($id): ?Submission + { + return $this->query()->where('id', $id)->first(); + } + + public function save($submission) + { + $this->store + ->store($submission->form()->handle()) + ->save($submission); + } + + public function delete($submission) + { + $this->store + ->store($submission->form()->handle()) + ->delete($submission); + } + + public function query() + { + return app(SubmissionQueryBuilder::class); + } + + public function make(): Submission + { + return app(Submission::class); + } + + public static function bindings(): array + { + return [ + Submission::class => \Statamic\Forms\Submission::class, + SubmissionQueryBuilder::class => \Statamic\Stache\Query\SubmissionQueryBuilder::class, + ]; + } +} diff --git a/src/Stache/ServiceProvider.php b/src/Stache/ServiceProvider.php index fdb4c8cf77..6981985207 100644 --- a/src/Stache/ServiceProvider.php +++ b/src/Stache/ServiceProvider.php @@ -7,6 +7,7 @@ use Statamic\Facades\File; use Statamic\Facades\Site; use Statamic\Stache\Query\EntryQueryBuilder; +use Statamic\Stache\Query\SubmissionQueryBuilder; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\Store\FlockStore; @@ -31,6 +32,10 @@ public function register() $this->app->bind(AssetQueryBuilder::class, function () { return new AssetQueryBuilder($this->app->make(Stache::class)->store('assets')); }); + + $this->app->bind(SubmissionQueryBuilder::class, function () { + return new SubmissionQueryBuilder($this->app->make(Stache::class)->store('form-submissions')); + }); } public function boot() diff --git a/src/Stache/Stores/FormSubmissionsStore.php b/src/Stache/Stores/FormSubmissionsStore.php new file mode 100644 index 0000000000..41b82471bc --- /dev/null +++ b/src/Stache/Stores/FormSubmissionsStore.php @@ -0,0 +1,59 @@ +id(); + } + + public function getItemFilter(SplFileInfo $file) + { + $dir = Str::finish($this->directory(), '/'); + $relative = Str::after(Path::tidy($file->getPathname()), $dir); + + return $file->getExtension() === 'yaml' && substr_count($relative, '/') === 0; + } + + public function makeItemFromFile($path, $contents) + { + $handle = pathinfo($path, PATHINFO_FILENAME); + + try { + $data = YAML::file($path)->parse($contents); + } catch (ParseException $e) { + $data = []; + Log::warning('Could not parse form submission file: '.$path); + } + + $form = pathinfo($path, PATHINFO_DIRNAME); + $form = Str::after($form, $this->parent->directory()); + + $form = Form::find($form); + + $submission = FormSubmission::make() + ->id($handle) + ->form($form) + ->data($data); + + return $submission; + } +} diff --git a/src/Stache/Stores/SubmissionsStore.php b/src/Stache/Stores/SubmissionsStore.php new file mode 100644 index 0000000000..7ff0a9dbf2 --- /dev/null +++ b/src/Stache/Stores/SubmissionsStore.php @@ -0,0 +1,22 @@ +map(function ($form) { + return $this->store($form->handle()); + }); + } +} diff --git a/tests/Forms/SubmissionQueryBuilderTest.php b/tests/Forms/SubmissionQueryBuilderTest.php new file mode 100644 index 0000000000..58b0dcd07c --- /dev/null +++ b/tests/Forms/SubmissionQueryBuilderTest.php @@ -0,0 +1,389 @@ +save(); + FormSubmission::make()->form($form)->data(['a' => true])->save(); + FormSubmission::make()->form($form)->data(['b' => true])->save(); + FormSubmission::make()->form($form)->data(['c' => true])->save(); + + $submissions = FormSubmission::whereForm('test'); + $this->assertInstanceOf(Collection::class, $submissions); + $this->assertEveryItemIsInstanceOf(Submission::class, $submissions); + } + + /** @test */ + public function it_filters_using_wheres() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 'bar'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 'foo'])->save(); + + $submissions = FormSubmission::query()->where('test', 'foo')->get(); + $this->assertEquals(['a', 'c'], $submissions->map->get('id')->sort()->values()->all()); + } + + /** @test */ + public function it_filters_using_or_wheres() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 'bar'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 'baz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'test' => 'raz'])->save(); + + $submissions = FormSubmission::query()->where('test', 'foo')->orWhere('test', 'bar')->get(); + $this->assertEquals(['a', 'd', 'b'], $submissions->map->get('id')->values()->all()); + } + + /** @test */ + public function it_filters_using_or_where_ins() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 'bar'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 'baz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'test' => 'raz'])->save(); + + $submissions = FormSubmission::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->get(); + + $this->assertEquals(['a', 'b', 'd', 'e'], $submissions->map->get('id')->values()->all()); + } + + /** @test **/ + public function it_filters_using_or_where_not_ins() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 'bar'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 'baz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'test' => 'raz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'f', 'test' => 'taz'])->save(); + + $submissions = FormSubmission::query()->whereNotIn('test', ['foo', 'bar'])->orWhereNotIn('test', ['foo', 'raz'])->get(); + + $this->assertEquals(['c', 'f'], $submissions->map->get('id')->values()->all()); + } + + /** @test */ + public function it_filters_using_nested_wheres() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 'bar'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 'baz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'test' => 'raz'])->save(); + + $submissions = FormSubmission::query() + ->where(function ($query) { + $query->where('test', 'foo'); + }) + ->orWhere(function ($query) { + $query->where('test', 'baz'); + }) + ->orWhere('test', 'raz') + ->get(); + + $this->assertCount(4, $submissions); + $this->assertEquals(['a', 'c', 'd', 'e'], $submissions->map->get('id')->sort()->values()->all()); + } + + /** @test */ + public function it_filters_using_nested_where_ins() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 'bar'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 'baz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'test' => 'raz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'f', 'test' => 'chaz'])->save(); + + $submissions = FormSubmission::query() + ->where(function ($query) { + $query->where('test', 'foo'); + }) + ->orWhere(function ($query) { + $query->whereIn('test', ['baz', 'raz']); + }) + ->orWhere('test', 'chaz') + ->get(); + + $this->assertCount(5, $submissions); + $this->assertEquals(['a', 'c', 'd', 'e', 'f'], $submissions->map->get('id')->sort()->values()->all()); + } + + /** @test */ + public function it_filters_using_nested_where_not_ins() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 'bar'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 'baz'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'test' => 'foo'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'test' => 'raz'])->save(); + + $submissions = FormSubmission::query() + ->where('test', 'foo') + ->orWhere(function ($query) { + $query->whereNotIn('test', ['baz', 'raz']); + }) + ->get(); + + $this->assertCount(3, $submissions); + $this->assertEquals(['a', 'b', 'd'], $submissions->map->get('id')->sort()->values()->all()); + } + + /** @test */ + public function it_sorts() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'test' => 4])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'test' => 2])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'test' => 1])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'test' => 5])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'test' => 3])->save(); + + $submissions = FormSubmission::query()->orderBy('test')->get(); + $this->assertEquals(['c', 'b', 'e', 'a', 'd'], $submissions->map->get('id')->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_column() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a', 'title' => 'Post 1', 'other_title' => 'Not Post 1'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b', 'title' => 'Post 2', 'other_title' => 'Not Post 2'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c', 'title' => 'Post 3', 'other_title' => 'Post 3'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'd', 'title' => 'Post 4', 'other_title' => 'Post 4'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'e', 'title' => 'Post 5', 'other_title' => 'Not Post 5'])->save(); + + $submissions = FormSubmission::query()->whereColumn('title', 'other_title')->get(); + + $this->assertCount(2, $submissions); + $this->assertEquals(['c', 'd'], $submissions->map->get('id')->all()); + + $submissions = FormSubmission::query()->whereColumn('title', '!=', 'other_title')->get(); + + $this->assertCount(3, $submissions); + $this->assertEquals(['a', 'b', 'e'], $submissions->map->get('id')->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_date() + { + $this->createWhereDateTestTerms(); + + $entries = FormSubmission::query()->whereDate('date', '2021-11-15')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->sort()->values()->all()); + + $entries = FormSubmission::query()->whereDate('date', 1637000264)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->sort()->values()->all()); + + $entries = FormSubmission::query()->whereDate('date', '>=', '2021-11-15')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->sort()->values()->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_month() + { + $this->createWhereDateTestTerms(); + + $entries = FormSubmission::query()->whereMonth('date', 11)->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->sort()->values()->all()); + + $entries = FormSubmission::query()->whereMonth('date', '<', 11)->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 4'], $entries->map->title->sort()->values()->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_day() + { + $this->createWhereDateTestTerms(); + + $entries = FormSubmission::query()->whereDay('date', 15)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->sort()->values()->all()); + + $entries = FormSubmission::query()->whereDay('date', '<', 15)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 2', 'Post 4'], $entries->map->title->sort()->values()->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_year() + { + $this->createWhereDateTestTerms(); + + $entries = FormSubmission::query()->whereYear('date', 2021)->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->sort()->values()->all()); + + $entries = FormSubmission::query()->whereYear('date', '<', 2021)->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 4'], $entries->map->title->sort()->values()->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_time() + { + $this->createWhereDateTestTerms(); + + $entries = FormSubmission::query()->whereTime('date', '09:00')->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 2'], $entries->map->title->sort()->values()->all()); + + $entries = FormSubmission::query()->whereTime('date', '>', '09:00')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 4'], $entries->map->title->sort()->values()->all()); + } + + private function createWhereDateTestTerms() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['title' => 'Post 1'])->id(1637008264)->save(); + FormSubmission::make()->form($form)->data(['title' => 'Post 2'])->id(1636621200)->save(); + FormSubmission::make()->form($form)->data(['title' => 'Post 3'])->id(1636934400)->save(); + FormSubmission::make()->form($form)->data(['title' => 'Post 4'])->id(1600008264)->save(); + } + + /** @test **/ + public function submissions_are_found_using_where_json_contains() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => '1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + FormSubmission::make()->form($form)->data(['id' => '2', 'test_taxonomy' => ['taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + FormSubmission::make()->form($form)->data(['id' => '5', 'test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = FormSubmission::query()->whereJsonContains('test_taxonomy', ['taxonomy-1', 'taxonomy-5'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['1', '3', '5'], $entries->map->get('id')->all()); + + $entries = FormSubmission::query()->whereJsonContains('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['1', '3'], $entries->map->get('id')->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_json_doesnt_contain() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => '1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + FormSubmission::make()->form($form)->data(['id' => '2', 'test_taxonomy' => ['taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + FormSubmission::make()->form($form)->data(['id' => '5', 'test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = FormSubmission::query()->whereJsonDoesntContain('test_taxonomy', ['taxonomy-1'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['2', '4', '5'], $entries->map->get('id')->all()); + + $entries = FormSubmission::query()->whereJsonDoesntContain('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['2', '4', '5'], $entries->map->get('id')->all()); + } + + /** @test **/ + public function submissions_are_found_using_or_where_json_contains() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => '1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + FormSubmission::make()->form($form)->data(['id' => '2', 'test_taxonomy' => ['taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + FormSubmission::make()->form($form)->data(['id' => '5', 'test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = FormSubmission::query()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonContains('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['1', '3', '5'], $entries->map->get('id')->all()); + } + + /** @test **/ + public function submissions_are_found_using_or_where_json_doesnt_contain() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => '1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + FormSubmission::make()->form($form)->data(['id' => '2', 'test_taxonomy' => ['taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + FormSubmission::make()->form($form)->data(['id' => '5', 'test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = FormSubmission::query()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonDoesntContain('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['1', '3', '2', '4'], $entries->map->get('id')->all()); + } + + /** @test **/ + public function submissions_are_found_using_where_json_length() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => '1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + FormSubmission::make()->form($form)->data(['id' => '2', 'test_taxonomy' => ['taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + FormSubmission::make()->form($form)->data(['id' => '4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + FormSubmission::make()->form($form)->data(['id' => '5', 'test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = FormSubmission::query()->whereJsonLength('test_taxonomy', 1)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['2', '5'], $entries->map->get('id')->all()); + } + + /** @test */ + public function submissions_are_found_using_offset() + { + $form = tap(Form::make('test'))->save(); + FormSubmission::make()->form($form)->data(['id' => 'a'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'b'])->save(); + FormSubmission::make()->form($form)->data(['id' => 'c'])->save(); + + $submissions = FormSubmission::query()->get(); + $this->assertEquals(['a', 'b', 'c'], $submissions->map->get('id')->all()); + + $submissions = FormSubmission::query()->offset(1)->get(); + $this->assertEquals(['b', 'c'], $submissions->map->get('id')->all()); + } +} diff --git a/tests/Forms/SubmissionTest.php b/tests/Forms/SubmissionTest.php index 70a5279d3b..730d3a4c7f 100644 --- a/tests/Forms/SubmissionTest.php +++ b/tests/Forms/SubmissionTest.php @@ -10,10 +10,13 @@ use Statamic\Events\SubmissionSaving; use Statamic\Facades\Blueprint; use Statamic\Facades\Form; +use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; class SubmissionTest extends TestCase { + use PreventSavingStacheItemsToDisk; + /** @test */ public function the_id_is_generated_the_first_time_but_can_be_overridden() { diff --git a/tests/Stache/FeatureTest.php b/tests/Stache/FeatureTest.php index ddac20b275..ac227514d1 100644 --- a/tests/Stache/FeatureTest.php +++ b/tests/Stache/FeatureTest.php @@ -39,6 +39,7 @@ public function setUp(): void $stache->store('collection-trees')->directory($dir.'/content/structures/collections'); $stache->store('nav-trees')->directory($dir.'/content/structures/navigation'); $stache->store('users')->directory($dir.'/users'); + $stache->store('form-submissions')->directory($dir.'/content/submissions'); }); } diff --git a/tests/Stache/Repositories/SubmissionRepositoryTest.php b/tests/Stache/Repositories/SubmissionRepositoryTest.php new file mode 100644 index 0000000000..92b060ac00 --- /dev/null +++ b/tests/Stache/Repositories/SubmissionRepositoryTest.php @@ -0,0 +1,58 @@ +sites(['en', 'fr']); + $this->app->instance(Stache::class, $stache); + $this->directory = __DIR__.'/../__fixtures__/content/submissions'; + $stache->registerStores([ + (new SubmissionsStore($stache, app('files')))->directory($this->directory), + ]); + + $this->repo = new SubmissionRepository($stache); + + $contact = Form::make('contact_form')->save(); + Form::make('sign_up')->save(); + } + + /** @test */ + public function it_gets_all_submissions() + { + $submissions = $this->repo->all(); + + $this->assertInstanceOf(Collection::class, $submissions); + $this->assertCount(4, $submissions); + $this->assertEveryItemIsInstanceOf(Submission::class, $submissions); + } + + /** @test */ + public function it_saves_a_submission_to_the_stache_and_to_a_file() + { + $submission = SubmissionAPI::make()->id('new'); + $submission->form(Form::find('contact_form')); + $submission->data(['foo' => 'bar']); + $this->assertNull($this->repo->find('new')); + + $this->repo->save($submission); + + $this->assertNotNull($item = $this->repo->find('new')); + $this->assertEquals(['foo' => 'bar'], $item->data()->all()); + $this->assertTrue(file_exists($this->directory.'/contact_form/new.yaml')); + @unlink($this->directory.'/contact_form/new.yaml'); + } +} diff --git a/tests/Stache/Stores/FormSubmissionStoreTest.php b/tests/Stache/Stores/FormSubmissionStoreTest.php new file mode 100644 index 0000000000..b55c801039 --- /dev/null +++ b/tests/Stache/Stores/FormSubmissionStoreTest.php @@ -0,0 +1,61 @@ +parent = (new SubmissionsStore)->directory( + $this->directory = Path::tidy(__DIR__.'/../__fixtures__/content/submissions') + ); + + Stache::registerStore($this->parent); + + Stache::store('form-submissions')->directory($this->directory); + } + + /** @test */ + public function it_makes_entry_instances_from_files() + { + $item = $this->parent->store('contact_form')->makeItemFromFile( + Path::tidy($this->directory).'/contact_form/1631083591.2832.yaml', + "name: John Smith\nmessage: Hello" + ); + + $this->assertInstanceOf(Submission::class, $item); + $this->assertEquals('1631083591.2832', $item->id()); + $this->assertEquals('John Smith', $item->get('name')); + $this->assertEquals(['name' => 'John Smith', 'message' => 'Hello'], $item->data()->all()); + $this->assertTrue(Carbon::createFromFormat('Y-m-d H:i:s', '2021-09-08 06:46:31')->eq($item->date()->startOfSecond())); + } + + /** @test */ + public function it_saves_to_disk() + { + $form = tap(Facades\Form::make('test_form'))->save(); + $submission = Facades\FormSubmission::make()->form($form); + $submission->set('title', 'Test'); + + $this->parent->store('test_form')->save($submission); + + $this->assertStringEqualsFile($path = $this->directory.'/test_form/'.$submission->id().'.yaml', $submission->fileContents()); + @unlink($path); + $this->assertFileDoesNotExist($path); + + $this->assertEquals($path, $this->parent->store('test_form')->paths()->get($submission->id())); + } +} diff --git a/tests/Stache/__fixtures__/content/submissions/contact_form/1631083591.2832.yaml b/tests/Stache/__fixtures__/content/submissions/contact_form/1631083591.2832.yaml new file mode 100644 index 0000000000..7383bc51e7 --- /dev/null +++ b/tests/Stache/__fixtures__/content/submissions/contact_form/1631083591.2832.yaml @@ -0,0 +1,5 @@ +name: Someone +email: someone@test.com +telephone: '079' +message: '123' +mailing_list: '1' diff --git a/tests/Stache/__fixtures__/content/submissions/contact_form/1638174892.0304.yaml b/tests/Stache/__fixtures__/content/submissions/contact_form/1638174892.0304.yaml new file mode 100644 index 0000000000..36eba24162 --- /dev/null +++ b/tests/Stache/__fixtures__/content/submissions/contact_form/1638174892.0304.yaml @@ -0,0 +1,4 @@ +name: 'Someone Else' +email: another@test.com +telephone: '079' +message: 'This is my message to you' diff --git a/tests/Stache/__fixtures__/content/submissions/contact_form/1638181789.4144.yaml b/tests/Stache/__fixtures__/content/submissions/contact_form/1638181789.4144.yaml new file mode 100644 index 0000000000..2025e05a7e --- /dev/null +++ b/tests/Stache/__fixtures__/content/submissions/contact_form/1638181789.4144.yaml @@ -0,0 +1,5 @@ +name: 'A third Person' +email: third@test.com +telephone: '028' +message: 'This is my message' +mailing_list: '1' diff --git a/tests/Stache/__fixtures__/content/submissions/sign_up/1631087772.4503.yaml b/tests/Stache/__fixtures__/content/submissions/sign_up/1631087772.4503.yaml new file mode 100644 index 0000000000..fea94286ba --- /dev/null +++ b/tests/Stache/__fixtures__/content/submissions/sign_up/1631087772.4503.yaml @@ -0,0 +1,2 @@ +name: 'Test' +email: test@test.com diff --git a/tests/StatamicTest.php b/tests/StatamicTest.php index 54a445b37a..e8f637966b 100644 --- a/tests/StatamicTest.php +++ b/tests/StatamicTest.php @@ -159,6 +159,7 @@ public function native_query_builder_aliases_are_bound() 'terms' => \Statamic\Stache\Query\TermQueryBuilder::class, 'assets' => \Statamic\Assets\QueryBuilder::class, 'users' => \Statamic\Stache\Query\UserQueryBuilder::class, + 'form-submissions' => \Statamic\Stache\Query\SubmissionQueryBuilder::class, ]; foreach ($aliases as $alias => $class) { diff --git a/tests/TestCase.php b/tests/TestCase.php index 2401ed41ae..35e47189e6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -105,6 +105,7 @@ protected function getEnvironmentSetUp($app) $app['config']->set('statamic.stache.stores.asset-containers.directory', __DIR__.'/__fixtures__/content/assets'); $app['config']->set('statamic.stache.stores.nav-trees.directory', __DIR__.'/__fixtures__/content/structures/navigation'); $app['config']->set('statamic.stache.stores.collection-trees.directory', __DIR__.'/__fixtures__/content/structures/collections'); + $app['config']->set('statamic.stache.stores.form-submissions.directory', __DIR__.'/__fixtures__/content/submissions'); $app['config']->set('statamic.api.enabled', true); $app['config']->set('statamic.graphql.enabled', true);