From fa2e622a762a414f35f907846611e6c061451bd8 Mon Sep 17 00:00:00 2001
From: Sebastian Castro <sebastian.castro@protonmail.com>
Date: Thu, 14 Jan 2021 18:31:51 +0100
Subject: [PATCH] Notify users about failed import or elements to moderate

---
 config/services.yaml                          |  9 ++-
 src/Admin/ImportAdmin.php                     |  7 ++
 src/Admin/UserAdmin.php                       | 16 +++-
 src/Command/CheckVoteCommand.php              | 10 ++-
 src/Command/ImportSourceCommand.php           |  9 ++-
 .../RemoveAbandonnedProjectsCommand.php       |  1 -
 src/Document/ImportDynamic.php                | 26 +++++++
 src/Document/User.php                         | 76 +++++++++++++++++++
 src/Repository/ElementRepository.php          | 19 +++++
 src/Services/ElementImportService.php         | 18 ++++-
 src/Services/UserNotificationService.php      | 70 +++++++++++++++++
 11 files changed, 252 insertions(+), 9 deletions(-)
 create mode 100644 src/Services/UserNotificationService.php

diff --git a/config/services.yaml b/config/services.yaml
index cb29c3f33..0cd60a8dc 100755
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -59,6 +59,10 @@ services:
       arguments:
         $rootDB: '%root_db%'
 
+    App\Services\UserNotificationService:
+      arguments:
+        $baseUrl: '%base_url%'
+
     # Commands
     App\Command\UpdateProjectsInfoCommand:
       arguments:
@@ -68,6 +72,7 @@ services:
       arguments:
         $baseUrl: '%base_url%'
 
+
     # Overide FOS Registration Controller
     App\Controller\RegistrationController:
       tags: ['controller.service_arguments']
@@ -272,7 +277,9 @@ services:
         arguments: [~, App\Document\Configuration, 'App\Controller\Admin\ConfigurationAdminController']
         tags:
             - { name: sonata.admin, manager_type: doctrine_mongodb, group: sonata_user, label: Configuration }
-
+    
+    # -- Other ---
+        
     admin.etiquettes_config:
         class: App\Admin\StampAdmin
         arguments: [~, App\Document\Stamp, '' ]
diff --git a/src/Admin/ImportAdmin.php b/src/Admin/ImportAdmin.php
index 15e37beb9..05869ebc7 100755
--- a/src/Admin/ImportAdmin.php
+++ b/src/Admin/ImportAdmin.php
@@ -60,6 +60,13 @@ class ImportAdmin extends AbstractAdmin
                 ->end()
                 ->with('Paramètres', ['class' => 'col-md-12'])
                     ->add('refreshFrequencyInDays', null, ['required' => false, 'label' => 'Fréquence de mise à jours des données en jours (laisser vide pour ne jamais mettre à jour automatiquement'])
