trial app

master
Denis Ranneft 2 years ago
commit aadc3c50d5
Signed by: immortal
GPG Key ID: 2F3893C622654BDF

@ -0,0 +1,7 @@
SERVER_NAME=trial
POSTGRES_USER=trail_user
POSTGRES_PASSWORD=trial_password
POSTGRES_DB=trial_db
PGPORT=5433
TLS_MODE=tls_selfsigned
TLS_AUTO_EMAIL=jhon.doe@example.com

4
.gitignore vendored

@ -0,0 +1,4 @@
/.env
/.idea
/docker/caddy/config/*
/docker/caddy/data/*

@ -0,0 +1,21 @@
# Trial project
Make sure you have `docker` with `docker compose` installed.
Copy `.env.example` file and fill in necessary env vars such as hostname or email
(that one needed for self-signed cert for Caddy).
You may want to change Postgres port, username and password - use corresponding env vars in `env`.
Default ports are rewritten in `docker-compose.yml` and in `./docker/caddy/etc/Caddyfile`.
**HTTP** port is 8000 and **HTTPS** port is 8443. In order to change listening ports of
your desire please make sure you changed them in both places.
To access the test you will need to write the server name in your `/etc/hosts` file. E.g.
```
~ #> echo "trial 127.0.0.1" >> /etc/hosts
```
Run `docker compose up -d`
To access the test then just simply visit `https://trial:8443` (you need to agree with self-signed cert).

@ -0,0 +1,5 @@
APP_ENV=dev
APP_SECRET=eb09be4fcc136ed9174c7136f13b98e4
DATABASE_URL="postgresql://trial_user:trial_password@postgres:5433/trial_db?serverVersion=16&charset=utf8"

17
app/.gitignore vendored

@ -0,0 +1,17 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

@ -0,0 +1,10 @@
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';

@ -0,0 +1,3 @@
@import "~bootstrap/scss/bootstrap";

@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

@ -0,0 +1,79 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.12",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.1",
"symfony/console": "6.4.*",
"symfony/dotenv": "6.4.*",
"symfony/flex": "^2",
"symfony/form": "6.4.*",
"symfony/framework-bundle": "6.4.*",
"symfony/runtime": "6.4.*",
"symfony/twig-bundle": "6.4.*",
"symfony/uid": "6.4.*",
"symfony/validator": "6.4.*",
"symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "6.4.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.4.*"
}
},
"require-dev": {
"symfony/maker-bundle": "^1.57"
}
}

5242
app/composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,11 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
];

@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

@ -0,0 +1,52 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver:
auto_mapping: true
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

@ -0,0 +1,21 @@
framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
annotations: false
http_method_override: false
handle_all_throwables: true
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
php_errors:
log: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

@ -0,0 +1,12 @@
framework:
router:
utf8: true
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

@ -0,0 +1,6 @@
twig:
file_name_pattern: '*.twig'
form_themes: ['bootstrap_5_layout.html.twig']
when@test:
twig:
strict_variables: true

@ -0,0 +1,4 @@
framework:
uid:
default_uuid_version: 7
time_based_uuid_version: 7

@ -0,0 +1,13 @@
framework:
validation:
email_validation_mode: html5
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

@ -0,0 +1,45 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
#when@prod:
# webpack_encore:
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# # Available in version 1.2
# cache: true
#when@test:
# webpack_encore:
# strict_mode: false

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

@ -0,0 +1,14 @@
parameters:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'

@ -0,0 +1,354 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240331093351 extends AbstractMigration
{
public const TESTS = [[
'id' => 1,
'name' => 'Trial1'
]];
public const QUESTIONS = [
[
'id' => 1,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "1"}, {"type": "op", "value": "+"}, {"type": "number", "value": "1"}]',
'answers' => [
[[
'type' => 'number',
'value' => '3'
]],
[[
'type' => 'number',
'value' => '2'
]],
[[
'type' => 'number',
'value' => '0'
]]
],
],
[
'id' => 2,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "2"}, {"type": "op", "value": "+"}, {"type": "number", "value": "2"}]',
'answers' => [
[[
'type' => 'number',
'value' => '4'
]],
[
[
'type' => 'number',
'value' => '3'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '1'
]
],
[[
'type' => 'number',
'value' => '10'
]]
],
],
[
'id' => 3,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "3"}, {"type": "op", "value": "+"}, {"type": "number", "value": "3"}]',
'answers' => [
[
[
'type' => 'number',
'value' => '1'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '5'
]
],
[[
'type' => 'number',
'value' => '1'
]],
[[
'type' => 'number',
'value' => '6'
]],
[
[
'type' => 'number',
'value' => '2'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '4'
]
]
],
],
[
'id' => 4,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "4"}, {"type": "op", "value": "+"}, {"type": "number", "value": "4"}]',
'answers' => [
[[
'type' => 'number',
'value' => '8'
]],
[[
'type' => 'number',
'value' => '4'
]],
[[
'type' => 'number',
'value' => '0'
]],
[
[
'type' => 'number',
'value' => '0'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '8'
]
],
],
],
[
'id' => 5,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "5"}, {"type": "op", "value": "+"}, {"type": "number", "value": "5"}]',
'answers' => [
[[
'type' => 'number',
'value' => '6'
]],
[[
'type' => 'number',
'value' => '18'
]],
[[
'type' => 'number',
'value' => '10'
]],
[[
'type' => 'number',
'value' => '9'
]],
[[
'type' => 'number',
'value' => '0'
]],
],
],
[
'id' => 6,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "6"}, {"type": "op", "value": "+"}, {"type": "number", "value": "6"}]',
'answers' => [
[[
'type' => 'number',
'value' => '3'
]],
[[
'type' => 'number',
'value' => '9'
]],
[[
'type' => 'number',
'value' => '0'
]],
[[
'type' => 'number',
'value' => '12'
]],
[
[
'type' => 'number',
'value' => '5'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '7'
]
],
],
],
[
'id' => 7,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "7"}, {"type": "op", "value": "+"}, {"type": "number", "value": "7"}]',
'answers' => [
[[
'type' => 'number',
'value' => '5'
]],
[[
'type' => 'number',
'value' => '14'
]],
],
],
[
'id' => 8,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "8"}, {"type": "op", "value": "+"}, {"type": "number", "value": "8"}]',
'answers' => [
[[
'type' => 'number',
'value' => '16'
]],
[[
'type' => 'number',
'value' => '12'
]],
[[
'type' => 'number',
'value' => '9'
]],
[[
'type' => 'number',
'value' => '5'
]],
],
],
[
'id' => 9,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "9"}, {"type": "op", "value": "+"}, {"type": "number", "value": "9"}]',
'answers' => [
[[
'type' => 'number',
'value' => '18'
]],
[[
'type' => 'number',
'value' => '9'
]],
[
[
'type' => 'number',
'value' => '17'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '1'
]
],
[
[
'type' => 'number',
'value' => '2'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '16'
]
],
],
],
[
'id' => 10,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "10"}, {"type": "op", "value": "+"}, {"type": "number", "value": "10"}]',
'answers' => [
[[
'type' => 'number',
'value' => '0'
]],
[[
'type' => 'number',
'value' => '2'
]],
[[
'type' => 'number',
'value' => '8'
]],
[[
'type' => 'number',
'value' => '20'
]],
],
],
];
public function getDescription(): string
{
return 'Initial';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE question_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE SEQUENCE test_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE question (id INT NOT NULL, test_id INT NOT NULL, prompt JSON NOT NULL, answers JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_B6F7494E1E5D0459 ON question (test_id)');
$this->addSql('CREATE TABLE test (id INT NOT NULL, name VARCHAR(60) NOT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE question ADD CONSTRAINT FK_B6F7494E1E5D0459 FOREIGN KEY (test_id) REFERENCES test (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE SEQUENCE user_answer_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE user_answer (id INT NOT NULL, question_id INT NOT NULL, date_created TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_correct BOOLEAN NOT NULL, answer JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_BF8F51181E27F6BF ON user_answer (question_id)');
$this->addSql('ALTER TABLE user_answer ADD CONSTRAINT FK_BF8F51181E27F6BF FOREIGN KEY (question_id) REFERENCES question (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE question_id_seq CASCADE');
$this->addSql('DROP SEQUENCE test_id_seq CASCADE');
$this->addSql('ALTER TABLE question DROP CONSTRAINT FK_B6F7494E1E5D0459');
$this->addSql('DROP TABLE question');
$this->addSql('DROP TABLE test');
$this->addSql('DROP SEQUENCE user_answer_id_seq CASCADE');
$this->addSql('ALTER TABLE user_answer DROP CONSTRAINT FK_BF8F51181E27F6BF');
$this->addSql('DROP TABLE user_answer');
}
}

@ -0,0 +1,335 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240331093352 extends AbstractMigration
{
public const TESTS = [[
'id' => 1,
'name' => 'Trial1'
]];
public const QUESTIONS = [
[
'id' => 1,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "1"}, {"type": "op", "value": "+"}, {"type": "number", "value": "1"}]',
'answers' => [
[[
'type' => 'number',
'value' => '3'
]],
[[
'type' => 'number',
'value' => '2'
]],
[[
'type' => 'number',
'value' => '0'
]]
],
],
[
'id' => 2,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "2"}, {"type": "op", "value": "+"}, {"type": "number", "value": "2"}]',
'answers' => [
[[
'type' => 'number',
'value' => '4'
]],
[
[
'type' => 'number',
'value' => '3'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '1'
]
],
[[
'type' => 'number',
'value' => '10'
]]
],
],
[
'id' => 3,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "3"}, {"type": "op", "value": "+"}, {"type": "number", "value": "3"}]',
'answers' => [
[
[
'type' => 'number',
'value' => '1'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '5'
]
],
[[
'type' => 'number',
'value' => '1'
]],
[[
'type' => 'number',
'value' => '6'
]],
[
[
'type' => 'number',
'value' => '2'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '4'
]
]
],
],
[
'id' => 4,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "4"}, {"type": "op", "value": "+"}, {"type": "number", "value": "4"}]',
'answers' => [
[[
'type' => 'number',
'value' => '8'
]],
[[
'type' => 'number',
'value' => '4'
]],
[[
'type' => 'number',
'value' => '0'
]],
[
[
'type' => 'number',
'value' => '0'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '8'
]
],
],
],
[
'id' => 5,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "5"}, {"type": "op", "value": "+"}, {"type": "number", "value": "5"}]',
'answers' => [
[[
'type' => 'number',
'value' => '6'
]],
[[
'type' => 'number',
'value' => '18'
]],
[[
'type' => 'number',
'value' => '10'
]],
[[
'type' => 'number',
'value' => '9'
]],
[[
'type' => 'number',
'value' => '0'
]],
],
],
[
'id' => 6,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "6"}, {"type": "op", "value": "+"}, {"type": "number", "value": "6"}]',
'answers' => [
[[
'type' => 'number',
'value' => '3'
]],
[[
'type' => 'number',
'value' => '9'
]],
[[
'type' => 'number',
'value' => '0'
]],
[[
'type' => 'number',
'value' => '12'
]],
[
[
'type' => 'number',
'value' => '5'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '7'
]
],
],
],
[
'id' => 7,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "7"}, {"type": "op", "value": "+"}, {"type": "number", "value": "7"}]',
'answers' => [
[[
'type' => 'number',
'value' => '5'
]],
[[
'type' => 'number',
'value' => '14'
]],
],
],
[
'id' => 8,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "8"}, {"type": "op", "value": "+"}, {"type": "number", "value": "8"}]',
'answers' => [
[[
'type' => 'number',
'value' => '16'
]],
[[
'type' => 'number',
'value' => '12'
]],
[[
'type' => 'number',
'value' => '9'
]],
[[
'type' => 'number',
'value' => '5'
]],
],
],
[
'id' => 9,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "9"}, {"type": "op", "value": "+"}, {"type": "number", "value": "9"}]',
'answers' => [
[[
'type' => 'number',
'value' => '18'
]],
[[
'type' => 'number',
'value' => '9'
]],
[
[
'type' => 'number',
'value' => '17'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '1'
]
],
[
[
'type' => 'number',
'value' => '2'
],
[
'type' => 'op',
'value' => '+'
],
[
'type' => 'number',
'value' => '16'
]
],
],
],
[
'id' => 10,
'test_id' => 1,
'prompt' => '[{"type": "number", "value": "10"}, {"type": "op", "value": "+"}, {"type": "number", "value": "10"}]',
'answers' => [
[[
'type' => 'number',
'value' => '0'
]],
[[
'type' => 'number',
'value' => '2'
]],
[[
'type' => 'number',
'value' => '8'
]],
[[
'type' => 'number',
'value' => '20'
]],
],
],
];
public function getDescription(): string
{
return 'Initial';
}
public function up(Schema $schema): void
{
// fill in questions
foreach(self::TESTS as $TEST) {
$this->connection->insert('test', $TEST);
}
foreach(self::QUESTIONS as $QUESTION) {
$QUESTION['answers'] = json_encode($QUESTION['answers']);
$this->connection->insert('question', $QUESTION);
}
}
}

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240331142711 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user_answer ADD uuid VARCHAR(60) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE user_answer DROP uuid');
}
}

@ -0,0 +1,25 @@
{
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@symfony/webpack-encore": "^4.0.0",
"bootstrap": "^5.3.3",
"core-js": "^3.23.0",
"regenerator-runtime": "^0.13.9",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-notifier": "^1.15.0"
},
"license": "UNLICENSED",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"dependencies": {
"node-sass": "^9.0.0",
"sass-loader": "^14.1.1"
}
}

@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

@ -0,0 +1,122 @@
<?php
namespace App\Controller;
use App\Entity\Test;
use App\Entity\UserAnswer;
use App\Form\Type\UserAnswerType;
use App\Service\QuestionService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Uid\Factory\UuidFactory;
class IndexController extends AbstractController
{
public function __construct(
private readonly QuestionService $questionService,
private readonly UuidFactory $uuidFactory,
private readonly RequestStack $requestStack,
private readonly EntityManagerInterface $em
)
{
}
#[Route('/', name: 'index')]
public function index(Request $request): Response
{
$test = $this->em->getRepository(Test::class)->find(1);
$session = $request->getSession();
if (!$session->has('step')) {
$step = $this->getStep($test, $this->getUuid());
$session->set('step', $step);
}
$step = $session->get('step');
$form = $this->getForm($test, $step);
if ($request->getMethod() === 'POST') {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->em->flush();
if ($step = $this->getStep($test, $this->getUuid())) {
$session->set('step', $step);
$form = $this->getForm($test, $step);
} else {
$session->set('test_' . $test->getId(), true);
$session->remove('step');
$oldUuid = $this->getUuid();
$this->getUuid(true);
return $this->redirectToRoute('test_results', ['test_id' => $test->getId(), 'uuid' => $oldUuid]);
}
}
}
return $this->render('index/index.html.twig', ['form' => $form, 'test' => $test, 'step' => $step - 1]);
}
#[Route('/test_results/{test_id}/{uuid}', name: 'test_results', requirements: ['test_id' => '\d+'])]
public function testResults(EntityManagerInterface $em, int $test_id, string $uuid): Response
{
$test = $em->getRepository(Test::class)->find($test_id);
if (!$test) {
$this->redirectToRoute('index');
}
$existingAnswers = $this->em->getRepository(UserAnswer::class)->findBy(['uuid' => $uuid]);
if (count($existingAnswers) != $test->getQuestions()->count()) {
$this->redirectToRoute('index');
}
return $this->render('index/results.html.twig',[
'test' => $test,
'existingAnswers' => $existingAnswers,
'service' => $this->questionService,
'uuid' => $uuid,
]);
}
private function getForm(Test $test, $step): FormInterface
{
$question = $test->getQuestions()->findFirst(fn($i, $q) => $q->getId() === $step);
$questionModel = $this->questionService->getQuestionModel($question);
$userAnswer = (new UserAnswer())->setQuestion($question)->setUuid($this->getUuid());
$this->em->persist($userAnswer);
return $this->createForm(UserAnswerType::class, $userAnswer, [
'questionModel' => $questionModel,
'question' => $question,
'test' => $test,
]);
}
private function getUuid($forceNew = false)
{
$session = $this->requestStack->getCurrentRequest()->getSession();
if (!$session->has('uuid') || $forceNew) {
$uuid = $this->uuidFactory->create();
$session->set('uuid', $uuid);
} else {
$uuid = $session->get('uuid');
}
return $uuid;
}
private function getStep(Test $test, string $uuid): int
{
$existingAnswers = array_map(fn($a) => $a->getQuestion()->getId(), $this->em->getRepository(UserAnswer::class)->findBy(['uuid' => $uuid]));
$allSteps = $test->getQuestions()->map(fn($q) => $q->getId())->toArray();
$availableSteps = array_diff($allSteps, $existingAnswers);
shuffle($availableSteps);
return current($availableSteps);
}
}

@ -0,0 +1,66 @@
<?php
namespace App\Entity;
use App\Repository\QuestionRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: QuestionRepository::class)]
class Question
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private array $prompt = [];
#[ORM\Column]
private array $answers = [];
#[ORM\ManyToOne(inversedBy: 'questions')]
#[ORM\JoinColumn(nullable: false)]
private Test $test;
public function getId(): ?int
{
return $this->id;
}
public function getPrompt(): array
{
return $this->prompt;
}
public function setPrompt(array $prompt): static
{
$this->prompt = $prompt;
return $this;
}
public function getAnswers(): array
{
return $this->answers;
}
public function setAnswers(array $answers): static
{
$this->answers = $answers;
return $this;
}
public function getTest(): Test
{
return $this->test;
}
public function setTest(Test $test): static
{
$this->test = $test;
return $this;
}
}

@ -0,0 +1,75 @@
<?php
namespace App\Entity;
use App\Repository\TestRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TestRepository::class)]
class Test
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 60)]
private ?string $name = null;
#[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'test', orphanRemoval: true)]
private Collection $questions;
public function __construct()
{
$this->questions = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @return Collection<int, Question>
*/
public function getQuestions(): Collection
{
return $this->questions;
}
public function addQuestion(Question $question): static
{
if (!$this->questions->contains($question)) {
$this->questions->add($question);
$question->setTest($this);
}
return $this;
}
public function removeQuestion(Question $question): static
{
if ($this->questions->removeElement($question)) {
// set the owning side to null (unless already changed)
if ($question->getTest() === $this) {
$question->setTest(null);
}
}
return $this;
}
}

