]> git.immae.eu Git - github/wallabag/wallabag.git/commitdiff
Merge pull request #1478 from K-Phoen/rule-based-tags
authorJeremy Benoist <j0k3r@users.noreply.github.com>
Sun, 6 Dec 2015 13:31:26 +0000 (14:31 +0100)
committerJeremy Benoist <j0k3r@users.noreply.github.com>
Sun, 6 Dec 2015 13:31:26 +0000 (14:31 +0100)
Rule based tags

26 files changed:
app/AppKernel.php
app/config/config.yml
composer.json
composer.lock
src/Wallabag/CoreBundle/Command/TagAllCommand.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Controller/ConfigController.php
src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php
src/Wallabag/CoreBundle/Entity/Config.php
src/Wallabag/CoreBundle/Entity/Entry.php
src/Wallabag/CoreBundle/Entity/TaggingRule.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Form/Type/TaggingRuleType.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Helper/ContentProxy.php
src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Operator/PHP/Matches.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Repository/TaggingRuleRepository.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Resources/config/services.yml
src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig
src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
src/Wallabag/CoreBundle/Tests/Controller/ConfigControllerTest.php
src/Wallabag/CoreBundle/Tests/Controller/EntryControllerTest.php
src/Wallabag/CoreBundle/Tests/Form/DataTransformer/StringToListTransformerTest.php [new file with mode: 0644]
src/Wallabag/CoreBundle/Tests/Helper/ContentProxyTest.php
src/Wallabag/CoreBundle/Tests/Helper/RuleBasedTaggerTest.php [new file with mode: 0644]
src/Wallabag/UserBundle/Repository/UserRepository.php

index 2475fe162fd419224b416c99f7cb9331d80dea3d..1eacb348ec7d1d93a68de19919ea7863b82fdc4f 100644 (file)
@@ -29,6 +29,7 @@ class AppKernel extends Kernel
             new FOS\OAuthServerBundle\FOSOAuthServerBundle(),
             new Wallabag\UserBundle\WallabagUserBundle(),
             new Scheb\TwoFactorBundle\SchebTwoFactorBundle(),