+                    ->add('usersToNotify', ModelType::class, [
+                        'class' => 'App\Document\User',
+                        'required' => false,
+                        'multiple' => true,
+                        'btn_add' => false,
+                        'label' => "Utilisateurs à notifier en cas d'erreur, ou lorsque de nouveaux champs/catégories sont à faire correspondre", ], ['admin_code' => 'admin.option_hidden'])
+                    
                     ->add('moderateElements', null, [
                         'required' => false, 
                         'label' => 'Modérer les éléments importés',
diff --git a/src/Admin/UserAdmin.php b/src/Admin/UserAdmin.php
index 2d09fbbae..e62b05815 100755
--- a/src/Admin/UserAdmin.php
+++ b/src/Admin/UserAdmin.php
@@ -176,7 +176,21 @@ class UserAdmin extends AbstractAdmin
             ->tab('User')
                 ->with('General', ['class' => 'col-md-6'])->end()
                 ->with('Status', ['class' => 'col-md-6'])->end()
-                ->with('Groups', ['class' => 'col-md-12'])->end()
+                ->with('Groups', ['class' => 'col-md-6'])->end()
+                ->with('Notifications', ['class' => 'col-md-12'])
+                    ->add('watchModeration', null, ['label' => "Etre notifié par email lorsque des éléments sont à modérer", 'required' => false])
+                    ->add('watchModerationOnlyWithOptions', ModelType::class, [
+                        'class' => 'App\Document\Option',
+                        'required' => false,
+                        'multiple' => true,
+                        'btn_add' => false,
+                        'label' => 'Seulement pour les éléments ayant une des catégories suivante', ], ['admin_code' => 'admin.option_hidden'])
+                    ->add('watchModerationOnlyWithPostCodes', null, [
+                        'label' => "Seulement pour les éléments avec code postal", 
+                        'label_attr' => ['title' => "Séparés par des virgules. On peut utiliser le symbole * pour choisir tout un département, par example : 40*, 47*, 48500"],
+                        'required' => false,
+                        'attr' => ['placeholder' => '40*, 47*, 48500']])
+                ->end()
             ->end()
             ->tab('Security')
                 ->with('Roles', ['class' => 'col-md-12'])->end()
diff --git a/src/Command/CheckVoteCommand.php b/src/Command/CheckVoteCommand.php
index 4a3869ac4..19138e975 100755
--- a/src/Command/CheckVoteCommand.php
+++ b/src/Command/CheckVoteCommand.php
@@ -3,6 +3,7 @@
 namespace App\Command;
 
 use App\Services\ElementVoteService;
+use App\Services\UserNotificationService;
 use Doctrine\ODM\MongoDB\DocumentManager;
 use App\Services\DocumentManagerFactory;
 use Psr\Log\LoggerInterface;
@@ -14,9 +15,11 @@ class CheckVoteCommand extends GoGoAbstractCommand
 {
     public function __construct(DocumentManagerFactory $dm, LoggerInterface $commandsLogger,
                                TokenStorageInterface $security,
-                               ElementVoteService $voteService)
+                               ElementVoteService $voteService,
+                               UserNotificationService $notifService)
     {
         $this->voteService = $voteService;
+        $this->notifService = $notifService;
         parent::__construct($dm, $commandsLogger, $security);
     }
 
@@ -33,7 +36,7 @@ class CheckVoteCommand extends GoGoAbstractCommand
         $elementRepo = $dm->getRepository('App\Document\Element');
         $elements = $elementRepo->findPendings();
 
-        foreach ($elements as $key => $element) {
+        foreach ($elements as $element) {
             $this->voteService->checkVotes($element);
             $dm->persist($element);
         }
@@ -41,5 +44,8 @@ class CheckVoteCommand extends GoGoAbstractCommand
         $dm->flush();
 
         $output->writeln('Nombre elements checkés : '.count($elements));
+
+        // send notif here so we don't need to create another command
+        $this->notifService->sendModerationNotifications();
     }
 }
diff --git a/src/Command/ImportSourceCommand.php b/src/Command/ImportSourceCommand.php
index 8830e4ef0..ffe9ce6b4 100644
--- a/src/Command/ImportSourceCommand.php
+++ b/src/Command/ImportSourceCommand.php
@@ -6,6 +6,7 @@ use App\Document\ImportState;
 use App\Services\ElementImportService;
 use Doctrine\ODM\MongoDB\DocumentManager;
 use App\Services\DocumentManagerFactory;
+use App\Services\UserNotificationService;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
@@ -16,9 +17,11 @@ class ImportSourceCommand extends GoGoAbstractCommand
 {
     public function __construct(DocumentManagerFactory $dm, LoggerInterface $commandsLogger,
                                TokenStorageInterface $security,
-                               ElementImportService $importService)
+                               ElementImportService $importService,
+                               UserNotificationService $notifService)
     {
         $this->importService = $importService;
+        $this->notifService = $notifService;
         parent::__construct($dm, $commandsLogger, $security);
     }
 
@@ -47,7 +50,8 @@ class ImportSourceCommand extends GoGoAbstractCommand
             }
 
             $this->log('Updating source '.$import->getSourceName().' for project '.$input->getArgument('dbname').' begins...');