@ -0,0 +1,105 @@
<?php
namespace App\Entity;
use App\Model\AnswerModel;
use App\Repository\UserAnswerRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserAnswerRepository::class)]
class UserAnswer
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?Question $question = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTimeInterface $date_created = null;
#[ORM\Column(type: Types::BOOLEAN)]
private bool $is_correct = false;
#[ORM\Column]
private array $answer = [];
#[ORM\Column(length: 60)]
private ?string $uuid;
public function getId(): ?int
{
return $this->id;
}
public function getQuestion(): ?Question
{
return $this->question;
}
public function setQuestion(?Question $question): static
{
$this->question = $question;
return $this;
}
public function getDateCreated(): ?\DateTimeInterface
{
return $this->date_created;
}
public function setDateCreated(\DateTimeInterface $date_created): static
{
$this->date_created = $date_created;
return $this;
}
public function isCorrect(): bool
{
return $this->is_correct;
}
public function setIsCorrect(bool $isCorrect): static
{
$this->is_correct = $isCorrect;
return $this;
}
/**
* @return array<AnswerModel>
*/
public function getAnswer(): array
{
return $this->answer;
}
public function setAnswer(array $answer): static
{
$this->answer = $answer;
return $this;
}
/**
* @return string
*/
public function getUuid(): string
{
return $this->uuid;
}
public function setUuid(string $uuid): static
{
$this->uuid = $uuid;
return $this;
}
}

