From e510fc3203d15cc56b444d7a6603d958dabfdee7 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 4 Dec 2024 12:28:23 +0100 Subject: [PATCH] Add Announcements (#603) * add announcement entity * add announcement partial and show in home page * add template for donation * improve buttons in announcement partials * add close button * add permissions and copies for announcement admin * add necessary FormsProcessors and EntityNormalizer to use Entity instead of Model! * add Announcement admin * fix announcement api controller * add slider for announcements * add lang table and some fixes * add Announcement Model * change repository to use model instead of entity * change admin controller and views to use new model * change use of entity to model in public views of announcements * add missing translations * add announcements without donation to projects landing * change to now show announcement and make it appear instead of remove it * add announcements to channel layouts * add announcements to blog * change name of localStorage variable * remove entity announcement * update datetime of migration * fix error on announcement admin form --- Resources/permissions.yml | 4 + Resources/roles.yml | 1 + .../templates/default/channel/layout.php | 18 +- .../responsive/admin/announcements/edit.php | 25 +++ .../responsive/admin/announcements/layout.php | 14 ++ .../responsive/admin/announcements/list.php | 20 ++ .../templates/responsive/blog/layout.php | 2 + .../channel/partials/javascript.php | 2 +- Resources/templates/responsive/layout.php | 1 + .../partials/components/announcements.php | 28 +++ .../announcements/partials/donation.php | 8 + .../announcements/partials/general.php | 3 + .../announcements/partials/project.php | 3 + .../templates/responsive/project/layout.php | 1 + .../project/partials/javascript.php | 3 +- .../20240920152538_goteo_announcement.php | 78 ++++++++ public/assets/js/components/announcements.js | 60 ++++++ public/assets/sass/common.scss | 1 + .../assets/sass/components/_announcement.scss | 45 +++++ public/assets/sass/layouts/_channel.scss | 11 ++ .../responsive/blog/partials/javascript.php | 6 +- .../responsive/home/partials/javascript.php | 2 +- src/Goteo/Application/Config.php | 2 + .../Admin/AnnouncementAdminController.php | 131 +++++++++++++ .../Api/AnnouncementApiController.php | 80 ++++++++ src/Goteo/Controller/BlogController.php | 15 +- src/Goteo/Controller/ChannelController.php | 7 +- src/Goteo/Controller/IndexController.php | 10 +- src/Goteo/Controller/ProjectController.php | 7 +- src/Goteo/Core/Controller.php | 25 +++ .../Library/Forms/EntityFormProcessor.php | 27 +++ .../Library/Forms/Model/AnnouncementForm.php | 128 ++++++++++++ src/Goteo/Model/Announcement.php | 122 ++++++++++++ .../Repository/AnnouncementRepository.php | 183 ++++++++++++++++++ src/Goteo/Util/Foil/Extension/ModelsData.php | 8 + src/Goteo/Util/Form/FormFinder.php | 15 +- .../Util/ModelNormalizer/EntityNormalizer.php | 35 ++++ .../Util/ModelNormalizer/ModelNormalizer.php | 85 ++++---- .../Transformer/AnnouncementTransformer.php | 45 +++++ .../Transformer/EntityTransformer.php | 106 ++++++++++ src/Routes/api_routes.php | 6 + translations/ca/admin.yml | 9 +- translations/ca/general.yml | 4 + translations/ca/labels.yml | 1 + translations/ca/roles.yml | 1 + translations/ca/translator.yml | 2 + translations/en/admin.yml | 7 + translations/en/general.yml | 4 + translations/en/labels.yml | 1 + translations/en/roles.yml | 1 + translations/en/translator.yml | 2 + translations/es/admin.yml | 8 +- translations/es/general.yml | 4 + translations/es/labels.yml | 1 + translations/es/roles.yml | 1 + 55 files changed, 1342 insertions(+), 77 deletions(-) create mode 100644 Resources/templates/responsive/admin/announcements/edit.php create mode 100644 Resources/templates/responsive/admin/announcements/layout.php create mode 100644 Resources/templates/responsive/admin/announcements/list.php create mode 100644 Resources/templates/responsive/partials/components/announcements.php create mode 100644 Resources/templates/responsive/partials/components/announcements/partials/donation.php create mode 100644 Resources/templates/responsive/partials/components/announcements/partials/general.php create mode 100644 Resources/templates/responsive/partials/components/announcements/partials/project.php create mode 100644 db/migrations/20240920152538_goteo_announcement.php create mode 100644 public/assets/js/components/announcements.js create mode 100644 public/assets/sass/components/_announcement.scss create mode 100644 src/Goteo/Controller/Admin/AnnouncementAdminController.php create mode 100644 src/Goteo/Controller/Api/AnnouncementApiController.php create mode 100644 src/Goteo/Library/Forms/EntityFormProcessor.php create mode 100644 src/Goteo/Library/Forms/Model/AnnouncementForm.php create mode 100644 src/Goteo/Model/Announcement.php create mode 100644 src/Goteo/Repository/AnnouncementRepository.php create mode 100644 src/Goteo/Util/ModelNormalizer/EntityNormalizer.php create mode 100644 src/Goteo/Util/ModelNormalizer/Transformer/AnnouncementTransformer.php create mode 100644 src/Goteo/Util/ModelNormalizer/Transformer/EntityTransformer.php diff --git a/Resources/permissions.yml b/Resources/permissions.yml index 4f953ef170..cd13733d87 100644 --- a/Resources/permissions.yml +++ b/Resources/permissions.yml @@ -252,6 +252,10 @@ admin-module-communication: # Can access to module CommunicationAdminController model: null relational: null +admin-module-announcements: # Can access to module AnnouncementAdminController + model: null + relational: null + receive-test-communications: # Can receive test communications model: null relational: null diff --git a/Resources/roles.yml b/Resources/roles.yml index 95fe3baba8..38032e0836 100644 --- a/Resources/roles.yml +++ b/Resources/roles.yml @@ -135,6 +135,7 @@ admin: - edit-any-matcher # View any editing matcher - remove-any-matcher # Remove any matcher - admin-module-channels # Can access to channels admin modules + - admin-module-announcements # Can access to announcements admin modules level: 70 superadmin: diff --git a/Resources/templates/default/channel/layout.php b/Resources/templates/default/channel/layout.php index d7cd791e1b..4e214b530f 100644 --- a/Resources/templates/default/channel/layout.php +++ b/Resources/templates/default/channel/layout.php @@ -1,12 +1,14 @@ channel; +$channel = $this->channel; $this->layout("layout", [ 'bodyClass' => 'channel home', - 'title' => $this->text('regular-channel').' '.$channel->name, + 'title' => $this->text('regular-channel') . ' ' . $channel->name, 'meta_description' => $channel->description - ]); +]); + +$this->supply('announcements', $this->insert("partials/components/announcements")); $this->section('content'); @@ -17,9 +19,9 @@
- side_order as $sideitem => $sideitemName) { + side_order as $sideitem => $sideitemName) { echo $this->insert("channel/partials/side/$sideitem"); - } ?> + } ?>
@@ -32,8 +34,8 @@ section('footer') ?> append() ?> diff --git a/Resources/templates/responsive/admin/announcements/edit.php b/Resources/templates/responsive/admin/announcements/edit.php new file mode 100644 index 0000000000..61d1bda43d --- /dev/null +++ b/Resources/templates/responsive/admin/announcements/edit.php @@ -0,0 +1,25 @@ +layout('admin/announcements/layout'); + +$this->section('admin-search-box-addons'); +?> + text('admin-back-list') ?> + +replace() ?> + +section('admin-container-body') ?> + +announcement->id; +?> + +

text('admin-announcement-edit', "#{$id}") : $this->text('admin-announcement-add') ?>

+ + +form_form($this->raw('form')) ?> + +
+ + +replace() ?> diff --git a/Resources/templates/responsive/admin/announcements/layout.php b/Resources/templates/responsive/admin/announcements/layout.php new file mode 100644 index 0000000000..c2d877c47b --- /dev/null +++ b/Resources/templates/responsive/admin/announcements/layout.php @@ -0,0 +1,14 @@ +layout('admin/container'); + +$this->section('admin-container-head'); + +?> +

