head and foot

This commit is contained in:
tykayn 2020-10-26 11:39:04 +01:00
parent bfdceb4a1f
commit d286d6291e
36 changed files with 8496 additions and 427 deletions

4
.env
View File

@ -19,6 +19,8 @@ ADMIN_TOKEN=CHANGE_THIS_STRING_HERE
APP_SECRET=CHANGE_THIS_STRING_THERE
# Base website url, should contain https:// and having no trailing slash. example: BASE_URL=https://framadate.org
BASE_URL=https://YOUR_WEBSITE
WEBSITE_NAME=Framadate_2
WEBSITE_LOGO=logo.svg
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
#TRUSTED_HOSTS='^localhost|example\.com$'
###< symfony/framework-bundle ###
@ -47,3 +49,5 @@ SUPPORT_EMAIL=YOUR_EMAIL
###> symfony/mailer ###
MAILER_DSN=smtp://localhost
###< symfony/mailer ###
DATABASE_URL=mysql://db_user:db_pass@127.0.0.1:5432/db_name

View File

@ -1,33 +0,0 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=597b0529ac702d27dcb9089f7e69c362
# Base website url, should contain https:// and having no trailing slash. example: BASE_URL=https://framadate.org
BASE_URL=https://framadate-api.cipherbliss.com
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
#TRUSTED_HOSTS='^localhost|example\.com$'
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11"
# IMPORTANT: You MUST also configure your db driver and server_version in config/packages/doctrine.yaml
# CHANGE THIS TO SUIT YOUR PRODUCTION ENV
DATABASE_URL=mysql://root:plopplop01@127.0.0.1:5432/symfony
###< doctrine/doctrine-bundle ###

7
.gitignore vendored
View File

@ -37,3 +37,10 @@ public/styles.css
###< symfony/phpunit-bridge ###
node_modules
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

14
assets/app.js Normal file
View File