+            new KPhoen\RulerZBundle\KPhoenRulerZBundle(),
         );
 
         if (in_array($this->getEnvironment(), array('dev', 'test'))) {
index 285fbd7cf10f96a06d5b622923ebfe6b4c7bd499..82c5e7c997c1a2225139c917ca21b254e03fc189 100644 (file)
@@ -189,3 +189,7 @@ scheb_two_factor:
         sender_email: %twofactor_sender%
         digits: 6
         template: WallabagUserBundle:Authentication:form.html.twig
+
+kphoen_rulerz:
+    executors:
+        doctrine: true
index 5892dc1aa5c566d7a0cc5a2f0cab0130f5db4b08..26608cdceadd04522b83ab77d062dfa7351a2dd5 100644 (file)
@@ -57,7 +57,8 @@
         "friendsofsymfony/oauth-server-bundle": "^1.4@dev",
         "scheb/two-factor-bundle": "~1.4",
         "grandt/phpepub": "~4.0",
-        "wallabag/php-mobi": "~1.0.0"
+        "wallabag/php-mobi": "~1.0.0",
+        "kphoen/rulerz-bundle": "~0.10"
     },
     "require-dev": {
         "doctrine/doctrine-fixtures-bundle": "~2.2.0",
index b7b5d142b973c4d24711896366ea766ffe20f4ea..eea3f8a0dc0950ff06e042a57d08bd30c60488ee 100644 (file)
@@ -4,7 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "a9ec461e17166dcda1563dd55f6ff861",
+    "hash": "fed9468f6c830b0f81899daad7670af7",
+    "content-hash": "394f8a6ca5162f2d2756dbbee0ff5aae",
     "packages": [
         {
             "name": "doctrine/annotations",
             ],
             "time": "2014-10-12 19:18:40"
         },
+        {
+            "name": "hoa/compiler",
+            "version": "2.15.10.29",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Compiler.git",
+                "reference": "ec0849fd3c1472fbcd86c3c961981f0cfe1f8d39"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Compiler/zipball/ec0849fd3c1472fbcd86c3c961981f0cfe1f8d39",
+                "reference": "ec0849fd3c1472fbcd86c3c961981f0cfe1f8d39",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/core": "~2.0",
+                "hoa/file": "~0.0",
+                "hoa/iterator": "~1.0",
+                "hoa/math": "~0.0",
+                "hoa/regex": "~0.0",
+                "hoa/visitor": "~1.0"
+            },
+            "require-dev": {
+                "hoa/json": "~1.0",
+                "hoa/test": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Compiler\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Compiler library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "algebraic",
+                "ast",
+                "compiler",
+                "context-free",
+                "coverage",
+                "exhaustive",
+                "grammar",
+                "isotropic",
+                "language",
+                "lexer",
+                "library",
+                "ll1",
+                "llk",
+                "parser",
+                "pp",
+                "random",
+                "regular",
+                "rule",
+                "sampler",
+                "syntax",
+                "token",
+                "trace",
+                "uniform"
+            ],
+            "time": "2015-10-29 21:35:12"
+        },
+        {
+            "name": "hoa/core",
+            "version": "2.15.11.09",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Core.git",
+                "reference": "5538b1e90e2c66c90df5cc45e03fb85d047be900"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Core/zipball/5538b1e90e2c66c90df5cc45e03fb85d047be900",
+                "reference": "5538b1e90e2c66c90df5cc45e03fb85d047be900",
+                "shasum": ""
+            },
+            "require": {
+                "ext-spl": "*",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "hoa/test": "~1.0"
+            },
+            "suggest": {
+                "ext-mbstring": "ext/mbstring must be present (or a third implementation).",
+                "hoa/cli": "To use the `hoa` script."
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Core\\": "."
+                },
+                "files": [
+                    "Core.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Core library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "consistency",
+                "core",
+                "data",
+                "event",
+                "library",
+                "listener",
+                "parameter",
+                "protocol"
+            ],
+            "time": "2015-11-09 06:51:06"
+        },
+        {
+            "name": "hoa/file",
+            "version": "0.15.11.09",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/File.git",
+                "reference": "f46fe552ff79cb6c93a2ff9c25cfbc134fbd57ee"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/File/zipball/f46fe552ff79cb6c93a2ff9c25cfbc134fbd57ee",
+                "reference": "f46fe552ff79cb6c93a2ff9c25cfbc134fbd57ee",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/core": "~2.0",
+                "hoa/iterator": "~1.0",
+                "hoa/stream": "~0.0"
+            },
+            "require-dev": {
+                "hoa/test": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\File\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\File library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "Socket",
+                "directory",
+                "file",
+                "finder",
+                "library",
+                "link",
+                "temporary"
+            ],
+            "time": "2015-11-09 06:55:20"
+        },
+        {
+            "name": "hoa/iterator",
+            "version": "1.15.10.29",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Iterator.git",
+                "reference": "a64ed9fd62579a34e4450134d6d1abdf77d54435"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/a64ed9fd62579a34e4450134d6d1abdf77d54435",
+                "reference": "a64ed9fd62579a34e4450134d6d1abdf77d54435",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/core": "~2.0"
+            },
+            "require-dev": {
+                "hoa/test": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Iterator\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Iterator library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "iterator",
+                "library"
+            ],
+            "time": "2015-10-29 21:37:16"
+        },
+        {
+            "name": "hoa/math",
+            "version": "0.15.10.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Math.git",
+                "reference": "62631c65d9a4f1b8bb4c4a3d6cdff0e8971d684e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Math/zipball/62631c65d9a4f1b8bb4c4a3d6cdff0e8971d684e",
+                "reference": "62631c65d9a4f1b8bb4c4a3d6cdff0e8971d684e",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/compiler": "~2.0",
+                "hoa/core": "~2.0",
+                "hoa/iterator": "~1.0"
+            },
+            "require-dev": {
+                "hoa/test": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Math\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Math library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "arrangement",
+                "combination",
+                "combinatorics",
+                "counting",
+                "library",
+                "math",
+                "permutation",
+                "sampler",
+                "set"
+            ],
+            "time": "2015-10-26 15:22:52"
+        },
+        {
+            "name": "hoa/regex",
+            "version": "0.15.08.13",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Regex.git",
+                "reference": "2ef8a77ef3885ca202fcd9c31a8e54c44cd04232"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Regex/zipball/2ef8a77ef3885ca202fcd9c31a8e54c44cd04232",
+                "reference": "2ef8a77ef3885ca202fcd9c31a8e54c44cd04232",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/core": "~2.0",
+                "hoa/math": "~0.0",
+                "hoa/ustring": "~3.0",
+                "hoa/visitor": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Regex\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Regex library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "compiler",
+                "library",
+                "regex"
+            ],
+            "time": "2015-08-13 06:48:47"
+        },
+        {
+            "name": "hoa/ruler",
+            "version": "1.15.11.09",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Ruler.git",
+                "reference": "9afc9ae032d40b6dc10bff85c9126cf516953925"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Ruler/zipball/9afc9ae032d40b6dc10bff85c9126cf516953925",
+                "reference": "9afc9ae032d40b6dc10bff85c9126cf516953925",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/compiler": "~2.0",
+                "hoa/core": "~2.0",
+                "hoa/file": "~0.0",
+                "hoa/visitor": "~1.0"
+            },
+            "require-dev": {
+                "hoa/test": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Ruler\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Ruler library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "library",
+                "ruler"
+            ],
+            "time": "2015-11-09 06:58:52"
+        },
+        {
+            "name": "hoa/stream",
+            "version": "0.15.10.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Stream.git",
+                "reference": "011ab91d942f1d7096deade4c8a10fe57d51c5b3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Stream/zipball/011ab91d942f1d7096deade4c8a10fe57d51c5b3",
+                "reference": "011ab91d942f1d7096deade4c8a10fe57d51c5b3",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/core": "~2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Stream\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Stream library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "Context",
+                "bucket",
+                "composite",
+                "filter",
+                "in",
+                "library",
+                "out",
+                "protocol",
+                "stream",
+                "wrapper"
+            ],
+            "time": "2015-10-22 06:30:43"
+        },
+        {
+            "name": "hoa/ustring",
+            "version": "3.15.11.09",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Ustring.git",
+                "reference": "8506be4910212b1a2beb9014763a8a4fbd871001"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/8506be4910212b1a2beb9014763a8a4fbd871001",
+                "reference": "8506be4910212b1a2beb9014763a8a4fbd871001",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/core": "~2.0"
+            },
+            "require-dev": {
+                "hoa/test": "~1.0"
+            },
+            "suggest": {
+                "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().",
+                "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()."
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Ustring\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Ustring library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "library",
+                "search",
+                "string",
+                "unicode"
+            ],
+            "time": "2015-11-09 06:44:33"
+        },
+        {
+            "name": "hoa/visitor",
+            "version": "1.15.08.17",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/hoaproject/Visitor.git",
+                "reference": "e30bfff741f71979f6476a41548e34afe8053c67"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/hoaproject/Visitor/zipball/e30bfff741f71979f6476a41548e34afe8053c67",
+                "reference": "e30bfff741f71979f6476a41548e34afe8053c67",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/core": "~2.0"
+            },
+            "require-dev": {
+                "hoa/test": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Hoa\\Visitor\\": "."
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net"
+                },
+                {
+                    "name": "Hoa community",
+                    "homepage": "http://hoa-project.net/"
+                }
+            ],
+            "description": "The Hoa\\Visitor library.",
+            "homepage": "http://hoa-project.net/",
+            "keywords": [
+                "library",
+                "structure",
+                "visit",
+                "visitor"
+            ],
+            "time": "2015-08-17 06:30:58"
+        },
         {
             "name": "htmlawed/htmlawed",
             "version": "1.1.19",
         },
         {
             "name": "incenteev/composer-parameter-handler",
-            "version": "v2.1.1",
+            "version": "v2.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Incenteev/ParameterHandler.git",
-                "reference": "84a205fe80a46101607bafbc423019527893ddd0"
+                "reference": "d7ce7f06136109e81d1cb9d57066c4d4a99cf1cc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Incenteev/ParameterHandler/zipball/84a205fe80a46101607bafbc423019527893ddd0",
-                "reference": "84a205fe80a46101607bafbc423019527893ddd0",
+                "url": "https://api.github.com/repos/Incenteev/ParameterHandler/zipball/d7ce7f06136109e81d1cb9d57066c4d4a99cf1cc",
+                "reference": "d7ce7f06136109e81d1cb9d57066c4d4a99cf1cc",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.3",
-                "symfony/yaml": "~2.0"
+                "symfony/yaml": "~2.3|~3.0"
             },
             "require-dev": {
                 "composer/composer": "1.0.*@dev",
             "keywords": [
                 "parameters management"
             ],
-            "time": "2015-06-03 08:27:03"
+            "time": "2015-11-10 17:04:01"
         },
         {
             "name": "j0k3r/graby",
         },
         {
             "name": "j0k3r/php-readability",
-            "version": "v1.0.8",
+            "version": "v1.0.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/j0k3r/php-readability.git",
-                "reference": "f71c3a419623f821c245e0a003edfbf2c67f278e"
+                "reference": "41d7440c6e6130bacd50808342fe566e28f536fb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/j0k3r/php-readability/zipball/f71c3a419623f821c245e0a003edfbf2c67f278e",
-                "reference": "f71c3a419623f821c245e0a003edfbf2c67f278e",
+                "url": "https://api.github.com/repos/j0k3r/php-readability/zipball/41d7440c6e6130bacd50808342fe566e28f536fb",
+                "reference": "41d7440c6e6130bacd50808342fe566e28f536fb",
                 "shasum": ""
             },
             "require": {
-                "ext-tidy": ">=1.2",
                 "php": ">=5.3.3"
             },
             "type": "library",
                 "extraction",
                 "html"
             ],
-            "time": "2015-09-23 19:09:38"
+            "time": "2015-11-10 08:55:29"
         },
         {
             "name": "j0k3r/safecurl",
             ],
             "time": "2013-12-05 14:36:11"
         },