@ -0,0 +1,12 @@
<?php
namespace App\Enum;
enum OperatorsEnum: string
{
case ADD = '+';
case SUB = '-';
case MUL = '*';
case DIV = '/';
case EQL = '=';
}

@ -0,0 +1,9 @@
<?php
namespace App\Enum;
enum QuestionPartTypeEnum: string
{
case NUMBER = 'number';
case OPERATOR = 'op';
}

@ -0,0 +1,80 @@
<?php
namespace App\Form\Type;
use App\Entity\Question;
use App\Entity\Test;
use App\Entity\UserAnswer;
use App\Model\AnswerModel;
use App\Model\QuestionModel;
use App\Service\QuestionService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Event\PostSubmitEvent;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class UserAnswerType extends AbstractType
{
public function __construct(private readonly QuestionService $questionService)
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var QuestionModel $questionModel */
$questionModel = $options['questionModel'];
$answers = $questionModel->getAnswers();
shuffle($answers);
$builder
->add('answer', ChoiceType::class, [
'choices' => $answers,
'choice_label' => function (?AnswerModel $answerModel): string {
return $answerModel->getName();
},
'label' => sprintf('Pick up answer(s) for %s', $questionModel->getPrompt()),
'required' => true,
'multiple' => true,
'expanded' => true,
'constraints' => [
new NotBlank()
]
])
->add('save', SubmitType::class, ['label' => 'Answer'])
->addEventListener(FormEvents::POST_SUBMIT, [$this, 'checkAnswer'])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setRequired(['test', 'question', 'questionModel'])
->setDefaults([
'data_class' => UserAnswer::class,
])
->setAllowedTypes('test', Test::class)
->setAllowedTypes('questionModel', QuestionModel::class)
->setAllowedTypes('question', Question::class)
;
}
public function checkAnswer(PostSubmitEvent $event): void
{
/** @var UserAnswer $userAnswer */
$userAnswer = $event->getData();
/** @var Question $question */
$question = $event->getForm()->getConfig()->getOption('question');
$answer = $userAnswer
->setDateCreated(new \DateTime())
->setQuestion($question)
->getAnswer();
$userAnswer->setIsCorrect($this->questionService->isValidAnswer($question, $answer));
$userAnswer->setAnswer(array_map(fn($a) => $a->getConfig(), $answer));
}
}