@ -0,0 +1,14 @@
/*
* 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';
// Need jQuery? Install it with "yarn add jquery", then uncomment to import it.
// import $ from 'jquery';
console.log('Hello Webpack Encore! Edit me in assets/app.js');

3
assets/styles/app.scss Normal file
View File

@ -0,0 +1,3 @@
@import 'pages/libs';
@import 'pages/global';
@import 'pages/home';

View File

@ -0,0 +1,3 @@
body {
background-color: lightgray;
}

View File

View File

@ -0,0 +1,2 @@
@import '~font-awesome/css/font-awesome.min.css';
@import '~tailwindcss/dist/tailwind.min.css';

View File

@ -25,9 +25,11 @@
"symfony/mailer": "4.3.*",
"symfony/maker-bundle": "^1.14",
"symfony/orm-pack": "^1.0",
"symfony/security-csrf": "4.3.*",
"symfony/swiftmailer-bundle": "^3.4",
"symfony/twig-bundle": "4.3.*",
"symfony/validator": "4.3.*",
"symfony/webpack-encore-bundle": "^1.7",
"symfony/yaml": "4.3.*"
},
"require-dev": {

170
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7c5bdef20c7e94f4117ae2b1ef35f486",
"content-hash": "437d493ff28b8ee8bba95a0ca2000d1c",
"packages": [
{
"name": "doctrine/annotations",
@ -3145,6 +3145,62 @@
],
"time": "2019-11-12T09:31:26+00:00"
},
{
"name": "symfony/asset",
"version": "v4.3.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset.git",
"reference": "5bdbd8878b69e3be16d036890ea3081172ea28c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset/zipball/5bdbd8878b69e3be16d036890ea3081172ea28c5",
"reference": "5bdbd8878b69e3be16d036890ea3081172ea28c5",
"shasum": ""
},
"require": {
"php": "^7.1.3"
},
"require-dev": {
"symfony/http-foundation": "~3.4|~4.0",
"symfony/http-kernel": "~3.4|~4.0"
},
"suggest": {
"symfony/http-foundation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.3-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Asset\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Asset Component",
"homepage": "https://symfony.com",
"time": "2020-01-04T12:24:57+00:00"
},
{
"name": "symfony/cache",
"version": "v4.3.10",
@ -5366,6 +5422,65 @@
"homepage": "https://symfony.com",
"time": "2020-01-21T11:08:18+00:00"
},
{
"name": "symfony/security-csrf",
"version": "v4.3.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/security-csrf.git",
"reference": "9e435026ab45f073880d1fbe0e1b17e7df6bf642"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/security-csrf/zipball/9e435026ab45f073880d1fbe0e1b17e7df6bf642",
"reference": "9e435026ab45f073880d1fbe0e1b17e7df6bf642",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/security-core": "~3.4|~4.0"
},
"conflict": {
"symfony/http-foundation": "<3.4"
},
"require-dev": {
"symfony/http-foundation": "~3.4|~4.0"
},
"suggest": {
"symfony/http-foundation": "For using the class SessionTokenStorage."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.3-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Security\\Csrf\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Security Component - CSRF Library",
"homepage": "https://symfony.com",
"time": "2020-01-04T12:24:57+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v1.1.8",
@ -5927,6 +6042,59 @@
],
"time": "2020-01-01T11:51:43+00:00"
},
{
"name": "symfony/webpack-encore-bundle",
"version": "v1.7.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/webpack-encore-bundle.git",
"reference": "5c0f659eceae87271cce54bbdfb05ed8ec9007bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/5c0f659eceae87271cce54bbdfb05ed8ec9007bd",
"reference": "5c0f659eceae87271cce54bbdfb05ed8ec9007bd",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/asset": "^3.4 || ^4.0 || ^5.0",
"symfony/config": "^3.4 || ^4.0 || ^5.0",
"symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0",
"symfony/http-kernel": "^3.4 || ^4.0 || ^5.0",
"symfony/service-contracts": "^1.0 || ^2.0"
},
"require-dev": {
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0",
"symfony/phpunit-bridge": "^4.3.5 || ^5.0",
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0",
"symfony/web-link": "^3.4 || ^4.0 || ^5.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/webpack-encore",
"url": "https://github.com/symfony/webpack-encore"
}
},
"autoload": {
"psr-4": {
"Symfony\\WebpackEncoreBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration with your Symfony app & Webpack Encore!",
"time": "2020-01-31T15:31:59+00:00"
},
{
"name": "symfony/yaml",
"version": "v4.3.10",

View File

@ -15,4 +15,5 @@ return [
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
Liip\TestFixturesBundle\LiipTestFixturesBundle::class => ['test' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
];

View File

@ -0,0 +1,3 @@
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

View File

@ -0,0 +1,4 @@
#webpack_encore:
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Available in version 1.2
#cache: true

View File

@ -0,0 +1,2 @@
#webpack_encore:
# strict_mode: false

View File

@ -4,3 +4,5 @@ twig:
strict_variables: '%kernel.debug%'
globals:
BASE_URL: '%env(BASE_URL)%'
WEBSITE_NAME: '%env(WEBSITE_NAME)%'
WEBSITE_LOGO: '%env(WEBSITE_LOGO)%'

View File

@ -0,0 +1,25 @@
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
# 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 http2 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:
# pass "frontend" as the 3rg arg to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
# frontend: '%kernel.project_dir%/public/frontend/build'
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Put in config/packages/prod/webpack_encore.yaml
# cache: true

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "date-poll-api",
"version": "1.0.0",
"description": "API date to make surveys, kind of the new Framadate",
"main": "index.js",
"directories": {
"doc": "doc",
"test": "tests"
},
"dependencies": {
"@symfony/webpack-encore": "^0.31.0",
"font-awesome": "^4.7.0",
"tailwindcss": "^1.9.6"
},
"devDependencies": {
"node-sass": "^4.14.1",
"sass-loader": "^9.0.0",
"webpack-notifier": "1.6.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production"
},
"repository": {
"type": "git",
"url": "https://framagit.org/tykayn/date-poll-api.git"
},
"keywords": [
"survey",
"poll",
"sondage",
"api",
"symfony"
],
"author": "tykayn",
"license": "AGPL-3.0-or-later"
}

View File

@ -3,6 +3,8 @@
namespace App\Controller;
use App\Entity\Owner;
use App\Entity\Poll;
use App\Repository\PollRepository;
use App\Service\MailService;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\Route;
@ -13,10 +15,22 @@ use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
/**
* Class DefaultController
* @package App\Controller
* @Route("/api/v1",name="api_")
* @Route("/page",name="api_")
*/
class DefaultController extends FramadateController {
/**
* @Get(path ="/",
* name = "page_home")
*/
public function indexAction() {
$polls = $this->getDoctrine()->getRepository( Poll::class )->findAll();
return $this->render( 'pages/home.html.twig',[
'polls' => $polls,
]);
}
}

View File