+        {
+            "name": "kphoen/rulerz",
+            "version": "0.14.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/K-Phoen/rulerz.git",
+                "reference": "608649b148ffdf3437600cc0f450d59b0579148d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/K-Phoen/rulerz/zipball/608649b148ffdf3437600cc0f450d59b0579148d",
+                "reference": "608649b148ffdf3437600cc0f450d59b0579148d",
+                "shasum": ""
+            },
+            "require": {
+                "hoa/ruler": "~1.0",
+                "php": ">=5.4",
+                "symfony/property-access": "~2.3"
+            },
+            "require-dev": {
+                "behat/behat": "~3.0",
+                "coduo/phpspec-data-provider-extension": "~1.0,!=1.0.2",
+                "doctrine/orm": "~2.4",
+                "elasticsearch/elasticsearch": "~1.0",
+                "illuminate/database": "~5.0",
+                "mikey179/vfsstream": "~1.4",
+                "phpspec/phpspec": "~2.0",
+                "pomm-project/cli": "~2.0@dev",
+                "pomm-project/foundation": "~2.0@dev",
+                "pomm-project/model-manager": "~2.0.@dev",
+                "ruflin/elastica": "~1.0",
+                "vlucas/phpdotenv": "~2.1"
+            },
+            "suggest": {
+                "doctrine/orm": "To execute rules as Doctrine queries",
+                "elasticsearch/elasticsearch": "To execute rules as Elasticsearch queries",
+                "kphoen/rulerz-spec-builder": "If you want a specification builder",
+                "pomm-project/model-manager": "To execute rules as Pomm queries"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "RulerZ\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Kévin Gomez",
+                    "email": "contact@kevingomez.fr"
+                }
+            ],
+            "description": "Powerful implementation of the Specification pattern",
+            "homepage": "https://github.com/K-Phoen/RulerZ",
+            "keywords": [
+                "doctrine",
+                "specification"
+            ],
+            "time": "2015-10-31 20:54:37"
+        },
+        {
+            "name": "kphoen/rulerz-bundle",
+            "version": "0.11.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/K-Phoen/RulerZBundle.git",
+                "reference": "dcaaed69d8252fa1e3a25802f8cf697947570778"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/K-Phoen/RulerZBundle/zipball/dcaaed69d8252fa1e3a25802f8cf697947570778",
+                "reference": "dcaaed69d8252fa1e3a25802f8cf697947570778",
+                "shasum": ""
+            },
+            "require": {
+                "kphoen/rulerz": "~0.1, >=0.13.0",
+                "symfony/framework-bundle": "~2.3|~3.0",
+                "symfony/validator": "~2.3|~3.0"
+            },
+            "require-dev": {
+                "matthiasnoback/symfony-dependency-injection-test": "~0.7",
+                "mikey179/vfsstream": "~1.0",
+                "phpunit/phpunit": "~4.8"
+            },
+            "type": "symfony-bundle",
+            "autoload": {
+                "psr-4": {
+                    "KPhoen\\RulerZBundle\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Kévin Gomez",
+                    "email": "contact@kevingomez.fr"
+                }
+            ],
+            "description": "Symfony2 Bundle for RulerZ",
+            "homepage": "https://github.com/K-Phoen/RulerZBundle",
+            "keywords": [
+                "doctrine",
+                "ruler",
+                "rulerz",
+                "specification"
+            ],
+            "time": "2015-11-13 13:00:14"
+        },
         {
             "name": "kriswallsmith/assetic",
             "version": "v1.3.1",
         },
         {
             "name": "sensio/framework-extra-bundle",
-            "version": "v3.0.10",
+            "version": "v3.0.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git",
-                "reference": "18fc2063c4d6569cdca47a39fbac32342eb65f3c"
+                "reference": "a79e205737b58d557c05caef6dfa8f94d8084bca"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/18fc2063c4d6569cdca47a39fbac32342eb65f3c",
-                "reference": "18fc2063c4d6569cdca47a39fbac32342eb65f3c",
+                "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/a79e205737b58d557c05caef6dfa8f94d8084bca",
+                "reference": "a79e205737b58d557c05caef6dfa8f94d8084bca",
                 "shasum": ""
             },
             "require": {
                 "doctrine/common": "~2.2",
-                "symfony/framework-bundle": "~2.3"
+                "symfony/framework-bundle": "~2.3|~3.0"
             },
             "require-dev": {
-                "symfony/expression-language": "~2.4",
-                "symfony/security-bundle": "~2.4"
+                "symfony/expression-language": "~2.4|~3.0",
+                "symfony/security-bundle": "~2.4|~3.0"
             },
             "suggest": {
                 "symfony/expression-language": "",
                 "annotations",
                 "controllers"
             ],
-            "time": "2015-08-03 11:59:27"
+            "time": "2015-10-28 15:47:04"
         },
         {
             "name": "sensiolabs/security-checker",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "4.8.16",
+            "version": "4.8.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e"
+                "reference": "fa33d4ad96481b91df343d83e8c8aabed6b1dfd3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/625f8c345606ed0f3a141dfb88f4116f0e22978e",
-                "reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fa33d4ad96481b91df343d83e8c8aabed6b1dfd3",
+                "reference": "fa33d4ad96481b91df343d83e8c8aabed6b1dfd3",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2015-10-23 06:48:33"
+            "time": "2015-11-11 11:32:49"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
diff --git a/src/Wallabag/CoreBundle/Command/TagAllCommand.php b/src/Wallabag/CoreBundle/Command/TagAllCommand.php
new file mode 100644 (file)
index 0000000..2cf3f80
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace Wallabag\CoreBundle\Command;
+
+use Doctrine\ORM\NoResultException;
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TagAllCommand extends ContainerAwareCommand
+{
+    protected function configure()
+    {
+        $this
+            ->setName('wallabag:tag:all')
+            ->setDescription('Tag all entries using the tagging rules.')
+            ->addArgument(
+               'username',
+               InputArgument::REQUIRED,
+               'User to tag entries for.'
+            )
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        try {
+            $user = $this->getUser($input->getArgument('username'));
+        } catch (NoResultException $e) {
+            $output->writeln(sprintf('<error>User %s not found.</error>', $input->getArgument('username')));
+
+            return 1;
+        }
+        $tagger = $this->getContainer()->get('wallabag_core.rule_based_tagger');
+
+        $output->write(sprintf('Tagging entries for user « <info>%s</info> »... ', $user->getUserName()));
+
+        $entries = $tagger->tagAllForUser($user);
+
+        $em = $this->getDoctrine()->getManager();
+        foreach ($entries as $entry) {
+            $em->persist($entry);
+        }
+        $em->flush();
+
+        $output->writeln('<info>Done.</info>');
+    }
+
+    /**
+     * Fetches a user from its username.
+     *
+     * @param string $username
+     *
+     * @return \Wallabag\UserBundle\Entity\User
+     */
+    private function getUser($username)
+    {
+        return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username);
+    }
+
+    private function getDoctrine()
+    {
+        return $this->getContainer()->get('doctrine');
+    }
+}
index 8bbe4ca06e5fac841566fc3676335d45f419457c..7a187710f3ee07e24fcfb7de49b821e65af347f4 100644 (file)
@@ -7,9 +7,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Wallabag\CoreBundle\Entity\Config;
+use Wallabag\CoreBundle\Entity\TaggingRule;
 use Wallabag\UserBundle\Entity\User;
 use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
 use Wallabag\CoreBundle\Form\Type\UserInformationType;
+use Wallabag\CoreBundle\Form\Type\TaggingRuleType;
 use Wallabag\CoreBundle\Form\Type\NewUserType;
 use Wallabag\CoreBundle\Form\Type\RssType;
 use Wallabag\CoreBundle\Tools\Utils;
@@ -98,6 +100,24 @@ class ConfigController extends Controller
             return $this->redirect($this->generateUrl('config'));
         }
 
+        // handle tagging rule
+        $taggingRule = new TaggingRule();
+        $newTaggingRule = $this->createForm(new TaggingRuleType(), $taggingRule, array('action' => $this->generateUrl('config').'#set5'));
+        $newTaggingRule->handleRequest($request);
+
+        if ($newTaggingRule->isValid()) {
+            $taggingRule->setConfig($config);
+            $em->persist($taggingRule);
+            $em->flush();
+
+            $this->get('session')->getFlashBag()->add(
+                'notice',
+                'Tagging rules updated'
+            );
+
+            return $this->redirect($this->generateUrl('config'));
+        }
+
         // handle adding new user
         $newUser = $userManager->createUser();
         // enable created user by default
@@ -136,6 +156,7 @@ class ConfigController extends Controller
                 'pwd' => $pwdForm->createView(),
                 'user' => $userForm->createView(),
                 'new_user' => $newUserForm->createView(),
+                'new_tagging_rule' => $newTaggingRule->createView(),
             ),
             'rss' => array(
                 'username' => $user->getUsername(),
@@ -167,6 +188,33 @@ class ConfigController extends Controller
         return $request->headers->get('referer') ? $this->redirect($request->headers->get('referer')) : $this->redirectToRoute('config');
     }
 