-            $result = $this->importService->startImport($import);
+            // $this->importService->setDm($dm);
+            $result = $this->importService->startImport($import, $manuallyStarted = false);
             $this->log($result);
         } catch (\Exception $e) {
             $this->dm->persist($import);
@@ -55,6 +59,7 @@ class ImportSourceCommand extends GoGoAbstractCommand
             $message = $e->getMessage().'</br>'.$e->getFile().' LINE '.$e->getLine();
             $import->setCurrMessage($message);
             $this->error('Source: '.$import->getSourceName().' - '.$message);
+            $this->notifService->notifyImportError($import);
         }
     }
 }
diff --git a/src/Command/RemoveAbandonnedProjectsCommand.php b/src/Command/RemoveAbandonnedProjectsCommand.php
index 3515ef7fd..408c096fb 100755
--- a/src/Command/RemoveAbandonnedProjectsCommand.php
+++ b/src/Command/RemoveAbandonnedProjectsCommand.php
@@ -2,7 +2,6 @@
 
 namespace App\Command;
 
-use App\Document\User;
 use App\Services\MailService;
 use Doctrine\ODM\MongoDB\DocumentManager;
 use App\Services\DocumentManagerFactory;
diff --git a/src/Document/ImportDynamic.php b/src/Document/ImportDynamic.php
index 9535af363..edb8cfdb9 100644
--- a/src/Document/ImportDynamic.php
+++ b/src/Document/ImportDynamic.php
@@ -31,6 +31,12 @@ class ImportDynamic extends Import
      */
     private $nextRefresh = null;
 
+    /**
+     * Users to be tonified when error during import, or where new ontology/taxonomy mapping
+     * @MongoDB\ReferenceMany(targetDocument="App\Document\User")
+     */
+    private $usersToNotify;
+
     public function isDynamicImport()
     {
         return true;
@@ -112,4 +118,24 @@ class ImportDynamic extends Import
         $this->osmQueriesJson = $json;
         return $this;
     }
+
+    /**
+     * Get users to be tonified when error during import, or where new ontology/taxonomy mapping
+     */ 
+    public function getUsersToNotify()
+    {
+        return $this->usersToNotify;
+    }
+
+    /**
+     * Set users to be tonified when error during import, or where new ontology/taxonomy mapping
+     *
+     * @return  self
+     */ 
+    public function setUsersToNotify($usersToNotify)
+    {
+        $this->usersToNotify = $usersToNotify;
+
+        return $this;
+    }
 }
diff --git a/src/Document/User.php b/src/Document/User.php
index 72e8985ce..0d475b2b9 100644
--- a/src/Document/User.php
+++ b/src/Document/User.php
@@ -77,6 +77,22 @@ class User extends BaseUser
      */
     protected $nextNewsletterDate;
 
+    /**
+     * Be notified by email when an Element need moderation
+     * @MongoDB\Field(type="bool")
+     */
+    protected $watchModeration;
+
+    /**
+     * @MongoDB\ReferenceMany(targetDocument="App\Document\Option", cascade={"persist"})
+     */
+    protected $watchModerationOnlyWithOptions;
+
+    /**
+     * @MongoDB\Field(type="string")
+     */
+    protected $watchModerationOnlyWithPostCodes;
+
     /**
      * @MongoDB\Field(type="int")
      */
@@ -895,4 +911,64 @@ class User extends BaseUser
     {
         return $this->communsData;
     }