@ -2,409 +2,93 @@
namespace App\Controller;
use App\Entity\Choice;
use App\Entity\Owner;
use App\Entity\Poll;
use FOS\RestBundle\Controller\Annotations\Delete;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\Post;
use FOS\RestBundle\Controller\Annotations\Put;
use FOS\RestBundle\Controller\Annotations\Route;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\SerializerBuilder;
use JMS\Serializer\SerializerInterface;
use Swift_Mailer;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Form\PollType;
use App\Repository\PollRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class DefaultController
* @package App\Controller
* @Route("/api/v1/poll",name="api_")
* @Route("/poll")
*/
class PollController extends FramadateController {
/**
* @Get(
* path = "/",
* name = "get_all_polls"
* )
*/
public function getAllPollsAction() {
$repository = $this->getDoctrine()->getRepository( Poll::class );
$data = $repository->findall();
class PollController extends AbstractController
{
/**
* @Route("/", name="poll_index", methods={"GET"})
*/
public function index(PollRepository $pollRepository): Response
{
return $this->render('poll/index.html.twig', [
'polls' => $pollRepository->findAll(),
]);
}
/**
* @Route("/new", name="poll_new", methods={"GET","POST"})
*/
public function new(Request $request): Response
{
$poll = new Poll();
$form = $this->createForm(PollType::class, $poll);
$form->handleRequest($request);
return $this->json( [
'message' => 'here are your polls',
'poll' => $data,
],
200 );
}
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($poll);
$entityManager->flush();
/**
* @Get(
* path = "/{id}",
* name = "get_poll",
* requirements = {"poll_id"="\d+"}
* )
* @param SerializerInterface $serializer
* @param Poll $poll
* @param Request $request
*
* @return JsonResponse|Response
*/
public function getPollConfig(
SerializerInterface $serializer,
Poll $poll,
Request $request
) {
$pass = $poll->getPassword();
$data = $request->getContent();
$data = json_decode( $data, true );
return $this->redirectToRoute('poll_index');
}
$comments = $poll->getComments();
return $this->render('poll/new.html.twig', [
'poll' => $poll,
'form' => $form->createView(),
]);
}
$returnedPoll = [
'message' => 'your poll config',
'poll' => $poll,
'stacks_count' => count( $poll->getStacksOfVotes() ),
'stacks' => $poll->getStacksOfVotes(),
'choices_count' => $poll->computeAnswers(),
'choices' => $poll->getChoices(),
'comments' => $comments,
'comments_count' => count( $comments ),
];
/**
* @Route("/{id}", name="poll_show", methods={"GET"})
*/
public function show(Poll $poll): Response
{
return $this->render('poll/show.html.twig', [
'poll' => $poll,
]);
}
/**
* password protected content
*/
if ( $pass && $pass !== md5($data[ 'password_input' ])) {
return $this->json( [
'message' => 'your password ' . $data[ 'password_input' ] . ' is wrong, and you should feel bad',
'data' => null,
],
403 );
} else {
$jsonResponse = $serializer->serialize($returnedPoll, 'json');
/**
* @Route("/{id}/edit", name="poll_edit", methods={"GET","POST"})
*/
public function edit(Request $request, Poll $poll): Response
{
$form = $this->createForm(PollType::class, $poll);
$form->handleRequest($request);
$response = new Response($jsonResponse);
$response->headers->set('Content-Type', 'application/json');
$response->setStatusCode(200);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $response;
}
return $this->redirectToRoute('poll_index');
}
}
return $this->render('poll/edit.html.twig', [
'poll' => $poll,
'form' => $form->createView(),
]);
}
/**
* @Put(
* path = "/{id}/{token}",
* name = "update_poll",
* requirements = {"content"="\w+", "poll_id"="\d+"}
* )
*/
public function updatePollConfig(
Poll $poll,
string $token,
Request $request
) {
if ( $poll->getAdminKey() !== $token ) {
return $this->json( [
'message' => 'you are NOT allowed to update the poll ' . $poll->getTitle(),
],
403 );
}
// TODO check validity of request
// update only if we have the admin key
$em = $this->getDoctrine()->getManager();
$em->persist( $poll );
$em->flush();
return $this->json( [
'message' => 'you updated the poll ' . $poll->getTitle(),
],
200 );
}
/**
* @Post(
* path = "/",
* name = "new_poll",
* requirements = {"creator"="\w+"}
* )
* @param Request $request
*
* @return JsonResponse
*/
public function newPollAction( Request $request ) {
$data = $request->getContent();
$serializer = SerializerBuilder::create()->build();
try {
$newpoll = $serializer->deserialize( $data, 'App\Entity\Poll', 'json' );
} catch(RuntimeException $e) {
return $this->json(["message" => "Incorrect JSON in request"], 400);
}
$newpoll
->setAdminKey( $newpoll->generateAdminKey() )
->setCreationDate( new DateTime() )
->setModificationPolicy( 'nobody' );
$timeStamp = time() + ( 3600 * 24 * 90 ); // 90 days by default
$newpoll->setExpiracyDate( ( new DateTime() )->setTimestamp( $timeStamp ),
new DateTimeZone( 'Europe/Paris' ) );
$data = json_decode( $data, true );
$em = $this->getDoctrine()->getRepository( Owner::class );
$foundOwner = $em->findOneBy( [ 'email' => $data[ 'owner' ][ 'email' ] ] );
$userWasFound = false;
if ( ! $foundOwner ) {
//create a new owner
$owner = new Owner();
$owner->setPseudo( $data[ 'owner' ][ 'pseudo' ] );
$owner->setEmail( $data[ 'owner' ][ 'email' ] );
$foundOwner = $owner;
} else {
$userWasFound = true;
}
// link the owner and the poll
$newpoll->setOwner( $foundOwner );
$foundOwner->addPoll( $newpoll );
$em = $this->getDoctrine()->getManager();
$em->persist( $newpoll );
$em->persist( $foundOwner );
// emails
$newpoll->setMailOnComment( true );
$newpoll->setMailOnVote( true );
$newpoll->setHideResults( false );
// possible answers
$newpoll->setAllowedAnswers( [ 'yes' ] );
if ( $data[ 'voteChoices' ] ) {
switch ( $data[ 'voteChoices' ] ) {
case "only_yes":
default:
break;
}
}
// setup the password, converting the raw with md5 hash
if ( $data[ 'password' ] ) {
$newpoll->setPassword( $data[ 'password' ] );
}
// manage choices
// text kind of answers, dates are below
if ( $data[ 'pollType' ] == 'classic' ) {
$choices = $data[ 'dateList' ];
foreach ( $choices as $c ) {
$newChoice = new Choice();
$newChoice
->setPoll( $newpoll )
// ->setUrl( $c[ 'url' ] )
->setName( $c[ 'literal' ] );
$em->persist( $newChoice );
// TODO add also choices for each time range in a day
}
} elseif ( $data[ 'pollType' ] == 'dates' ) {
if ( $data[ 'allowSeveralHours' ] == true ) {
// different hours spans
$choices = $data[ 'dateList' ];
} else {
//TODO (Sébastien) I assume this shouldn't be empty ?
// all days have the same hour spans
}
}
$em->persist( $newpoll );
$em->flush();
$precision = '';
if ( $userWasFound ) {
$precision = 'from an existing user : ' . $foundOwner->getEmail();
}
$this->sendCreationMailAction( $foundOwner, $newpoll );
return $this->json( [
'message' => 'you created a poll ' . $precision,
'poll' => $newpoll,
'password_protected' => is_string( $newpoll->getPassword() ),
'admin_key' => $newpoll->getAdminKey(),
'owner_modifier_token' => $foundOwner->getModifierToken(),
],
201 );
}
/**
* @Get(
* path = "/mail/test-mail-poll/{emailChoice}",
* name = "test-mail-poll",
* )
*
* send the creation mail to owner
*
* @param Owner $admin_user
* @param Poll $poll
* @param Swift_Mailer $mailer
*
* @return int
* not that the email tktest_commentateur@tktest.com does not really exist
*/
// public function sendCreationMailAction( Owner $admin_user, Poll $poll, \Swift_Mailer $mailer) {
public function testSendCreationMailAction(
$emailChoice = 'tktest_commentateur@tktest.com'
) {
$em = $this->getDoctrine()->getRepository( Owner::class );
$foundOwner = $em->findOneByEmail( $emailChoice );
if ( $foundOwner ) {
$poll = $foundOwner->getPolls()[ 0 ];
$comment = $foundOwner->getComments()[ 0 ];
$sent = $this->sendOwnerPollsAction( $foundOwner, $poll );
if ( $sent ) {
return $this->json( [ "message" => "test email sent to " . $foundOwner->getEmail() . "!" ], 200 );
}
}
return $this->json( [ "message" => "user with this email was not found" ], 400 );
}
/**
* @Delete(
* path = "/{id}",
* name = "poll_delete",
* requirements = {"accessToken"="\w+", "poll_id"="\d+"}
* )
* @param Poll $poll
* @param $accessToken
*
* @return JsonResponse
*/
public
function deletePollAction(
Poll $poll,
$accessToken
) {
if ( $accessToken == $poll->getAdminKey() ) {
$em = $this->getDoctrine()->getManager();
$em->remove( $poll );
$em->flush();
return $this->json( [
'message' => 'boom! le sondage et ses objets assocités a été supprimé',
] );
} else {
return $this->json( [
'message' => 'le token d\'autorisation est invalide, vous ne pouvez pas modifier ce sondage',
] );
}
}
/**
* Check is a slug is already taken by a poll
* @Get(
* path = "/slug/{slug}",
* name = "check_slug_is_unique",
* )
*/
public function checkSlugIsUniqueAction( string $slug ) {
$emPoll = $this->getDoctrine()->getRepository( Poll::class );
$found = $emPoll->findOneByCustomUrl( $slug );
$elaborated_message_version = false;
if ( $found ) {
if ( ! $elaborated_message_version ) {
return $this->json( null,
204 );
}
// we should use an other slug
return $this->json( [
'message' => ' NO, this slug is already taken on this Framadate instance ',
'data' => [
'slug' => $slug,
],
],
204 );
}
if ( ! $elaborated_message_version ) {
return $this->json( null,
404 );
}
return $this->json( [
'message' => ' yes this slug is available on this Framadate instance ',
'data' => [
'slug' => $slug,
],
],
404 );
}
/**
* Get Admin poll config
* @Get(
* path = "/admin/{token}",
* name = "get_admin_config",
* )
* @param SerializerInterface $serializer
* @param $token
*
* @return JsonResponse|Response
*/
public function getAdministrationConfig(SerializerInterface $serializer, $token ) {
$emPoll = $this->getDoctrine()->getRepository( Poll::class );
$pollFound = $emPoll->findOneByAdminKey( $token );
if ( $pollFound ) {
$poll = $pollFound;
$comments = $poll->getComments();
$stacks = $poll->getStacksOfVotes();
$returnedPoll = [
'message' => 'your poll config',
'poll' => $poll,
'stacks_count' => count( $stacks ),
'stacks' => $stacks,
'choices_count' => $poll->computeAnswers(),
'choices' => $poll->getChoices(),
'comments' => $comments,
'comments_count' => count( $comments ),
'token' => $token,
];
$jsonResponse = $serializer->serialize($returnedPoll, 'json');
$response = new Response($jsonResponse);
$response->headers->set('Content-Type', 'application/json');
$response->setStatusCode(200);
return $response;
}
return $this->json( [
'message' => 'You are not allowed to do anything with this token',
'data' => [
'token' => $token,
],
],
403 );
}
/**
* @Route("/{id}", name="poll_delete", methods={"DELETE"})
*/
public function delete(Request $request, Poll $poll): Response
{
if ($this->isCsrfTokenValid('delete'.$poll->getId(), $request->request->get('_token'))) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->remove($poll);
$entityManager->flush();
}
return $this->redirectToRoute('poll_index');
}
}