+    /**
+     * Deletes a tagging rule and redirect to the config homepage.
+     *
+     * @param TaggingRule $rule
+     *
+     * @Route("/tagging-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_tagging_rule")
+     *
+     * @return \Symfony\Component\HttpFoundation\RedirectResponse
+     */
+    public function deleteTaggingRule(TaggingRule $rule)
+    {
+        if ($this->getUser()->getId() != $rule->getConfig()->getUser()->getId()) {
+            throw $this->createAccessDeniedException('You can not access this tagging ryle.');
+        }
+
+        $em = $this->getDoctrine()->getManager();
+        $em->remove($rule);
+        $em->flush();
+
+        $this->get('session')->getFlashBag()->add(
+            'notice',
+            'Tagging rule deleted'
+        );
+
+        return $this->redirect($this->generateUrl('config'));
+    }
+
     /**
      * Retrieve config for the current user.
      * If no config were found, create a new one.
index cb0c52c499394fd2c674f1b3c6d10cf05fa1635e..84b78a89b8a4f7db73eee519eee2fe28c05a958b 100644 (file)
@@ -6,6 +6,7 @@ use Doctrine\Common\DataFixtures\AbstractFixture;
 use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
 use Doctrine\Common\Persistence\ObjectManager;
 use Wallabag\CoreBundle\Entity\Config;
+use Wallabag\CoreBundle\Entity\TaggingRule;
 
 class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface
 {
@@ -15,6 +16,13 @@ class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface
     public function load(ObjectManager $manager)
     {
         $adminConfig = new Config($this->getReference('admin-user'));
+        $taggingRule = new TaggingRule();
+
+        $taggingRule->setConfig($adminConfig);
+        $taggingRule->setRule('title matches "wallabag"');
+        $taggingRule->setTags(['wallabag']);
+        $manager->persist($taggingRule);
+
         $adminConfig->setTheme('material');
         $adminConfig->setItemsPerPage(30);
         $adminConfig->setLanguage('en_US');
index b2a1915a4fdbd22f921e5ecae5212da9ff922d46..2ca4182e6700da790f8f6f02e1fa44701bbee1ed 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Wallabag\CoreBundle\Entity;
 
+use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Validator\Constraints as Assert;
 
@@ -76,12 +77,19 @@ class Config
      */
     private $user;
 
+    /**
+     * @ORM\OneToMany(targetEntity="Wallabag\CoreBundle\Entity\TaggingRule", mappedBy="config", cascade={"remove"})
+     * @ORM\OrderBy({"id" = "ASC"})
+     */
+    private $taggingRules;
+
     /*
      * @param User     $user
      */
     public function __construct(\Wallabag\UserBundle\Entity\User $user)
     {
         $this->user = $user;
+        $this->taggingRules = new ArrayCollection();
     }
 
     /**
@@ -237,4 +245,24 @@ class Config
     {
         return $this->rssLimit;
     }
+
+    /**
+     * @param TaggingRule $rule
+     *
+     * @return Config
+     */
+    public function addTaggingRule(TaggingRule $rule)
+    {
+        $this->taggingRules[] = $rule;
+
+        return $this;
+    }
+
+    /**
+     * @return ArrayCollection<TaggingRule>
+     */
+    public function getTaggingRules()
+    {
+        return $this->taggingRules;
+    }
 }
index 5aa582f8d434a1c4638ca3127a41d9efe5501cfb..608ed2f0d4ababf41230640cfb45804b137cf687 100644 (file)
@@ -458,6 +458,10 @@ class Entry
      */
     public function addTag(Tag $tag)
     {
+        if ($this->tags->contains($tag)) {
+            return;
+        }
+
         $this->tags[] = $tag;
         $tag->addEntry($this);
     }
diff --git a/src/Wallabag/CoreBundle/Entity/TaggingRule.php b/src/Wallabag/CoreBundle/Entity/TaggingRule.php
new file mode 100644 (file)
index 0000000..4eab590
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+
+namespace Wallabag\CoreBundle\Entity;
+
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Validator\Constraints as Assert;
+use KPhoen\RulerZBundle\Validator\Constraints as RulerZAssert;
+
+/**
+ * Tagging rule.
+ *
+ * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\TaggingRuleRepository")
+ * @ORM\Table
+ * @ORM\Entity
+ */
+class TaggingRule
+{
+    /**
+     * @var int
+     *
+     * @ORM\Column(name="id", type="integer")
+     * @ORM\Id
+     * @ORM\GeneratedValue(strategy="AUTO")
+     */
+    private $id;
+
+    /**
+     * @var string
+     *
+     * @Assert\NotBlank()
+     * @RulerZAssert\ValidRule(
+     *  allowed_variables={"title", "url", "isArchived", "isStared", "content", "language", "mimetype", "readingTime", "domainName"},
+     *  allowed_operators={">", "<", ">=", "<=", "=", "is", "!=", "and", "not", "or", "matches"}
+     * )
+     * @ORM\Column(name="rule", type="string", nullable=false)
+     */
+    private $rule;
+
+    /**
+     * @var array
+     *
+     * @Assert\NotBlank()
+     * @ORM\Column(name="tags", type="simple_array", nullable=false)
+     */
+    private $tags = [];
+
+    /**
+     * @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\Config", inversedBy="taggingRules")
+     */
+    private $config;
+
+    /**
+     * Get id.
+     *
+     * @return int
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Set rule.
+     *
+     * @param string $rule
+     *
+     * @return TaggingRule
+     */
+    public function setRule($rule)
+    {
+        $this->rule = $rule;
+
+        return $this;
+    }
+
+    /**
+     * Get rule.
+     *
+     * @return string
+     */
+    public function getRule()
+    {
+        return $this->rule;
+    }
+
+    /**
+     * Set tags.
+     *
+     * @param array<string> $tags
+     *
+     * @return TaggingRule
+     */
+    public function setTags(array $tags)
+    {
+        $this->tags = $tags;
+
+        return $this;
+    }
+
+    /**
+     * Get tags.
+     *
+     * @return array<string>
+     */
+    public function getTags()
+    {
+        return $this->tags;
+    }
+
+    /**
+     * Set config.
+     *
+     * @param Config $config
+     *
+     * @return TaggingRule
+     */
+    public function setConfig(Config $config)
+    {
+        $this->config = $config;
+
+        return $this;
+    }
+
+    /**
+     * Get config.
+     *
+     * @return Config
+     */
+    public function getConfig()
+    {
+        return $this->config;
+    }
+}
diff --git a/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php b/src/Wallabag/CoreBundle/Form/DataTransformer/StringToListTransformer.php
new file mode 100644 (file)
index 0000000..23488d3
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+namespace Wallabag\CoreBundle\Form\DataTransformer;
+
+use Doctrine\Common\Persistence\ObjectManager;
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms a comma-separated list to a proper PHP array.
+ * Example: the string "foo, bar" will become the array ["foo", "bar"]
+ */
+class StringToListTransformer implements DataTransformerInterface
+{
+    /**
+     * @var string
+     */
+    private $separator;
+
+    /**
+     * @param string $separator The separator used in the list.
+     */
+    public function __construct($separator = ',')
+    {
+        $this->separator = $separator;
+    }
+
+    /**
+     * Transforms a list to a string.
+     *
+     * @param array|null $list
+     *
+     * @return string
+     */
+    public function transform($list)
+    {
+        if (null === $list) {
+            return '';
+        }
+
+        return implode($this->separator, $list);
+    }
+
+    /**
+     * Transforms a string to a list.
+     *
+     * @param  string $string
+     *
+     * @return array|null
+     */
+    public function reverseTransform($string)
+    {
+        if ($string === null) {
+            return null;
+        }
+
+        return array_values(array_filter(array_map('trim', explode($this->separator, $string))));
+    }
+}
diff --git a/src/Wallabag/CoreBundle/Form/Type/TaggingRuleType.php b/src/Wallabag/CoreBundle/Form/Type/TaggingRuleType.php
new file mode 100644 (file)
index 0000000..7fbba38
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+namespace Wallabag\CoreBundle\Form\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+use Wallabag\CoreBundle\Form\DataTransformer\StringToListTransformer;
+
+class TaggingRuleType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options)
+    {
+        $builder
+            ->add('rule', 'text', array('required' => true))
+            ->add('save', 'submit')
+        ;
+
+        $tagsField = $builder
+            ->create('tags', 'text')
+            ->addModelTransformer(new StringToListTransformer(','));
+
+        $builder->add($tagsField);
+    }
+
+    public function configureOptions(OptionsResolver $resolver)
+    {
+        $resolver->setDefaults(array(
+            'data_class' => 'Wallabag\CoreBundle\Entity\TaggingRule',
+        ));
+    }
+
+    public function getName()
+    {
+        return 'tagging_rule';
+    }
+}
index 7fb41393127c8bcdaab1a11511a988e93bb6cb16..3d585e6180786222603cb3f0be2e76b3ac18c8af 100644 (file)
@@ -3,6 +3,7 @@
 namespace Wallabag\CoreBundle\Helper;
 
 use Graby\Graby;