@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

@ -0,0 +1,30 @@
<?php
namespace App\Model;
final class AnswerModel
{
public function __construct(
private readonly string $name,
private readonly string $value,
private readonly array $config
)
{
}
public function getName(): string
{
return $this->name;
}
public function getValue(): string
{
return $this->value;
}
public function getConfig(): array
{
return $this->config;
}
}

@ -0,0 +1,27 @@
<?php
namespace App\Model;
final class QuestionModel
{
public function __construct(
private readonly string $prompt,
private readonly array $answers
)
{
}
public function getPrompt(): string
{
return $this->prompt;
}
/**
* @return
*/
public function getAnswers(): array
{
return $this->answers;
}
}

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\Question;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Question>
*
* @method Question|null find($id, $lockMode = null, $lockVersion = null)
* @method Question|null findOneBy(array $criteria, array $orderBy = null)
* @method Question[] findAll()
* @method Question[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class QuestionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Question::class);
}
}

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\Test;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Test>
*
* @method Test|null find($id, $lockMode = null, $lockVersion = null)
* @method Test|null findOneBy(array $criteria, array $orderBy = null)
* @method Test[] findAll()
* @method Test[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TestRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Test::class);
}
}

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\UserAnswer;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<UserAnswer>
*
* @method UserAnswer|null find($id, $lockMode = null, $lockVersion = null)
* @method UserAnswer|null findOneBy(array $criteria, array $orderBy = null)
* @method UserAnswer[] findAll()
* @method UserAnswer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserAnswerRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, UserAnswer::class);
}
}