View File

@ -0,0 +1,412 @@
<?php
namespace App\Controller\api;
use App\Controller\FramadateController;
use App\Entity\Choice;
use App\Entity\Owner;
use App\Entity\Poll;
use FOS\RestBundle\Controller\Annotations\Delete;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\Post;
use FOS\RestBundle\Controller\Annotations\Put;
use FOS\RestBundle\Controller\Annotations\Route;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\SerializerBuilder;
use JMS\Serializer\SerializerInterface;
use Swift_Mailer;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Class DefaultController
* @package App\Controller
* @Route("/api/v1/poll",name="api_")
*/
class PollController extends FramadateController {
/**
* @Get(
* path = "/",
* name = "get_all_polls"
* )
*/
public function getAllPollsAction() {
$repository = $this->getDoctrine()->getRepository( Poll::class );
$data = $repository->findall();
return $this->json( [
'message' => 'here are your polls',
'poll' => $data,
],
200 );
}
/**
* @Get(
* path = "/{id}",
* name = "get_poll",
* requirements = {"poll_id"="\d+"}
* )
* @param SerializerInterface $serializer
* @param Poll $poll
* @param Request $request
*
* @return JsonResponse|Response
*/
public function getPollConfig(
SerializerInterface $serializer,
Poll $poll,
Request $request
) {
$pass = $poll->getPassword();
$data = $request->getContent();
$data = json_decode( $data, true );
$comments = $poll->getComments();
$returnedPoll = [
'message' => 'your poll config',
'poll' => $poll,
'stacks_count' => count( $poll->getStacksOfVotes() ),
'stacks' => $poll->getStacksOfVotes(),
'choices_count' => $poll->computeAnswers(),
'choices' => $poll->getChoices(),
'comments' => $comments,
'comments_count' => count( $comments ),
];
/**
* password protected content
*/
if ( $pass && $pass !== md5( $data[ 'password_input' ] ) ) {
return $this->json( [
'message' => 'your password ' . $data[ 'password_input' ] . ' is wrong, and you should feel bad',
'data' => null,
],
403 );
} else {
$jsonResponse = $serializer->serialize( $returnedPoll, 'json' );
$response = new Response( $jsonResponse );
$response->headers->set( 'Content-Type', 'application/json' );
$response->setStatusCode( 200 );
return $response;
}
}
/**
* @Put(
* path = "/{id}/{token}",
* name = "update_poll",
* requirements = {"content"="\w+", "poll_id"="\d+"}
* )
*/
public function updatePollConfig(
Poll $poll,
string $token,
Request $request
) {
if ( $poll->getAdminKey() !== $token ) {
return $this->json( [
'message' => 'you are NOT allowed to update the poll ' . $poll->getTitle(),
],
403 );
}
// TODO check validity of request
// update only if we have the admin key
$em = $this->getDoctrine()->getManager();
$em->persist( $poll );
$em->flush();
return $this->json( [
'message' => 'you updated the poll ' . $poll->getTitle(),
],
200 );
}
/**
* @Post(
* path = "/",
* name = "new_poll",
* requirements = {"creator"="\w+"}
* )
* @param Request $request
*
* @return JsonResponse
*/
public function newPollAction( Request $request ) {
$data = $request->getContent();
$serializer = SerializerBuilder::create()->build();
try {
$newpoll = $serializer->deserialize( $data, 'App\Entity\Poll', 'json' );
} catch ( RuntimeException $e ) {
return $this->json( [ "message" => "Incorrect JSON in request" ], 400 );
}
$newpoll
->setAdminKey( $newpoll->generateAdminKey() )
->setCreationDate( new DateTime() )
->setModificationPolicy( 'nobody' );
$timeStamp = time() + ( 3600 * 24 * 90 ); // 90 days by default
$newpoll->setExpiracyDate( ( new DateTime() )->setTimestamp( $timeStamp ),
new DateTimeZone( 'Europe/Paris' ) );
$data = json_decode( $data, true );
$em = $this->getDoctrine()->getRepository( Owner::class );
$foundOwner = $em->findOneBy( [ 'email' => $data[ 'owner' ][ 'email' ] ] );
$userWasFound = false;
if ( ! $foundOwner ) {
//create a new owner
$owner = new Owner();
$owner->setPseudo( $data[ 'owner' ][ 'pseudo' ] );
$owner->setEmail( $data[ 'owner' ][ 'email' ] );
$foundOwner = $owner;
} else {
$userWasFound = true;
}
// link the owner and the poll
$newpoll->setOwner( $foundOwner );
$foundOwner->addPoll( $newpoll );
$em = $this->getDoctrine()->getManager();
$em->persist( $newpoll );
$em->persist( $foundOwner );
// emails
$newpoll->setMailOnComment( true );
$newpoll->setMailOnVote( true );
$newpoll->setHideResults( false );
// possible answers
$newpoll->setAllowedAnswers( [ 'yes' ] );
if ( $data[ 'voteChoices' ] ) {
switch ( $data[ 'voteChoices' ] ) {
case "only_yes":
default:
break;
}
}
// setup the password, converting the raw with md5 hash
if ( $data[ 'password' ] ) {
$newpoll->setPassword( $data[ 'password' ] );
}
// manage choices
// text kind of answers, dates are below
if ( $data[ 'pollType' ] == 'classic' ) {
$choices = $data[ 'dateList' ];
foreach ( $choices as $c ) {
$newChoice = new Choice();
$newChoice
->setPoll( $newpoll )
// ->setUrl( $c[ 'url' ] )
->setName( $c[ 'literal' ] );
$em->persist( $newChoice );
// TODO add also choices for each time range in a day
}
} elseif ( $data[ 'pollType' ] == 'dates' ) {
if ( $data[ 'allowSeveralHours' ] == true ) {
// different hours spans
$choices = $data[ 'dateList' ];
} else {
//TODO (Sébastien) I assume this shouldn't be empty ?
// all days have the same hour spans
}
}
$em->persist( $newpoll );
$em->flush();
$precision = '';
if ( $userWasFound ) {
$precision = 'from an existing user : ' . $foundOwner->getEmail();
}
$this->sendCreationMailAction( $foundOwner, $newpoll );
return $this->json( [
'message' => 'you created a poll ' . $precision,
'poll' => $newpoll,
'password_protected' => is_string( $newpoll->getPassword() ),
'admin_key' => $newpoll->getAdminKey(),
'owner_modifier_token' => $foundOwner->getModifierToken(),
],
201 );
}
/**
* @Get(
* path = "/mail/test-mail-poll/{emailChoice}",
* name = "test-mail-poll",
* )
*
* send the creation mail to owner
*
* @param Owner $admin_user
* @param Poll $poll
* @param Swift_Mailer $mailer
*
* @return int
* not that the email tktest_commentateur@tktest.com does not really exist
*/
// public function sendCreationMailAction( Owner $admin_user, Poll $poll, \Swift_Mailer $mailer) {
public function testSendCreationMailAction(
$emailChoice = 'tktest_commentateur@tktest.com'
) {
$em = $this->getDoctrine()->getRepository( Owner::class );
$foundOwner = $em->findOneByEmail( $emailChoice );
if ( $foundOwner ) {
$poll = $foundOwner->getPolls()[ 0 ];
$comment = $foundOwner->getComments()[ 0 ];
$sent = $this->sendOwnerPollsAction( $foundOwner, $poll );
if ( $sent ) {
return $this->json( [ "message" => "test email sent to " . $foundOwner->getEmail() . "!" ], 200 );
}
}
return $this->json( [ "message" => "user with this email was not found" ], 400 );
}
/**
* @Delete(
* path = "/{id}",
* name = "poll_delete",
* requirements = {"accessToken"="\w+", "poll_id"="\d+"}
* )
* @param Poll $poll
* @param $accessToken
*
* @return JsonResponse
*/
public
function deletePollAction(
Poll $poll,
$accessToken
) {
if ( $accessToken == $poll->getAdminKey() ) {
$em = $this->getDoctrine()->getManager();
$em->remove( $poll );
$em->flush();
return $this->json( [
'message' => 'boom! le sondage et ses objets assocités a été supprimé',
] );
} else {
return $this->json( [
'message' => 'le token d\'autorisation est invalide, vous ne pouvez pas modifier ce sondage',
] );
}
}
/**
* Check is a slug is already taken by a poll
* @Get(
* path = "/slug/{slug}",
* name = "check_slug_is_unique",
* )
*/
public function checkSlugIsUniqueAction( string $slug ) {
$emPoll = $this->getDoctrine()->getRepository( Poll::class );
$found = $emPoll->findOneByCustomUrl( $slug );
$elaborated_message_version = false;
if ( $found ) {
if ( ! $elaborated_message_version ) {
return $this->json( null,
204 );
}
// we should use an other slug
return $this->json( [
'message' => ' NO, this slug is already taken on this Framadate instance ',
'data' => [
'slug' => $slug,
],
],
204 );
}
if ( ! $elaborated_message_version ) {
return $this->json( null,
404 );
}
return $this->json( [
'message' => ' yes this slug is available on this Framadate instance ',
'data' => [
'slug' => $slug,
],
],
404 );
}
/**
* Get Admin poll config
* @Get(
* path = "/admin/{token}",
* name = "get_admin_config",
* )
*
* @param SerializerInterface $serializer
* @param $token
*
* @return JsonResponse|Response
*/
public function getAdministrationConfig( SerializerInterface $serializer, $token ) {
$emPoll = $this->getDoctrine()->getRepository( Poll::class );
$pollFound = $emPoll->findOneByAdminKey( $token );
if ( $pollFound ) {
$poll = $pollFound;
$comments = $poll->getComments();
$stacks = $poll->getStacksOfVotes();
$returnedPoll = [
'message' => 'your poll config',
'poll' => $poll,
'stacks_count' => count( $stacks ),
'stacks' => $stacks,
'choices_count' => $poll->computeAnswers(),
'choices' => $poll->getChoices(),
'comments' => $comments,
'comments_count' => count( $comments ),
'token' => $token,
];
$jsonResponse = $serializer->serialize( $returnedPoll, 'json' );
$response = new Response( $jsonResponse );
$response->headers->set( 'Content-Type', 'application/json' );
$response->setStatusCode( 200 );
return $response;
}
return $this->json( [
'message' => 'You are not allowed to do anything with this token',
'data' => [
'token' => $token,
],
],
403 );
}
}