+use Psr\Log\LoggerInterface as Logger;
 use Wallabag\CoreBundle\Entity\Entry;
 use Wallabag\CoreBundle\Tools\Utils;
 
@@ -13,10 +14,14 @@ use Wallabag\CoreBundle\Tools\Utils;
 class ContentProxy
 {
     protected $graby;
+    protected $tagger;
+    protected $logger;
 
-    public function __construct(Graby $graby)
+    public function __construct(Graby $graby, RuleBasedTagger $tagger, Logger $logger)
     {
-        $this->graby = $graby;
+        $this->graby  = $graby;
+        $this->tagger = $tagger;
+        $this->logger = $logger;
     }
 
     /**
@@ -59,6 +64,15 @@ class ContentProxy
             $entry->setPreviewPicture($content['open_graph']['og_image']);
         }
 
+        try {
+            $this->tagger->tag($entry);
+        } catch (\Exception $e) {
+            $this->logger->error('Error while trying to automatically tag an entry.', array(
+                'entry_url' => $url,
+                'error_msg' => $e->getMessage(),
+            ));
+        }
+
         return $entry;
     }
 }
diff --git a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php
new file mode 100644 (file)
index 0000000..3f9953c
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+
+namespace Wallabag\CoreBundle\Helper;
+
+use RulerZ\RulerZ;
+
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Entity\Tag;
+use Wallabag\CoreBundle\Repository\EntryRepository;
+use Wallabag\CoreBundle\Repository\TagRepository;
+use Wallabag\UserBundle\Entity\User;
+
+class RuleBasedTagger
+{
+    private $rulerz;
+    private $tagRepository;
+    private $entryRepository;
+
+    public function __construct(RulerZ $rulerz, TagRepository $tagRepository, EntryRepository $entryRepository)
+    {
+        $this->rulerz          = $rulerz;
+        $this->tagRepository   = $tagRepository;
+        $this->entryRepository = $entryRepository;
+    }
+
+    /**
+     * Add tags from rules defined by the user.
+     *
+     * @param Entry $entry Entry to tag.
+     */
+    public function tag(Entry $entry)
+    {
+        $rules = $this->getRulesForUser($entry->getUser());
+
+        foreach ($rules as $rule) {
+            if (!$this->rulerz->satisfies($entry, $rule->getRule())) {
+                continue;
+            }
+
+            foreach ($rule->getTags() as $label) {
+                $tag = $this->getTag($entry->getUser(), $label);
+
+                $entry->addTag($tag);
+            }
+        }
+    }
+
+    /**
+     * Apply all the tagging rules defined by a user on its entries.
+     *
+     * @param User $user
+     *
+     * @return array<Entry> A list of modified entries.
+     */
+    public function tagAllForUser(User $user)
+    {
+        $rules   = $this->getRulesForUser($user);
+        $entries = array();
+
+        foreach ($rules as $rule) {
+            $qb      = $this->entryRepository->getBuilderForAllByUser($user->getId());
+            $entries = $this->rulerz->filter($qb, $rule->getRule());
+
+            foreach ($entries as $entry) {
+                foreach ($rule->getTags() as $label) {
+                    $tag = $this->getTag($user, $label);
+
+                    $entry->addTag($tag);
+                    $entries[] = $entry;
+                }
+            }
+        }
+
+        return $entries;
+    }
+
+    /**
+     * Fetch a tag for a user.
+     *
+     * @param User   $user
+     * @param string $label The tag's label.
+     *
+     * @return Tag
+     */
+    private function getTag(User $user, $label)
+    {
+        $tag = $this->tagRepository->findOneByLabelAndUserId($label, $user->getId());
+
+        if (!$tag) {
+            $tag = new Tag($user);
+            $tag->setLabel($label);
+        }
+
+        return $tag;
+    }
+
+    /**
+     * Retrieves the tagging rules for a given user.
+     *
+     * @param User $user
+     *
+     * @return array<TaggingRule>
+     */
+    private function getRulesForUser(User $user)
+    {
+        return $user->getConfig()->getTaggingRules();
+    }
+}
diff --git a/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php b/src/Wallabag/CoreBundle/Operator/Doctrine/Matches.php
new file mode 100644 (file)
index 0000000..e6bb03b
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+namespace Wallabag\CoreBundle\Operator\Doctrine;
+
+/**
+ * Provides a "matches" operator used for tagging rules.
+ *
+ * It asserts that a given pattern is contained in a subject, in a
+ * case-insensitive way.
+ *
+ * This operator will be used to compile tagging rules in DQL, usable
+ * by Doctrine ORM.
+ * It's registered in RulerZ using a service (wallabag.operator.doctrine.matches);
+ */
+class Matches
+{
+    public function __invoke($subject, $pattern)
+    {
+        if ($pattern[0] === "'") {
+            $pattern = sprintf("'%%%s%%'", substr($pattern, 1, -1));
+        }
+
+        return sprintf('UPPER(%s) LIKE UPPER(%s)', $subject, $pattern);
+    }
+}
diff --git a/src/Wallabag/CoreBundle/Operator/PHP/Matches.php b/src/Wallabag/CoreBundle/Operator/PHP/Matches.php
new file mode 100644 (file)
index 0000000..987ed2a
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace Wallabag\CoreBundle\Operator\PHP;
+
+/**
+ * Provides a "matches" operator used for tagging rules.
+ *
+ * It asserts that a given pattern is contained in a subject, in a
+ * case-insensitive way.
+ *
+ * This operator will be used to compile tagging rules in PHP, usable
+ * directly on Entry objects for instance.
+ * It's registered in RulerZ using a service (wallabag.operator.array.matches);
+ */
+class Matches
+{
+    public function __invoke($subject, $pattern)
+    {
+        return stripos($subject, $pattern) !== false;
+    }
+}
diff --git a/src/Wallabag/CoreBundle/Repository/TaggingRuleRepository.php b/src/Wallabag/CoreBundle/Repository/TaggingRuleRepository.php
new file mode 100644 (file)
index 0000000..de38073
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+
+namespace Wallabag\CoreBundle\Repository;
+
+use Doctrine\ORM\EntityRepository;
+
+class TaggingRuleRepository extends EntityRepository
+{
+}
index 8e21b0528aeb95c701f5095a6f625be988a207d0..c92b4eb37c843d49ca5ec12b1b68d08dffd7cfc4 100644 (file)
@@ -53,6 +53,27 @@ services:
         class: Wallabag\CoreBundle\Helper\ContentProxy
         arguments:
             - @wallabag_core.graby