text('admin-announcements') ?>

+ + insert('admin/partials/search_box') ?> + + supply('admin-announcements-head') ?> + +replace() ?> diff --git a/Resources/templates/responsive/admin/announcements/list.php b/Resources/templates/responsive/admin/announcements/list.php new file mode 100644 index 0000000000..fb3c8ea21b --- /dev/null +++ b/Resources/templates/responsive/admin/announcements/list.php @@ -0,0 +1,20 @@ +layout('admin/announcements/layout'); + +$this->section('admin-search-box-addons'); +?> + text('admin-announcement-add') ?> + +replace() ?> + +section('admin-container-body') ?> + +
text('admin-list-total', $this->total) ?>
+ +insert('admin/partials/material_table', ['list' => $this->model_list_entries($this->list, ['id', 'title', 'type', 'active', 'actions'])]) ?> + + + + +replace() ?> diff --git a/Resources/templates/responsive/blog/layout.php b/Resources/templates/responsive/blog/layout.php index 081f5a9a3c..01ff8f6203 100644 --- a/Resources/templates/responsive/blog/layout.php +++ b/Resources/templates/responsive/blog/layout.php @@ -27,6 +27,8 @@ $this->section('content'); +$this->supply('announcements', $this->insert("partials/components/announcements")); + ?>
diff --git a/Resources/templates/responsive/channel/partials/javascript.php b/Resources/templates/responsive/channel/partials/javascript.php index cf522053ce..2359317ae3 100644 --- a/Resources/templates/responsive/channel/partials/javascript.php +++ b/Resources/templates/responsive/channel/partials/javascript.php @@ -6,4 +6,4 @@ - + diff --git a/Resources/templates/responsive/layout.php b/Resources/templates/responsive/layout.php index 0b3c5b85d9..3e9b866526 100644 --- a/Resources/templates/responsive/layout.php +++ b/Resources/templates/responsive/layout.php @@ -77,6 +77,7 @@ supply('header', $this->insert("partials/header")) ?> supply('search', $this->insert("partials/search")) ?> + supply('announcements', $this->insert("partials/components/announcements")) ?>
supply('sidebar', $this->insert("partials/sidebar", ['sidebarMenu' => $sidebar])) ?> diff --git a/Resources/templates/responsive/partials/components/announcements.php b/Resources/templates/responsive/partials/components/announcements.php new file mode 100644 index 0000000000..4c6e550ce6 --- /dev/null +++ b/Resources/templates/responsive/partials/components/announcements.php @@ -0,0 +1,28 @@ +announcements; +if ($announcements) : + + + $id = md5(serialize($announcements)); +?> +
+ + +
+
+ +
+
+

title ?>

+

description ?>