39
src/Form/PollType.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace App\Form;
use App\Entity\Poll;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PollType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('customUrl')
->add('description')
->add('creationDate')
->add('expiracyDate')
->add('kind')
->add('allowedAnswers')
->add('modificationPolicy')
->add('mailOnComment')
->add('mailOnVote')
->add('hideResults')
->add('showResultEvenIfPasswords')
->add('password')
->add('adminKey')
->add('owner')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Poll::class,
]);
}
}

View File

@ -208,7 +208,7 @@
"version": "2.2.3"
},
"php": {
"version": "7.3"
"version": "7.4"
},
"phpdocumentor/reflection-common": {
"version": "2.0.0"
@ -243,6 +243,9 @@
"swiftmailer/swiftmailer": {
"version": "v6.2.3"
},
"symfony/asset": {
"version": "v4.3.11"
},
"symfony/browser-kit": {
"version": "v4.3.11"
},
@ -430,6 +433,9 @@
"symfony/security-core": {
"version": "v4.3.5"
},
"symfony/security-csrf": {
"version": "v4.3.11"
},
"symfony/service-contracts": {
"version": "v1.1.7"
},
@ -496,6 +502,25 @@
"ref": "dae9b39fd6717970be7601101ce5aa960bf53d9a"
}
},
"symfony/webpack-encore-bundle": {
"version": "1.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "1.6",
"ref": "69e1d805ad95964088bd510c05995e87dc391564"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/assets.yaml",
"config/packages/prod/webpack_encore.yaml",
"config/packages/test/webpack_encore.yaml",
"config/packages/webpack_encore.yaml",
"package.json",
"webpack.config.js"
]
},
"symfony/yaml": {
"version": "v4.3.5"
},