+            - @wallabag_core.rule_based_tagger
+            - @logger
+
+    wallabag_core.rule_based_tagger:
+        class: Wallabag\CoreBundle\Helper\RuleBasedTagger
+        arguments:
+            - @rulerz
+            - @wallabag_core.tag_repository
+            - @wallabag_core.entry_repository
+
+    wallabag_core.entry_repository:
+        class: Wallabag\CoreBundle\Repository\EntryRepository
+        factory: [ @doctrine.orm.default_entity_manager, getRepository ]
+        arguments:
+            - WallabagCoreBundle:Entry
+
+    wallabag_core.tag_repository:
+        class: Wallabag\CoreBundle\Repository\TagRepository
+        factory: [ @doctrine.orm.default_entity_manager, getRepository ]
+        arguments:
+            - WallabagCoreBundle:Tag
 
     wallabag_core.registration_confirmed:
         class: Wallabag\CoreBundle\EventListener\RegistrationConfirmedListener
@@ -70,3 +91,13 @@ services:
         arguments:
             - %wallabag_url%
             - src/Wallabag/CoreBundle/Resources/views/themes/_global/public/img/appicon/apple-touch-icon-152.png
+
+    wallabag.operator.array.matches:
+        class: Wallabag\CoreBundle\Operator\PHP\Matches
+        tags:
+            - { name: rulerz.operator, executor: rulerz.executor.array, operator: matches }
+
+    wallabag.operator.doctrine.matches:
+        class: Wallabag\CoreBundle\Operator\Doctrine\Matches
+        tags:
+            - { name: rulerz.operator, executor: rulerz.executor.doctrine, operator: matches, inline: true }
index 7a7d6af1f359d66529b12b6b36af80983b18a3a5..cc797c6323a5841dcd02bde55ca338011ebd23de 100644 (file)
         {{ form_rest(form.pwd) }}
     </form>
 
+    <h2>{% trans %}Tagging rules{% endtrans %}</h2>
+
+    <ul>
+        {% for tagging_rule in app.user.config.taggingRules %}
+        <li>
+            if « {{ tagging_rule.rule }} » then tag as « {{ tagging_rule.tags|join(', ') }} »
+            <a href="{{ path('delete_tagging_rule', {id: tagging_rule.id}) }}" title="{% trans %}Delete{% endtrans %}" class="tool delete icon-trash icon"></a>
+        </li>
+        {% endfor %}
+    </ul>
+
+    <form action="{{ path('config') }}" method="post" {{ form_enctype(form.new_tagging_rule) }}>
+        {{ form_errors(form.new_tagging_rule) }}
+
+        <fieldset class="w500p inline">
+            <div class="row">
+                {{ form_label(form.new_tagging_rule.rule) }}
+                {{ form_errors(form.new_tagging_rule.rule) }}
+                {{ form_widget(form.new_tagging_rule.rule) }}
+            </div>
+        </fieldset>
+
+        <fieldset class="w500p inline">
+            <div class="row">
+                {{ form_label(form.new_tagging_rule.tags) }}
+                {{ form_errors(form.new_tagging_rule.tags) }}
+                {{ form_widget(form.new_tagging_rule.tags) }}
+            </div>
+        </fieldset>
+
+        {{ form_rest(form.new_tagging_rule) }}
+    </form>
+
     {% if is_granted('ROLE_SUPER_ADMIN') %}
     <h2>{% trans %}Add a user{% endtrans %}</h2>
 
index 8f121a2b756c594bee8e0da6b21af560fac22e03..d060311d4400f1784a92cbcbe561e3fa39f93d11 100644 (file)
@@ -15,8 +15,9 @@
                         <li class="tab col s3"><a href="#set2">{% trans %}RSS{% endtrans %}</a></li>
                         <li class="tab col s3"><a href="#set3">{% trans %}User information{% endtrans %}</a></li>
                         <li class="tab col s3"><a href="#set4">{% trans %}Password{% endtrans %}</a></li>
+                        <li class="tab col s3"><a href="#set5">{% trans %}Tagging rules{% endtrans %}</a></li>
                         {% if is_granted('ROLE_SUPER_ADMIN') %}
-                        <li class="tab col s3"><a href="#set5">{% trans %}Add a user{% endtrans %}</a></li>
+                        <li class="tab col s3"><a href="#set6">{% trans %}Add a user{% endtrans %}</a></li>
                         {% endif %}
                         </ul>
                     </div>
                         </form>
                     </div>
 
-                    {% if is_granted('ROLE_SUPER_ADMIN') %}
                     <div id="set5" class="col s12">