+
+
+ type ?> + insert("partials/components/announcements/partials/$type", ['announcement' => $announcement]) ?> +
+
+ +
+
+
+ diff --git a/Resources/templates/responsive/partials/components/announcements/partials/donation.php b/Resources/templates/responsive/partials/components/announcements/partials/donation.php new file mode 100644 index 0000000000..cb0037111b --- /dev/null +++ b/Resources/templates/responsive/partials/components/announcements/partials/donation.php @@ -0,0 +1,8 @@ +announcement; ?> + + diff --git a/Resources/templates/responsive/partials/components/announcements/partials/general.php b/Resources/templates/responsive/partials/components/announcements/partials/general.php new file mode 100644 index 0000000000..00cbc69081 --- /dev/null +++ b/Resources/templates/responsive/partials/components/announcements/partials/general.php @@ -0,0 +1,3 @@ +announcement; ?> + +cta_text ?> diff --git a/Resources/templates/responsive/partials/components/announcements/partials/project.php b/Resources/templates/responsive/partials/components/announcements/partials/project.php new file mode 100644 index 0000000000..00cbc69081 --- /dev/null +++ b/Resources/templates/responsive/partials/components/announcements/partials/project.php @@ -0,0 +1,3 @@ +announcement; ?> + +cta_text ?> diff --git a/Resources/templates/responsive/project/layout.php b/Resources/templates/responsive/project/layout.php index 948d651315..3bca6665c6 100644 --- a/Resources/templates/responsive/project/layout.php +++ b/Resources/templates/responsive/project/layout.php @@ -57,6 +57,7 @@ echo $this->insert('project/widgets/micro', ['project' => $project, 'admin' => $this->admin]); $this->replace(); +$this->supply('announcements', $this->insert("partials/components/announcements")); $this->section('content'); diff --git a/Resources/templates/responsive/project/partials/javascript.php b/Resources/templates/responsive/project/partials/javascript.php index f24ad943b4..ef2c80b6fc 100644 --- a/Resources/templates/responsive/project/partials/javascript.php +++ b/Resources/templates/responsive/project/partials/javascript.php @@ -2,4 +2,5 @@ - \ No newline at end of file + + diff --git a/db/migrations/20240920152538_goteo_announcement.php b/db/migrations/20240920152538_goteo_announcement.php new file mode 100644 index 0000000000..f571101109 --- /dev/null +++ b/db/migrations/20240920152538_goteo_announcement.php @@ -0,0 +1,78 @@ + { + $announcements.style.display = 'none'; + localStorage.setItem(itemId, true); + }); + + if (!localStorage.getItem(itemId)) + $announcements.style.display = 'block'; + + $(".slider-announcements").slick({ + dots: false, + infinite: true, + autoplay: false, + autoplaySpeed: 7000, + speed: 500, + fade: true, + cssEase: "linear", + responsive: [ + { + breakpoint: 500, + settings: { + dots: true, + arrows: false + } + } + ] + }); +}); diff --git a/public/assets/sass/common.scss b/public/assets/sass/common.scss index eab29722c8..fc1c68fd62 100644 --- a/public/assets/sass/common.scss +++ b/public/assets/sass/common.scss @@ -26,6 +26,7 @@ @import 'components/values'; @import 'components/workshops_slider'; @import 'components/dataset'; +@import 'components/announcement'; @import 'misc/misc'; @import 'misc/charts'; diff --git a/public/assets/sass/components/_announcement.scss b/public/assets/sass/components/_announcement.scss new file mode 100644 index 0000000000..b0179ff683 --- /dev/null +++ b/public/assets/sass/components/_announcement.scss @@ -0,0 +1,45 @@ +.announcement { + background-color: $background-dark-lilac; + color: $color-white; + display: none; + + h1 { + text-wrap: pretty; + } + + p { + text-wrap: balance; + } + + button.close { + position: relative; + top: 1em; + right: 1em; + z-index: 1; + opacity: 0.8; + color: $color-white; + } + + .container { + margin-top: 1em; + margin-bottom: 1em; + + .row { + display: flex; + + @media (max-width: $breakpoint-sm) { + display: block; + } + } + + .cta { + margin: auto; + } + + .grid-donation { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1em + } + } +} diff --git a/public/assets/sass/layouts/_channel.scss b/public/assets/sass/layouts/_channel.scss index 9622d979bb..dc8b0824d0 100644 --- a/public/assets/sass/layouts/_channel.scss +++ b/public/assets/sass/layouts/_channel.scss @@ -35,6 +35,11 @@ body.channel { } } + nav.top-navbar:has(~ .announcement:not([style*="display:none"])) { + min-height: fit-content; + padding-bottom: 1em; + } + background: $color-white; .btn { @@ -326,6 +331,12 @@ body.channel { } .top-navbar { + + :has(+ .announcement) { + min-height: fit-content; + padding-bottom: 1em; + } + &.premium { padding-top: 2em; } diff --git a/public/templates/responsive/blog/partials/javascript.php b/public/templates/responsive/blog/partials/javascript.php index ee4732b454..6e21e8d6f6 100644 --- a/public/templates/responsive/blog/partials/javascript.php +++ b/public/templates/responsive/blog/partials/javascript.php @@ -1,8 +1,6 @@ - + + - - - diff --git a/public/templates/responsive/home/partials/javascript.php b/public/templates/responsive/home/partials/javascript.php index f84a37c5f9..7acb35b003 100644 --- a/public/templates/responsive/home/partials/javascript.php +++ b/public/templates/responsive/home/partials/javascript.php @@ -6,5 +6,5 @@ + - diff --git a/src/Goteo/Application/Config.php b/src/Goteo/Application/Config.php index 453c0c786c..71d64126f6 100644 --- a/src/Goteo/Application/Config.php +++ b/src/Goteo/Application/Config.php @@ -16,6 +16,7 @@ use Goteo\Application\Config\YamlSettingsLoader; use Goteo\Console\UsersSend; use Goteo\Controller\Admin\AccountsSubController; +use Goteo\Controller\Admin\AnnouncementAdminController; use Goteo\Controller\Admin\BannersSubController; use Goteo\Controller\Admin\BlogAdminController; use Goteo\Controller\Admin\CategoriesAdminController; @@ -346,6 +347,7 @@ static public function addAdminControllers() AdminController::addSubController(ChannelProjectsAdminController::class); AdminController::addSubController(ImpactDataAdminController::class); AdminController::addSubController(SubscriptionsAdminController::class); + AdminController::addSubController(AnnouncementAdminController::class); } static public function addLegacyAdminControllers() diff --git a/src/Goteo/Controller/Admin/AnnouncementAdminController.php b/src/Goteo/Controller/Admin/AnnouncementAdminController.php new file mode 100644 index 0000000000..5ff4a361ae --- /dev/null +++ b/src/Goteo/Controller/Admin/AnnouncementAdminController.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with this source code. + */ + +namespace Goteo\Controller\Admin; + +use Symfony\Component\Routing\Route; +use Symfony\Component\HttpFoundation\Request; + +use Goteo\Application\Exception\ModelException; +use Goteo\Application\Message; +use Goteo\Application\Session; +use Goteo\Application\Exception\ModelNotFoundException; +use Goteo\Model\Announcement; +use Goteo\Library\Text; +use Goteo\Library\Forms\FormModelException; +use Goteo\Library\Forms\Model\AnnouncementForm; +use Goteo\Repository\AnnouncementRepository; + +class AnnouncementAdminController extends AbstractAdminController +{ + protected static $icon = ''; + + protected static AnnouncementRepository $announcementRepository; + + public function __construct() + { + $this->announcementRepository = new AnnouncementRepository(); + } + + public static function getGroup(): string + { + return 'communications'; + } + + public static function getRoutes(): array + { + return [ + new Route( + '/', + ['_controller' => __CLASS__ . "::listAction"] + ), + new Route( + '/edit/{id}', + [ + '_controller' => __CLASS__ . "::editAction" + ] + ), + new Route( + '/add', + ['_controller' => __CLASS__ . "::editAction"] + ), + new Route( + '/delete/{id}', + ['_controller' => __CLASS__ . "::deleteAction"] + ) + ]; + } + + public function listAction(Request $request) + { + + $count = $this->announcementRepository->count(); + $announcements = $this->announcementRepository->getList(0, $count); + + + return $this->viewResponse('admin/announcements/list', [ + 'list' => $announcements, + 'total' => $count, + 'limit' => $count, + ]); + } + + public function editAction(Request $request, int $id = null) + { + + if ($id) { + $announcement = $this->announcementRepository->getById($id); + } else { + $announcement = new Announcement(); + } + + $defaults = []; + + $processor = $this->getModelForm(AnnouncementForm::class, $announcement, $defaults, [], $request); + $processor->createForm(); + + $form = $processor->getForm(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + try { + $processor->save($form); + Message::info(Text::get('admin-' . ($id ? 'edit' : 'add') . '-entry-ok')); + return $this->redirect("/admin/announcement"); + } catch (FormModelException $e) { + Message::error($e->getMessage()); + } + } + + return $this->viewResponse('admin/announcements/edit', [ + 'announcement' => $announcement, + 'form' => $form->createView() + ]); + } + + public function deleteAction(Request $request, int $id) + { + + try { + $announcement = $this->announcementRepository->getById($id); + } catch (ModelNotFoundException $e) { + Message::error($e->getMessage()); + return $this->redirect('/admin/announcement'); + } + + try { + $this->announcementRepository->delete($announcement); + } catch (ModelException $e) { + Message::error($e->getMessage()); + } + + return $this->redirect('/admin/announcement'); + } +} diff --git a/src/Goteo/Controller/Api/AnnouncementApiController.php b/src/Goteo/Controller/Api/AnnouncementApiController.php new file mode 100644 index 0000000000..c035763992 --- /dev/null +++ b/src/Goteo/Controller/Api/AnnouncementApiController.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with this source code. + */ + +namespace Goteo\Controller\Api; + +use Symfony\Component\HttpFoundation\Request; +use Goteo\Application\Exception\ControllerAccessDeniedException; +use Goteo\Application\Exception\ModelNotFoundException; +use Goteo\Application\Message; +use Goteo\Repository\AnnouncementRepository; + +class AnnouncementApiController extends AbstractApiController +{ + + protected AnnouncementRepository $announcementRepository; + + public function __construct() + { + + parent::__construct(); + $this->announcementRepository = new AnnouncementRepository(); + } + + protected function validateAnnouncement(int $id) + { + + if (!$this->user) + throw new ControllerAccessDeniedException(); + + $announcement = $this->announcementRepository->getById($id); + + if (!$announcement) + throw new ModelNotFoundException(); + + if (!$this->is_admin) + throw new ControllerAccessDeniedException(); + + return $announcement; + } + + public function announcementPropertyAction(Request $request, $id, $prop) + { + $announcement = $this->validateAnnouncement($id); + + if (!$announcement) throw new ModelNotFoundException(); + + if (!$prop == 'active') + return []; + + if ($request->isMethod(Request::METHOD_PUT) && $request->request->has('value')) { + + if (!$this->user || !$this->user->hasPerm('admin-module-announcements')) + throw new ControllerAccessDeniedException(); + + $value = $request->request->getBoolean('value'); + $announcement->active = $value; + + $errors = []; + $this->announcementRepository->persist($announcement, $errors); + + $result['value'] = $announcement->active; + + if ($errors = Message::getErrors()) { + $result['error'] = true; + $result['message'] = implode("\n", $errors); + } + if ($messages = Message::getMessages()) { + $result['message'] = implode("\n", $messages); + } + } + return $this->jsonResponse($result); + } +} diff --git a/src/Goteo/Controller/BlogController.php b/src/Goteo/Controller/BlogController.php index 3ab9a90239..132393b323 100644 --- a/src/Goteo/Controller/BlogController.php +++ b/src/Goteo/Controller/BlogController.php @@ -20,14 +20,19 @@ use Goteo\Model\Blog\Post; use Goteo\Model\Blog\Post\Tag; use Goteo\Model\Node\NodePost; +use Goteo\Repository\AnnouncementRepository; use Symfony\Component\HttpFoundation\Request; class BlogController extends Controller { + private AnnouncementRepository $announcementRepository; + public function __construct() { $this->dbReplica(true); $this->dbCache(true); View::setTheme('responsive'); + + $this->announcementRepository = new AnnouncementRepository(); } public function indexAction(Request $request, $section='', $tag='') { @@ -56,6 +61,8 @@ public function indexAction(Request $request, $section='', $tag='') { $blog_sections = Post::getListSections(); $tag = Tag::get($tag); + $announcements = $this->announcementRepository->getActiveList(); + return $this->viewResponse('blog/list', [ 'banners' => $banners, 'list_posts' => $list_posts, @@ -63,7 +70,8 @@ public function indexAction(Request $request, $section='', $tag='') { 'section' => $section, 'tag' => $tag, 'limit' => $limit, - 'total' => $total + 'total' => $total, + 'announcements' => $announcements ]); } @@ -107,11 +115,14 @@ public function postAction(Request $request, $slug) $related_posts = Post::getList(['tag' => $first_key_tags, 'excluded' => $post->id ], true, 0, $limit = 3, false); + $announcements = $this->announcementRepository->getActiveList(); + return $this->viewResponse('blog/post', [ 'post' => $post, 'blog_sections' => $blog_sections, 'related_posts' => $related_posts, - 'author' => $author + 'author' => $author, + 'announcements' => $announcements ]); } } diff --git a/src/Goteo/Controller/ChannelController.php b/src/Goteo/Controller/ChannelController.php index e507b181af..c4e1b82d19 100644 --- a/src/Goteo/Controller/ChannelController.php +++ b/src/Goteo/Controller/ChannelController.php @@ -43,6 +43,7 @@ use Goteo\Model\SocialCommitment; use Goteo\Model\Project\ProjectLocation; use Goteo\Model\Questionnaire; +use Goteo\Repository\AnnouncementRepository; use Symfony\Component\HttpFoundation\Response; @@ -108,6 +109,9 @@ private function setChannelContext($id) $sectionsCount = NodeSections::getList(['node' => $channel->id], 0, 0, true); $sections = array_column(NodeSections::getList(['node' => $channel->id], 0, $sectionsCount), null, 'section'); + $announcementRepository = new AnnouncementRepository(); + $announcementList = $announcementRepository->getActiveList(); + $this->contextVars([ 'channel' => $channel, 'side_order' => $side_order, @@ -118,7 +122,8 @@ private function setChannelContext($id) 'types' => $types, 'colors' => $colors, 'url_project_create' => '/channel/' . $id . '/create', - 'nodeSections' => $sections + 'nodeSections' => $sections, + 'announcements' => $announcementList ], 'channel/'); } diff --git a/src/Goteo/Controller/IndexController.php b/src/Goteo/Controller/IndexController.php index b71096ee2d..e4c8db0e86 100644 --- a/src/Goteo/Controller/IndexController.php +++ b/src/Goteo/Controller/IndexController.php @@ -20,6 +20,7 @@ use Goteo\Model\Stories; use Goteo\Model\Node; use Goteo\Model\Sponsor; +use Goteo\Repository\AnnouncementRepository; use Goteo\Util\Stats\Stats; use Symfony\Component\HttpFoundation\Response; @@ -50,12 +51,16 @@ public function indexAction(): Response $projects_by_footprint = []; $sdg_by_footprint = []; - foreach($footprints as $footprint) { + foreach ($footprints as $footprint) { $footprintImpactData[$footprint->id] = $footprint->getListOfImpactData(['source' => 'manual']); $projects_by_footprint[$footprint->id] = Project::getByFootprint(['footprints' => $footprint->id, 'rand' => true, 'amount_bigger_than' => 7000]); $sdg_by_footprint[$footprint->id] = Sdg::getList(['footprint' => $footprint->id]); } + $announcementRepository = new AnnouncementRepository(); + $announcementList = $announcementRepository->getActiveList(); + + return $this->viewResponse('home/index', [ 'banners' => $banners, 'projects' => $projects, @@ -71,13 +76,14 @@ public function indexAction(): Response 'projects_by_footprint' => $projects_by_footprint, 'sdg_by_footprint' => $sdg_by_footprint, 'footprint_impact_data' => $footprintImpactData, + 'announcements' => $announcementList ]); } private function getSponsors(): array { $sponsors = []; - foreach(Sponsor::getTypes() as $type) { + foreach (Sponsor::getTypes() as $type) { $sponsors[$type] = Sponsor::getList(['type' => $type]); } return $sponsors; diff --git a/src/Goteo/Controller/ProjectController.php b/src/Goteo/Controller/ProjectController.php index c0b3163db5..69910d868b 100644 --- a/src/Goteo/Controller/ProjectController.php +++ b/src/Goteo/Controller/ProjectController.php @@ -42,6 +42,7 @@ use Goteo\Model\Project\ProjectLocation; use Goteo\Model\Project\ProjectMilestone; use Goteo\Model\SocialCommitment; +use Goteo\Repository\AnnouncementRepository; use Spipu\Html2Pdf\Exception\Html2PdfException; use Spipu\Html2Pdf\Html2Pdf; use Symfony\Component\HttpFoundation\JsonResponse; @@ -141,6 +142,9 @@ public function createAction(Request $request) { protected function view(Request $request, $project, $show, $post = null) { DB::cache(true); + $announcementRepository = new AnnouncementRepository(); + $announcementList = $announcementRepository->getActiveWithoutDonationList(); + if( !$project instanceOf Project ) { $project = Project::get($project, Lang::current(false)); } @@ -197,7 +201,8 @@ protected function view(Request $request, $project, $show, $post = null) { 'blog' => null, 'related_projects' => $related_projects, 'widget_code' => $widget_code, - 'footprints' => $footprints + 'footprints' => $footprints, + 'announcements' => $announcementList ]; $impactDataProjectByFootprint = []; diff --git a/src/Goteo/Core/Controller.php b/src/Goteo/Core/Controller.php index aaf650f8dd..1987dfdf54 100644 --- a/src/Goteo/Core/Controller.php +++ b/src/Goteo/Core/Controller.php @@ -159,4 +159,29 @@ public function getModelForm( return $processor; } + public function getEntityForm( + string $formClass, + $model, + array $defaults = [], + array $options = [], + Request $request = null, + array $formBuilderOptions = ['csrf_protection' => false] + ): FormProcessorInterface { + /** @var $finder FormFinder */ + $finder = App::getService('app.forms.finder'); + + $finder->setModel($model); + $validate = $mock_validation = false; + if ($request) { + $validate = $request->query->has('validate'); + $mock_validation = $validate && $request->isMethod('get'); + } + + $finder->setBuilder($this->createFormBuilder($defaults, 'autoform', $formBuilderOptions)); + $processor = $finder->getInstance($formClass, $options); + + $processor->setFullValidation($validate, $mock_validation); + + return $processor; + } } diff --git a/src/Goteo/Library/Forms/EntityFormProcessor.php b/src/Goteo/Library/Forms/EntityFormProcessor.php new file mode 100644 index 0000000000..bbc6d629c0 --- /dev/null +++ b/src/Goteo/Library/Forms/EntityFormProcessor.php @@ -0,0 +1,27 @@ +setBuilder($builder); + $this->setEntity($entity); + $this->setOptions($options); + } + + public function setEntity($entity): EntityFormProcessor + { + $this->entity = $entity; + return $this; + } + + public function getEntity() { + return $this->entity; + } +} \ No newline at end of file diff --git a/src/Goteo/Library/Forms/Model/AnnouncementForm.php b/src/Goteo/Library/Forms/Model/AnnouncementForm.php new file mode 100644 index 0000000000..22cc1b2f74 --- /dev/null +++ b/src/Goteo/Library/Forms/Model/AnnouncementForm.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with this source code. + */ + +namespace Goteo\Library\Forms\Model; + +use Goteo\Application\Session; +use Goteo\Library\Forms\AbstractFormProcessor; +use Goteo\Library\Forms\FormModelException; +use Goteo\Library\Text; +use Goteo\Model\Announcement; +use Goteo\Repository\AnnouncementRepository; +use Goteo\Util\Form\Type\BooleanType; +use Goteo\Util\Form\Type\ChoiceType; +use Goteo\Util\Form\Type\SubmitType; +use Goteo\Util\Form\Type\TextType; +use Goteo\Util\Form\Type\UrlType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Validator\Constraints; + + +class AnnouncementForm extends AbstractFormProcessor +{ + + public function createForm(): AnnouncementForm + { + /** @var Announcement $announcement */ + $announcement = $this->getModel(); + $builder = $this->getBuilder(); + + $builder + ->add('title', TextType::class, [ + 'label' => 'regular-title', + 'required' => true, + 'data' => $announcement->title, + 'constraints' => [ + new Constraints\NotBlank(), + ], + ]) + ->add('description', TextType::class, [ + 'label' => 'regular-description', + 'required' => true, + 'data' => $announcement->description, + 'constraints' => [ + new Constraints\NotBlank(), + ] + ]) + ->add('type', ChoiceType::class, [ + 'label' => 'regular-type', + 'required' => true, + 'data' => $announcement->type, + 'choices' => $this->getAnnouncementTypes() + ]) + ->add('cta_url', UrlType::class, [ + 'label' => 'regular-cta', + 'required' => false, + 'data' => $announcement->cta_url, + 'attr' => ['help' => Text::get('admin-announcement-cta-text')] + ]) + ->add('cta_text', TextType::class, [ + 'label' => 'regular-cta-text', + 'required' => false, + 'data' => $announcement->cta_text, + 'attr' => ['help' => Text::get('admin-announcement-cta-text')] + ]) + ->add('active', BooleanType::class, [ + 'label' => 'regular-active', + 'required' => false, + 'data' => $announcement->active + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'regular-submit', + 'attr' => ['class' => 'btn btn-cyan'], + 'icon_class' => 'fa fa-save' + ]); + + return $this; + } + + public function save(FormInterface $form = null, $force_save = false) + { + if (!$form) $form = $this->getBuilder()->getForm(); + if (!$form->isValid() && !$force_save) throw new FormModelException(Text::get('form-has-errors')); + + $data = $form->getData(); + + /** @var Announcement $announcement */ + $announcement = $this->getModel(); + + $announcement->title = $data['title']; + $announcement->description = $data['description']; + $announcement->active = $data['active']; + $announcement->type = $data['type']; + $announcement->lang = Session::get('lang'); + + if (isset($data['cta_url'])) + $announcement->cta_url = $data['cta_url']; + + if (isset($data['cta_text'])) + $announcement->cta_text = $data['cta_text']; + + $announcementRepository = new AnnouncementRepository(); + + $errors = []; + $announcementRepository->persist($announcement, $errors); + + return $this; + } + + private function getAnnouncementTypes(): array + { + $types = Announcement::getTypes(); + $ret = []; + + foreach ($types as $type) { + $ret[Text::get("admin-announcements-type-$type")] = $type; + } + + return $ret; + } +} diff --git a/src/Goteo/Model/Announcement.php b/src/Goteo/Model/Announcement.php new file mode 100644 index 0000000000..3ea9ff65da --- /dev/null +++ b/src/Goteo/Model/Announcement.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with this source code. + */ + +namespace Goteo\Model; + +use Goteo\Application\Lang; +use Goteo\Core\Model; +use Goteo\Model\Project; +use PDO; +use PDOException; + +class Announcement extends Model +{ + + const TYPE_GENERAL = 'general'; + const TYPE_DONATION = 'donation'; + const TYPE_PROJECT = 'project'; + + public ?int $id = null; + public ?string $title = null; + public ?string $description = null; + public ?Project $project = null; + public string $type = self::TYPE_GENERAL; + public string $lang; + public ?string $cta_url = null; + public ?string $cta_text = null; + public bool $active = false; + public ?\DateTime $start_date; + public ?\DateTime $end_date; + + public function getList(array $filter = [], int $offset = 0, int $limit = 10, $lang = null): array + { + if (!$lang) $lang = Lang::current(); + + $sql = "SELECT announcement.* + FROM announcement + LIMIT $limit + OFFSET $offset"; + + return $this->query($sql)->fetchAll(PDO::FETCH_CLASS, Announcement::class); + } + + public static function getLangFields(): array + { + return ['title', 'description', 'cta_url', 'cta_text']; + } + + public function validate(&$errors = []): bool + { + + if (!$this->title) + $errors['title'] = 'Missing title'; + + if (!$this->description) + $errors['description'] = 'Missing description'; + + return empty($errors); + } + + + public function save(&$errors = []): bool + { + if (!$this->validate($errors)) return false; + + $fields = [ + 'id', + 'title', + 'description', + 'project', + 'type', + 'lang', + 'cta_url', + 'cta_text', + 'active', + 'start_date', + 'end_date' + ]; + + try { + $this->dbInsertUpdate($fields); + } catch (PDOException $e) { + $errors[] = "Error insert/update announcemenet: " . $e->getMessage(); + return false; + } + } + + public function hasCTA(): bool + { + return (isset($this->cta_text) && isset($this->cta_url)); + } + + public function isAProjectAnnouncement(): bool + { + return self::TYPE_PROJECT == $this->type; + } + + public function isAGeneralAnnouncement(): bool + { + return self::TYPE_GENERAL == $this->type; + } + + public function isADonationAnnouncement(): bool + { + return self::TYPE_DONATION == $this->type; + } + + public static function getTypes(): array + { + return [ + self::TYPE_GENERAL, + self::TYPE_DONATION, + self::TYPE_PROJECT + ]; + } +} diff --git a/src/Goteo/Repository/AnnouncementRepository.php b/src/Goteo/Repository/AnnouncementRepository.php new file mode 100644 index 0000000000..3789f82a64 --- /dev/null +++ b/src/Goteo/Repository/AnnouncementRepository.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with + */ + +namespace Goteo\Repository; + +use Goteo\Application\Exception\ModelException; +use Goteo\Application\Exception\ModelNotFoundException; +use Goteo\Model\Announcement; +use Goteo\Model\Project; +use PDO; +use PDOException; + +class AnnouncementRepository extends BaseRepository +{ + protected ?string $table = 'announcement'; + + public function getById(int $id): Announcement + { + $sql = "SELECT announcement.* + FROM announcement + WHERE announcement.id = ?"; + + $announcement = $this->query($sql, [$id])->fetchObject(Announcement::class); + + if (!$announcement instanceof Announcement) + throw new ModelNotFoundException("Announcement with id $id not found"); + + return $announcement; + } + + public function count(): int + { + $sql = "SELECT count(announcement.id) + FROM announcement"; + + return $this->query($sql)->fetchColumn(); + } + + /** + * @return Announcement[] + */ + public function getList(int $offset = 0, int $limit = 10): array + { + $sql = "SELECT announcement.* + FROM announcement + LIMIT $limit + OFFSET $offset"; + + return $this->query($sql)->fetchAll(PDO::FETCH_CLASS, Announcement::class); + } + + /** + * @return Announcement[] + */ + public function getActiveList(int $offset = 0, int $limit = 10): array + { + $sql = "SELECT announcement.* + FROM announcement + WHERE active + LIMIT $limit + OFFSET $offset"; + + return $this->query($sql)->fetchAll(PDO::FETCH_CLASS, Announcement::class); + } + + public function getActiveWithoutDonationList(int $offset = 0, int $limit = 10): array + { + $sql = "SELECT announcement.* + FROM announcement + WHERE active and type != ? + LIMIT $limit + OFFSET $offset"; + + return $this->query($sql, [Announcement::TYPE_DONATION])->fetchAll(PDO::FETCH_CLASS, Announcement::class); + } + + public function persist(Announcement $announcement, array &$errors = []): Announcement + { + if ($announcement->id) + return $this->update($announcement, $errors); + + return $this->create($announcement, $errors); + } + + private function create(Announcement $announcement, &$errors = []): Announcement + { + $fields = [ + 'title' => ':title', + 'description' => ':description', + 'type' => ':type', + 'lang' => ':lang', + 'project_id' => ':project_id', + 'cta_url' => ':cta_url', + 'cta_text' => ':cta_text', + 'active' => ':active' + ]; + + $project = $announcement->project; + if ($project instanceof Project) + $project = $project->id; + + $values = [ + ':title' => $announcement->title, + ':description' => $announcement->description, + ':type' => $announcement->type, + ':lang' => $announcement->lang, + ':project_id' => $project, + ':cta_url' => $announcement->cta_url, + ':cta_text' => $announcement->cta_text, + ':active' => $announcement->active + ]; + + $sql = "INSERT INTO `$this->table` (" . implode(',', array_keys($fields)) . ") VALUES (" . implode(',', array_values($fields)) . ")"; + + try { + $this->query($sql, $values); + $announcement->id = $this->insertId(); + } catch (PDOException $e) { + $errors[] = $e->getMessage(); + throw new ModelException($e->getMessage()); + } + + return $announcement; + } + + private function update(Announcement $announcement, &$errors = []): Announcement + { + $fields = [ + 'id' => ':id', + 'title' => ':title', + 'description' => ':description', + 'type' => ':type', + 'lang' => ':lang', + 'project_id' => ':project_id', + 'cta_url' => ':cta_url', + 'cta_text' => ':cta_text', + 'active' => ':active' + ]; + + $project = $announcement->project; + if ($project instanceof Project) + $project = $project->id; + + + $values = [ + ':id' => $announcement->id, + ':title' => $announcement->title, + ':description' => $announcement->description, + 'type' => $announcement->type, + ':lang' => $announcement->lang, + ':project_id' => $project, + ':cta_url' => $announcement->cta_url, + ':cta_text' => $announcement->cta_text, + ':active' => $announcement->active, + ]; + + $sql = "REPLACE INTO `$this->table` (" . implode(',', array_keys($fields)) . ") VALUES (" . implode(',', array_values($fields)) . ")"; + try { + $this->query($sql, $values); + } catch (PDOException $exception) { + $errors[] = $exception->getMessage(); + } + + return $announcement; + } + + public function delete(Announcement $announcement): void + { + $sql = "DELETE FROM $this->table WHERE $this->table.id = :id"; + try { + $this->query($sql, [':id' => $announcement->id]); + } catch (PDOException $exception) { + throw new ModelException($exception->getMessage()); + } + } +} diff --git a/src/Goteo/Util/Foil/Extension/ModelsData.php b/src/Goteo/Util/Foil/Extension/ModelsData.php index 086a4f8ff9..90751603de 100644 --- a/src/Goteo/Util/Foil/Extension/ModelsData.php +++ b/src/Goteo/Util/Foil/Extension/ModelsData.php @@ -15,6 +15,7 @@ use Goteo\Core\Model as CoreModel; use Goteo\Model; use Goteo\Util\ModelNormalizer\ModelNormalizer; +use Goteo\Util\ModelNormalizer\EntityNormalizer; class ModelsData implements ExtensionInterface { @@ -65,15 +66,22 @@ public function model_static() * @return [type] [description] */ public function model_list_entries($list, $keys = null) { + $array = []; if(!is_array($list)) return $array; + foreach($list as $key => $ob) { if($ob instanceOf CoreModel) { $normalizer = new ModelNormalizer($ob, $keys); $array[] = $normalizer->get(); + } else { + $normalizer = new EntityNormalizer($ob, $keys); + $array[] = $normalizer->get(); } + } + return $array; } diff --git a/src/Goteo/Util/Form/FormFinder.php b/src/Goteo/Util/Form/FormFinder.php index df7dd32c1d..b521baa904 100644 --- a/src/Goteo/Util/Form/FormFinder.php +++ b/src/Goteo/Util/Form/FormFinder.php @@ -11,9 +11,9 @@ namespace Goteo\Util\Form; use Symfony\Component\Form\FormBuilderInterface; -use Goteo\Core\Model; -class FormFinder { +class FormFinder +{ private $builder; private $model; @@ -23,21 +23,24 @@ public function setBuilder(FormBuilderInterface $builder): FormFinder return $this; } - public function getBuilder() { + public function getBuilder() + { return $this->builder; } - public function setModel(Model $model): FormFinder + public function setModel($model): FormFinder { $this->model = $model; return $this; } - public function getModel() { + public function getModel() + { return $this->model; } - public function getInstance(string $formClass, array $options = []) { + public function getInstance(string $formClass, array $options = []) + { return new $formClass($this->builder, $this->model, $options); } } diff --git a/src/Goteo/Util/ModelNormalizer/EntityNormalizer.php b/src/Goteo/Util/ModelNormalizer/EntityNormalizer.php new file mode 100644 index 0000000000..c719c86013 --- /dev/null +++ b/src/Goteo/Util/ModelNormalizer/EntityNormalizer.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with this source code. + */ +namespace Goteo\Util\ModelNormalizer; + +use Goteo\Util\ModelNormalizer\Transformer; +use Goteo\Application\Session; +/** + * This class allows to get an object standarized for its use in views + */ +class EntityNormalizer { + protected $model; + protected $keys; + + public function __construct($model,array $keys = null) { + $this->model = $model; + $this->keys = $keys; + } + + /** + * Returns the normalized object + * @return Goteo\Util\ModelNormalizer\TransformerInterface + */ + public function get() { + $ob->setUser(Session::getUser())->rebuild(); + + return $ob; + } +} diff --git a/src/Goteo/Util/ModelNormalizer/ModelNormalizer.php b/src/Goteo/Util/ModelNormalizer/ModelNormalizer.php index 812c2f501c..f12d632c24 100644 --- a/src/Goteo/Util/ModelNormalizer/ModelNormalizer.php +++ b/src/Goteo/Util/ModelNormalizer/ModelNormalizer.php @@ -7,20 +7,24 @@ * For the full copyright and license information, please view the README.md * and LICENSE files that was distributed with this source code. */ + namespace Goteo\Util\ModelNormalizer; use Goteo\Core\Model as CoreModel; use Goteo\Model; use Goteo\Util\ModelNormalizer\Transformer; use Goteo\Application\Session; + /** * This class allows to get an object standarized for its use in views */ -class ModelNormalizer { +class ModelNormalizer +{ protected $model; protected $keys; - public function __construct(CoreModel $model,array $keys = null) { + public function __construct(CoreModel $model, array $keys = null) + { $this->model = $model; $this->keys = $keys; } @@ -29,72 +33,57 @@ public function __construct(CoreModel $model,array $keys = null) { * Returns the normalized object * @return Goteo\Util\ModelNormalizer\TransformerInterface */ - public function get() { - if($this->model instanceOf Model\User) { + public function get() + { + if ($this->model instanceof Model\User) { $ob = new Transformer\UserTransformer($this->model, $this->keys); - } - elseif($this->model instanceOf Model\Stories) { + } elseif ($this->model instanceof Model\Stories) { $ob = new Transformer\StoriesTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\Node\NodeStories) { + } elseif ($this->model instanceof Model\Node\NodeStories) { $ob = new Transformer\ChannelStoriesTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\Node\NodeResource) { + } elseif ($this->model instanceof Model\Node\NodeResource) { $ob = new Transformer\ChannelResourceTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\Node\NodePost) { + } elseif ($this->model instanceof Model\Node\NodePost) { $ob = new Transformer\ChannelPostsTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\Node\NodeProgram) { + } elseif ($this->model instanceof Model\Node\NodeProgram) { $ob = new Transformer\ChannelProgramTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\Node\NodeSections) { + } elseif ($this->model instanceof Model\Node\NodeSections) { $ob = new Transformer\ChannelSectionTransformer($this->model, $this->keys); - } - elseif ($this->model instanceof Model\Node\NodeProject) { + } elseif ($this->model instanceof Model\Node\NodeProject) { $ob = new Transformer\ChannelProjectTransformer($this->model, $this->keys); - } - elseif( - $this->model instanceOf Model\Category - || $this->model instanceOf Model\Sphere - || $this->model instanceOf Model\SocialCommitment - || $this->model instanceOf Model\Footprint - || $this->model instanceOf Model\Sdg + } elseif ( + $this->model instanceof Model\Category + || $this->model instanceof Model\Sphere + || $this->model instanceof Model\SocialCommitment + || $this->model instanceof Model\Footprint + || $this->model instanceof Model\Sdg ) { $ob = new Transformer\CategoriesTransformer($this->model, $this->keys); - } - elseif($this->model instanceOf Model\Blog\Post) { + } elseif ($this->model instanceof Model\Blog\Post) { $ob = new Transformer\PostTransformer($this->model, $this->keys); - } - elseif($this->model instanceOf Model\Promote) { + } elseif ($this->model instanceof Model\Promote) { $ob = new Transformer\PromoteTransformer($this->model, $this->keys); - } - elseif($this->model instanceOf Model\Filter) { + } elseif ($this->model instanceof Model\Filter) { $ob = new Transformer\FilterTransformer($this->model, $this->keys); - } - elseif($this->model instanceOf Model\Communication) { + } elseif ($this->model instanceof Model\Communication) { $ob = new Transformer\CommunicationTransformer($this->model, $this->keys); - } - elseif($this->model instanceOf Model\Workshop) { + } elseif ($this->model instanceof Model\Workshop) { $ob = new Transformer\WorkshopTransformer($this->model, $this->keys); - } - elseif( - $this->model instanceOf Model\Mail) { + } elseif ( + $this->model instanceof Model\Mail + ) { $ob = new Transformer\MailTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\ImpactData) { + } elseif ($this->model instanceof Model\ImpactData) { $ob = new Transformer\ImpactDataTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\ImpactData\ImpactDataProject) { + } elseif ($this->model instanceof Model\ImpactData\ImpactDataProject) { $ob = new Transformer\ImpactDataProjectTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\ImpactItem\ImpactProjectItem) { + } elseif ($this->model instanceof Model\ImpactItem\ImpactProjectItem) { $ob = new Transformer\ImpactProjectItemTransformer($this->model, $this->keys); - } - elseif ($this->model instanceOf Model\ImpactItem\ImpactProjectItemCost) { + } elseif ($this->model instanceof Model\ImpactItem\ImpactProjectItemCost) { $ob = new Transformer\ImpactProjectItemCostTransformer($this->model, $this->keys); - } - else $ob = new Transformer\GenericTransformer($this->model, $this->keys); + } elseif ($this->model instanceof Model\Announcement) { + $ob = new Transformer\AnnouncementTransformer($this->model, $this->keys); + } else $ob = new Transformer\GenericTransformer($this->model, $this->keys); $ob->setUser(Session::getUser())->rebuild(); diff --git a/src/Goteo/Util/ModelNormalizer/Transformer/AnnouncementTransformer.php b/src/Goteo/Util/ModelNormalizer/Transformer/AnnouncementTransformer.php new file mode 100644 index 0000000000..7e5a1ff5ea --- /dev/null +++ b/src/Goteo/Util/ModelNormalizer/Transformer/AnnouncementTransformer.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with this source code. + */ + +namespace Goteo\Util\ModelNormalizer\Transformer; + +use Goteo\Library\Text; +use Goteo\Model\Announcement; + +class AnnouncementTransformer extends AbstractTransformer +{ + + protected $keys = ['id', 'title', 'description', 'type']; + + public function getType(): string + { + /** @var Announcement */ + $model = $this->model; + $type = $model->type; + return Text::get("admin-announcements-type-$type"); + } + + public function getActions() + { + /** @var Announcement */ + $model = $this->model; + + if (!$this->getUser()) return []; + + $id = $model->id; + $ret = [ + 'edit' => "/admin/announcement/edit/$id", + 'translate' => "/translate/announcement/$id", + 'delete' => "/admin/announcement/delete/$id", + ]; + + return $ret; + } +} diff --git a/src/Goteo/Util/ModelNormalizer/Transformer/EntityTransformer.php b/src/Goteo/Util/ModelNormalizer/Transformer/EntityTransformer.php new file mode 100644 index 0000000000..8c76e735fc --- /dev/null +++ b/src/Goteo/Util/ModelNormalizer/Transformer/EntityTransformer.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the README.md + * and LICENSE files that was distributed with this source code. + */ +namespace Goteo\Util\ModelNormalizer\Transformer; + +use Goteo\Model\Image; + +abstract class EntityTransformer extends AbstractTransformer { + protected $model; + protected $user; + protected $keys = ['id', 'name']; + + public function __construct($model, array $keys = null) { + $this->model = $model; + if($keys) { + $this->setKeys($keys); + } + } + + public function getRawValue($key) { + if ($key == 'id') + return $this->model->getId(); + + if ($key == 'active') + return $this->model->isActive(); + + if ($key == 'actions') + return null; + + $method = 'get' . ucfirst($key); + + return $this->model->$method(); + } + + + public function getModel() { + return $this->model; + } + + /** + * Return an API url to modifiy the property (or empty if doesn't exist) + * @param [type] $prop [description] + * @return [type] [description] + */ + public function getApiProperty($prop) { + return '/api/' . $this->getModelName(). '/' . $this->model->getId() . "/property/$prop"; + } + + public function getId() { + return $this->model->getId(); + } + + public function getName() { + return $this->model->getName ? $this->model->getName() : $this->model->getTitle(); + } + + public function getImage() { + return Image::get($this->model->getImage())->getLink(64, 64, true); + } + + public function getIcon() { + return $this->model->getIcon()->getLink(64, 64, true); + } + + public function getTitle() { + return $this->model->getTitle(); + } + + public function getSubTitle() { + return $this->model->getSubtitle(); + } + + public function getDate() { + return $this->model->getDate() ? \date_formater($this->model->getDate()) : \date_formater($this->model->getDateIn()); + } + + public function getActions() { + if(!$u = $this->getUser()) return []; + $ret = ['edit' => '/admin/' . $this->getModelName() . '/edit/' . $this->model->getId()]; + return $ret; + } + + public function getLangs() { + return $this->model->getLangsAvailable(); + } + + public function getActive() { + return $this->model->isActive(); + } + + public function getOrder() { + return $this->model->getOrder(); + } + + public function getCity() { + return $this->model->getCity(); + } + + +} diff --git a/src/Routes/api_routes.php b/src/Routes/api_routes.php index c79027355c..9bdcc69c2f 100644 --- a/src/Routes/api_routes.php +++ b/src/Routes/api_routes.php @@ -499,4 +499,10 @@ array('_controller' => 'Goteo\Controller\Api\DataSetApiController::dataSetsAction') )); +// Promote change property of active +$api->add('api-promote-property', new Route( + '/announcement/{id}/property/{prop}', + array('_controller' => 'Goteo\Controller\Api\AnnouncementApiController::announcementPropertyAction') +)); + return $api; diff --git a/translations/ca/admin.yml b/translations/ca/admin.yml index 03d9231dd1..29ea8b9380 100644 --- a/translations/ca/admin.yml +++ b/translations/ca/admin.yml @@ -314,4 +314,11 @@ admin-impact-data-source-channel: "Canal" admin-impact-data-result_msg-help: "Aquest camp permet afegir un missatge al càlcul del impacte d'aquesta Data d'Impacte. Per utilitzar-lo haurem de fer servir les variables %amount i %result, on %amount fa referència a la quantitat estimada per l'usuari i %result al resultat d'impacte calculat." admin-impact-data-type-estimation: "Estimació" admin-impact-data-type-real: "Real" -admin-subscriptions: "Subscripcions" \ No newline at end of file +admin-subscriptions: "Subscripcions" +admin-announcements: "Anuncis" +admin-announcement-add: "Afegir anunci" +admin-announcement-edit: "Editar anunci" +admin-announcements-type-general: "General" +admin-announcements-type-donation: "Donacions" +admin-announcements-type-project: "Projectes" +admin-announcement-cta-text: "Aquest CTA només es mostrarà si el tipus d'anunci no és de donació" diff --git a/translations/ca/general.yml b/translations/ca/general.yml index a026193b5e..b2f5bc3f94 100644 --- a/translations/ca/general.yml +++ b/translations/ca/general.yml @@ -263,3 +263,7 @@ regular-operation-type: "Operació de càlcul" regular-posts: "Publicacions" regular-subscriptions: "Subscripcions" regular-channels: "Canals" +regular-active: "Actiu" +regular-cta: "Call To Action" +regular-cta-url: "Call To Action URL" +regular-cta-text: "Text del Call To Action" diff --git a/translations/ca/labels.yml b/translations/ca/labels.yml index 00de53abe4..8e5b46383c 100644 --- a/translations/ca/labels.yml +++ b/translations/ca/labels.yml @@ -10,6 +10,7 @@ accounts-lb-move: 'Reubicant l''aportació' accounts-lb-report: 'Informe de projecte' accounts-lb-update: 'Canviant l''estat de l''aportació' accounts-lb-viewer: 'Veient logs' +announcement-lb: 'Anuncis' banners-lb: Banner banners-lb-add: 'Nou Banner' banners-lb-cancel: 'Cancel·lant l''aportació' diff --git a/translations/ca/roles.yml b/translations/ca/roles.yml index b099ea618c..9df8bcc79f 100644 --- a/translations/ca/roles.yml +++ b/translations/ca/roles.yml @@ -52,6 +52,7 @@ role-perm-name-admin-module-categories: 'Pot accedir al mòdul de Categories' role-perm-name-admin-module-workshops: 'Pot accedir al mòdul de tallers' role-perm-name-admin-module-promote: 'Pot accedir al mòdul de descatar projectes a la home' role-perm-name-admin-module-channels: "Pot accedir als mòduls d'administració dels canals" +role-perm-name-admin-module-announcements: "Pot accedir al módul d'administració dels anuncis" role-perm-name-create-matcher: 'Pot crear matchers' role-perm-name-edit-matcher: 'Pot editar matchers' role-perm-name-remove-matcher: 'Pot esborrar els seus matchers' diff --git a/translations/ca/translator.yml b/translations/ca/translator.yml index 111cbdbc3b..ecc6daac29 100644 --- a/translations/ca/translator.yml +++ b/translations/ca/translator.yml @@ -27,6 +27,8 @@ translator-field-text: Text translator-field-title: Títol translator-field-subtitle: Subtítol translator-field-twitter_msg: 'Missatge de twitter' +translator-field-cta_url: 'Call To Action Url' +translator-field-cta_text: 'Call To Action' translator-glossary: 'Glossari de termes' translator-icon: 'Tipus de retorn/recompensa' translator-info: 'Idees de about' diff --git a/translations/en/admin.yml b/translations/en/admin.yml index e77db39fc7..757899498d 100644 --- a/translations/en/admin.yml +++ b/translations/en/admin.yml @@ -553,3 +553,10 @@ admin-impact-data-result_msg-help: "This field allows you to add a message to th admin-impact-data-type-estimation: "Estimation" admin-impact-data-type-real: "Real" admin-subscriptions: "Subscriptions" +admin-announcement-add: "Add announcement" +admin-announcement-edit: "Editar anuncio" +admin-announcements: "Announcements" +admin-announcements-type-general: "General" +admin-announcements-type-donation: "Donations" +admin-announcements-type-project: "Projects" +admin-announcement-cta-text: "This CTA will only be shown if the type is not Donations" diff --git a/translations/en/general.yml b/translations/en/general.yml index 4aba3380aa..a2b81a8a8d 100644 --- a/translations/en/general.yml +++ b/translations/en/general.yml @@ -269,3 +269,7 @@ regular-operation-type: "Operation type" regular-posts: "Posts" regular-subscriptions: "Subscriptions" regular-channels: "Channels" +regular-active: "Active" +regular-cta: "Call To Action" +regular-cta-url: "Call To Action URL" +regular-cta-text: "Call To Action text" diff --git a/translations/en/labels.yml b/translations/en/labels.yml index 3a3b4492db..2ad9d29038 100644 --- a/translations/en/labels.yml +++ b/translations/en/labels.yml @@ -10,6 +10,7 @@ accounts-lb-move: 'Relocating the contribution' accounts-lb-report: 'Project report' accounts-lb-update: 'Changing the status of the contribution' accounts-lb-viewer: 'Watching logs' +announcement-lb: 'Announcements' banners-lb: Banner banners-lb-add: 'New Banner' banners-lb-cancel: 'Canceling contribution' diff --git a/translations/en/roles.yml b/translations/en/roles.yml index 1d20cecd9f..56c73f4db2 100644 --- a/translations/en/roles.yml +++ b/translations/en/roles.yml @@ -55,6 +55,7 @@ role-perm-name-admin-module-categories: 'can access to Categories module in admi role-perm-name-admin-module-workshops: 'Can access the workshop module' role-perm-name-admin-module-promote: 'Can access the module to promote projects to the home page' role-perm-name-admin-module-channels: "Can access the channel's administration modules" +role-perm-name-admin-module-announcements: 'Can access to the announcements admin module' role-perm-name-receive-test-communications: 'Can receive test communications' role-perm-name-highlight-project: 'Can feature own projects in their creator profile' role-perm-name-create-matcher: 'Create own matcher' diff --git a/translations/en/translator.yml b/translations/en/translator.yml index eb732b4c80..01a6a3fa37 100644 --- a/translations/en/translator.yml +++ b/translations/en/translator.yml @@ -30,6 +30,8 @@ translator-field-title: Title translator-field-subtitle: Subtitle translator-field-twitter_msg: 'Twitter message' translator-field-twitter_msg_owner: 'Twitter author' +translator-field-cta_url: 'Call To Action Url' +translator-field-cta_text: 'Call To Action' translator-glossary: 'Glossary of terms' translator-home: Home translator-icon: 'Types of return/reward' diff --git a/translations/es/admin.yml b/translations/es/admin.yml index c0f6ee5b70..97e64de1a4 100644 --- a/translations/es/admin.yml +++ b/translations/es/admin.yml @@ -620,5 +620,11 @@ admin-impact-data-source-channel: "Canal" admin-impact-data-result_msg-help: "Este campo permite añadir un mensaje al cálculo del impacto de este Dato de Impacto. Para utilizarlo deberemos usar las variables %amount y %result, donde %amount hace referencia a la cantidad estimada por el usuario y %result al resultado de impacto calculado." admin-impact-data-type-estimation: "Estimación" admin-impact-data-type-real: "Real" - admin-subscriptions: "Suscripciones" +admin-announcements: "Anuncios" +admin-announcement-add: "Añadir anuncio" +admin-announcement-edit: "Modificar anuncio" +admin-announcements-type-general: "General" +admin-announcements-type-donation: "Donaciones" +admin-announcements-type-project: "Proyectos" +admin-announcement-cta-text: "Este CTA solo se mostrará si el tipo de anuncio no es de donación" diff --git a/translations/es/general.yml b/translations/es/general.yml index d62f6fbb84..0a18d30ddf 100644 --- a/translations/es/general.yml +++ b/translations/es/general.yml @@ -285,3 +285,7 @@ regular-operation-type: "Operación de cálculo" regular-posts: "Publicaciones" regular-subscriptions: "Suscripciones" regular-channels: "Canales" +regular-active: "Activo" +regular-cta: "Call To Action" +regular-cta-url: "Call To Action URL" +regular-cta-text: "Texto del Call To Action" diff --git a/translations/es/labels.yml b/translations/es/labels.yml index ba67fd1097..0ccdc27a1a 100644 --- a/translations/es/labels.yml +++ b/translations/es/labels.yml @@ -10,6 +10,7 @@ accounts-lb-move: 'Reubicando el aporte' accounts-lb-report: 'Informe de proyecto' accounts-lb-update: 'Cambiando el estado al aporte' accounts-lb-viewer: 'Viendo logs' +announcement-lb: 'Anuncios' banners-lb: Banner banners-lb-add: 'Nuevo Banner' banners-lb-cancel: 'Cancelando aporte' diff --git a/translations/es/roles.yml b/translations/es/roles.yml index b409e3b988..7fecb4c667 100644 --- a/translations/es/roles.yml +++ b/translations/es/roles.yml @@ -61,6 +61,7 @@ role-perm-name-admin-module-categories: 'Puede acceder al módulo de Categorías role-perm-name-admin-module-workshops: 'Puede acceder al módulo de talleres' role-perm-name-admin-module-promote: 'Puede acceder al módulo para destacar proyectos en la home' role-perm-name-admin-module-channels: 'Puede acceder a los módulos de administración de los canales' +role-perm-name-admin-module-announcements: 'Puede acceder al módulo de administración de los anuncios' role-perm-name-create-matcher: 'Puede crear matchers' role-perm-name-edit-matcher: 'Puede editar matchers'