+
+    /**
+     * Get be notified by email when an Element need moderation
+     */ 
+    public function getWatchModeration()
+    {
+        return $this->watchModeration;
+    }
+
+    /**
+     * Set be notified by email when an Element need moderation
+     *
+     * @return  self
+     */ 
+    public function setWatchModeration($watchModeration)
+    {
+        $this->watchModeration = $watchModeration;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of watchModerationOnlyWithOptions
+     */ 
+    public function getWatchModerationOnlyWithOptions()
+    {
+        return $this->watchModerationOnlyWithOptions;
+    }
+
+    /**
+     * Set the value of watchModerationOnlyWithOptions
+     *
+     * @return  self
+     */ 
+    public function setWatchModerationOnlyWithOptions($watchModerationOnlyWithOptions)
+    {
+        $this->watchModerationOnlyWithOptions = $watchModerationOnlyWithOptions;
+
+        return $this;
+    }
+
+    /**
+     * Get the value of watchModerationOnlyWithPostCodes
+     */ 
+    public function getWatchModerationOnlyWithPostCodes()
+    {
+        return $this->watchModerationOnlyWithPostCodes;
+    }
+
+    /**
+     * Set the value of watchModerationOnlyWithPostCodes
+     *
+     * @return  self
+     */ 
+    public function setWatchModerationOnlyWithPostCodes($watchModerationOnlyWithPostCodes)
+    {
+        $this->watchModerationOnlyWithPostCodes = $watchModerationOnlyWithPostCodes;
+
+        return $this;
+    }
 }
diff --git a/src/Repository/ElementRepository.php b/src/Repository/ElementRepository.php
index b7dc966c8..638821dc9 100755
--- a/src/Repository/ElementRepository.php
+++ b/src/Repository/ElementRepository.php
@@ -220,6 +220,25 @@ class ElementRepository extends DocumentRepository
         return $qb->getQuery()->execute();
     }
 
+    public function findModerationElementToNotifyToUser($user)
+    {
+        $qb = $this->createQueryBuilder('App\Document\Element');
+        $qb->field('moderationState')->notEqual(ModerationState::NotNeeded);
+        $qb->field('status')->gt(ElementStatus::AdminRefused);
+        $optionsIds = [];
+        foreach($user->getWatchModerationOnlyWithOptions() as $option)
+            $optionsIds[] = $option->getId();
+        if (count($optionsIds)> 0) 
+            $qb->field('optionValues.optionId')->in($optionsIds);
+        if ($user->getWatchModerationOnlyWithPostCodes()) {
+            $regexp = str_replace(',', '|', $user->getWatchModerationOnlyWithPostCodes());
+            $regexp = "/" . str_replace(' ', '', $regexp) . "/";
+            $qb->field('address.postalCode')->equals(new \MongoRegex($regexp));
+        }
+            
+        return $qb->count()->getQuery()->execute();
+    }
+
     private function queryToArray($qb)
     {
         return $qb->hydrate(false)->getQuery()->execute()->toArray();
diff --git a/src/Services/ElementImportService.php b/src/Services/ElementImportService.php
index 7660b6c6b..ff3b0fefe 100755
--- a/src/Services/ElementImportService.php
+++ b/src/Services/ElementImportService.php
@@ -24,22 +24,28 @@ class ElementImportService
     protected $elementIdsErrors = [];
     protected $errorsMessages = [];
     protected $errorsCount = [];
+    protected $manuallyStarted = true;
 
     /**
      * Constructor.
      */
     public function __construct(DocumentManager $dm, ElementImportOneService $importOneService,
                               ElementImportMappingService $mappingService,
-                              TaxonomyJsonGenerator $taxonomyJsonGenerator)
+                              TaxonomyJsonGenerator $taxonomyJsonGenerator,
+                              UserNotificationService $notifService)
     {
         $this->dm = $dm;
         $this->importOneService = $importOneService;
         $this->mappingService = $mappingService;
         $this->taxonomyJsonGenerator = $taxonomyJsonGenerator;
+        $this->notifService = $notifService;
     }
 