@ -0,0 +1,132 @@
<?php
namespace App\Service;
use App\Entity\Question;
use App\Enum\OperatorsEnum;
use App\Enum\QuestionPartTypeEnum;
use App\Model\AnswerModel;
use App\Model\QuestionModel;
class QuestionService
{
public function getQuestionModel(Question $question): QuestionModel
{
$questionAnswers = $question->getAnswers();
$answers = array_map(
fn($answer, $i) => new AnswerModel($this->assembleString($answer), $i, $answer),
$questionAnswers,
array_keys($questionAnswers)
);
return new QuestionModel(
$this->assembleString($question->getPrompt(), true),
$answers
);
}
/**
* @param Question $question
* @param array<AnswerModel> $answer
* @return boolean
*/
public function isValidAnswer(Question $question, array $answer): bool
{
$prompt = $question->getPrompt();
$result = $this->calculate($prompt);
$answerResults = array_map(fn($a) => $this->calculate($a->getConfig()), $answer);
foreach($answerResults as $answerResult) {
if($answerResult !== $result) {
return false;
}
}
return true;
}
private function calculate(array $entries): float
{
$result = 0;
$count = count($entries);
$prev = null;
for($i=0; $i < $count; $i++) {
$entry = $entries[$i];
$curr = QuestionPartTypeEnum::tryFrom($entry['type']);
if(!$curr) {
throw new \RuntimeException(sprintf('Invalid entry type: %s', $entry['type']));
}
if ($curr === QuestionPartTypeEnum::OPERATOR && $prev !== QuestionPartTypeEnum::NUMBER) {
throw new \RuntimeException('Unexpected part, got OPERATOR expected NUMBER');
}
if ($curr === QuestionPartTypeEnum::NUMBER && $prev !== null && $prev !== QuestionPartTypeEnum::OPERATOR) {
throw new \RuntimeException('Unexpected part, got NUMBER expected OPERATOR');
}
if ($curr === QuestionPartTypeEnum::NUMBER && $prev === null) {
$prev = $curr;
$result += (float) $entry['value'];
}
if ($curr === QuestionPartTypeEnum::OPERATOR && $prev === QuestionPartTypeEnum::NUMBER) {
$nextEntry = $entries[$i + 1];
$next = QuestionPartTypeEnum::tryFrom($nextEntry['type']);
if(!$next) {
throw new \RuntimeException(sprintf('Invalid entry type: %s', $entry['type']));
};
$curr = OperatorsEnum::tryFrom($entry['value']);
switch ($curr) {
case OperatorsEnum::ADD:
$result += (float) $nextEntry['value'];
break;
case OperatorsEnum::SUB:
$result -= (float) $nextEntry['value'];
break;
case OperatorsEnum::MUL:
$result *= (float) $nextEntry['value'];
break;
case OperatorsEnum::DIV:
if((double) $nextEntry['value'] === 0.0) {
throw new \RuntimeException('Division by 0');
}
$result /= (float) $nextEntry['value'];
break;
case OperatorsEnum::EQL:
throw new \RuntimeException('Unexpected operator');
}
$prev = $curr;
$i++;
}
}
return $result;
}
public function assembleString(array $entries, $isPrompt = false): string
{
$ret = [];
foreach($entries as $entry) {
$type = QuestionPartTypeEnum::tryFrom($entry['type']);
$ret[] = match($type) {
QuestionPartTypeEnum::OPERATOR => OperatorsEnum::tryFrom($entry['value'])->value,
QuestionPartTypeEnum::NUMBER => $entry['value'],
default => null
};
}
if ($isPrompt) {
$ret[] = OperatorsEnum::EQL->value;
}
return implode(' ', array_filter($ret, fn($r) => $r !== null));
}
}

