From d908885e2fe4c8dd147a7e2146cc915b79eaae0b Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Tue, 3 Mar 2015 23:38:52 +0300 Subject: [PATCH] Base web implementation. --- README.md | 74 ++------ app/AppKernel.php | 1 + .../Version20150303224825.php | 22 +++ .../Version20150305184842.php | 23 +++ .../Version20150316014139.php | 32 ++++ app/Resources/translations/messages.ru.xliff | 119 ++++++++++++ app/Resources/views/base.html.twig | 65 +++++-- app/Resources/views/layout.html.twig | 45 +++++ app/Resources/views/sidebar.html.twig | 12 ++ app/config/config.yml | 22 ++- app/config/parameters.yml.dist | 5 + composer.json | 5 +- composer.lock | 176 +++++++++++++++++- .../Controller/CopypasteController.php | 154 +++++++++++++++ .../CopyPasteBundle/Entity/Copypaste.php | 35 ++-- .../CopyPasteBundle/Entity/Language.php | 62 ++++-- .../CopyPasteBundle/Form/CopypasteType.php | 90 +++++++++ .../Resources/config/routing.yml | 4 +- .../Resources/config/routing/copypaste.yml | 21 +++ .../Resources/public/css/base.css | 44 +++++ .../Resources/public/images/favicon.ico | Bin 0 -> 1406 bytes .../Resources/public/js/copypaste.js | 9 + .../Resources/views/Copypaste/new.html.twig | 6 + .../Resources/views/Copypaste/show.html.twig | 76 ++++++++ .../views/Form/form_paste_create.html.twig | 42 +++++ web/apple-touch-icon.png | Bin 10784 -> 0 bytes 26 files changed, 1023 insertions(+), 121 deletions(-) create mode 100644 app/DoctrineMigrations/Version20150303224825.php create mode 100644 app/DoctrineMigrations/Version20150305184842.php create mode 100644 app/DoctrineMigrations/Version20150316014139.php create mode 100644 app/Resources/translations/messages.ru.xliff create mode 100644 app/Resources/views/layout.html.twig create mode 100644 app/Resources/views/sidebar.html.twig create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Controller/CopypasteController.php create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Form/CopypasteType.php create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing/copypaste.yml create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Resources/public/css/base.css create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Resources/public/images/favicon.ico create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Resources/public/js/copypaste.js create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/new.html.twig create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/show.html.twig create mode 100644 src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Form/form_paste_create.html.twig delete mode 100644 web/apple-touch-icon.png diff --git a/README.md b/README.md index b97ef99..66934dc 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,17 @@ -Symfony Standard Edition -======================== -Welcome to the Symfony Standard Edition - a fully-functional Symfony2 -application that you can use as the skeleton for your new applications. -For details on how to download and get started with Symfony, see the -[Installation][1] chapter of the Symfony Documentation. +```shell +npm install -g less +``` -What's inside? --------------- +... -The Symfony Standard Edition is configured with the following defaults: +```shell +php app/console braincrafted:bootstrap:install +``` - * An AppBundle you can use to start coding; +... - * Twig as the only configured template engine; - - * Doctrine ORM/DBAL; - - * Swiftmailer; - - * Annotations enabled for everything. - -It comes pre-configured with the following bundles: - - * **FrameworkBundle** - The core Symfony framework bundle - - * [**SensioFrameworkExtraBundle**][6] - Adds several enhancements, including - template and routing annotation capability - - * [**DoctrineBundle**][7] - Adds support for the Doctrine ORM - - * [**TwigBundle**][8] - Adds support for the Twig templating engine - - * [**SecurityBundle**][9] - Adds security by integrating Symfony's security - component - - * [**SwiftmailerBundle**][10] - Adds support for Swiftmailer, a library for - sending emails - - * [**MonologBundle**][11] - Adds support for Monolog, a logging library - - * [**AsseticBundle**][12] - Adds support for Assetic, an asset processing - library - - * **WebProfilerBundle** (in dev/test env) - Adds profiling functionality and - the web debug toolbar - - * **SensioDistributionBundle** (in dev/test env) - Adds functionality for - configuring and working with Symfony distributions - - * [**SensioGeneratorBundle**][13] (in dev/test env) - Adds code generation - capabilities - -All libraries and bundles included in the Symfony Standard Edition are -released under the MIT or BSD license. - -Enjoy! - -[1]: http://symfony.com/doc/2.7/book/installation.html -[6]: http://symfony.com/doc/2.7/bundles/SensioFrameworkExtraBundle/index.html -[7]: http://symfony.com/doc/2.7/book/doctrine.html -[8]: http://symfony.com/doc/2.7/book/templating.html -[9]: http://symfony.com/doc/2.7/book/security.html -[10]: http://symfony.com/doc/2.7/cookbook/email.html -[11]: http://symfony.com/doc/2.7/cookbook/logging/monolog.html -[12]: http://symfony.com/doc/2.7/cookbook/assetic/asset_management.html -[13]: http://symfony.com/doc/2.7/bundles/SensioGeneratorBundle/index.html +```shell +php app/console assetic:dump +``` \ No newline at end of file diff --git a/app/AppKernel.php b/app/AppKernel.php index 9be1531..bb83ba1 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -19,6 +19,7 @@ class AppKernel extends Kernel new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(), new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), new DT\Bundle\GeshiBundle\DTGeshiBundle(), + new Braincrafted\Bundle\BootstrapBundle\BraincraftedBootstrapBundle(), new Skobkin\Bundle\CopyPasteBundle\SkobkinCopyPasteBundle(), ); diff --git a/app/DoctrineMigrations/Version20150303224825.php b/app/DoctrineMigrations/Version20150303224825.php new file mode 100644 index 0000000..4460679 --- /dev/null +++ b/app/DoctrineMigrations/Version20150303224825.php @@ -0,0 +1,22 @@ +addSql('UPDATE copypastes SET secret=NULL WHERE secret=\'\''); + } + + public function down(Schema $schema) + { + $this->addSql('UPDATE copypastes SET secret=\'\' WHERE secret=NULL'); + } +} diff --git a/app/DoctrineMigrations/Version20150305184842.php b/app/DoctrineMigrations/Version20150305184842.php new file mode 100644 index 0000000..4eac22a --- /dev/null +++ b/app/DoctrineMigrations/Version20150305184842.php @@ -0,0 +1,23 @@ +addSql('UPDATE copypastes SET date_expire=NULL WHERE date_expire=\'0000-00-00 00:00:00\''); + + } + + public function down(Schema $schema) + { + $this->addSql('UPDATE copypastes SET date_expire=\'0000-00-00 00:00:00\' WHERE date_expire=NULL'); + } +} diff --git a/app/DoctrineMigrations/Version20150316014139.php b/app/DoctrineMigrations/Version20150316014139.php new file mode 100644 index 0000000..d28ba5c --- /dev/null +++ b/app/DoctrineMigrations/Version20150316014139.php @@ -0,0 +1,32 @@ + is_enabled + * - is_preferred added + */ +class Version20150316014139 extends AbstractMigration +{ + public function up(Schema $schema) + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE languages CHANGE enabled is_enabled TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE languages ADD is_preferred TINYINT(1) NOT NULL'); + $this->addSql('CREATE INDEX idx_preferred ON languages (is_preferred)'); + } + + public function down(Schema $schema) + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE languages CHANGE is_enabled enabled TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE languages DROP is_preferred'); + $this->addSql('DROP INDEX idx_preferred ON languages'); + } +} diff --git a/app/Resources/translations/messages.ru.xliff b/app/Resources/translations/messages.ru.xliff new file mode 100644 index 0000000..576199b --- /dev/null +++ b/app/Resources/translations/messages.ru.xliff @@ -0,0 +1,119 @@ + + + + + + header_title + CopyPaste + + + sidebar_title + Последние + + + header_menu_add + Добавить + + + header_menu_about + О сервисе + + + header_menu_api + API + + + paste_add_form_text + Текст + + + paste_add_form_description + Описание + + + paste_add_form_file_name + Имя файла + + + paste_add_form_author + Автор + + + paste_add_form_expire + Удалить через + + + paste_add_form_private + Секретно + + + paste_add_form_language + Язык + + + Create + Создать + + + paste_view_tab_view + Просмотр + + + paste_view_tab_edit + Правка + + + paste_show_qr_show + Показать QR + + + paste_show_download + Скачать + + + 5 minutes + 5 минут + + + 1 hour + 1 час + + + 3 hours + 3 часа + + + 12 hours + 12 часов + + + 1 day + 1 день + + + 1 week + 1 неделя + + + 1 month + 1 месяц + + + 3 months + 3 месяца + + + 6 months + 6 месяцев + + + 1 year + 1 год + + + Never + Никогда + + + + \ No newline at end of file diff --git a/app/Resources/views/base.html.twig b/app/Resources/views/base.html.twig index bafd28d..54bb649 100644 --- a/app/Resources/views/base.html.twig +++ b/app/Resources/views/base.html.twig @@ -1,13 +1,52 @@ - - - - - {% block title %}Welcome!{% endblock %} - {% block stylesheets %}{% endblock %} - - - - {% block body %}{% endblock %} - {% block javascripts %}{% endblock %} - - +{% extends '::layout.html.twig' -%} + +{%- block css -%} + {{- parent() -}} + +{% endblock %} +{% block javascript %} + {{- parent() -}} + +{% endblock %} + +{%- block header -%} + +{%- endblock -%} + +{%- block sidebar -%} + {{ render(controller('SkobkinCopyPasteBundle:CopyPaste:sidebar')) }} +{%- endblock -%} + +{% block content %}{% endblock %} diff --git a/app/Resources/views/layout.html.twig b/app/Resources/views/layout.html.twig new file mode 100644 index 0000000..8bbbd95 --- /dev/null +++ b/app/Resources/views/layout.html.twig @@ -0,0 +1,45 @@ + + + + {%- block head -%} + {% block title %}CopyPaste{% endblock %} + + + {%- block css -%} + + {%- endblock -%} + {# HTML5 Shim and Respond.js add IE8 support of HTML5 elements and media queries #} + {% include 'BraincraftedBootstrapBundle::ie8-support.html.twig' %} + {%- endblock -%} + + + {% block body %} +
+ {% block containter %} +
+ {% block header %}{% endblock %} +
+ +
+
+ {% block sidebar %}{% endblock %} +
+
+ {% block content %}{% endblock %} +
+
+ +
+ {% block footer %}{% endblock %} +
+ {% endblock %} +
+ + {% block javascript %} + + {# Include all JavaScripts, compiled by Assetic #} + + {% endblock %} + {% endblock %} + + \ No newline at end of file diff --git a/app/Resources/views/sidebar.html.twig b/app/Resources/views/sidebar.html.twig new file mode 100644 index 0000000..9be9cee --- /dev/null +++ b/app/Resources/views/sidebar.html.twig @@ -0,0 +1,12 @@ +
+
+
{{ 'sidebar_title' | trans() }}
+ +
+
diff --git a/app/config/config.yml b/app/config/config.yml index b916725..804aaaa 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -5,7 +5,7 @@ imports: framework: #esi: ~ - #translator: { fallbacks: ["%locale%"] } + translator: { fallbacks: ["%locale%"] } secret: "%secret%" router: resource: "%kernel.root_dir%/config/routing.yml" @@ -37,12 +37,32 @@ assetic: bundles: [ ] #java: /usr/bin/java filters: + less: + node: "%nodejs_executable%" + node_paths: %nodejs_paths% + apply_to: "\.less$" cssrewrite: ~ #closure: # jar: "%kernel.root_dir%/Resources/java/compiler.jar" #yui_css: # jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar" +braincrafted_bootstrap: + output_dir: bootstrap + assets_dir: %kernel.root_dir%/../vendor/twbs/bootstrap + jquery_path: %kernel.root_dir%/../vendor/components/jquery/jquery.js + less_filter: less + fonts_dir: %kernel.root_dir%/../web/bootstrap/fonts + auto_configure: + assetic: true + twig: true + knp_menu: true + knp_paginator: true + customize: + variables_file: ~ + bootstrap_output: %kernel.root_dir%/Resources/less/bootstrap.less + bootstrap_template: BraincraftedBootstrapBundle:Bootstrap:bootstrap.less.twig + # Doctrine Configuration doctrine: dbal: diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index 1da778f..497b294 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -18,3 +18,8 @@ parameters: # A secret key that's used to generate certain security-related tokens secret: ThisTokenIsNotSoSecretChangeIt + + # Other stuff + nodejs_executable: /usr/bin/node + nodejs_paths: [/usr/lib/node_modules/] + \ No newline at end of file diff --git a/composer.json b/composer.json index f675302..474c990 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,10 @@ "doctrine/doctrine-migrations-bundle": "2.1.*@dev", "doctrine/doctrine-fixtures-bundle": "2.2.*", "theodordiaconu/geshi": "dev-master", - "theodordiaconu/geshi-bundle" : "dev-master" + "theodordiaconu/geshi-bundle" : "dev-master", + "braincrafted/bootstrap-bundle": "dev-master", + "twbs/bootstrap": "3.3.*@dev", + "components/jquery": "dev-master" }, "require-dev": { "sensio/generator-bundle": "~2.3" diff --git a/composer.lock b/composer.lock index 071deca..b263cff 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,110 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "e36598c346f19a7d683f1c1f3d58399f", + "hash": "06492598a20c478faba66c1691e8490c", "packages": [ + { + "name": "braincrafted/bootstrap-bundle", + "version": "dev-master", + "target-dir": "Braincrafted/Bundle/BootstrapBundle", + "source": { + "type": "git", + "url": "https://github.com/braincrafted/bootstrap-bundle.git", + "reference": "2dc17466f95a035869b51c845024f6a92bc1d6d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/braincrafted/bootstrap-bundle/zipball/2dc17466f95a035869b51c845024f6a92bc1d6d1", + "reference": "2dc17466f95a035869b51c845024f6a92bc1d6d1", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/console": "~2.3", + "symfony/finder": "~2.3", + "symfony/form": "~2.3", + "symfony/framework-bundle": "~2.3", + "symfony/twig-bundle": "~2.3" + }, + "require-dev": { + "knplabs/knp-menu": "~2.0@alpha", + "knplabs/knp-menu-bundle": "~2.0@alpha", + "knplabs/knp-paginator-bundle": "dev-master", + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "3.7.*", + "symfony/assetic-bundle": "~2.3" + }, + "suggest": { + "knplabs/knp-menu": "Required to use KnpMenuBundle.", + "knplabs/knp-menu-bundle": "BraincraftedBootstrapBundle styles the menus provided by KnpMenuBundle.", + "knplabs/knp-paginator-bundle": "BraincraftedBootstrapBundle styles the pagination provided by KnpPaginatorBundle.", + "twbs/bootstrap": "Twitter Bootstrap provides the assets (images, CSS and JS)" + }, + "type": "symfony-bundle", + "autoload": { + "psr-0": { + "Braincrafted\\Bundle\\BootstrapBundle": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florian Eckerstorfer", + "email": "florian@eckerstorfer.co", + "homepage": "http://florian.ec" + } + ], + "description": "BraincraftedBootstrapBundle integrates Bootstrap into Symfony2 by providing templates, Twig extensions, services and commands.", + "keywords": [ + "bootstrap" + ], + "time": "2015-01-31 10:32:31" + }, + { + "name": "components/jquery", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/components/jquery.git", + "reference": "f8edb647d1833fd5d57410ab70860f03534cb2e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/components/jquery/zipball/f8edb647d1833fd5d57410ab70860f03534cb2e8", + "reference": "f8edb647d1833fd5d57410ab70860f03534cb2e8", + "shasum": "" + }, + "type": "component", + "extra": { + "component": { + "scripts": [ + "jquery.js" + ], + "files": [ + "jquery.min.js", + "jquery.min.map", + "jquery-migrate.js", + "jquery-migrate.min.js" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Resig", + "email": "jeresig@gmail.com" + } + ], + "description": "jQuery JavaScript Library", + "homepage": "http://jquery.com", + "time": "2014-12-23 06:48:45" + }, { "name": "doctrine/annotations", "version": "v1.2.3", @@ -816,12 +918,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "058a4635ac9b80a5b1997df28a754e64b1425a1d" + "reference": "312fb5939d5b9dbd66e515374533933e7c5cc8a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/058a4635ac9b80a5b1997df28a754e64b1425a1d", - "reference": "058a4635ac9b80a5b1997df28a754e64b1425a1d", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/312fb5939d5b9dbd66e515374533933e7c5cc8a6", + "reference": "312fb5939d5b9dbd66e515374533933e7c5cc8a6", "shasum": "" }, "require": { @@ -866,7 +968,7 @@ "database", "migrations" ], - "time": "2015-02-19 08:13:05" + "time": "2015-03-02 18:28:52" }, { "name": "doctrine/orm", @@ -1626,12 +1728,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/symfony.git", - "reference": "755ea09a440c1b8ea560e403b442fc9f53e0ae93" + "reference": "12cf04f8f341573a146cdc32722f72ae2deb0579" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/symfony/zipball/755ea09a440c1b8ea560e403b442fc9f53e0ae93", - "reference": "755ea09a440c1b8ea560e403b442fc9f53e0ae93", + "url": "https://api.github.com/repos/symfony/symfony/zipball/12cf04f8f341573a146cdc32722f72ae2deb0579", + "reference": "12cf04f8f341573a146cdc32722f72ae2deb0579", "shasum": "" }, "require": { @@ -1735,7 +1837,7 @@ "keywords": [ "framework" ], - "time": "2015-03-02 10:21:01" + "time": "2015-03-03 08:32:45" }, { "name": "theodordiaconu/geshi", @@ -1817,6 +1919,57 @@ ], "time": "2013-03-29 12:54:06" }, + { + "name": "twbs/bootstrap", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/twbs/bootstrap.git", + "reference": "ddd09f6fe773c4a99929c169b80882ed955a6227" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/ddd09f6fe773c4a99929c169b80882ed955a6227", + "reference": "ddd09f6fe773c4a99929c169b80882ed955a6227", + "shasum": "" + }, + "replace": { + "twitter/bootstrap": "self.version" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jacob Thornton", + "email": "jacobthornton@gmail.com" + }, + { + "name": "Mark Otto", + "email": "markdotto@gmail.com" + } + ], + "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", + "homepage": "http://getbootstrap.com", + "keywords": [ + "JS", + "css", + "framework", + "front-end", + "less", + "mobile-first", + "responsive", + "web" + ], + "time": "2015-03-03 06:03:42" + }, { "name": "twig/extensions", "version": "v1.2.0", @@ -1984,7 +2137,10 @@ "doctrine/migrations": 20, "doctrine/doctrine-migrations-bundle": 20, "theodordiaconu/geshi": 20, - "theodordiaconu/geshi-bundle": 20 + "theodordiaconu/geshi-bundle": 20, + "braincrafted/bootstrap-bundle": 20, + "twbs/bootstrap": 20, + "components/jquery": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Controller/CopypasteController.php b/src/Skobkin/Bundle/CopyPasteBundle/Controller/CopypasteController.php new file mode 100644 index 0000000..c815ffd --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Controller/CopypasteController.php @@ -0,0 +1,154 @@ +createCreateForm($paste); + $form->handleRequest($request); + + if ($form->isValid()) { + $em = $this->getDoctrine()->getManager(); + + if ($form->get('private')->getData()) { + // Private paste + $paste->setSecret(substr(md5($paste->getText()), 0, 16)); + } + + $expire = (int) $form->get('expire')->getData(); + //var_dump($expire); exit(); + if ($expire) { + $dateExpire = new \DateTime(); + $dateExpire->add(new \DateInterval('PT' . $expire . 'S')); + $paste->setDateExpire($dateExpire); + } + + $paste->setIp($request->getClientIp()); + + $em->persist($paste); + $em->flush(); + + if ($paste->isPrivate()) { + return $this->redirect($this->generateUrl('copypaste_show_private', ['id' => $paste->getId(), 'secret' => $paste->getSecret()])); + } else { + return $this->redirect($this->generateUrl('copypaste_show_public', ['id' => $paste->getId()])); + } + } + + throw new $this->createAccessDeniedException('Sorry :('); + } + + /** + * Creates a form to create a Copypaste entity. + * + * @param Copypaste $entity The entity + * + * @return Form The form + */ + private function createCreateForm(Copypaste $entity) + { + $form = $this->createForm(new CopypasteType(), $entity, [ + 'action' => $this->generateUrl('copypaste_create'), + 'method' => 'POST', + ]); + + $form->add('submit', 'submit', array('label' => 'Create')); + + return $form; + } + + /** + * Displays a form to create a new Copypaste entity. + * + * @return Response + */ + public function newAction() + { + $paste = new Copypaste(); + $createForm = $this->createCreateForm($paste); + + return $this->render('SkobkinCopyPasteBundle:Copypaste:new.html.twig', [ + 'entity' => $paste, + 'form_create' => $createForm->createView(), + ]); + } + + /** + * Finds and displays a Copypaste entity. + * + * @return Response + */ + public function showAction($id, $secret) + { + $em = $this->getDoctrine()->getManager(); + + /* @var $paste Copypaste */ + $paste = $em->getRepository('SkobkinCopyPasteBundle:Copypaste')->findOneBy([ + 'id' =>$id, + 'secret' => $secret + ]); + + if (!$paste) { + throw $this->createNotFoundException('Unable to find copypaste.'); + } + + $editForm = $this->createCreateForm($paste); + + /* @var $highlighter GeshiHighlighter */ + $highlighter = $this->get('dt_geshi.highlighter'); + + $highlightedCode = $highlighter->highlight($paste->getText(), $paste->getLanguage()->getCode(), function(GeSHi $geshi) { + $geshi->set_header_type(GESHI_HEADER_PRE); + $geshi->enable_line_numbers(GESHI_NO_LINE_NUMBERS); + }); + + return $this->render('SkobkinCopyPasteBundle:Copypaste:show.html.twig', [ + 'paste' => $paste, + 'highlighted_text' => $highlightedCode, + 'form_create' => $editForm->createView(), + ]); + } + + /** + * Main page + * + * @return Response + */ + public function sidebarAction() + { + $em = $this->getDoctrine()->getManager(); + + $pastes = $em->getRepository('SkobkinCopyPasteBundle:Copypaste')->findBy( + ['secret' => null], + ['id' => 'DESC'], + // @todo move to the config + 15 + ); + + return $this->render('::sidebar.html.twig', ['pastes' => $pastes]); + } + + +} diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Entity/Copypaste.php b/src/Skobkin/Bundle/CopyPasteBundle/Entity/Copypaste.php index ed58077..071be71 100644 --- a/src/Skobkin/Bundle/CopyPasteBundle/Entity/Copypaste.php +++ b/src/Skobkin/Bundle/CopyPasteBundle/Entity/Copypaste.php @@ -3,17 +3,16 @@ namespace Skobkin\Bundle\CopyPasteBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; /** * Copypaste * - * @ORM\Table( - * name="copypastes", - * indexes={ - * @ORM\Index(name="idx_expire", columns={"date_expire"}) - * } - * ) + * @ORM\Table(name="copypastes", indexes={ + * @ORM\Index(name="idx_expire", columns={"date_expire"}) + * }) * @ORM\Entity + * @ORM\HasLifecycleCallbacks() */ class Copypaste { @@ -38,7 +37,7 @@ class Copypaste * * @ORM\Column(name="description", type="text", nullable=true) */ - private $description; + private $description = null; /** * @var Language @@ -53,14 +52,14 @@ class Copypaste * * @ORM\Column(name="file_name", type="string", length=128, nullable=true) */ - private $fileName; + private $fileName = null; /** * @var string * * @ORM\Column(name="author", type="string", length=48, nullable=true) */ - private $author; + private $author = null; /** * @var \DateTime @@ -74,11 +73,12 @@ class Copypaste * * @ORM\Column(name="date_expire", type="datetime", nullable=true) */ - private $dateExpire; + private $dateExpire = null; /** * @var string * + * @Assert\Ip * @ORM\Column(name="ip", type="string", length=48, nullable=false) */ private $ip; @@ -88,9 +88,15 @@ class Copypaste * * @ORM\Column(name="secret", type="string", length=16, nullable=true) */ - private $secret; - + private $secret = null; + /** + * @ORM\PrePersist + */ + public function prePersist() + { + $this->datePublished = new \DateTime(); + } /** * Get id @@ -101,6 +107,11 @@ class Copypaste { return $this->id; } + + public function __toString() + { + return $this->id; + } /** * Set text diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Entity/Language.php b/src/Skobkin/Bundle/CopyPasteBundle/Entity/Language.php index 8ef60a1..8eaa8b0 100644 --- a/src/Skobkin/Bundle/CopyPasteBundle/Entity/Language.php +++ b/src/Skobkin/Bundle/CopyPasteBundle/Entity/Language.php @@ -7,13 +7,11 @@ use Doctrine\ORM\Mapping as ORM; /** * Language * - * @ORM\Table( - * name="languages", - * indexes={ - * @ORM\Index(name="idx_enabled", columns={"enabled"}), - * @ORM\Index(name="idx_code", columns={"code"}) - * } - * ) + * @ORM\Table(name="languages", indexes={ + * @ORM\Index(name="idx_enabled", columns={"is_enabled"}), + * @ORM\Index(name="idx_preferred", columns={"is_preferred"}), + * @ORM\Index(name="idx_code", columns={"code"}) + * }) * @ORM\Entity */ class Language @@ -40,13 +38,20 @@ class Language * @ORM\Column(name="code", type="string", length=24, nullable=false) */ private $code; + + /** + * @var boolean + * + * @ORM\Column(name="is_preferred", type="boolean", nullable=false) + */ + private $isPreferred = false; /** * @var boolean * - * @ORM\Column(name="enabled", type="boolean", nullable=false) + * @ORM\Column(name="is_enabled", type="boolean", nullable=false) */ - private $enabled; + private $isEnabled; @@ -59,6 +64,11 @@ class Language { return $this->id; } + + public function __toString() + { + return $this->name; + } /** * Set name @@ -86,7 +96,7 @@ class Language /** * Set code * - * @param string $file + * @param string $code * @return Lang */ public function setCode($code) @@ -107,35 +117,47 @@ class Language } /** - * Set enabled + * Set isEnabled * - * @param boolean $enabled + * @param boolean $isEnabled * @return Lang */ - public function setEnabled($enabled) + public function setIsEnabled($isEnabled) { - $this->enabled = $enabled; + $this->isEnabled = $isEnabled; return $this; } /** - * Get enabled + * Get isEnabled * * @return boolean */ - public function getEnabled() + public function getIsEnabled() { - return $this->enabled; + return $this->isEnabled; } /** - * Check if language is enabled + * Get isPreferred * * @return boolean */ - public function isEnabled() + function getIsPreferred() { - return $this->enabled; + return $this->isPreferred; + } + + /** + * Set isPreferred + * + * @param boolean $isPreferred + */ + function setIsPreferred($isPreferred) + { + $this->isPreferred = $isPreferred; + + return $this; } } diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Form/CopypasteType.php b/src/Skobkin/Bundle/CopyPasteBundle/Form/CopypasteType.php new file mode 100644 index 0000000..c1d69a4 --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Form/CopypasteType.php @@ -0,0 +1,90 @@ +add('text', 'textarea', ['label' => 'paste_add_form_text']) + ->add('description', 'textarea', [ + 'label' => 'paste_add_form_description', + 'required' => false, + ]) + ->add('fileName', 'text', [ + 'label' => 'paste_add_form_file_name', + 'required' => false, + ]) + ->add('author', 'text', [ + 'label' => 'paste_add_form_author', + 'required' => false, + ]) + ->add('expire', 'choice', [ + 'label' => 'paste_add_form_expire', + 'mapped' => false, + // @todo move to config + 'choices' => [ + 300 => '5 minutes', + 3600 => '1 hour', + 10800 => '3 hours', + 43200 => '12 hours', + 86400 => '1 day', + 604800 => '1 week', + 2419200 => '1 month', + 7257600 => '3 months', + 14515200 => '6 months', + 29030400 => '1 year', + 0 => 'Never', + ] + ]) + ->add('private', 'checkbox', [ + 'label' => 'paste_add_form_private', + 'required' => false, + 'mapped' => false + ]) + ->add('language', 'entity', [ + 'label' => 'paste_add_form_language', + 'class' => 'Skobkin\Bundle\CopyPasteBundle\Entity\Language', + 'query_builder' => function (EntityRepository $repo) { + /* @var $qb QueryBuilder */ + return $repo->createQueryBuilder('lang') + ->where('lang.isEnabled = :enabled') + ->addOrderBy('lang.isPreferred', 'DESC') + ->addOrderBy('lang.code') + ->setParameter('enabled', true); + }, + //'preferred_choices' => [] + ]) + ->add('actions', 'form_actions') + ; + } + + /** + * @param OptionsResolverInterface $resolver + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults([ + 'data_class' => 'Skobkin\Bundle\CopyPasteBundle\Entity\Copypaste' + ]); + } + + /** + * @return string + */ + public function getName() + { + return 'skobkin_bundle_copypastebundle_copypaste'; + } +} diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing.yml b/src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing.yml index 8b13789..8e4a1d7 100644 --- a/src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing.yml +++ b/src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing.yml @@ -1 +1,3 @@ - +skobkin_copy_paste: + resource: "@SkobkinCopyPasteBundle/Resources/config/routing/copypaste.yml" + prefix: / diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing/copypaste.yml b/src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing/copypaste.yml new file mode 100644 index 0000000..45938d3 --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Resources/config/routing/copypaste.yml @@ -0,0 +1,21 @@ +copypaste_show_public: + path: /{id} + defaults: { _controller: "SkobkinCopyPasteBundle:Copypaste:show", secret: null } + requirements: + id: \d+ + +copypaste_show_private: + path: /{id}/{secret} + defaults: { _controller: "SkobkinCopyPasteBundle:Copypaste:show" } + requirements: + id: \d+ + secret: \w{16} + +copypaste_new: + path: / + defaults: { _controller: "SkobkinCopyPasteBundle:Copypaste:new" } + +copypaste_create: + path: /create + defaults: { _controller: "SkobkinCopyPasteBundle:Copypaste:create" } + methods: POST \ No newline at end of file diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/css/base.css b/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/css/base.css new file mode 100644 index 0000000..bdfb38b --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/css/base.css @@ -0,0 +1,44 @@ +/* + Created on : Mar 3, 2015, 6:53:42 PM + Author : Alexey Skobkin +*/ + +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; + padding-top: 70px; +} + +.navbar-brand { + font-weight: bold; +} + +/* @todo Fix crutches and bycicles */ +.editor-head button[type="submit"] { + margin-top: 24px; +} + +.editor-description { + margin-top: 10px; +} + +.editor-head .form-group, .editor-head .checkbox { + margin-bottom: 0; +} + +.paste-main #tab-paste-edit, .paste-main #tab-paste-view { + padding-top: 10px; +} + +.paste-description-content { + margin-top: 10px; +} + +.paste-main .paste-tabs { + margin-top: 10px; +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/images/favicon.ico b/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e59f188229838b422a67e0d9917bf36e97c48733 GIT binary patch literal 1406 zcmeH@u?>JA6ht2}CYDszmX78aY{Dw6WrcT8n7D!;a`3o+GzUpLXU4i#*hQyg>yQ$g q&H?lO^6Lgg1JOV<@S6sDhqgdTQ12;wUEJgq&8ps(J!hQtALkx+{T9Oj literal 0 HcmV?d00001 diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/js/copypaste.js b/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/js/copypaste.js new file mode 100644 index 0000000..977dfde --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Resources/public/js/copypaste.js @@ -0,0 +1,9 @@ +$(function () { + var $qr_link = $('#paste-qr-code'); + $qr_link.popover({ + content: '', + placement: 'bottom', + trigger: 'focus', + html: true + }); +}); \ No newline at end of file diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/new.html.twig b/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/new.html.twig new file mode 100644 index 0000000..69f87c5 --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/new.html.twig @@ -0,0 +1,6 @@ +{% extends '::base.html.twig' %} + +{% block content %} + {# This form recieves form_create object from current context #} + {% include 'SkobkinCopyPasteBundle:Form:form_paste_create.html.twig' %} +{% endblock %} diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/show.html.twig b/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/show.html.twig new file mode 100644 index 0000000..dff9048 --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Copypaste/show.html.twig @@ -0,0 +1,76 @@ +{% extends '::base.html.twig' %} + +{% block content %} +
+
+
+ {% if paste.author %} + {{ paste.author }} + {% else %} + anonymous + {% endif %} +
+
{{ paste.language }}
+
{{ paste.datePublished | date('Y.m.d H:i') }}
+
+ {% if paste.dateExpire %} + {{ paste.dateExpire | date('Y.m.d H:i') }} + {% endif %} +
+ + +
+ {% if paste.description %} +
+
+
+ {{ paste.description | nl2br }} +
+
+
+ {% endif %} +
+
+
+ + + + +
+
+
+ {{ highlighted_text | raw }} +
+
+
+ {# This form recieves form_create object from current context #} + {% include 'SkobkinCopyPasteBundle:Form:form_paste_create.html.twig' %} +
+
+
+
+
+
+{% endblock %} diff --git a/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Form/form_paste_create.html.twig b/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Form/form_paste_create.html.twig new file mode 100644 index 0000000..25fc7c2 --- /dev/null +++ b/src/Skobkin/Bundle/CopyPasteBundle/Resources/views/Form/form_paste_create.html.twig @@ -0,0 +1,42 @@ +
+ {{ form_start(form_create) }} +
+
+
+ {{ form_row(form_create.author) }} +
+
+ {{ form_row(form_create.language) }} +
+
+ {{ form_row(form_create.expire) }} + {{ form_row(form_create.private) }} +
+
+
+ {{ form_widget(form_create.submit) }} +
+
+
+
+
+ {{ form_label(form_create.text) }} + {{ form_errors(form_create.text) }} + {{ form_widget(form_create.text, {'attr': {'rows': 10}}) }} +
+
+
+
+ {{ form_row(form_create.fileName) }} +
+
+
+
+ {{ form_label(form_create.description) }} + {{ form_errors(form_create.description) }} + {{ form_widget(form_create.description) }} +
+
+
+ {{ form_end(form_create) }} +
\ No newline at end of file diff --git a/web/apple-touch-icon.png b/web/apple-touch-icon.png deleted file mode 100644 index 11f17e6d89ee3b416218ede42b66ec1dd81507f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10784 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 f!