-    public function startImport($import)
+    public function setDm($dm) { $this->dm = $dm; }
+
+    public function startImport($import, $manuallyStarted = true)
     {
+        $this->manuallyStarted = $manuallyStarted;
         $this->countElementCreated = 0;
         $this->countElementUpdated = 0;
         $this->countElementNothingToDo = 0;
@@ -326,6 +332,14 @@ class ElementImportService
 
             $import->setCurrState($totalErrors > 0 ? ($totalErrors == $size ? ImportState::Failed : ImportState::Errors) : ImportState::Completed);
             $import->setCurrMessage($log->displayMessage());
+            if ($import->isDynamicImport() && !$this->manuallyStarted) {
+                if ($totalErrors > 0) {
+                    $this->notifService->notifyImportError($import);
+                }
+                if ($import->getNewOntologyToMap() || $import->getNewTaxonomyToMap()) {
+                    $this->notifService->notifyImportMapping($import);
+                }
+            }
 
             $this->dm->flush();
         } catch (\Error $e) {
diff --git a/src/Services/UserNotificationService.php b/src/Services/UserNotificationService.php
new file mode 100644
index 000000000..212329064
--- /dev/null
+++ b/src/Services/UserNotificationService.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Services;
+
+use Doctrine\ODM\MongoDB\DocumentManager;
+use Symfony\Component\Routing\RouterInterface;
+
+class UserNotificationService
+{
+    public function __construct(DocumentManager $dm, MailService $mailService,
+                                RouterInterface $router, $baseUrl)
+    {
+        $this->dm = $dm;
+        $this->mailService = $mailService;
+        $this->baseUrl = $baseUrl;
+        $this->router = $router;
+    }
+
+    function sendModerationNotifications()
+    {
+        $users = $this->dm->getRepository('App\Document\User')
+                    ->findByWatchModeration(true);
+        foreach ($users as $user) {
+            $elementsCount = $this->dm->getRepository('App\Document\Element')
+                                  ->findModerationElementToNotifyToUser($user);
+            if ($elementsCount > 0) {
+                $config = $this->dm->getRepository('App\Document\Configuration')->findConfiguration();
+
+                $subject = "Des éléments sont à modérer sur {$config->getAppName()}";
+                $url = $this->generateRoute($config, 'gogo_directory');
+                $editPreferenceUrl = $this->generateRoute($config, 'admin_app_user_edit', ['id' => $user->getId()]);
+                $elementsCountText = $elementsCount == 1 ? "{$config->getElementDisplayName()} est" : "{$config->getElementDisplayNamePlural()} sont";
+                $content = "Bonjour !</br></br>$elementsCount $elementsCountText à modérer sur la carte \"{$config->getAppName()}\"</br></br>
+                <a href='{$url}'>Accéder à la carte</a></br></br>
+                Pour changer vos préférences de notification, <a href='$editPreferenceUrl'>cliquez ici</a>";
+                $this->mailService->sendMail($user->getEmail(), $subject, $content);
+            }
+        }    
+    }
+
+    private function generateRoute($config, $route, $params = [])
+    {
+        return 'http://'.$config->getDbName().'.'.$this->baseUrl . $this->router->generate($route, $params);
+    }
+
+    function notifyImportError($import)
+    {
+        if (!$import->isDynamicImport()) return;
+        $import->getUsersToNotify()->count();
+        foreach($import->getUsersToNotify() as $user) {
+            $config = $this->dm->getRepository('App\Document\Configuration')->findConfiguration();
+            $importUrl = $this->generateRoute($config, 'admin_app_import_edit', ['id' => $import->getId()]);
+            $subject = "Des erreurs ont eu lieu lors d'un import sur {$config->getAppName()}";
+            $content = "Bonjour !</br></br>L'import {$import->getSourceName()} semble avoir quelques soucis.. <a href='$importUrl'>Cliquez ici</a> pour essayer d'y remédier";
+            $this->mailService->sendMail($user->getEmail(), $subject, $content);
+        }
+    }
+
+    function notifyImportMapping($import)
+    {
+        if (!$import->isDynamicImport()) return;
+        foreach($import->getUsersToNotify() as $user) {
+            $config = $this->dm->getRepository('App\Document\Configuration')->findConfiguration();
+            $importUrl = $this->generateRoute($config, 'admin_app_import_edit', ['id' => $import->getId()]);
+            $subject = "Action requise pour un import sur {$config->getAppName()}";
+            $content = "Bonjour !</br></br>L'import {$import->getSourceName()} a de nouveaux champs ou de nouvelles catégories qui auraient peut être besoin de votre attention.. <a href='$importUrl'>Cliquez ici</a> pour accéder aux tables de correspondances";
+            $this->mailService->sendMail($user->getEmail(), $subject, $content);
+        }
+    }
+}
\ No newline at end of file
-- 
GitLab