+                        <div class="row">
+                            <div class="input-field col s12">
+                                <ul>
+                                    {% for tagging_rule in app.user.config.taggingRules %}
+                                    <li>
+                                        if « {{ tagging_rule.rule }} » then tag as « {{ tagging_rule.tags|join(', ') }} »
+                                        <a href="{{ path('delete_tagging_rule', {id: tagging_rule.id}) }}" title="{% trans %}Delete{% endtrans %}">
+                                            <i class="tool grey-text delete mdi-action-delete"></i>
+                                        </a>
+                                    </li>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+                        </div>
+
+                        {{ form_start(form.new_tagging_rule) }}
+                            {{ form_errors(form.new_tagging_rule) }}
+
+                            <div class="row">
+                                <div class="input-field col s12">
+                                    {{ form_label(form.new_tagging_rule.rule) }}
+                                    {{ form_errors(form.new_tagging_rule.rule) }}
+                                    {{ form_widget(form.new_tagging_rule.rule) }}
+                                </div>
+                            </div>
+
+                            <div class="row">
+                                <div class="input-field col s12">
+                                    {{ form_label(form.new_tagging_rule.tags) }}
+                                    {{ form_errors(form.new_tagging_rule.tags) }}
+                                    {{ form_widget(form.new_tagging_rule.tags) }}
+                                </div>
+                            </div>
+
+                            <div class="hidden">{{ form_rest(form.new_tagging_rule) }}</div>
+                            <button class="btn waves-effect waves-light" type="submit" name="action">
+                                {% trans %}Save{% endtrans %}
+                            </button>
+                        </form>
+
+                        <div class="row">
+                            <div class="input-field col s12">
+                                <h4>{% trans %}FAQ{% endtrans %}</h4>
+
+                                <h5>{% trans %}What does « tagging rules » mean?{% endtrans %}</h5>
+                                <p class="help">
+                                    {% trans %}
+                                    They are rules used by Wallabag to automatically tag new entries.<br />
+                                    Each time a new entry is added, all the tagging rules will be used to add
+                                    the tags you configured, thus saving you the trouble to manually classify
+                                    your entries.
+                                    {% endtrans %}
+                                </p>
+
+                                <h5>{% trans %}How do I use them?{% endtrans %}</h5>
+                                <p class="help">
+                                    {% trans %}
+                                    Let assume you want to tag new entries as « <i>short reading</i> » when the reading time is inferior to 3 minutes.<br />
+                                    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>
+                                    field.<br />
+                                    Several tags can added simultaneously by separating them by a comma: « <i>short reading, must read</i> »<br />
+                                    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> »
+                                    {% endtrans %}
+                                </p>
+
+                                <h5>{% trans %}Which variables and operators can I use to write rules?{% endtrans %}</h5>
+                                <p class="help">
+                                    {% trans %}The following variables and operators can be used to create tagging rules:{% endtrans %}
+
+                                    <table>
+                                        <thead>
+                                            <tr>
+                                                <th>{% trans %}Variable{% endtrans %}</th>
+                                                <th>{% trans %}Meaning{% endtrans %}</th>
+                                                <th>{% trans %}Operator{% endtrans %}</th>
+                                                <th>{% trans %}Meaning{% endtrans %}</th>
+                                            </tr>
+                                        </thead>
+
+                                        <tbody>
+                                            <tr>
+                                                <td>title</td>
+                                                <td>{% trans %}Title of the entry{% endtrans %}</td>
+                                                <td>&lt;=</td>
+                                                <td>{% trans %}Less than…{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>url</td>
+                                                <td>{% trans %}URL of the entry{% endtrans %}</td>
+                                                <td>&lt;</td>
+                                                <td>{% trans %}Strictly less than…{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>isArchived</td>
+                                                <td>{% trans %}Whether the entry is archived or not{% endtrans %}</td>
+                                                <td>=&gt;</td>
+                                                <td>{% trans %}Greater than…{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>isStared</td>
+                                                <td>{% trans %}Whether the entry is starred or not{% endtrans %}</td>
+                                                <td>&gt;</td>
+                                                <td>{% trans %}Strictly greater than…{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>content</td>
+                                                <td>{% trans %}The entry's content{% endtrans %}</td>
+                                                <td>=</td>
+                                                <td>{% trans %}Equal to…{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>language</td>
+                                                <td>{% trans %}The entry's language{% endtrans %}</td>
+                                                <td>!=</td>
+                                                <td>{% trans %}Not equal to…{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>mimetype</td>
+                                                <td>{% trans %}The entry's mime-type{% endtrans %}</td>
+                                                <td>OR</td>
+                                                <td>{% trans %}One rule or another{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>readingTime</td>
+                                                <td>{% trans %}The estimated entry's reading time, in minutes{% endtrans %}</td>
+                                                <td>AND</td>
+                                                <td>{% trans %}One rule and another{% endtrans %}</td>
+                                            </tr>
+                                            <tr>
+                                                <td>domainName</td>
+                                                <td>{% trans %}The domain name of the entry{% endtrans %}</td>
+                                                <td>matches</td>
+                                                <td>
+                                                    {% trans %}
+                                                    Tests that a <i>subject</i> is matches a <i>search</i> (case-insensitive).<br />
+                                                    Example: <code>title matches "football"</code>
+                                                    {% endtrans %}
+                                                </td>
+                                            </tr>
+                                        </tbody>
+                                    </table>
+                                </p>
+                            </div>
+                        </div>
+                    </div>
+
+                    {% if is_granted('ROLE_SUPER_ADMIN') %}
+                    <div id="set6" class="col s12">
                         {{ form_start(form.new_user) }}
                             {{ form_errors(form.new_user) }}
 
index 7085151ae405f567a440df36606213af22dd4062..7b32354f8b61822af84d72d1a6056063fd753910 100644 (file)
@@ -479,4 +479,59 @@ class ConfigControllerTest extends WallabagCoreTestCase
         $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(array('_text')));
         $this->assertContains($expectedMessage, $alert[0]);
     }
+
+    public function testTaggingRuleCreation()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/config');
+
+        $this->assertTrue($client->getResponse()->isSuccessful());
+
+        $form = $crawler->filter('button[id=tagging_rule_save]')->form();
+
+        $data = array(
+            'tagging_rule[rule]' => 'readingTime <= 3',
+            'tagging_rule[tags]' => 'short reading',
+        );
+
+        $client->submit($form, $data);
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->followRedirect();
+
+        $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
+        $this->assertContains('Tagging rules updated', $alert[0]);
+
+        $deleteLink = $crawler->filter('.delete')->last()->link();
+
+        $crawler = $client->click($deleteLink);
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->followRedirect();
+        $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
+        $this->assertContains('Tagging rule deleted', $alert[0]);
+    }
+
+    public function dataForTaggingRuleFailed()
+    {
+        return array(
+            array(
+                array(
+                    'rss_config[rule]' => 'unknownVar <= 3',
+                    'rss_config[tags]' => 'cool tag',
+                ),
+                'The variable « unknownVar » does not exist.',
+            ),
+            array(
+                array(
+                    'rss_config[rule]' => 'length(domainName) <= 42',
+                    'rss_config[tags]' => 'cool tag',
+                ),
+                'The operator « length » does not exist.',
+            ),
+        );
+    }
 }
index 56b4c9e41af815259f43ca815f7efa9486138536..af62aee8d85f80fd2d1018a858cc14677583bf5e 100644 (file)
@@ -102,6 +102,44 @@ class EntryControllerTest extends WallabagCoreTestCase
         $this->assertContains('Google', $alert[0]);
     }
 
+    /**
+     * This test will require an internet connection.
+     */
+    public function testPostNewThatWillBeTaggued()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/new');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $form = $crawler->filter('button[type=submit]')->form();
+
+        $data = array(
+            'entry[url]' => $url = 'https://github.com/wallabag/wallabag',
+        );
+
+        $client->submit($form, $data);
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->followRedirect();
+
+        $em = $client->getContainer()
+            ->get('doctrine.orm.entity_manager');
+        $entry = $em
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->findOneByUrl($url);
+        $tags = $entry->getTags();
+
+        $this->assertCount(1, $tags);
+        $this->assertEquals('wallabag', $tags[0]->getLabel());
+
+        $em->remove($entry);
+        $em->flush();
+    }
+
     public function testArchive()
     {
         $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 (file)
index 0000000..d114e5f
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+namespace Wallabag\CoreBundle\Tests\Form\DataTransformer;
+
+use Wallabag\CoreBundle\Form\DataTransformer\StringToListTransformer;
+
+class StringToListTransformerTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @dataProvider transformProvider
+     */
+    public function testTransformWithValidData($inputData, $expectedResult)
+    {
+        $transformer = new StringToListTransformer();
+
+        $this->assertSame($expectedResult, $transformer->transform($inputData));
+    }
+
+    public function transformProvider()
+    {
+        return array(
+            array( null,                                 '' ),
+            array( array(),                              '' ),
+            array( array('single value'),                'single value' ),
+            array( array('first value', 'second value'), 'first value,second value' ),
+        );
+    }
+
+    /**
+     * @dataProvider reverseTransformProvider
+     */
+    public function testReverseTransformWithValidData($inputData, $expectedResult)
+    {
+        $transformer = new StringToListTransformer();
+
+        $this->assertSame($expectedResult, $transformer->reverseTransform($inputData));
+    }
+
+    public function reverseTransformProvider()
+    {
+        return array(
+            array( null,                            null ),
+            array( '',                              array() ),
+            array( 'single value',                  array('single value') ),
+            array( 'first value,second value',      array('first value', 'second value') ),
+            array( 'first value,     second value', array('first value', 'second value') ),
+            array( 'first value,  ,  second value', array('first value', 'second value') ),
+        );
+    }
+}
index 4bce4708f2ae1e42adeb50119dc63057eb4e8de4..ef7cbd5b2a8c974362f852f7fc77196c932fd3fc 100644 (file)
@@ -2,6 +2,9 @@
 
 namespace Wallabag\CoreBundle\Tests\Helper;
 
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Psr\Log\NullLogger;
+
 use Wallabag\CoreBundle\Entity\Entry;
 use Wallabag\UserBundle\Entity\User;
 use Wallabag\CoreBundle\Helper\ContentProxy;
@@ -10,6 +13,10 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase
 {
     public function testWithEmptyContent()
     {
+        $tagger = $this->getTaggerMock();
+        $tagger->expects($this->once())
+            ->method('tag');
+
         $graby = $this->getMockBuilder('Graby\Graby')
             ->setMethods(array('fetchContent'))
             ->disableOriginalConstructor()
@@ -25,7 +32,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase
                 'language' => '',
             ));
 
-        $proxy = new ContentProxy($graby);
+        $proxy = new ContentProxy($graby, $tagger, $this->getLogger());
         $entry = $proxy->updateEntry(new Entry(new User()), 'http://0.0.0.0');
 
         $this->assertEquals('http://0.0.0.0', $entry->getUrl());
@@ -40,6 +47,10 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase
 
     public function testWithEmptyContentButOG()
     {
+        $tagger = $this->getTaggerMock();
+        $tagger->expects($this->once())
+            ->method('tag');
+
         $graby = $this->getMockBuilder('Graby\Graby')
             ->setMethods(array('fetchContent'))
             ->disableOriginalConstructor()
@@ -59,7 +70,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase
                 ),
             ));
 
