diff options
Diffstat (limited to 'src')
22 files changed, 1133 insertions, 7 deletions
diff --git a/src/Wallabag/CoreBundle/Command/TagAllCommand.php b/src/Wallabag/CoreBundle/Command/TagAllCommand.php new file mode 100644 index 00000000..2cf3f808 --- /dev/null +++ b/src/Wallabag/CoreBundle/Command/TagAllCommand.php | |||
@@ -0,0 +1,66 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Command; | ||
4 | |||
5 | use Doctrine\ORM\NoResultException; | ||
6 | use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; | ||
7 | use Symfony\Component\Console\Input\InputArgument; | ||
8 | use Symfony\Component\Console\Input\InputInterface; | ||
9 | use Symfony\Component\Console\Output\OutputInterface; | ||
10 | |||
11 | class TagAllCommand extends ContainerAwareCommand | ||
12 | { | ||
13 | protected function configure() | ||
14 | { | ||
15 | $this | ||
16 | ->setName('wallabag:tag:all') | ||
17 | ->setDescription('Tag all entries using the tagging rules.') | ||
18 | ->addArgument( | ||
19 | 'username', | ||
20 | InputArgument::REQUIRED, | ||
21 | 'User to tag entries for.' | ||
22 | ) | ||
23 | ; | ||
24 | } | ||
25 | |||
26 | protected function execute(InputInterface $input, OutputInterface $output) | ||
27 | { | ||
28 | try { | ||
29 | $user = $this->getUser($input->getArgument('username')); | ||
30 | } catch (NoResultException $e) { | ||
31 | $output->writeln(sprintf('<error>User %s not found.</error>', $input->getArgument('username'))); | ||
32 | |||
33 | return 1; | ||
34 | } | ||
35 | $tagger = $this->getContainer()->get('wallabag_core.rule_based_tagger'); | ||
36 | |||
37 | $output->write(sprintf('Tagging entries for user « <info>%s</info> »... ', $user->getUserName())); | ||
38 | |||
39 | $entries = $tagger->tagAllForUser($user); | ||
40 | |||
41 | $em = $this->getDoctrine()->getManager(); | ||
42 | foreach ($entries as $entry) { | ||
43 | $em->persist($entry); | ||
44 | } | ||
45 | $em->flush(); | ||
46 | |||
47 | $output->writeln('<info>Done.</info>'); | ||
48 | } | ||
49 | |||
50 | /** | ||
51 | * Fetches a user from its username. | ||
52 | * | ||
53 | * @param string $username | ||
54 | * | ||
55 | * @return \Wallabag\UserBundle\Entity\User | ||
56 | */ | ||
57 | private function getUser($username) | ||
58 | { | ||
59 | return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username); | ||
60 | } | ||
61 | |||
62 | private function getDoctrine() | ||
63 | { | ||
64 | return $this->getContainer()->get('doctrine'); | ||
65 | } | ||
66 | } | ||
diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index 8bbe4ca0..7a187710 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php | |||
@@ -7,9 +7,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; | |||
7 | use Symfony\Component\HttpFoundation\Request; | 7 | use Symfony\Component\HttpFoundation\Request; |
8 | use Symfony\Component\HttpFoundation\JsonResponse; | 8 | use Symfony\Component\HttpFoundation\JsonResponse; |
9 | use Wallabag\CoreBundle\Entity\Config; | 9 | use Wallabag\CoreBundle\Entity\Config; |
10 | use Wallabag\CoreBundle\Entity\TaggingRule; | ||
10 | use Wallabag\UserBundle\Entity\User; | 11 | use Wallabag\UserBundle\Entity\User; |
11 | use Wallabag\CoreBundle\Form\Type\ChangePasswordType; | 12 | use Wallabag\CoreBundle\Form\Type\ChangePasswordType; |
12 | use Wallabag\CoreBundle\Form\Type\UserInformationType; | 13 | use Wallabag\CoreBundle\Form\Type\UserInformationType; |
14 | use Wallabag\CoreBundle\Form\Type\TaggingRuleType; | ||
13 | use Wallabag\CoreBundle\Form\Type\NewUserType; | 15 | use Wallabag\CoreBundle\Form\Type\NewUserType; |
14 | use Wallabag\CoreBundle\Form\Type\RssType; | 16 | use Wallabag\CoreBundle\Form\Type\RssType; |
15 | use Wallabag\CoreBundle\Tools\Utils; | 17 | use Wallabag\CoreBundle\Tools\Utils; |
@@ -98,6 +100,24 @@ class ConfigController extends Controller | |||
98 | return $this->redirect($this->generateUrl('config')); | 100 | return $this->redirect($this->generateUrl('config')); |
99 | } | 101 | } |
100 | 102 | ||
103 | // handle tagging rule | ||
104 | $taggingRule = new TaggingRule(); | ||
105 | $newTaggingRule = $this->createForm(new TaggingRuleType(), $taggingRule, array('action' => $this->generateUrl('config').'#set5')); | ||
106 | $newTaggingRule->handleRequest($request); | ||
107 | |||
108 | if ($newTaggingRule->isValid()) { | ||
109 | $taggingRule->setConfig($config); | ||
110 | $em->persist($taggingRule); | ||
111 | $em->flush(); | ||
112 | |||
113 | $this->get('session')->getFlashBag()->add( | ||
114 | 'notice', | ||
115 | 'Tagging rules updated' | ||
116 | ); | ||
117 | |||
118 | return $this->redirect($this->generateUrl('config')); | ||
119 | } | ||
120 | |||
101 | // handle adding new user | 121 | // handle adding new user |
102 | $newUser = $userManager->createUser(); | 122 | $newUser = $userManager->createUser(); |
103 | // enable created user by default | 123 | // enable created user by default |
@@ -136,6 +156,7 @@ class ConfigController extends Controller | |||
136 | 'pwd' => $pwdForm->createView(), | 156 | 'pwd' => $pwdForm->createView(), |
137 | 'user' => $userForm->createView(), | 157 | 'user' => $userForm->createView(), |
138 | 'new_user' => $newUserForm->createView(), | 158 | 'new_user' => $newUserForm->createView(), |
159 | 'new_tagging_rule' => $newTaggingRule->createView(), | ||
139 | ), | 160 | ), |
140 | 'rss' => array( | 161 | 'rss' => array( |
141 | 'username' => $user->getUsername(), | 162 | 'username' => $user->getUsername(), |
@@ -168,6 +189,33 @@ class ConfigController extends Controller | |||
168 | } | 189 | } |
169 | 190 | ||
170 | /** | 191 | /** |
192 | * Deletes a tagging rule and redirect to the config homepage. | ||
193 | * | ||
194 | * @param TaggingRule $rule | ||
195 | * | ||
196 | * @Route("/tagging-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_tagging_rule") | ||
197 | * | ||
198 | * @return \Symfony\Component\HttpFoundation\RedirectResponse | ||
199 | */ | ||
200 | public function deleteTaggingRule(TaggingRule $rule) | ||
201 | { | ||
202 | if ($this->getUser()->getId() != $rule->getConfig()->getUser()->getId()) { | ||
203 | throw $this->createAccessDeniedException('You can not access this tagging ryle.'); | ||
204 | } | ||
205 | |||
206 | $em = $this->getDoctrine()->getManager(); | ||
207 | $em->remove($rule); | ||
208 | $em->flush(); | ||
209 | |||
210 | $this->get('session')->getFlashBag()->add( | ||
211 | 'notice', | ||
212 | 'Tagging rule deleted' | ||
213 | ); | ||
214 | |||
215 | return $this->redirect($this->generateUrl('config')); | ||
216 | } | ||
217 | |||
218 | /** | ||
171 | * Retrieve config for the current user. | 219 | * Retrieve config for the current user. |
172 | * If no config were found, create a new one. | 220 | * If no config were found, create a new one. |
173 | * | 221 | * |
diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php index cb0c52c4..84b78a89 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php | |||
@@ -6,6 +6,7 @@ use Doctrine\Common\DataFixtures\AbstractFixture; | |||
6 | use Doctrine\Common\DataFixtures\OrderedFixtureInterface; | 6 | use Doctrine\Common\DataFixtures\OrderedFixtureInterface; |
7 | use Doctrine\Common\Persistence\ObjectManager; | 7 | use Doctrine\Common\Persistence\ObjectManager; |
8 | use Wallabag\CoreBundle\Entity\Config; | 8 | use Wallabag\CoreBundle\Entity\Config; |
9 | use Wallabag\CoreBundle\Entity\TaggingRule; | ||
9 | 10 | ||
10 | class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface | 11 | class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface |
11 | { | 12 | { |
@@ -15,6 +16,13 @@ class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface | |||
15 | public function load(ObjectManager $manager) | 16 | public function load(ObjectManager $manager) |
16 | { | 17 | { |
17 | $adminConfig = new Config($this->getReference('admin-user')); | 18 | $adminConfig = new Config($this->getReference('admin-user')); |
19 | $taggingRule = new TaggingRule(); | ||
20 | |||
21 | $taggingRule->setConfig($adminConfig); | ||
22 | $taggingRule->setRule('title matches "wallabag"'); | ||
23 | $taggingRule->setTags(['wallabag']); | ||
24 | $manager->persist($taggingRule); | ||
25 | |||
18 | $adminConfig->setTheme('material'); | 26 | $adminConfig->setTheme('material'); |
19 | $adminConfig->setItemsPerPage(30); | 27 | $adminConfig->setItemsPerPage(30); |
20 | $adminConfig->setLanguage('en_US'); | 28 | $adminConfig->setLanguage('en_US'); |
diff --git a/src/Wallabag/CoreBundle/Entity/Config.php b/src/Wallabag/CoreBundle/Entity/Config.php index b2a1915a..2ca4182e 100644 --- a/src/Wallabag/CoreBundle/Entity/Config.php +++ b/src/Wallabag/CoreBundle/Entity/Config.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Wallabag\CoreBundle\Entity; | 3 | namespace Wallabag\CoreBundle\Entity; |
4 | 4 | ||
5 | use Doctrine\Common\Collections\ArrayCollection; | ||
5 | use Doctrine\ORM\Mapping as ORM; | 6 | use Doctrine\ORM\Mapping as ORM; |
6 | use Symfony\Component\Validator\Constraints as Assert; | 7 | use Symfony\Component\Validator\Constraints as Assert; |
7 | 8 | ||
@@ -76,12 +77,19 @@ class Config | |||
76 | */ | 77 | */ |
77 | private $user; | 78 | private $user; |
78 | 79 | ||
80 | /** | ||
81 | * @ORM\OneToMany(targetEntity="Wallabag\CoreBundle\Entity\TaggingRule", mappedBy="config", cascade={"remove"}) | ||
82 | * @ORM\OrderBy({"id" = "ASC"}) | ||
83 | */ | ||
84 | private $taggingRules; | ||
85 | |||
79 | /* | 86 | /* |
80 | * @param User $user | 87 | * @param User $user |
81 | */ | 88 | */ |
82 | public function __construct(\Wallabag\UserBundle\Entity\User $user) | 89 | public function __construct(\Wallabag\UserBundle\Entity\User $user) |
83 | { | 90 | { |
84 | $this->user = $user; | 91 | $this->user = $user; |
92 | $this->taggingRules = new ArrayCollection(); | ||
85 | } | 93 | } |
86 | 94 | ||
87 | /** | 95 | /** |
@@ -237,4 +245,24 @@ class Config | |||
237 | { | 245 | { |
238 | return $this->rssLimit; | 246 | return $this->rssLimit; |
239 | } | 247 | } |
248 | |||
249 | /** | ||
250 | * @param TaggingRule $rule | ||
251 | * | ||
252 | * @return Config | ||
253 | */ | ||
254 | public function addTaggingRule(TaggingRule $rule) | ||
255 | { | ||
256 | $this->taggingRules[] = $rule; | ||
257 | |||
258 | return $this; | ||
259 | } | ||
260 | |||
261 | /** | ||
262 | * @return ArrayCollection<TaggingRule> | ||
263 | */ | ||
264 | public function getTaggingRules() | ||
265 | { | ||
266 | return $this->taggingRules; | ||
267 | } | ||
240 | } | 268 | } |
diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index 5aa582f8..608ed2f0 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php | |||
@@ -458,6 +458,10 @@ class Entry | |||
458 | */ | 458 | */ |
459 | public function addTag(Tag $tag) | 459 | public function addTag(Tag $tag) |
460 | { | 460 | { |
461 | if ($this->tags->contains($tag)) { | ||
462 | return; | ||
463 | } | ||
464 | |||
461 | $this->tags[] = $tag; | 465 | $this->tags[] = $tag; |
462 | $tag->addEntry($this); | 466 | $tag->addEntry($this); |
463 | } | 467 | } |
diff --git a/src/Wallabag/CoreBundle/Entity/TaggingRule.php b/src/Wallabag/CoreBundle/Entity/TaggingRule.php new file mode 100644 index 00000000..4eab590f --- /dev/null +++ b/src/Wallabag/CoreBundle/Entity/TaggingRule.php | |||
@@ -0,0 +1,133 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Entity; | ||
4 | |||
5 | use Doctrine\ORM\Mapping as ORM; | ||
6 | use Symfony\Component\Validator\Constraints as Assert; | ||
7 | use KPhoen\RulerZBundle\Validator\Constraints as RulerZAssert; | ||
8 | |||
9 | /** | ||
10 | * Tagging rule. | ||
11 | * | ||
12 | * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\TaggingRuleRepository") | ||
13 | * @ORM\Table | ||
14 | * @ORM\Entity | ||
15 | */ | ||
16 | class TaggingRule | ||
17 | { | ||
18 | /** | ||
19 | * @var int | ||
20 | * | ||
21 | * @ORM\Column(name="id", type="integer") | ||
22 | * @ORM\Id | ||
23 | * @ORM\GeneratedValue(strategy="AUTO") | ||
24 | */ | ||
25 | private $id; | ||
26 | |||
27 | /** | ||
28 | * @var string | ||
29 | * | ||
30 | * @Assert\NotBlank() | ||
31 | * @RulerZAssert\ValidRule( | ||
32 | * allowed_variables={"title", "url", "isArchived", "isStared", "content", "language", "mimetype", "readingTime", "domainName"}, | ||
33 | * allowed_operators={">", "<", ">=", "<=", "=", "is", "!=", "and", "not", "or", "matches"} | ||
34 | * ) | ||
35 | * @ORM\Column(name="rule", type="string", nullable=false) | ||
36 | */ | ||
37 | private $rule; | ||
38 | |||
39 | /** | ||
40 | * @var array | ||
41 | * | ||
42 | * @Assert\NotBlank() | ||
43 | * @ORM\Column(name="tags", type="simple_array", nullable=false) | ||
44 | */ | ||
45 | private $tags = []; | ||
46 | |||
47 | /** | ||
48 | * @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\Config", inversedBy="taggingRules") | ||
49 | */ | ||
50 | private $config; | ||
51 | |||
52 | /** | ||
53 | * Get id. | ||
54 | * | ||
55 | * @return int | ||
56 | */ | ||
57 | public function getId() | ||
58 | { | ||
59 | return $this->id; | ||
60 | } | ||
61 | |||
62 | /** | ||
63 | * Set rule. | ||
64 | * | ||
65 | * @param string $rule | ||
66 | * | ||
67 | * @return TaggingRule | ||
68 | */ | ||
69 | public function setRule($rule) | ||
70 | { | ||
71 | $this->rule = $rule; | ||
72 | |||
73 | return $this; | ||
74 | } | ||
75 | |||
76 | /** | ||
77 | * Get rule. | ||
78 | * | ||
79 | * @return string | ||
80 | */ | ||
81 | public function getRule() | ||
82 | { | ||
83 | return $this->rule; | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Set tags. | ||
88 | * | ||
89 | * @param array<string> $tags | ||
90 | * | ||
91 | * @return TaggingRule | ||
92 | */ | ||
93 | public function setTags(array $tags) | ||
94 | { | ||
95 | $this->tags = $tags; | ||
96 | |||
97 | return $this; | ||
98 | } | ||
99 | |||
100 | /** | ||
101 | * Get tags. | ||
102 | * | ||
103 | * @return array<string> | ||
104 | */ | ||
105 | public function getTags() | ||
106 | { | ||
107 | return $this->tags; | ||
108 | } | ||
109 | |||
110 | /** | ||
111 | * Set config. | ||
112 | * | ||
113 | * @param Config $config | ||
114 | * | ||
115 | * @return TaggingRule | ||
116 | */ | ||
117 | public function setConfig(Config $config) | ||
118 | { | ||
119 | $this->config = $config; | ||
120 | |||
121 | return $this; | ||
122 | } | ||
123 | |||
124 | /** | ||
125 | * Get config. | ||
126 | * | ||
127 | * @return Config | ||
128 | */ | ||
129 | public function getConfig() | ||
130 | { | ||
131 | return $this->config; | ||
132 | } | ||
133 | } | ||
diff --git a/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php b/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php new file mode 100644 index 00000000..23488d35 --- /dev/null +++ b/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php | |||
@@ -0,0 +1,59 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Form\DataTransformer; | ||
4 | |||
5 | use Doctrine\Common\Persistence\ObjectManager; | ||
6 | use Symfony\Component\Form\DataTransformerInterface; | ||
7 | use Symfony\Component\Form\Exception\TransformationFailedException; | ||
8 | |||
9 | /** | ||
10 | * Transforms a comma-separated list to a proper PHP array. | ||
11 | * Example: the string "foo, bar" will become the array ["foo", "bar"] | ||
12 | */ | ||
13 | class StringToListTransformer implements DataTransformerInterface | ||
14 | { | ||
15 | /** | ||
16 | * @var string | ||
17 | */ | ||
18 | private $separator; | ||
19 | |||
20 | /** | ||
21 | * @param string $separator The separator used in the list. | ||
22 | */ | ||
23 | public function __construct($separator = ',') | ||
24 | { | ||
25 | $this->separator = $separator; | ||
26 | } | ||
27 | |||
28 | /** | ||
29 | * Transforms a list to a string. | ||
30 | * | ||
31 | * @param array|null $list | ||
32 | * | ||
33 | * @return string | ||
34 | */ | ||
35 | public function transform($list) | ||
36 | { | ||
37 | if (null === $list) { | ||
38 | return ''; | ||
39 | } | ||
40 | |||
41 | return implode($this->separator, $list); | ||
42 | } | ||
43 | |||
44 | /** | ||
45 | * Transforms a string to a list. | ||
46 | * | ||
47 | * @param string $string | ||
48 | * | ||
49 | * @return array|null | ||
50 | */ | ||
51 | public function reverseTransform($string) | ||
52 | { | ||
53 | if ($string === null) { | ||
54 | return null; | ||
55 | } | ||
56 | |||
57 | return array_values(array_filter(array_map('trim', explode($this->separator, $string)))); | ||
58 | } | ||
59 | } | ||
diff --git a/src/Wallabag/CoreBundle/Form/Type/TaggingRuleType.php b/src/Wallabag/CoreBundle/Form/Type/TaggingRuleType.php new file mode 100644 index 00000000..7fbba38a --- /dev/null +++ b/src/Wallabag/CoreBundle/Form/Type/TaggingRuleType.php | |||
@@ -0,0 +1,38 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Form\Type; | ||
4 | |||
5 | use Symfony\Component\Form\AbstractType; | ||
6 | use Symfony\Component\Form\FormBuilderInterface; | ||
7 | use Symfony\Component\OptionsResolver\OptionsResolver; | ||
8 | |||
9 | use Wallabag\CoreBundle\Form\DataTransformer\StringToListTransformer; | ||
10 | |||
11 | class TaggingRuleType extends AbstractType | ||
12 | { | ||
13 | public function buildForm(FormBuilderInterface $builder, array $options) | ||
14 | { | ||
15 | $builder | ||
16 | ->add('rule', 'text', array('required' => true)) | ||
17 | ->add('save', 'submit') | ||
18 | ; | ||
19 | |||
20 | $tagsField = $builder | ||
21 | ->create('tags', 'text') | ||
22 | ->addModelTransformer(new StringToListTransformer(',')); | ||
23 | |||
24 | $builder->add($tagsField); | ||
25 | } | ||
26 | |||
27 | public function configureOptions(OptionsResolver $resolver) | ||
28 | { | ||
29 | $resolver->setDefaults(array( | ||
30 | 'data_class' => 'Wallabag\CoreBundle\Entity\TaggingRule', | ||
31 | )); | ||
32 | } | ||
33 | |||
34 | public function getName() | ||
35 | { | ||
36 | return 'tagging_rule'; | ||
37 | } | ||
38 | } | ||
diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index 7fb41393..3d585e61 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php | |||
@@ -3,6 +3,7 @@ | |||
3 | namespace Wallabag\CoreBundle\Helper; | 3 | namespace Wallabag\CoreBundle\Helper; |
4 | 4 | ||
5 | use Graby\Graby; | 5 | use Graby\Graby; |
6 | use Psr\Log\LoggerInterface as Logger; | ||
6 | use Wallabag\CoreBundle\Entity\Entry; | 7 | use Wallabag\CoreBundle\Entity\Entry; |
7 | use Wallabag\CoreBundle\Tools\Utils; | 8 | use Wallabag\CoreBundle\Tools\Utils; |
8 | 9 | ||
@@ -13,10 +14,14 @@ use Wallabag\CoreBundle\Tools\Utils; | |||
13 | class ContentProxy | 14 | class ContentProxy |
14 | { | 15 | { |
15 | protected $graby; | 16 | protected $graby; |
17 | protected $tagger; | ||
18 | protected $logger; | ||
16 | 19 | ||
17 | public function __construct(Graby $graby) | 20 | public function __construct(Graby $graby, RuleBasedTagger $tagger, Logger $logger) |
18 | { | 21 | { |
19 | $this->graby = $graby; | 22 | $this->graby = $graby; |
23 | $this->tagger = $tagger; | ||
24 | $this->logger = $logger; | ||
20 | } | 25 | } |
21 | 26 | ||
22 | /** | 27 | /** |
@@ -59,6 +64,15 @@ class ContentProxy | |||
59 | $entry->setPreviewPicture($content['open_graph']['og_image']); | 64 | $entry->setPreviewPicture($content['open_graph']['og_image']); |
60 | } | 65 | } |
61 | 66 | ||
67 | try { | ||
68 | $this->tagger->tag($entry); | ||
69 | } catch (\Exception $e) { | ||
70 | $this->logger->error('Error while trying to automatically tag an entry.', array( | ||
71 | 'entry_url' => $url, | ||
72 | 'error_msg' => $e->getMessage(), | ||
73 | )); | ||
74 | } | ||
75 | |||
62 | return $entry; | 76 | return $entry; |
63 | } | 77 | } |
64 | } | 78 | } |
diff --git a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php new file mode 100644 index 00000000..3f9953c0 --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php | |||
@@ -0,0 +1,108 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Helper; | ||
4 | |||
5 | use RulerZ\RulerZ; | ||
6 | |||
7 | use Wallabag\CoreBundle\Entity\Entry; | ||
8 | use Wallabag\CoreBundle\Entity\Tag; | ||
9 | use Wallabag\CoreBundle\Repository\EntryRepository; | ||
10 | use Wallabag\CoreBundle\Repository\TagRepository; | ||
11 | use Wallabag\UserBundle\Entity\User; | ||
12 | |||
13 | class RuleBasedTagger | ||
14 | { | ||
15 | private $rulerz; | ||
16 | private $tagRepository; | ||
17 | private $entryRepository; | ||
18 | |||
19 | public function __construct(RulerZ $rulerz, TagRepository $tagRepository, EntryRepository $entryRepository) | ||
20 | { | ||
21 | $this->rulerz = $rulerz; | ||
22 | $this->tagRepository = $tagRepository; | ||
23 | $this->entryRepository = $entryRepository; | ||
24 | } | ||
25 | |||
26 | /** | ||
27 | * Add tags from rules defined by the user. | ||
28 | * | ||
29 | * @param Entry $entry Entry to tag. | ||
30 | */ | ||
31 | public function tag(Entry $entry) | ||
32 | { | ||
33 | $rules = $this->getRulesForUser($entry->getUser()); | ||
34 | |||
35 | foreach ($rules as $rule) { | ||
36 | if (!$this->rulerz->satisfies($entry, $rule->getRule())) { | ||
37 | continue; | ||
38 | } | ||
39 | |||
40 | foreach ($rule->getTags() as $label) { | ||
41 | $tag = $this->getTag($entry->getUser(), $label); | ||
42 | |||
43 | $entry->addTag($tag); | ||
44 | } | ||
45 | } | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * Apply all the tagging rules defined by a user on its entries. | ||
50 | * | ||
51 | * @param User $user | ||
52 | * | ||
53 | * @return array<Entry> A list of modified entries. | ||
54 | */ | ||
55 | public function tagAllForUser(User $user) | ||
56 | { | ||
57 | $rules = $this->getRulesForUser($user); | ||
58 | $entries = array(); | ||
59 | |||
60 | foreach ($rules as $rule) { | ||
61 | $qb = $this->entryRepository->getBuilderForAllByUser($user->getId()); | ||
62 | $entries = $this->rulerz->filter($qb, $rule->getRule()); | ||
63 | |||
64 | foreach ($entries as $entry) { | ||
65 | foreach ($rule->getTags() as $label) { | ||
66 | $tag = $this->getTag($user, $label); | ||
67 | |||
68 | $entry->addTag($tag); | ||
69 | $entries[] = $entry; | ||
70 | } | ||
71 | } | ||
72 | } | ||
73 | |||
74 | return $entries; | ||
75 | } | ||
76 | |||
77 | /** | ||
78 | * Fetch a tag for a user. | ||
79 | * | ||
80 | * @param User $user | ||
81 | * @param string $label The tag's label. | ||
82 | * | ||
83 | * @return Tag | ||
84 | */ | ||
85 | private function getTag(User $user, $label) | ||
86 | { | ||
87 | $tag = $this->tagRepository->findOneByLabelAndUserId($label, $user->getId()); | ||
88 | |||
89 | if (!$tag) { | ||
90 | $tag = new Tag($user); | ||
91 | $tag->setLabel($label); | ||
92 | } | ||
93 | |||
94 | return $tag; | ||
95 | } | ||
96 | |||
97 | /** | ||
98 | * Retrieves the tagging rules for a given user. | ||
99 | * | ||
100 | * @param User $user | ||
101 | * | ||
102 | * @return array<TaggingRule> | ||
103 | */ | ||
104 | private function getRulesForUser(User $user) | ||
105 | { | ||
106 | return $user->getConfig()->getTaggingRules(); | ||
107 | } | ||
108 | } | ||
diff --git a/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php b/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php new file mode 100644 index 00000000..e6bb03b1 --- /dev/null +++ b/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Operator\Doctrine; | ||
4 | |||
5 | /** | ||
6 | * Provides a "matches" operator used for tagging rules. | ||
7 | * | ||
8 | * It asserts that a given pattern is contained in a subject, in a | ||
9 | * case-insensitive way. | ||
10 | * | ||
11 | * This operator will be used to compile tagging rules in DQL, usable | ||
12 | * by Doctrine ORM. | ||
13 | * It's registered in RulerZ using a service (wallabag.operator.doctrine.matches); | ||
14 | */ | ||
15 | class Matches | ||
16 | { | ||
17 | public function __invoke($subject, $pattern) | ||
18 | { | ||
19 | if ($pattern[0] === "'") { | ||
20 | $pattern = sprintf("'%%%s%%'", substr($pattern, 1, -1)); | ||
21 | } | ||
22 | |||
23 | return sprintf('UPPER(%s) LIKE UPPER(%s)', $subject, $pattern); | ||
24 | } | ||
25 | } | ||
diff --git a/src/Wallabag/CoreBundle/Operator/PHP/Matches.php b/src/Wallabag/CoreBundle/Operator/PHP/Matches.php new file mode 100644 index 00000000..987ed2a5 --- /dev/null +++ b/src/Wallabag/CoreBundle/Operator/PHP/Matches.php | |||
@@ -0,0 +1,21 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Operator\PHP; | ||
4 | |||
5 | /** | ||
6 | * Provides a "matches" operator used for tagging rules. | ||
7 | * | ||
8 | * It asserts that a given pattern is contained in a subject, in a | ||
9 | * case-insensitive way. | ||
10 | * | ||
11 | * This operator will be used to compile tagging rules in PHP, usable | ||
12 | * directly on Entry objects for instance. | ||
13 | * It's registered in RulerZ using a service (wallabag.operator.array.matches); | ||
14 | */ | ||
15 | class Matches | ||
16 | { | ||
17 | public function __invoke($subject, $pattern) | ||
18 | { | ||
19 | return stripos($subject, $pattern) !== false; | ||
20 | } | ||
21 | } | ||
diff --git a/src/Wallabag/CoreBundle/Repository/TaggingRuleRepository.php b/src/Wallabag/CoreBundle/Repository/TaggingRuleRepository.php new file mode 100644 index 00000000..de380738 --- /dev/null +++ b/src/Wallabag/CoreBundle/Repository/TaggingRuleRepository.php | |||
@@ -0,0 +1,9 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Repository; | ||
4 | |||
5 | use Doctrine\ORM\EntityRepository; | ||
6 | |||
7 | class TaggingRuleRepository extends EntityRepository | ||
8 | { | ||
9 | } | ||
diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index 8e21b052..c92b4eb3 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.yml +++ b/src/Wallabag/CoreBundle/Resources/config/services.yml | |||
@@ -53,6 +53,27 @@ services: | |||
53 | class: Wallabag\CoreBundle\Helper\ContentProxy | 53 | class: Wallabag\CoreBundle\Helper\ContentProxy |
54 | arguments: | 54 | arguments: |
55 | - @wallabag_core.graby | 55 | - @wallabag_core.graby |
56 | - @wallabag_core.rule_based_tagger | ||
57 | - @logger | ||
58 | |||
59 | wallabag_core.rule_based_tagger: | ||
60 | class: Wallabag\CoreBundle\Helper\RuleBasedTagger | ||
61 | arguments: | ||
62 | - @rulerz | ||
63 | - @wallabag_core.tag_repository | ||
64 | - @wallabag_core.entry_repository | ||
65 | |||
66 | wallabag_core.entry_repository: | ||
67 | class: Wallabag\CoreBundle\Repository\EntryRepository | ||
68 | factory: [ @doctrine.orm.default_entity_manager, getRepository ] | ||
69 | arguments: | ||
70 | - WallabagCoreBundle:Entry | ||
71 | |||
72 | wallabag_core.tag_repository: | ||
73 | class: Wallabag\CoreBundle\Repository\TagRepository | ||
74 | factory: [ @doctrine.orm.default_entity_manager, getRepository ] | ||
75 | arguments: | ||
76 | - WallabagCoreBundle:Tag | ||
56 | 77 | ||
57 | wallabag_core.registration_confirmed: | 78 | wallabag_core.registration_confirmed: |
58 | class: Wallabag\CoreBundle\EventListener\RegistrationConfirmedListener | 79 | class: Wallabag\CoreBundle\EventListener\RegistrationConfirmedListener |
@@ -70,3 +91,13 @@ services: | |||
70 | arguments: | 91 | arguments: |
71 | - %wallabag_url% | 92 | - %wallabag_url% |
72 | - src/Wallabag/CoreBundle/Resources/views/themes/_global/public/img/appicon/apple-touch-icon-152.png | 93 | - src/Wallabag/CoreBundle/Resources/views/themes/_global/public/img/appicon/apple-touch-icon-152.png |
94 | |||
95 | wallabag.operator.array.matches: | ||
96 | class: Wallabag\CoreBundle\Operator\PHP\Matches | ||
97 | tags: | ||
98 | - { name: rulerz.operator, executor: rulerz.executor.array, operator: matches } | ||
99 | |||
100 | wallabag.operator.doctrine.matches: | ||
101 | class: Wallabag\CoreBundle\Operator\Doctrine\Matches | ||
102 | tags: | ||
103 | - { name: rulerz.operator, executor: rulerz.executor.doctrine, operator: matches, inline: true } | ||
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig index 7a7d6af1..cc797c63 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig | |||
@@ -145,6 +145,39 @@ | |||
145 | {{ form_rest(form.pwd) }} | 145 | {{ form_rest(form.pwd) }} |
146 | </form> | 146 | </form> |
147 | 147 | ||
148 | <h2>{% trans %}Tagging rules{% endtrans %}</h2> | ||
149 | |||
150 | <ul> | ||
151 | {% for tagging_rule in app.user.config.taggingRules %} | ||
152 | <li> | ||
153 | if « {{ tagging_rule.rule }} » then tag as « {{ tagging_rule.tags|join(', ') }} » | ||
154 | <a href="{{ path('delete_tagging_rule', {id: tagging_rule.id}) }}" title="{% trans %}Delete{% endtrans %}" class="tool delete icon-trash icon"></a> | ||
155 | </li> | ||
156 | {% endfor %} | ||
157 | </ul> | ||
158 | |||
159 | <form action="{{ path('config') }}" method="post" {{ form_enctype(form.new_tagging_rule) }}> | ||
160 | {{ form_errors(form.new_tagging_rule) }} | ||
161 | |||
162 | <fieldset class="w500p inline"> | ||
163 | <div class="row"> | ||
164 | {{ form_label(form.new_tagging_rule.rule) }} | ||
165 | {{ form_errors(form.new_tagging_rule.rule) }} | ||
166 | {{ form_widget(form.new_tagging_rule.rule) }} | ||
167 | </div> | ||
168 | </fieldset> | ||
169 | |||
170 | <fieldset class="w500p inline"> | ||
171 | <div class="row"> | ||
172 | {{ form_label(form.new_tagging_rule.tags) }} | ||
173 | {{ form_errors(form.new_tagging_rule.tags) }} | ||
174 | {{ form_widget(form.new_tagging_rule.tags) }} | ||
175 | </div> | ||
176 | </fieldset> | ||
177 | |||
178 | {{ form_rest(form.new_tagging_rule) }} | ||
179 | </form> | ||
180 | |||
148 | {% if is_granted('ROLE_SUPER_ADMIN') %} | 181 | {% if is_granted('ROLE_SUPER_ADMIN') %} |
149 | <h2>{% trans %}Add a user{% endtrans %}</h2> | 182 | <h2>{% trans %}Add a user{% endtrans %}</h2> |
150 | 183 | ||
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig index 8f121a2b..d060311d 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig | |||
@@ -15,8 +15,9 @@ | |||
15 | <li class="tab col s3"><a href="#set2">{% trans %}RSS{% endtrans %}</a></li> | 15 | <li class="tab col s3"><a href="#set2">{% trans %}RSS{% endtrans %}</a></li> |
16 | <li class="tab col s3"><a href="#set3">{% trans %}User information{% endtrans %}</a></li> | 16 | <li class="tab col s3"><a href="#set3">{% trans %}User information{% endtrans %}</a></li> |
17 | <li class="tab col s3"><a href="#set4">{% trans %}Password{% endtrans %}</a></li> | 17 | <li class="tab col s3"><a href="#set4">{% trans %}Password{% endtrans %}</a></li> |
18 | <li class="tab col s3"><a href="#set5">{% trans %}Tagging rules{% endtrans %}</a></li> | ||
18 | {% if is_granted('ROLE_SUPER_ADMIN') %} | 19 | {% if is_granted('ROLE_SUPER_ADMIN') %} |
19 | <li class="tab col s3"><a href="#set5">{% trans %}Add a user{% endtrans %}</a></li> | 20 | <li class="tab col s3"><a href="#set6">{% trans %}Add a user{% endtrans %}</a></li> |
20 | {% endif %} | 21 | {% endif %} |
21 | </ul> | 22 | </ul> |
22 | </div> | 23 | </div> |
@@ -183,8 +184,155 @@ | |||
183 | </form> | 184 | </form> |
184 | </div> | 185 | </div> |
185 | 186 | ||
186 | {% if is_granted('ROLE_SUPER_ADMIN') %} | ||
187 | <div id="set5" class="col s12"> | 187 | <div id="set5" class="col s12"> |
188 | <div class="row"> | ||
189 | <div class="input-field col s12"> | ||
190 | <ul> | ||
191 | {% for tagging_rule in app.user.config.taggingRules %} | ||
192 | <li> | ||
193 | if « {{ tagging_rule.rule }} » then tag as « {{ tagging_rule.tags|join(', ') }} » | ||
194 | <a href="{{ path('delete_tagging_rule', {id: tagging_rule.id}) }}" title="{% trans %}Delete{% endtrans %}"> | ||
195 | <i class="tool grey-text delete mdi-action-delete"></i> | ||
196 | </a> | ||
197 | </li> | ||
198 | {% endfor %} | ||
199 | </ul> | ||
200 | </div> | ||
201 | </div> | ||
202 | |||
203 | {{ form_start(form.new_tagging_rule) }} | ||
204 | {{ form_errors(form.new_tagging_rule) }} | ||
205 | |||
206 | <div class="row"> | ||
207 | <div class="input-field col s12"> | ||
208 | {{ form_label(form.new_tagging_rule.rule) }} | ||
209 | {{ form_errors(form.new_tagging_rule.rule) }} | ||
210 | {{ form_widget(form.new_tagging_rule.rule) }} | ||
211 | </div> | ||
212 | </div> | ||
213 | |||
214 | <div class="row"> | ||
215 | <div class="input-field col s12"> | ||
216 | {{ form_label(form.new_tagging_rule.tags) }} | ||
217 | {{ form_errors(form.new_tagging_rule.tags) }} | ||
218 | {{ form_widget(form.new_tagging_rule.tags) }} | ||
219 | </div> | ||
220 | </div> | ||
221 | |||
222 | <div class="hidden">{{ form_rest(form.new_tagging_rule) }}</div> | ||
223 | <button class="btn waves-effect waves-light" type="submit" name="action"> | ||
224 | {% trans %}Save{% endtrans %} | ||
225 | </button> | ||
226 | </form> | ||
227 | |||
228 | <div class="row"> | ||
229 | <div class="input-field col s12"> | ||
230 | <h4>{% trans %}FAQ{% endtrans %}</h4> | ||
231 | |||
232 | <h5>{% trans %}What does « tagging rules » mean?{% endtrans %}</h5> | ||
233 | <p class="help"> | ||
234 | {% trans %} | ||
235 | They are rules used by Wallabag to automatically tag new entries.<br /> | ||
236 | Each time a new entry is added, all the tagging rules will be used to add | ||
237 | the tags you configured, thus saving you the trouble to manually classify | ||
238 | your entries. | ||
239 | {% endtrans %} | ||
240 | </p> | ||
241 | |||
242 | <h5>{% trans %}How do I use them?{% endtrans %}</h5> | ||
243 | <p class="help"> | ||
244 | {% trans %} | ||
245 | Let assume you want to tag new entries as « <i>short reading</i> » when the reading time is inferior to 3 minutes.<br /> | ||
246 | In that case, you should put « readingTime <= 3 » in the <i>Rule</i> field and « <i>short reading</i> » in the <i>Tags</i> | ||
247 | field.<br /> | ||
248 | Several tags can added simultaneously by separating them by a comma: « <i>short reading, must read</i> »<br /> | ||
249 | Complex rules can be written by using predefined operators: if « <i>readingTime >= 5 AND domainName = "github.com"</i> » then tag as « <i>long reading, github </i> » | ||
250 | {% endtrans %} | ||
251 | </p> | ||
252 | |||
253 | <h5>{% trans %}Which variables and operators can I use to write rules?{% endtrans %}</h5> | ||
254 | <p class="help"> | ||
255 | {% trans %}The following variables and operators can be used to create tagging rules:{% endtrans %} | ||
256 | |||
257 | <table> | ||
258 | <thead> | ||
259 | <tr> | ||
260 | <th>{% trans %}Variable{% endtrans %}</th> | ||
261 | <th>{% trans %}Meaning{% endtrans %}</th> | ||
262 | <th>{% trans %}Operator{% endtrans %}</th> | ||
263 | <th>{% trans %}Meaning{% endtrans %}</th> | ||
264 | </tr> | ||
265 | </thead> | ||
266 | |||
267 | <tbody> | ||
268 | <tr> | ||
269 | <td>title</td> | ||
270 | <td>{% trans %}Title of the entry{% endtrans %}</td> | ||
271 | <td><=</td> | ||
272 | <td>{% trans %}Less than…{% endtrans %}</td> | ||
273 | </tr> | ||
274 | <tr> | ||
275 | <td>url</td> | ||
276 | <td>{% trans %}URL of the entry{% endtrans %}</td> | ||
277 | <td><</td> | ||
278 | <td>{% trans %}Strictly less than…{% endtrans %}</td> | ||
279 | </tr> | ||
280 | <tr> | ||
281 | <td>isArchived</td> | ||
282 | <td>{% trans %}Whether the entry is archived or not{% endtrans %}</td> | ||
283 | <td>=></td> | ||
284 | <td>{% trans %}Greater than…{% endtrans %}</td> | ||
285 | </tr> | ||
286 | <tr> | ||
287 | <td>isStared</td> | ||
288 | <td>{% trans %}Whether the entry is starred or not{% endtrans %}</td> | ||
289 | <td>></td> | ||
290 | <td>{% trans %}Strictly greater than…{% endtrans %}</td> | ||
291 | </tr> | ||
292 | <tr> | ||
293 | <td>content</td> | ||
294 | <td>{% trans %}The entry's content{% endtrans %}</td> | ||
295 | <td>=</td> | ||
296 | <td>{% trans %}Equal to…{% endtrans %}</td> | ||
297 | </tr> | ||
298 | <tr> | ||
299 | <td>language</td> | ||
300 | <td>{% trans %}The entry's language{% endtrans %}</td> | ||
301 | <td>!=</td> | ||
302 | <td>{% trans %}Not equal to…{% endtrans %}</td> | ||
303 | </tr> | ||
304 | <tr> | ||
305 | <td>mimetype</td> | ||
306 | <td>{% trans %}The entry's mime-type{% endtrans %}</td> | ||
307 | <td>OR</td> | ||
308 | <td>{% trans %}One rule or another{% endtrans %}</td> | ||
309 | </tr> | ||
310 | <tr> | ||
311 | <td>readingTime</td> | ||
312 | <td>{% trans %}The estimated entry's reading time, in minutes{% endtrans %}</td> | ||
313 | <td>AND</td> | ||
314 | <td>{% trans %}One rule and another{% endtrans %}</td> | ||
315 | </tr> | ||
316 | <tr> | ||
317 | <td>domainName</td> | ||
318 | <td>{% trans %}The domain name of the entry{% endtrans %}</td> | ||
319 | <td>matches</td> | ||
320 | <td> | ||
321 | {% trans %} | ||
322 | Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br /> | ||
323 | Example: <code>title matches "football"</code> | ||
324 | {% endtrans %} | ||
325 | </td> | ||
326 | </tr> | ||
327 | </tbody> | ||
328 | </table> | ||
329 | </p> | ||
330 | </div> | ||
331 | </div> | ||
332 | </div> | ||
333 | |||
334 | {% if is_granted('ROLE_SUPER_ADMIN') %} | ||
335 | <div id="set6" class="col s12"> | ||
188 | {{ form_start(form.new_user) }} | 336 | {{ form_start(form.new_user) }} |
189 | {{ form_errors(form.new_user) }} | 337 | {{ form_errors(form.new_user) }} |
190 | 338 | ||
diff --git a/src/Wallabag/CoreBundle/Tests/Controller/ConfigControllerTest.php b/src/Wallabag/CoreBundle/Tests/Controller/ConfigControllerTest.php index 7085151a..7b32354f 100644 --- a/src/Wallabag/CoreBundle/Tests/Controller/ConfigControllerTest.php +++ b/src/Wallabag/CoreBundle/Tests/Controller/ConfigControllerTest.php | |||
@@ -479,4 +479,59 @@ class ConfigControllerTest extends WallabagCoreTestCase | |||
479 | $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(array('_text'))); | 479 | $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(array('_text'))); |
480 | $this->assertContains($expectedMessage, $alert[0]); | 480 | $this->assertContains($expectedMessage, $alert[0]); |
481 | } | 481 | } |
482 | |||
483 | public function testTaggingRuleCreation() | ||
484 | { | ||
485 | $this->logInAs('admin'); | ||
486 | $client = $this->getClient(); | ||
487 | |||
488 | $crawler = $client->request('GET', '/config'); | ||
489 | |||
490 | $this->assertTrue($client->getResponse()->isSuccessful()); | ||
491 | |||
492 | $form = $crawler->filter('button[id=tagging_rule_save]')->form(); | ||
493 | |||
494 | $data = array( | ||
495 | 'tagging_rule[rule]' => 'readingTime <= 3', | ||
496 | 'tagging_rule[tags]' => 'short reading', | ||
497 | ); | ||
498 | |||
499 | $client->submit($form, $data); | ||
500 | |||
501 | $this->assertEquals(302, $client->getResponse()->getStatusCode()); | ||
502 | |||
503 | $crawler = $client->followRedirect(); | ||
504 | |||
505 | $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text'))); | ||
506 | $this->assertContains('Tagging rules updated', $alert[0]); | ||
507 | |||
508 | $deleteLink = $crawler->filter('.delete')->last()->link(); | ||
509 | |||
510 | $crawler = $client->click($deleteLink); | ||
511 | $this->assertEquals(302, $client->getResponse()->getStatusCode()); | ||
512 | |||
513 | $crawler = $client->followRedirect(); | ||
514 | $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text'))); | ||
515 | $this->assertContains('Tagging rule deleted', $alert[0]); | ||
516 | } | ||
517 | |||
518 | public function dataForTaggingRuleFailed() | ||
519 | { | ||
520 | return array( | ||
521 | array( | ||
522 | array( | ||
523 | 'rss_config[rule]' => 'unknownVar <= 3', | ||
524 | 'rss_config[tags]' => 'cool tag', | ||
525 | ), | ||
526 | 'The variable « unknownVar » does not exist.', | ||
527 | ), | ||
528 | array( | ||
529 | array( | ||
530 | 'rss_config[rule]' => 'length(domainName) <= 42', | ||
531 | 'rss_config[tags]' => 'cool tag', | ||
532 | ), | ||
533 | 'The operator « length » does not exist.', | ||
534 | ), | ||
535 | ); | ||
536 | } | ||
482 | } | 537 | } |
diff --git a/src/Wallabag/CoreBundle/Tests/Controller/EntryControllerTest.php b/src/Wallabag/CoreBundle/Tests/Controller/EntryControllerTest.php index 56b4c9e4..af62aee8 100644 --- a/src/Wallabag/CoreBundle/Tests/Controller/EntryControllerTest.php +++ b/src/Wallabag/CoreBundle/Tests/Controller/EntryControllerTest.php | |||
@@ -102,6 +102,44 @@ class EntryControllerTest extends WallabagCoreTestCase | |||
102 | $this->assertContains('Google', $alert[0]); | 102 | $this->assertContains('Google', $alert[0]); |
103 | } | 103 | } |
104 | 104 | ||
105 | /** | ||
106 | * This test will require an internet connection. | ||
107 | */ | ||
108 | public function testPostNewThatWillBeTaggued() | ||
109 | { | ||
110 | $this->logInAs('admin'); | ||
111 | $client = $this->getClient(); | ||
112 | |||
113 | $crawler = $client->request('GET', '/new'); | ||
114 | |||
115 | $this->assertEquals(200, $client->getResponse()->getStatusCode()); | ||
116 | |||
117 | $form = $crawler->filter('button[type=submit]')->form(); | ||
118 | |||
119 | $data = array( | ||
120 | 'entry[url]' => $url = 'https://github.com/wallabag/wallabag', | ||
121 | ); | ||
122 | |||
123 | $client->submit($form, $data); | ||
124 | |||
125 | $this->assertEquals(302, $client->getResponse()->getStatusCode()); | ||
126 | |||
127 | $crawler = $client->followRedirect(); | ||
128 | |||
129 | $em = $client->getContainer() | ||
130 | ->get('doctrine.orm.entity_manager'); | ||
131 | $entry = $em | ||
132 | ->getRepository('WallabagCoreBundle:Entry') | ||
133 | ->findOneByUrl($url); | ||
134 | $tags = $entry->getTags(); | ||
135 | |||
136 | $this->assertCount(1, $tags); | ||
137 | $this->assertEquals('wallabag', $tags[0]->getLabel()); | ||
138 | |||
139 | $em->remove($entry); | ||
140 | $em->flush(); | ||
141 | } | ||
142 | |||
105 | public function testArchive() | 143 | public function testArchive() |
106 | { | 144 | { |
107 | $this->logInAs('admin'); | 145 | $this->logInAs('admin'); |
diff --git a/src/Wallabag/CoreBundle/Tests/Form/DataTransformer/StringToListTransformerTest.php b/src/Wallabag/CoreBundle/Tests/Form/DataTransformer/StringToListTransformerTest.php new file mode 100644 index 00000000..d114e5f3 --- /dev/null +++ b/src/Wallabag/CoreBundle/Tests/Form/DataTransformer/StringToListTransformerTest.php | |||
@@ -0,0 +1,50 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Tests\Form\DataTransformer; | ||
4 | |||
5 | use Wallabag\CoreBundle\Form\DataTransformer\StringToListTransformer; | ||
6 | |||
7 | class StringToListTransformerTest extends \PHPUnit_Framework_TestCase | ||
8 | { | ||
9 | /** | ||
10 | * @dataProvider transformProvider | ||
11 | */ | ||
12 | public function testTransformWithValidData($inputData, $expectedResult) | ||
13 | { | ||
14 | $transformer = new StringToListTransformer(); | ||
15 | |||
16 | $this->assertSame($expectedResult, $transformer->transform($inputData)); | ||
17 | } | ||
18 | |||
19 | public function transformProvider() | ||
20 | { | ||
21 | return array( | ||
22 | array( null, '' ), | ||
23 | array( array(), '' ), | ||
24 | array( array('single value'), 'single value' ), | ||
25 | array( array('first value', 'second value'), 'first value,second value' ), | ||
26 | ); | ||
27 | } | ||
28 | |||
29 | /** | ||
30 | * @dataProvider reverseTransformProvider | ||
31 | */ | ||
32 | public function testReverseTransformWithValidData($inputData, $expectedResult) | ||
33 | { | ||
34 | $transformer = new StringToListTransformer(); | ||
35 | |||
36 | $this->assertSame($expectedResult, $transformer->reverseTransform($inputData)); | ||
37 | } | ||
38 | |||
39 | public function reverseTransformProvider() | ||
40 | { | ||
41 | return array( | ||
42 | array( null, null ), | ||
43 | array( '', array() ), | ||
44 | array( 'single value', array('single value') ), | ||
45 | array( 'first value,second value', array('first value', 'second value') ), | ||
46 | array( 'first value, second value', array('first value', 'second value') ), | ||
47 | array( 'first value, , second value', array('first value', 'second value') ), | ||
48 | ); | ||
49 | } | ||
50 | } | ||
diff --git a/src/Wallabag/CoreBundle/Tests/Helper/ContentProxyTest.php b/src/Wallabag/CoreBundle/Tests/Helper/ContentProxyTest.php index 4bce4708..ef7cbd5b 100644 --- a/src/Wallabag/CoreBundle/Tests/Helper/ContentProxyTest.php +++ b/src/Wallabag/CoreBundle/Tests/Helper/ContentProxyTest.php | |||
@@ -2,6 +2,9 @@ | |||
2 | 2 | ||
3 | namespace Wallabag\CoreBundle\Tests\Helper; | 3 | namespace Wallabag\CoreBundle\Tests\Helper; |
4 | 4 | ||
5 | use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||
6 | use Psr\Log\NullLogger; | ||
7 | |||
5 | use Wallabag\CoreBundle\Entity\Entry; | 8 | use Wallabag\CoreBundle\Entity\Entry; |
6 | use Wallabag\UserBundle\Entity\User; | 9 | use Wallabag\UserBundle\Entity\User; |
7 | use Wallabag\CoreBundle\Helper\ContentProxy; | 10 | use Wallabag\CoreBundle\Helper\ContentProxy; |
@@ -10,6 +13,10 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
10 | { | 13 | { |
11 | public function testWithEmptyContent() | 14 | public function testWithEmptyContent() |
12 | { | 15 | { |
16 | $tagger = $this->getTaggerMock(); | ||
17 | $tagger->expects($this->once()) | ||
18 | ->method('tag'); | ||
19 | |||
13 | $graby = $this->getMockBuilder('Graby\Graby') | 20 | $graby = $this->getMockBuilder('Graby\Graby') |
14 | ->setMethods(array('fetchContent')) | 21 | ->setMethods(array('fetchContent')) |
15 | ->disableOriginalConstructor() | 22 | ->disableOriginalConstructor() |
@@ -25,7 +32,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
25 | 'language' => '', | 32 | 'language' => '', |
26 | )); | 33 | )); |
27 | 34 | ||
28 | $proxy = new ContentProxy($graby); | 35 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger()); |
29 | $entry = $proxy->updateEntry(new Entry(new User()), 'http://0.0.0.0'); | 36 | $entry = $proxy->updateEntry(new Entry(new User()), 'http://0.0.0.0'); |
30 | 37 | ||
31 | $this->assertEquals('http://0.0.0.0', $entry->getUrl()); | 38 | $this->assertEquals('http://0.0.0.0', $entry->getUrl()); |
@@ -40,6 +47,10 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
40 | 47 | ||
41 | public function testWithEmptyContentButOG() | 48 | public function testWithEmptyContentButOG() |
42 | { | 49 | { |
50 | $tagger = $this->getTaggerMock(); | ||
51 | $tagger->expects($this->once()) | ||
52 | ->method('tag'); | ||
53 | |||
43 | $graby = $this->getMockBuilder('Graby\Graby') | 54 | $graby = $this->getMockBuilder('Graby\Graby') |
44 | ->setMethods(array('fetchContent')) | 55 | ->setMethods(array('fetchContent')) |
45 | ->disableOriginalConstructor() | 56 | ->disableOriginalConstructor() |
@@ -59,7 +70,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
59 | ), | 70 | ), |
60 | )); | 71 | )); |
61 | 72 | ||
62 | $proxy = new ContentProxy($graby); | 73 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger()); |
63 | $entry = $proxy->updateEntry(new Entry(new User()), 'http://domain.io'); | 74 | $entry = $proxy->updateEntry(new Entry(new User()), 'http://domain.io'); |
64 | 75 | ||
65 | $this->assertEquals('http://domain.io', $entry->getUrl()); | 76 | $this->assertEquals('http://domain.io', $entry->getUrl()); |
@@ -74,6 +85,10 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
74 | 85 | ||
75 | public function testWithContent() | 86 | public function testWithContent() |
76 | { | 87 | { |
88 | $tagger = $this->getTaggerMock(); | ||
89 | $tagger->expects($this->once()) | ||
90 | ->method('tag'); | ||
91 | |||
77 | $graby = $this->getMockBuilder('Graby\Graby') | 92 | $graby = $this->getMockBuilder('Graby\Graby') |
78 | ->setMethods(array('fetchContent')) | 93 | ->setMethods(array('fetchContent')) |
79 | ->disableOriginalConstructor() | 94 | ->disableOriginalConstructor() |
@@ -94,7 +109,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
94 | ), | 109 | ), |
95 | )); | 110 | )); |
96 | 111 | ||
97 | $proxy = new ContentProxy($graby); | 112 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger()); |
98 | $entry = $proxy->updateEntry(new Entry(new User()), 'http://0.0.0.0'); | 113 | $entry = $proxy->updateEntry(new Entry(new User()), 'http://0.0.0.0'); |
99 | 114 | ||
100 | $this->assertEquals('http://1.1.1.1', $entry->getUrl()); | 115 | $this->assertEquals('http://1.1.1.1', $entry->getUrl()); |
@@ -106,4 +121,17 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
106 | $this->assertEquals(4.0, $entry->getReadingTime()); | 121 | $this->assertEquals(4.0, $entry->getReadingTime()); |
107 | $this->assertEquals('1.1.1.1', $entry->getDomainName()); | 122 | $this->assertEquals('1.1.1.1', $entry->getDomainName()); |
108 | } | 123 | } |
124 | |||
125 | private function getTaggerMock() | ||
126 | { | ||
127 | return $this->getMockBuilder('Wallabag\CoreBundle\Helper\RuleBasedTagger') | ||
128 | ->setMethods(array('tag')) | ||
129 | ->disableOriginalConstructor() | ||
130 | ->getMock(); | ||
131 | } | ||
132 | |||
133 | private function getLogger() | ||
134 | { | ||
135 | return new NullLogger(); | ||
136 | } | ||
109 | } | 137 | } |
diff --git a/src/Wallabag/CoreBundle/Tests/Helper/RuleBasedTaggerTest.php b/src/Wallabag/CoreBundle/Tests/Helper/RuleBasedTaggerTest.php new file mode 100644 index 00000000..5180f7dd --- /dev/null +++ b/src/Wallabag/CoreBundle/Tests/Helper/RuleBasedTaggerTest.php | |||
@@ -0,0 +1,167 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Tests\Helper; | ||
4 | |||
5 | use Wallabag\CoreBundle\Entity\Config; | ||
6 | use Wallabag\CoreBundle\Entity\Entry; | ||
7 | use Wallabag\CoreBundle\Entity\Tag; | ||
8 | use Wallabag\CoreBundle\Entity\TaggingRule; | ||
9 | use Wallabag\UserBundle\Entity\User; | ||
10 | use Wallabag\CoreBundle\Helper\RuleBasedTagger; | ||
11 | |||
12 | class RuleBasedTaggerTest extends \PHPUnit_Framework_TestCase | ||
13 | { | ||
14 | private $rulerz; | ||
15 | private $tagRepository; | ||
16 | private $entryRepository; | ||
17 | private $tagger; | ||
18 | |||
19 | public function setUp() | ||
20 | { | ||
21 | $this->rulerz = $this->getRulerZMock(); | ||
22 | $this->tagRepository = $this->getTagRepositoryMock(); | ||
23 | $this->entryRepository = $this->getEntryRepositoryMock(); | ||
24 | |||
25 | $this->tagger = new RuleBasedTagger($this->rulerz, $this->tagRepository, $this->entryRepository); | ||
26 | } | ||
27 | |||
28 | public function testTagWithNoRule() | ||
29 | { | ||
30 | $entry = new Entry($this->getUser()); | ||
31 | |||
32 | $this->tagger->tag($entry); | ||
33 | |||
34 | $this->assertTrue($entry->getTags()->isEmpty()); | ||
35 | } | ||
36 | |||
37 | public function testTagWithNoMatchingRule() | ||
38 | { | ||
39 | $taggingRule = $this->getTaggingRule('rule as string', array('foo', 'bar')); | ||
40 | $user = $this->getUser([$taggingRule]); | ||
41 | $entry = new Entry($user); | ||
42 | |||
43 | $this->rulerz | ||
44 | ->expects($this->once()) | ||
45 | ->method('satisfies') | ||
46 | ->with($entry, 'rule as string') | ||
47 | ->willReturn(false); | ||
48 | |||
49 | $this->tagger->tag($entry); | ||
50 | |||
51 | $this->assertTrue($entry->getTags()->isEmpty()); | ||
52 | } | ||
53 | |||
54 | public function testTagWithAMatchingRule() | ||
55 | { | ||
56 | $taggingRule = $this->getTaggingRule('rule as string', array('foo', 'bar')); | ||
57 | $user = $this->getUser([$taggingRule]); | ||
58 | $entry = new Entry($user); | ||
59 | |||
60 | $this->rulerz | ||
61 | ->expects($this->once()) | ||
62 | ->method('satisfies') | ||
63 | ->with($entry, 'rule as string') | ||
64 | ->willReturn(true); | ||
65 | |||
66 | $this->tagger->tag($entry); | ||
67 | |||
68 | $this->assertFalse($entry->getTags()->isEmpty()); | ||
69 | |||
70 | $tags = $entry->getTags(); | ||
71 | $this->assertSame('foo', $tags[0]->getLabel()); | ||
72 | $this->assertSame($user, $tags[0]->getUser()); | ||
73 | $this->assertSame('bar', $tags[1]->getLabel()); | ||
74 | $this->assertSame($user, $tags[1]->getUser()); | ||
75 | } | ||
76 | |||
77 | public function testTagWithAMixOfMatchingRules() | ||
78 | { | ||
79 | $taggingRule = $this->getTaggingRule('bla bla', array('hey')); | ||
80 | $otherTaggingRule = $this->getTaggingRule('rule as string', array('foo')); | ||
81 | |||
82 | $user = $this->getUser([$taggingRule, $otherTaggingRule]); | ||
83 | $entry = new Entry($user); | ||
84 | |||
85 | $this->rulerz | ||
86 | ->method('satisfies') | ||
87 | ->will($this->onConsecutiveCalls(false, true)); | ||
88 | |||
89 | $this->tagger->tag($entry); | ||
90 | |||
91 | $this->assertFalse($entry->getTags()->isEmpty()); | ||
92 | |||
93 | $tags = $entry->getTags(); | ||
94 | $this->assertSame('foo', $tags[0]->getLabel()); | ||
95 | $this->assertSame($user, $tags[0]->getUser()); | ||
96 | } | ||
97 | |||
98 | public function testWhenTheTagExists() | ||
99 | { | ||
100 | $taggingRule = $this->getTaggingRule('rule as string', array('foo')); | ||
101 | $user = $this->getUser([$taggingRule]); | ||
102 | $entry = new Entry($user); | ||
103 | $tag = new Tag($user); | ||
104 | |||
105 | $this->rulerz | ||
106 | ->expects($this->once()) | ||
107 | ->method('satisfies') | ||
108 | ->with($entry, 'rule as string') | ||
109 | ->willReturn(true); | ||
110 | |||
111 | $this->tagRepository | ||
112 | ->expects($this->once()) | ||
113 | ->method('findOneByLabelAndUserId') | ||
114 | ->willReturn($tag); | ||
115 | |||
116 | $this->tagger->tag($entry); | ||
117 | |||
118 | $this->assertFalse($entry->getTags()->isEmpty()); | ||
119 | |||
120 | $tags = $entry->getTags(); | ||
121 | $this->assertSame($tag, $tags[0]); | ||
122 | } | ||
123 | |||
124 | private function getUser(array $taggingRules = []) | ||
125 | { | ||
126 | $user = new User(); | ||
127 | $config = new Config($user); | ||
128 | |||
129 | $user->setConfig($config); | ||
130 | |||
131 | foreach ($taggingRules as $rule) { | ||
132 | $config->addTaggingRule($rule); | ||
133 | } | ||
134 | |||
135 | return $user; | ||
136 | } | ||
137 | |||
138 | private function getTaggingRule($rule, array $tags) | ||
139 | { | ||
140 | $taggingRule = new TaggingRule(); | ||
141 | $taggingRule->setRule($rule); | ||
142 | $taggingRule->setTags($tags); | ||
143 | |||
144 | return $taggingRule; | ||
145 | } | ||
146 | |||
147 | private function getRulerZMock() | ||
148 | { | ||
149 | return $this->getMockBuilder('RulerZ\RulerZ') | ||
150 | ->disableOriginalConstructor() | ||
151 | ->getMock(); | ||
152 | } | ||
153 | |||
154 | private function getTagRepositoryMock() | ||
155 | { | ||
156 | return $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository') | ||
157 | ->disableOriginalConstructor() | ||
158 | ->getMock(); | ||
159 | } | ||
160 | |||
161 | private function getEntryRepositoryMock() | ||
162 | { | ||
163 | return $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository') | ||
164 | ->disableOriginalConstructor() | ||
165 | ->getMock(); | ||
166 | } | ||
167 | } | ||
diff --git a/src/Wallabag/UserBundle/Repository/UserRepository.php b/src/Wallabag/UserBundle/Repository/UserRepository.php index c020f3ca..009c4881 100644 --- a/src/Wallabag/UserBundle/Repository/UserRepository.php +++ b/src/Wallabag/UserBundle/Repository/UserRepository.php | |||
@@ -23,4 +23,19 @@ class UserRepository extends EntityRepository | |||
23 | ->getQuery() | 23 | ->getQuery() |
24 | ->getOneOrNullResult(); | 24 | ->getOneOrNullResult(); |
25 | } | 25 | } |
26 | |||
27 | /** | ||
28 | * Find a user by its username. | ||
29 | * | ||
30 | * @param string $username | ||
31 | * | ||
32 | * @return User | ||
33 | */ | ||
34 | public function findOneByUserName($username) | ||
35 | { | ||
36 | return $this->createQueryBuilder('u') | ||
37 | ->andWhere('u.username = :username')->setParameter('username', $username) | ||
38 | ->getQuery() | ||
39 | ->getSingleResult(); | ||
40 | } | ||
26 | } | 41 | } |