@ -0,0 +1,150 @@
{
"doctrine/doctrine-bundle": {
"version": "2.12",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.12",
"ref": "67fe5ec6347935627365af33a056fd8fcb1871ab"
},
"files": [
"config/packages/doctrine.yaml",
"src/Entity/.gitignore",
"src/Repository/.gitignore"
]
},
"doctrine/doctrine-migrations-bundle": {
"version": "3.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.1",
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
},
"files": [
"config/packages/doctrine_migrations.yaml",
"migrations/.gitignore"
]
},
"symfony/console": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
},
"files": [
"bin/console"
]
},
"symfony/flex": {
"version": "2.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
},
"files": [
".env"
]
},
"symfony/framework-bundle": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "a91c965766ad3ff2ae15981801643330eb42b6a5"
},
"files": [
"config/packages/cache.yaml",
"config/packages/framework.yaml",
"config/preload.php",
"config/routes/framework.yaml",
"config/services.yaml",
"public/index.php",
"src/Controller/.gitignore",
"src/Kernel.php"
]
},
"symfony/maker-bundle": {
"version": "1.57",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/routing": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.2",
"ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6"
},
"files": [
"config/packages/routing.yaml",
"config/routes.yaml"
]
},
"symfony/twig-bundle": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
},
"files": [
"config/packages/twig.yaml",
"templates/base.html.twig"
]
},
"symfony/uid": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.2",
"ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558"
},
"files": [
"config/packages/uid.yaml"
]
},
"symfony/validator": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "c32cfd98f714894c4f128bb99aa2530c1227603c"
},
"files": [
"config/packages/validator.yaml"
]
},
"symfony/webpack-encore-bundle": {
"version": "2.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.0",
"ref": "082d754b3bd54b3fc669f278f1eea955cfd23cf5"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/webpack_encore.yaml",
"package.json",
"webpack.config.js"
]
},
"twig/extra-bundle": {
"version": "v3.8.0"
}
}