-        $proxy = new ContentProxy($graby);
+        $proxy = new ContentProxy($graby, $tagger, $this->getLogger());
         $entry = $proxy->updateEntry(new Entry(new User()), 'http://domain.io');
 
         $this->assertEquals('http://domain.io', $entry->getUrl());
@@ -74,6 +85,10 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase
 
     public function testWithContent()
     {
+        $tagger = $this->getTaggerMock();
+        $tagger->expects($this->once())
+            ->method('tag');
+
         $graby = $this->getMockBuilder('Graby\Graby')
             ->setMethods(array('fetchContent'))
             ->disableOriginalConstructor()
@@ -94,7 +109,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase
                 ),
             ));
 
-        $proxy = new ContentProxy($graby);
+        $proxy = new ContentProxy($graby, $tagger, $this->getLogger());
         $entry = $proxy->updateEntry(new Entry(new User()), 'http://0.0.0.0');
 
         $this->assertEquals('http://1.1.1.1', $entry->getUrl());
@@ -106,4 +121,17 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals(4.0, $entry->getReadingTime());
         $this->assertEquals('1.1.1.1', $entry->getDomainName());
     }
+
+    private function getTaggerMock()
+    {
+        return $this->getMockBuilder('Wallabag\CoreBundle\Helper\RuleBasedTagger')
+            ->setMethods(array('tag'))
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
+
+    private function getLogger()
+    {
+        return new NullLogger();
+    }
 }
diff --git a/src/Wallabag/CoreBundle/Tests/Helper/RuleBasedTaggerTest.php b/src/Wallabag/CoreBundle/Tests/Helper/RuleBasedTaggerTest.php
new file mode 100644 (file)
index 0000000..5180f7d
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+
+namespace Wallabag\CoreBundle\Tests\Helper;
+
+use Wallabag\CoreBundle\Entity\Config;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Entity\Tag;
+use Wallabag\CoreBundle\Entity\TaggingRule;
+use Wallabag\UserBundle\Entity\User;
+use Wallabag\CoreBundle\Helper\RuleBasedTagger;
+
+class RuleBasedTaggerTest extends \PHPUnit_Framework_TestCase
+{
+    private $rulerz;
+    private $tagRepository;
+    private $entryRepository;
+    private $tagger;
+
+    public function setUp()
+    {
+        $this->rulerz          = $this->getRulerZMock();
+        $this->tagRepository   = $this->getTagRepositoryMock();
+        $this->entryRepository = $this->getEntryRepositoryMock();
+
+        $this->tagger = new RuleBasedTagger($this->rulerz, $this->tagRepository, $this->entryRepository);
+    }
+
+    public function testTagWithNoRule()
+    {
+        $entry = new Entry($this->getUser());
+
+        $this->tagger->tag($entry);
+
+        $this->assertTrue($entry->getTags()->isEmpty());
+    }
+
+    public function testTagWithNoMatchingRule()
+    {
+        $taggingRule = $this->getTaggingRule('rule as string', array('foo', 'bar'));
+        $user        = $this->getUser([$taggingRule]);
+        $entry       = new Entry($user);
+
+        $this->rulerz
+            ->expects($this->once())
+            ->method('satisfies')
+            ->with($entry, 'rule as string')
+            ->willReturn(false);
+
+        $this->tagger->tag($entry);
+
+        $this->assertTrue($entry->getTags()->isEmpty());
+    }
+
+    public function testTagWithAMatchingRule()
+    {
+        $taggingRule = $this->getTaggingRule('rule as string', array('foo', 'bar'));
+        $user        = $this->getUser([$taggingRule]);
+        $entry       = new Entry($user);
+
+        $this->rulerz
+            ->expects($this->once())
+            ->method('satisfies')
+            ->with($entry, 'rule as string')
+            ->willReturn(true);
+
+        $this->tagger->tag($entry);
+
+        $this->assertFalse($entry->getTags()->isEmpty());
+
+        $tags = $entry->getTags();
+        $this->assertSame('foo', $tags[0]->getLabel());
+        $this->assertSame($user, $tags[0]->getUser());
+        $this->assertSame('bar', $tags[1]->getLabel());
+        $this->assertSame($user, $tags[1]->getUser());
+    }
+
+    public function testTagWithAMixOfMatchingRules()
+    {
+        $taggingRule      = $this->getTaggingRule('bla bla', array('hey'));
+        $otherTaggingRule = $this->getTaggingRule('rule as string', array('foo'));
+
+        $user  = $this->getUser([$taggingRule, $otherTaggingRule]);
+        $entry = new Entry($user);
+
+        $this->rulerz
+            ->method('satisfies')
+            ->will($this->onConsecutiveCalls(false, true));
+
+        $this->tagger->tag($entry);
+
+        $this->assertFalse($entry->getTags()->isEmpty());
+
+        $tags = $entry->getTags();
+        $this->assertSame('foo', $tags[0]->getLabel());
+        $this->assertSame($user, $tags[0]->getUser());
+    }
+
+    public function testWhenTheTagExists()
+    {
+        $taggingRule = $this->getTaggingRule('rule as string', array('foo'));
+        $user        = $this->getUser([$taggingRule]);
+        $entry       = new Entry($user);
+        $tag         = new Tag($user);
+
+        $this->rulerz
+            ->expects($this->once())
+            ->method('satisfies')
+            ->with($entry, 'rule as string')
+            ->willReturn(true);
+
+        $this->tagRepository
+            ->expects($this->once())
+            ->method('findOneByLabelAndUserId')
+            ->willReturn($tag);
+
+        $this->tagger->tag($entry);
+
+        $this->assertFalse($entry->getTags()->isEmpty());
+
+        $tags = $entry->getTags();
+        $this->assertSame($tag, $tags[0]);
+    }
+
+    private function getUser(array $taggingRules = [])
+    {
+        $user   = new User();
+        $config = new Config($user);
+
+        $user->setConfig($config);
+
+        foreach ($taggingRules as $rule) {
+            $config->addTaggingRule($rule);
+        }
+
+        return $user;
+    }
+
+    private function getTaggingRule($rule, array $tags)
+    {
+        $taggingRule = new TaggingRule();
+        $taggingRule->setRule($rule);
+        $taggingRule->setTags($tags);
+
+        return $taggingRule;
+    }
+
+    private function getRulerZMock()
+    {
+        return $this->getMockBuilder('RulerZ\RulerZ')
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
+
+    private function getTagRepositoryMock()
+    {
+        return $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
+
+    private function getEntryRepositoryMock()
+    {
+        return $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
+}
index c020f3ca9478a6418ec1e4bc6732610a5999fe7b..009c4881d0ea8945379e159894f00b4ca51a1f2d 100644 (file)
@@ -23,4 +23,19 @@ class UserRepository extends EntityRepository
             ->getQuery()
             ->getOneOrNullResult();
     }
+
+    /**
+     * Find a user by its username.
+     *
+     * @param string $username
+     *
+     * @return User
+     */
+    public function findOneByUserName($username)
+    {
+        return $this->createQueryBuilder('u')
+            ->andWhere('u.username = :username')->setParameter('username', $username)
+            ->getQuery()
+            ->getSingleResult();
+    }
 }