diff --git a/.gitignore b/.gitignore index 51dccc8..358fc8e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ app/config/parameters.yml composer.phar behat.yml +.idea +!web/uploads/.gitkeep +!web/uploads/media/.gitkeep diff --git a/README.md b/README.md index 5f3cc3b..5bb9512 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,8 @@ old view before 1.5 release ## License All what you can find at this repo shared under the MIT License + +## about filestack +As image uploader we use https://www.filestack.com/. +To use it You should visit https://dev.filestack.com/apps/ and create new app. +After that You should put your api_key to parameters.yml as filestack.api_key diff --git a/app/config/parameters.circle.yml b/app/config/parameters.circle.yml index be32adb..0ceeaf2 100644 --- a/app/config/parameters.circle.yml +++ b/app/config/parameters.circle.yml @@ -20,4 +20,8 @@ parameters: # default email for bot from which email is sent from_email: bot@email.com - admin_password: admin \ No newline at end of file + admin_password: admin + #filestack params + media.uploadPath: %kernel.root_dir%/../web/uploads/media/ + media.uploadUrl: /uploads/media/ + filestack.api_key: A3w1aZwZmQBibC09rAcZnz diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index b989851..4bc4227 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -21,3 +21,7 @@ parameters: from_email: bot@email.com admin_password: admin + + media.uploadPath: %kernel.root_dir%/../web/uploads/media/ + media.uploadUrl: /uploads/media/ + filestack.api_key: A3w1aZwZmQBibC09rAcZnz diff --git a/behat.yml.dist b/behat.yml.dist index d335998..36a8d57 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -13,6 +13,7 @@ backend: browser_name: chrome goutte: ~ selenium2: ~ + files_path: %behat.paths.base%/web/uploads/media/ Behat\Symfony2Extension\Extension: mink_driver: true diff --git a/circle.yml b/circle.yml index d0a97d3..19d20af 100644 --- a/circle.yml +++ b/circle.yml @@ -33,6 +33,7 @@ test: pre: - chmod -R 777 app/cache - chmod -R 777 app/logs + - chmod -R 777 web/uploads/media - wget http://selenium-release.storage.googleapis.com/2.48/selenium-server-standalone-2.48.2.jar - nohup bash -c "java -jar selenium-server-standalone-2.48.2.jar &" override: diff --git a/features/backend/media.feature b/features/backend/media.feature new file mode 100644 index 0000000..4fdd2eb --- /dev/null +++ b/features/backend/media.feature @@ -0,0 +1,181 @@ +@backend +Feature: Media settings + In order to update media settings + As an admin + I should be able to edit media settings + +Background: + Given following "Event": + | ref | title | description | startDate | endDate | venue | email | host | + | event | My event | My another awesome event! | 2016-03-01 10:00 | 2016-03-01 18:00 | Burj Khalifa Tower | eventator@email.com | http://localhost:8000 | + | event2 | My other event | My other awesome event! | 2016-03-01 10:00 | 2016-03-01 18:00 | Burj Khalifa Tower | eventator@gmail.com | http://eventator.loc:8080 | + | event3 | His event | His another awesome event! | 2016-04-01 10:00 | 2016-04-01 18:00 | Kuala-lumpur Tower | eventator@gmail.com | http://event.com | + And following "EventTranslation": + | event | locale | + | event | ru_RU | + | event | de_DE | + | event2 | ru_RU | + | event2 | de_DE | + | event3 | ru_RU | + And following "Organizer": + | ref | title | description | isActive | events | + | organizer | My organizer | My another awesome organizer! | 1 | event,event2,event3 | + | organizer2 | His organizer | His another awesome organizer! | 1 | event2,event3 | + And following "OrganizerTranslation": + | organizer | locale | + | organizer | ru_RU | + | organizer2 | de_DE | + And following "Speech": + | ref | title | description | language | event | + | speech | symfony propagation | world symfony expansion | ru | event | + | speech2 | php servers piece | php most popular language | en | event | + | speech3 | doctrine must have | what you docrtine project should have | en | event | + | speech4 | symfony propagation2 | world symfony expansion2 | en | event2 | + | speech5 | php servers piece2 | php most popular language2 | en | event2 | + | speech6 | doctrine must have2 | what you docrtine project should have2 | en | event2 | + | speech7 | symfony propagation2 | world symfony expansion2 | en | event3 | + | speech8 | php servers piece2 | php most popular language2 | en | event3 | + | speech9 | doctrine must have2 | what you docrtine project should have2 | en | event3 | + + And following "SpeechTranslation": + | speech | locale | + | speech | ru_RU | + | speech2 | de_DE | + | speech3 | de_DE | + | speech4 | ru_RU | + | speech5 | de_DE | + | speech6 | de_DE | + | speech7 | ru_RU | + | speech8 | de_DE | + | speech9 | de_DE | + And following "Program": + | ref | title | isTopic | isActive | startDate | endDate | events | speech | + | program | keynote | 1 | 1 | 2016-03-01 10:00 | 2016-03-01 10:30 | event | | + | program2 | alex_symfony | 0 | 1 | 2016-03-01 10:30 | 2016-03-01 11:30 | event | speech | + | program3 | phil_php | 0 | 1 | 2016-03-01 11:30 | 2016-03-01 12:30 | event | speech2 | + | program4 | coffee1 | 1 | 1 | 2016-03-01 12:30 | 2016-03-01 13:00 | event | | + | program5 | phil_doctrine | 0 | 1 | 2016-03-01 13:00 | 2016-03-01 14:30 | event | speech3 | + | program6 | end_keynote | 1 | 1 | 2016-03-01 14:30 | 2016-03-01 15:00 | event | | + | program7 | after_party | 1 | 1 | 2016-03-01 15:00 | 2016-03-01 18:00 | event | | + | program8 | keynote | 1 | 1 | 2016-04-01 10:00 | 2016-04-01 10:30 | event2 | | + | program9 | alex_symfony | 0 | 1 | 2016-04-01 10:30 | 2016-04-01 11:30 | event2 | speech4 | + | program10 | phil_php | 0 | 1 | 2016-04-01 11:30 | 2016-04-01 12:30 | event2 | speech5 | + | program11 | coffee1 | 1 | 1 | 2016-04-01 12:30 | 2016-04-01 13:00 | event2 | | + | program12 | phil_doctrine | 0 | 1 | 2016-04-01 13:00 | 2016-04-01 14:30 | event2 | speech6 | + | program13 | end_keynote | 1 | 1 | 2016-04-01 14:30 | 2016-04-01 15:00 | event2 | | + | program14 | after_party | 1 | 1 | 2016-04-01 15:00 | 2016-04-01 18:00 | event2 | | + | program15 | keynote | 1 | 1 | 2016-04-01 10:00 | 2016-04-01 10:30 | event3 | | + | program16 | alex_symfony | 0 | 1 | 2016-04-01 10:30 | 2016-04-01 11:30 | event3 | speech7 | + | program17 | phil_php | 0 | 1 | 2016-04-01 11:30 | 2016-04-01 12:30 | event3 | speech8 | + | program18 | coffee1 | 1 | 1 | 2016-04-01 12:30 | 2016-04-01 13:00 | event3 | | + | program19 | phil_doctrine | 0 | 1 | 2016-04-01 13:00 | 2016-04-01 14:30 | event3 | speech9 | + | program20 | end_keynote | 1 | 1 | 2016-04-01 14:30 | 2016-04-01 15:00 | event3 | | + | program21 | after_party | 1 | 1 | 2016-04-01 15:00 | 2016-04-01 18:00 | event3 | | + And following "ProgramTranslation": + | program | locale | + | program | ru_RU | + | program2 | de_DE | + | program3 | de_DE | + | program4 | de_DE | + | program5 | de_DE | + | program6 | de_DE | + | program7 | de_DE | + | program8 | de_DE | + | program9 | de_DE | + | program10 | de_DE | + | program11 | de_DE | + | program12 | de_DE | + | program13 | de_DE | + | program14 | de_DE | + | program15 | de_DE | + | program16 | de_DE | + | program17 | de_DE | + | program18 | de_DE | + | program19 | de_DE | + | program20 | de_DE | + | program21 | de_DE | + And following "Speaker": + | ref | firstName | lastName | Company | email | homepage | twitter | events | speeches | + | speaker | Phill | Pilow | Reseach Supplier | | | | event,event2,event3 | speech2,speech3,speech5,speech6,speech8,speech9 | + | speaker2 | Alex | Demchenko | KnpLabs | | http://451f.com.ua | https://twitter.com/twitter | event,event2,event3 | speech,speech4,speech7 | + And following "SpeakerTranslation": + | speaker | locale | + | speaker | ru_RU | + | speaker2 | de_DE | + And following "Sponsor": + | ref | company | description | homepage | type | isActive | events | + | sponsor | Reseach Supplier | NASA research center | http://nasa.gov.us | 1 | 1 | event,event2,event3 | + | sponsor2 | KnpLabs | Happy awesome developer | http://knplabs.com | 2 | 1 | event | + And following "SponsorTranslation": + | sponsor | locale | + | sponsor | de_DE | + | sponsor2 | de_DE | + And following "Media": + | title | filename | created_date | updated_date | + | Media1 | logo.png | 2017-10-27 16:02 | 2017-10-27 16:02 | + | Media2 | logo.png | 2017-10-27 17:02 | 2017-10-27 17:02 | + +@javascript +Scenario: Admin should have access to the media manage + Given I am sign in as admin + When I click "Media" + Then I wait for a form + Then I should see "Add Media" + And I should see the row containing "1;Media1" + And I should see the row containing "2;Media2" + When I click "Edit" on the row containing "1;Media1" + Then I wait for a form + Then I should see "Media settings" + +@javascript +Scenario: Admin should be able to add media + Given I am sign in as admin + When I click "Media" + Then I wait for a form + Then I should see "Add Media" + And I click "Add Media" + Then I wait for a form + Then I should see "Media settings" + And I fill in "Title" with "Media3" + And I press "Upload file" + Then I wait "10" seconds + Then I should see " My Device " + And I attach the file "test.png" to "fsp-fileUpload" + Then I should see "Edit Image" + Then I wait "15" seconds + And I click on the element with css selector ".fsp-button--primary" + Then I wait "10" seconds + And I press "Add" + Then I wait for a form + Then I should see "Media Media3 added." + Then I should see the row containing "3;Media3" + +@javascript +Scenario: Admin should be able to update media settings + Given I am sign in as admin + When I click "Media" + Then I wait for a form + Then I should see "Add Media" + And I should see the row containing "2;Media2" + When I click "Edit" on the row containing "2;Media2" + Then I wait for a form + Then I should see "Media settings" + And I fill in "Title" with "Media2_1" + And I fill in "Media Description" with "Awesome media" + And I fill in "Copyright Info" with "Test media" + And I press "Update" + Then I wait for a form + Then I should see "Media Media2_1 updated." + Then I should see the row containing "2;Media2_1" + +@javascript +Scenario: Admin should be delete to the media + Given I am sign in as admin + When I click "Media" + Then I wait for a form + Then I should see "Add Media" + And I should see the row containing "1;Media1" + Then I delete the record with id "1" + Then I wait for a form + Then I should see "Media deleted." + Then I should not see the row containing "1;Media1" diff --git a/src/Event/EventBundle/Controller/Backend/MediaController.php b/src/Event/EventBundle/Controller/Backend/MediaController.php new file mode 100644 index 0000000..0c7a55e --- /dev/null +++ b/src/Event/EventBundle/Controller/Backend/MediaController.php @@ -0,0 +1,121 @@ +container->getParameter('media.uploadPath'); + } + + public function getUploadUrl(){ + return $this->container->getParameter('media.uploadUrl'); + } + + public function indexAction() + { + return $this->render('EventEventBundle:Backend/Media:index.html.twig', array( + 'media' => $this->getRepository('EventEventBundle:Media')->findAll(), + 'uploadPath' => $this->getUploadPath(), + 'uploadUrl' => $this->getUploadUrl() + )); + } + + public function manageAction(Request $request, $id = null) + { + $oldFileName = ''; + if ($id === null) { + $media = new Media(); + } else { + $media = $this->findOr404('EventEventBundle:Media', $id); + $oldFileName = $media->getFilename(); + } + + $form = $this->createForm(MediaType::class, $media); + + if ($request->getMethod() === 'POST') { + $form->handleRequest($request); + + if ($form->isValid()) { + $media->setUpdated(new \DateTime()); + + if ($oldFileName !== $media->getFilename()) { + $url = $request->request->get('file_url'); + $path = $this->getUploadPath(); + $media->setFilename(time() . '_' . $media->getFilename()); + $file = $this->getFile($url); + + $fs = new Filesystem(); + + try { + $fs->dumpFile($path . $media->getFilename(), $file, 0777); + } catch (IOExceptionInterface $e) { + throw new Exception($e->getMessage()); + } + } + + $this->getManager()->persist($media); + $this->getManager()->flush(); + + $successFlashText = sprintf('Media %s updated.', $media->getTitle()); + if (!$id) { + $successFlashText = sprintf('Media %s added.', $media->getTitle()); + } + $this->setSuccessFlash($successFlashText); + + return $this->redirectToRoute('backend_media'); + } + } + + return $this->render('EventEventBundle:Backend/Media:manage.html.twig', [ + 'media' => $media, + 'form' => $form->createView(), + 'fs_api' => $this->container->getParameter('filestack.api_key'), + ]); + } + + protected function checkAccessFolder($path){ + if(is_dir($path)){ + chmod($path, 0777); + } else { + mkdir($path, 0777, true); + } + } + + protected function getFile($url) + { + $client = new Client(); + $file = $client->createRequest('GET', $url)->send()->getBody(true); + + return $file; + } + + public function deleteAction($id) + { + $this->isGrantedAdmin(); + $translator = $this->get('translator'); + + $entity = $this->findOr404('EventEventBundle:Media', $id); + $file = $this->getUploadPath() . $entity->getFilename(); + $this->getManager()->remove($entity); + $this->getManager()->flush(); + if (is_file($file)) { + unlink($file); + } + + $this->setSuccessFlash($translator->trans('Media deleted.')); + + return $this->redirectToRoute('backend_media'); + } +} diff --git a/src/Event/EventBundle/Entity/Media.php b/src/Event/EventBundle/Entity/Media.php new file mode 100644 index 0000000..a508544 --- /dev/null +++ b/src/Event/EventBundle/Entity/Media.php @@ -0,0 +1,268 @@ +getId())){ + $this->setCreated(new \DateTime()); + } + $this->setUpdated(new \DateTime()); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set title + * + * @param string $title + * @return Media + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * Get title + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set filename + * + * @param string $filename + * @return Media + */ + public function setFilename($filename) + { + $this->filename = $filename; + + return $this; + } + + /** + * Get filename + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + + /** + * Set description + * + * @param string $description + * @return Media + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Get description + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set copyrightInfo + * + * @param string $copyrightInfo + * @return Media + */ + public function setCopyrightInfo($copyrightInfo) + { + $this->copyrightInfo = $copyrightInfo; + + return $this; + } + + /** + * Get copyrightInfo + * + * @return string + */ + public function getCopyrightInfo() + { + return $this->copyrightInfo; + } + + /** + * Set mediaCredits + * + * @param string $mediaCredits + * @return Media + */ + public function setMediaCredits($mediaCredits) + { + $this->mediaCredits = $mediaCredits; + + return $this; + } + + /** + * Get mediaCredits + * + * @return string + */ + public function getMediaCredits() + { + return $this->mediaCredits; + } + + /** + * Set created + * + * @param \DateTime $created + * @return Media + */ + public function setCreated($created) + { + if (!$created instanceOf \DateTime) { + $created = \DateTime::createFromFormat('m/d/Y', $created); + } + + $this->created = $created; + + return $this; + } + + /** + * Get created + * + * @return \DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * Set updated + * + * @param \DateTime $updated + * @return Media + */ + public function setUpdated($updated) + { + if (!$updated instanceOf \DateTime) { + $updated = \DateTime::createFromFormat('m/d/Y', $updated); + } + + $this->updated = $updated; + + return $this; + } + + /** + * Get updated + * + * @return \DateTime + */ + public function getUpdated() + { + return $this->updated; + } + + public function __toString() + { + return $this->title; + } +} diff --git a/src/Event/EventBundle/Entity/Repository/MediaRepository.php b/src/Event/EventBundle/Entity/Repository/MediaRepository.php new file mode 100644 index 0000000..fbd17d1 --- /dev/null +++ b/src/Event/EventBundle/Entity/Repository/MediaRepository.php @@ -0,0 +1,11 @@ +add('title', TextType::class) + ->add('filename', HiddenType::class, ['required' => false]) + ->add('description', TextareaType::class, [ + 'label' => 'Media Description', + 'attr' => array('class' => 'input-xxlarge', 'rows' => 3), + 'required' => false + ]) + ->add('copyrightInfo', TextareaType::class, [ + 'label' => 'Copyright Info', + 'attr' => array('class' => 'input-xxlarge', 'rows' => 3), + 'required' => false + ]) + ->add('mediaCredits', TextareaType::class, [ + 'label' => 'Media Credits', + 'attr' => array('class' => 'input-xxlarge', 'rows' => 3), + 'required' => false + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => 'Event\EventBundle\Entity\Media' + ]); + } + + public function getBlockPrefix() + { + return 'media'; + } +} diff --git a/src/Event/EventBundle/Menu/Builder.php b/src/Event/EventBundle/Menu/Builder.php index d714cf4..ad41e31 100644 --- a/src/Event/EventBundle/Menu/Builder.php +++ b/src/Event/EventBundle/Menu/Builder.php @@ -34,6 +34,7 @@ public function sideBar(FactoryInterface $factory, array $options) $event->addChild('Sponsors', array('route' => 'backend_sponsor')); $event->addChild('Organizers', array('route' => 'backend_organizer')); $event->addChild('Calls For Paper', array('route' => 'backend_call_for_paper')); + $event->addChild('Media', array('route' => 'backend_media')); return $menu; } diff --git a/src/Event/EventBundle/Resources/config/routing/backend.yml b/src/Event/EventBundle/Resources/config/routing/backend.yml index ed61cb5..04bdec7 100644 --- a/src/Event/EventBundle/Resources/config/routing/backend.yml +++ b/src/Event/EventBundle/Resources/config/routing/backend.yml @@ -169,3 +169,22 @@ backend_call_for_paper_delete: id: \d+ options: expose: true + +# Media +backend_media: + path: /media + defaults: { _controller: EventEventBundle:Backend/Media:index } + +backend_media_add: + path: /media/add + defaults: { _controller: EventEventBundle:Backend/Media:manage } + +backend_media_edit: + path: /media/edit/{id} + defaults: { _controller: EventEventBundle:Backend/Media:manage, id: null } + requirements: { id: \d+ } + +backend_media_delete: + path: /media/delete/{id} + defaults: { _controller: EventEventBundle:Backend/Media:delete } + requirements: { id: \d+ } diff --git a/src/Event/EventBundle/Resources/public/css/style.css b/src/Event/EventBundle/Resources/public/css/style.css index 982da99..23ec902 100644 --- a/src/Event/EventBundle/Resources/public/css/style.css +++ b/src/Event/EventBundle/Resources/public/css/style.css @@ -128,3 +128,13 @@ form input { .events-list { text-align: right; } + +/* backend media uploader */ +.uploader-container { + margin: 10px 0; +} + +#media_title { + width: 530px; +} +/* backend media uploader */ \ No newline at end of file diff --git a/src/Event/EventBundle/Resources/views/Backend/Media/index.html.twig b/src/Event/EventBundle/Resources/views/Backend/Media/index.html.twig new file mode 100644 index 0000000..c09f9f1 --- /dev/null +++ b/src/Event/EventBundle/Resources/views/Backend/Media/index.html.twig @@ -0,0 +1,91 @@ +{% extends 'EventEventBundle:Backend:layout.html.twig' %} + +{% block content %} +
+ + + + + + + + + + + + + + + {% for file in media %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
{{ 'ID'|trans }}{{ 'Thumbnail'|trans }}{{ 'Title'|trans }}{{ 'Created'|trans }}{{ 'Updated'|trans }}{{ 'Actions'|trans }}
{{ file.id }} + no-img + + + {{ file.title }} + + {{ file.created|date }}{{ file.updated|date }} + +
No media found.
+
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + + +{% endblock %} diff --git a/src/Event/EventBundle/Resources/views/Backend/Media/manage.html.twig b/src/Event/EventBundle/Resources/views/Backend/Media/manage.html.twig new file mode 100644 index 0000000..7b531cc --- /dev/null +++ b/src/Event/EventBundle/Resources/views/Backend/Media/manage.html.twig @@ -0,0 +1,54 @@ +{% extends 'EventEventBundle:Backend:layout.html.twig' %} + +{% block content %} +
+ + + {{ form_start(form, {'method': 'POST', 'attr': {'id': 'media'}, 'action': path('backend_media_edit', {'id': media.id})}) }} +
+
+ {{ form_row(form.title) }} + {{ form_row(form.filename) }} +
+ + +
+ {{ form_row(form.description) }} + {{ form_row(form.copyrightInfo) }} + {{ form_row(form.mediaCredits) }} + +
+ + {{ form_rest(form) }} + + +
+ {{ form_end(form) }} + +
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + + + +{% endblock %} diff --git a/src/Event/EventBundle/Resources/views/Backend/layout.html.twig b/src/Event/EventBundle/Resources/views/Backend/layout.html.twig index 87407e2..e4888b1 100644 --- a/src/Event/EventBundle/Resources/views/Backend/layout.html.twig +++ b/src/Event/EventBundle/Resources/views/Backend/layout.html.twig @@ -39,6 +39,7 @@ {% for message in app.session.flashbag.get('success') %}
{{ message|trans }} + ×
{% endfor %} {% for message in app.session.flashbag.get('error') %} diff --git a/var/ci/behat.ci.yml b/var/ci/behat.ci.yml index bff84ae..76dca91 100644 --- a/var/ci/behat.ci.yml +++ b/var/ci/behat.ci.yml @@ -13,6 +13,7 @@ backend: browser_name: chrome goutte: ~ selenium2: ~ + files_path: %behat.paths.base%/web/uploads/media/ Behat\Symfony2Extension\Extension: mink_driver: true diff --git a/web/uploads/.gitkeep b/web/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/uploads/media/test.png b/web/uploads/media/test.png new file mode 100644 index 0000000..9d205e2 Binary files /dev/null and b/web/uploads/media/test.png differ