@ -0,0 +1,13 @@
{% extends 'layout.html.twig' %}
{% block body %}
<div class="d-flex flex-column-reverse flex-lg-row container w-100">
<div class="container pt-3">
<h4>Start you trial now: <strong>{{ test.getName() }}</strong></h4>
<div class="row">
{{ form(form) }}
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,23 @@
{% extends 'layout.html.twig' %}
{% block body %}
<div class="d-flex flex-column-reverse flex-lg-row container w-100">
<div class="container pt-3">
<h4>You finished trial: <strong>{{ test.getName() }}</strong></h4>
<h5>Your results <a href="{{ url('test_results', {'test_id': test.getId(), 'uuid': uuid}) }}">link</a></h5>
<div class="row">
<ul class="list-group">
{% for existingAnswer in existingAnswers %}
<li class="list-group-item {% if not existingAnswer.isCorrect() %}list-group-item-danger{% else %}list-group-item-success{% endif %}">
{{ service.getQuestionModel(existingAnswer.question).getPrompt() }}
{% for answer in existingAnswer.getAnswer() %}
{{ service.assembleString(answer) }}{% if not loop.last %},{% endif %}
{% endfor %}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en" class="h-100">
<head>
<meta charset="UTF-8">
<title>{% block title %}Trial app{% endblock %}</title>
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
<body class="d-flex flex-column h-100 m-0 p-0 w-100 bg-light">
<header class="p-3 text-bg-dark w-100">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-between">
<a href="{{ path('index') }}"
class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<span class="fs-4">Home</span>
</a>
</div>
</div>
</header>
{% block body %}
{% endblock %}
<footer class="mt-auto bg-dark text-bg-dark py-2 footer">
<p class="text-center">© {{ date().format('Y') }} Trial app</p>
</footer>
</body>
</html>