View File

@ -3,10 +3,25 @@
<head>
<meta charset="UTF-8">
<title>{% block title %}Framdate{% endblock %}</title>
{% block stylesheets %}{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('build/vendors~app.css') }}">
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
{% include 'split/header.html.twig' %}
{% block outerBody %}
<div class="container">
{% block body %}{% endblock %}
</div>
{% endblock %}
{% include 'split/footer.html.twig' %}
{% block javascripts %}
<script href="{{ asset('build/vendors~app.js') }}"></script>
<script href="{{ asset('build/app.js') }}"></script>
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,10 @@
{% extends 'base.html.twig' %}
{% block body %}
<section class="home">
<h1>Accueil</h1>
<div>
{{ polls|length }} sondages
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,5 @@
<form method="post" action="{{ path('poll_delete', {'id': poll.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ poll.id) }}">
<button class="btn">Delete</button>
</form>

View File

@ -0,0 +1,4 @@
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Save') }}</button>
{{ form_end(form) }}

View File

@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block title %}Edit Poll{% endblock %}
{% block body %}
<h1>Edit Poll</h1>
{{ include('poll/_form.html.twig', {'button_label': 'Update'}) }}
<a href="{{ path('poll_index') }}">back to list</a>
{{ include('poll/_delete_form.html.twig') }}
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends 'base.html.twig' %}
{% block title %}Poll index{% endblock %}
{% block body %}
<h1>Poll index</h1>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>CustomUrl</th>
<th>Description</th>
<th>CreationDate</th>
<th>ExpiracyDate</th>
<th>Kind</th>
<th>AllowedAnswers</th>
<th>ModificationPolicy</th>
<th>MailOnComment</th>
<th>MailOnVote</th>
<th>HideResults</th>
<th>ShowResultEvenIfPasswords</th>
<th>Password</th>
<th>AdminKey</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for poll in polls %}
<tr>
<td>{{ poll.id }}</td>
<td>{{ poll.title }}</td>
<td>{{ poll.customUrl }}</td>
<td>{{ poll.description }}</td>
<td>{{ poll.creationDate ? poll.creationDate|date('Y-m-d H:i:s') : '' }}</td>
<td>{{ poll.expiracyDate ? poll.expiracyDate|date('Y-m-d H:i:s') : '' }}</td>
<td>{{ poll.kind }}</td>
<td>{{ poll.allowedAnswers ? poll.allowedAnswers|join(', ') : '' }}</td>
<td>{{ poll.modificationPolicy }}</td>
<td>{{ poll.mailOnComment ? 'Yes' : 'No' }}</td>
<td>{{ poll.mailOnVote ? 'Yes' : 'No' }}</td>
<td>{{ poll.hideResults ? 'Yes' : 'No' }}</td>
<td>{{ poll.showResultEvenIfPasswords ? 'Yes' : 'No' }}</td>
<td>{{ poll.password }}</td>
<td>{{ poll.adminKey }}</td>
<td>
<a href="{{ path('poll_show', {'id': poll.id}) }}">show</a>
<a href="{{ path('poll_edit', {'id': poll.id}) }}">edit</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="16">no records found</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ path('poll_new') }}">Create new</a>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html.twig' %}
{% block title %}New Poll{% endblock %}
{% block body %}
<h1>Create new Poll</h1>
{{ include('poll/_form.html.twig') }}
<a href="{{ path('poll_index') }}">back to list</a>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% extends 'base.html.twig' %}
{% block title %}Poll{% endblock %}
{% block body %}
<h1>Poll</h1>
<table class="table">
<tbody>
<tr>
<th>Id</th>
<td>{{ poll.id }}</td>
</tr>
<tr>
<th>Title</th>
<td>{{ poll.title }}</td>
</tr>
<tr>
<th>CustomUrl</th>
<td>{{ poll.customUrl }}</td>
</tr>
<tr>
<th>Description</th>
<td>{{ poll.description }}</td>
</tr>
<tr>
<th>CreationDate</th>
<td>{{ poll.creationDate ? poll.creationDate|date('Y-m-d H:i:s') : '' }}</td>
</tr>
<tr>
<th>ExpiracyDate</th>
<td>{{ poll.expiracyDate ? poll.expiracyDate|date('Y-m-d H:i:s') : '' }}</td>
</tr>
<tr>
<th>Kind</th>
<td>{{ poll.kind }}</td>
</tr>
<tr>
<th>AllowedAnswers</th>
<td>{{ poll.allowedAnswers ? poll.allowedAnswers|join(', ') : '' }}</td>
</tr>
<tr>
<th>ModificationPolicy</th>
<td>{{ poll.modificationPolicy }}</td>
</tr>
<tr>
<th>MailOnComment</th>
<td>{{ poll.mailOnComment ? 'Yes' : 'No' }}</td>
</tr>
<tr>
<th>MailOnVote</th>
<td>{{ poll.mailOnVote ? 'Yes' : 'No' }}</td>
</tr>
<tr>
<th>HideResults</th>
<td>{{ poll.hideResults ? 'Yes' : 'No' }}</td>
</tr>
<tr>
<th>ShowResultEvenIfPasswords</th>
<td>{{ poll.showResultEvenIfPasswords ? 'Yes' : 'No' }}</td>
</tr>
<tr>
<th>Password</th>
<td>{{ poll.password }}</td>
</tr>
<tr>
<th>AdminKey</th>
<td>{{ poll.adminKey }}</td>
</tr>
</tbody>
</table>
<a href="{{ path('poll_index') }}">back to list</a>
<a href="{{ path('poll_edit', {'id': poll.id}) }}">edit</a>
{{ include('poll/_delete_form.html.twig') }}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% block footer %}
<footer>
<nav>
<ul>
<li>
<a href="/">
<i class="fa fa-home"></i>
</a>
</li>
</ul>
</nav>
</footer>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% block header %}
<header class="bg-purple-300 p-4 block">
<div class="container">
<nav>
<ul>
<li>
<a class="btn button rounded bg-purple-200 p-2" href="/">
<i class="fa fa-home"></i>
<img src="{{ WEBSITE_LOGO }}" alt="logo">
{{ WEBSITE_NAME }}
</a>
</li>
</ul>
</nav>
</div>
</header>
{% endblock %}

74
webpack.config.js Normal file
View File

@ -0,0 +1,74 @@
var 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 sub-directory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Add 1 entry for each "page" of your app
* (including one that's included on every page - e.g. "app")
*
* 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')
//.addEntry('page1', './assets/page1.js')
//.addEntry('page2', './assets/page2.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())
// enables @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = 3;
})
// enables Sass/SCSS support
.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// 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()
// uncomment if you use API Platform Admin (composer req api-admin)
//.enableReactPreset()
//.addEntry('admin', './assets/admin.js')
;
module.exports = Encore.getWebpackConfig();

7343
yarn.lock

File diff suppressed because it is too large Load Diff