aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorJeremy Benoist <j0k3r@users.noreply.github.com>2015-12-06 14:31:26 +0100
committerJeremy Benoist <j0k3r@users.noreply.github.com>2015-12-06 14:31:26 +0100
commita7f1921f7db312b5def3839393357f443dcbb52c (patch)
tree0522e03891433e9fdc9eb64d52b2d9651aadf1f7 /src
parent2e15e30bf0e634bbbc3a9678904953d015490ed2 (diff)
parent752b90d1f2e279d3662d5431b09c7587df2937ca (diff)
downloadwallabag-a7f1921f7db312b5def3839393357f443dcbb52c.tar.gz
wallabag-a7f1921f7db312b5def3839393357f443dcbb52c.tar.zst
wallabag-a7f1921f7db312b5def3839393357f443dcbb52c.zip
Merge pull request #1478 from K-Phoen/rule-based-tags
Rule based tags
Diffstat (limited to 'src')
-rw-r--r--src/Wallabag/CoreBundle/Command/TagAllCommand.php66
-rw-r--r--src/Wallabag/CoreBundle/Controller/ConfigController.php48
-rw-r--r--src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php8
-rw-r--r--src/Wallabag/CoreBundle/Entity/Config.php28
-rw-r--r--src/Wallabag/CoreBundle/Entity/Entry.php4
-rw-r--r--src/Wallabag/CoreBundle/Entity/TaggingRule.php133
-rw-r--r--src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php59
-rw-r--r--src/Wallabag/CoreBundle/Form/Type/TaggingRuleType.php38
-rw-r--r--src/Wallabag/CoreBundle/Helper/ContentProxy.php18
-rw-r--r--src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php108
-rw-r--r--src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php25
-rw-r--r--src/Wallabag/CoreBundle/Operator/PHP/Matches.php21
-rw-r--r--src/Wallabag/CoreBundle/Repository/TaggingRuleRepository.php9
-rw-r--r--src/Wallabag/CoreBundle/Resources/config/services.yml31
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig33
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig152
-rw-r--r--src/Wallabag/CoreBundle/Tests/Controller/ConfigControllerTest.php55
-rw-r--r--src/Wallabag/CoreBundle/Tests/Controller/EntryControllerTest.php38
-rw-r--r--src/Wallabag/CoreBundle/Tests/Form/DataTransformer/StringToListTransformerTest.php50
-rw-r--r--src/Wallabag/CoreBundle/Tests/Helper/ContentProxyTest.php34
-rw-r--r--src/Wallabag/CoreBundle/Tests/Helper/RuleBasedTaggerTest.php167
-rw-r--r--src/Wallabag/UserBundle/Repository/UserRepository.php15
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
3namespace Wallabag\CoreBundle\Command;
4
5use Doctrine\ORM\NoResultException;
6use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface;
10
11class 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;
7use Symfony\Component\HttpFoundation\Request; 7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpFoundation\JsonResponse; 8use Symfony\Component\HttpFoundation\JsonResponse;
9use Wallabag\CoreBundle\Entity\Config; 9use Wallabag\CoreBundle\Entity\Config;
10use Wallabag\CoreBundle\Entity\TaggingRule;
10use Wallabag\UserBundle\Entity\User; 11use Wallabag\UserBundle\Entity\User;
11use Wallabag\CoreBundle\Form\Type\ChangePasswordType; 12use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
12use Wallabag\CoreBundle\Form\Type\UserInformationType; 13use Wallabag\CoreBundle\Form\Type\UserInformationType;
14use Wallabag\CoreBundle\Form\Type\TaggingRuleType;
13use Wallabag\CoreBundle\Form\Type\NewUserType; 15use Wallabag\CoreBundle\Form\Type\NewUserType;
14use Wallabag\CoreBundle\Form\Type\RssType; 16use Wallabag\CoreBundle\Form\Type\RssType;
15use Wallabag\CoreBundle\Tools\Utils; 17use 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;
6use Doctrine\Common\DataFixtures\OrderedFixtureInterface; 6use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
7use Doctrine\Common\Persistence\ObjectManager; 7use Doctrine\Common\Persistence\ObjectManager;
8use Wallabag\CoreBundle\Entity\Config; 8use Wallabag\CoreBundle\Entity\Config;
9use Wallabag\CoreBundle\Entity\TaggingRule;
9 10
10class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface 11class 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
3namespace Wallabag\CoreBundle\Entity; 3namespace Wallabag\CoreBundle\Entity;
4 4
5use Doctrine\Common\Collections\ArrayCollection;
5use Doctrine\ORM\Mapping as ORM; 6use Doctrine\ORM\Mapping as ORM;
6use Symfony\Component\Validator\Constraints as Assert; 7use 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
3namespace Wallabag\CoreBundle\Entity;
4
5use Doctrine\ORM\Mapping as ORM;
6use Symfony\Component\Validator\Constraints as Assert;
7use 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 */
16class 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
3namespace Wallabag\CoreBundle\Form\DataTransformer;
4
5use Doctrine\Common\Persistence\ObjectManager;
6use Symfony\Component\Form\DataTransformerInterface;
7use 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 */
13class 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
3namespace Wallabag\CoreBundle\Form\Type;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\FormBuilderInterface;
7use Symfony\Component\OptionsResolver\OptionsResolver;
8
9use Wallabag\CoreBundle\Form\DataTransformer\StringToListTransformer;
10
11class 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 @@
3namespace Wallabag\CoreBundle\Helper; 3namespace Wallabag\CoreBundle\Helper;
4 4
5use Graby\Graby; 5use Graby\Graby;
6use Psr\Log\LoggerInterface as Logger;
6use Wallabag\CoreBundle\Entity\Entry; 7use Wallabag\CoreBundle\Entity\Entry;
7use Wallabag\CoreBundle\Tools\Utils; 8use Wallabag\CoreBundle\Tools\Utils;
8 9
@@ -13,10 +14,14 @@ use Wallabag\CoreBundle\Tools\Utils;
13class ContentProxy 14class 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
3namespace Wallabag\CoreBundle\Helper;
4
5use RulerZ\RulerZ;
6
7use Wallabag\CoreBundle\Entity\Entry;
8use Wallabag\CoreBundle\Entity\Tag;
9use Wallabag\CoreBundle\Repository\EntryRepository;
10use Wallabag\CoreBundle\Repository\TagRepository;
11use Wallabag\UserBundle\Entity\User;
12
13class 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
3namespace 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 */
15class 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
3namespace 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 */
15class 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
3namespace Wallabag\CoreBundle\Repository;
4
5use Doctrine\ORM\EntityRepository;
6
7class 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 &lt;= 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 &gt;= 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>&lt;=</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>&lt;</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>=&gt;</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>&gt;</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
3namespace Wallabag\CoreBundle\Tests\Form\DataTransformer;
4
5use Wallabag\CoreBundle\Form\DataTransformer\StringToListTransformer;
6
7class 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
3namespace Wallabag\CoreBundle\Tests\Helper; 3namespace Wallabag\CoreBundle\Tests\Helper;
4 4
5use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
6use Psr\Log\NullLogger;
7
5use Wallabag\CoreBundle\Entity\Entry; 8use Wallabag\CoreBundle\Entity\Entry;
6use Wallabag\UserBundle\Entity\User; 9use Wallabag\UserBundle\Entity\User;
7use Wallabag\CoreBundle\Helper\ContentProxy; 10use 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
3namespace Wallabag\CoreBundle\Tests\Helper;
4
5use Wallabag\CoreBundle\Entity\Config;
6use Wallabag\CoreBundle\Entity\Entry;
7use Wallabag\CoreBundle\Entity\Tag;
8use Wallabag\CoreBundle\Entity\TaggingRule;
9use Wallabag\UserBundle\Entity\User;
10use Wallabag\CoreBundle\Helper\RuleBasedTagger;
11
12class 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}