@ -0,0 +1,73 @@
const Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or subdirectory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.addEntry('app', './assets/app.js')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// configure Babel
// .configureBabel((config) => {
// config.plugins.push('@babel/a-babel-plugin');
// })
// enables and configure @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = '3.23';
})
// enables Sass/SCSS support
.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment if you use React
//.enableReactPreset()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
;
module.exports = Encore.getWebpackConfig();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,74 @@
version: '3.9'
name: trial
services:
db:
image: postgres
hostname: postgres
restart: unless-stopped
shm_size: 128mb
volumes:
- ./docker/pgsql/data:/var/lib/postgresql/data/pgdata
networks:
- dev-network
env_file:
- ./.env
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
ports:
- "${PGPORT}:${PGPORT}"
caddy:
image: caddy:latest
restart: unless-stopped
links:
- php
depends_on:
- php
volumes:
- ./docker/caddy/data:/data
- ./docker/caddy/config:/config
- ./docker/caddy/etc/Caddyfile:/etc/caddy/Caddyfile
- ./docker/caddy/etc/tls_auto:/etc/tls_auto
- ./docker/caddy/etc/tls_selfsigned:/etc/tls_selfsigned
- app:/var/www/trial
env_file:
- ./.env
ports:
- "8000:8000"
- "8443:8443"
- "8443:8443/udp"
logging:
driver: "json-file"
options:
max-size: "1M"
max-file: "10"
networks:
- dev-network
php:
build: ./docker/fpm
hostname: php
links:
- db
depends_on:
- db
volumes:
- app:/var/www/trial
- ./docker/fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf
networks:
- dev-network
env_file:
- ./.env
networks:
dev-network:
driver: bridge
volumes:
app:
driver: local
external: false
driver_opts:
type: none
o: bind
device: ./app

@ -0,0 +1,32 @@
{
default_sni {$SERVER_NAME}
http_port 8000
https_port 8443
}
{$SERVER_NAME} {
import /etc/{$TLS_MODE}
root * /var/www/trial/public
encode zstd gzip
file_server
php_fastcgi php:9000 {
resolve_root_symlink
}
@phpFile {
path *.php*
}
error @phpFile "Not found" 404
log {
output file /var/log/caddy.log
}
header / {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
}
}

@ -0,0 +1 @@
tls {$TLS_AUTO_EMAIL}

@ -0,0 +1,4 @@
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
libpq-dev \
&& docker-php-ext-install -j$(nproc) pdo pdo_pgsql \

@ -0,0 +1,10 @@
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
;listen = /run/php/trial-fpm.sock
pm = dynamic
pm.max_children = 3
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 1
Loading…
Cancel
Save