Merge pull request #4341 from wallabag/with-annotations-route

This commit is contained in:
Jérémy Benoist 2022-03-14 06:44:43 +01:00 committed by GitHub
commit 49d5f27cd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 141 additions and 10 deletions

View file

@ -69,11 +69,11 @@ security:
- { path: ^/logout, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_2FA_IN_PROGRESS] } - { path: ^/logout, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_2FA_IN_PROGRESS] }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /(unread|starred|archive|all).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /(unread|starred|archive|annotated|all).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/locale, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/locale, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /tags/(.*).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /tags/(.*).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/feed, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/feed, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /(unread|starred|archive).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } # For backwards compatibility - { path: /(unread|starred|archive|annotated).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } # For backwards compatibility
- { path: ^/share, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/share, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/settings, roles: ROLE_SUPER_ADMIN } - { path: ^/settings, roles: ROLE_SUPER_ADMIN }
- { path: ^/annotations, roles: ROLE_USER } - { path: ^/annotations, roles: ROLE_USER }

View file

@ -277,12 +277,26 @@ class EntryController extends Controller
return $this->showEntries('untagged', $request, $page); return $this->showEntries('untagged', $request, $page);
} }
/**
* Shows entries with annotations for current user.
*
* @param int $page
*
* @Route("/annotated/list/{page}", name="annotated", defaults={"page" = "1"})
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function showWithAnnotationsEntriesAction(Request $request, $page)
{
return $this->showEntries('annotated', $request, $page);
}
/** /**
* Shows random entry depending on the given type. * Shows random entry depending on the given type.
* *
* @param string $type * @param string $type
* *
* @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|all"}) * @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|annotated|all"})
* *
* @return \Symfony\Component\HttpFoundation\RedirectResponse * @return \Symfony\Component\HttpFoundation\RedirectResponse
*/ */
@ -563,6 +577,9 @@ class EntryController extends Controller
$qb = $repository->getBuilderForArchiveByUser($this->getUser()->getId()); $qb = $repository->getBuilderForArchiveByUser($this->getUser()->getId());
$formOptions['filter_archived'] = true; $formOptions['filter_archived'] = true;
break; break;
case 'annotated':
$qb = $repository->getBuilderForAnnotationsByUser($this->getUser()->getId());
break;
case 'unread': case 'unread':
$qb = $repository->getBuilderForUnreadByUser($this->getUser()->getId()); $qb = $repository->getBuilderForUnreadByUser($this->getUser()->getId());
$formOptions['filter_unread'] = true; $formOptions['filter_unread'] = true;

View file

@ -47,7 +47,7 @@ class ExportController extends Controller
* *
* @Route("/export/{category}.{format}", name="export_entries", requirements={ * @Route("/export/{category}.{format}", name="export_entries", requirements={
* "format": "epub|mobi|pdf|json|xml|txt|csv", * "format": "epub|mobi|pdf|json|xml|txt|csv",
* "category": "all|unread|starred|archive|tag_entries|untagged|search|same_domain" * "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain"
* }) * })
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
@ -80,6 +80,13 @@ class ExportController extends Controller
->getResult(); ->getResult();
$title = 'Search ' . $searchTerm; $title = 'Search ' . $searchTerm;
} elseif ('annotated' === $category) {
$entries = $repository->getBuilderForAnnotationsByUser(
$this->getUser()->getId()
)->getQuery()
->getResult();
$title = 'With annotations';
} else { } else {
$entries = $repository $entries = $repository
->$methodBuilder($this->getUser()->getId()) ->$methodBuilder($this->getUser()->getId())

View file

@ -93,7 +93,7 @@ class EntryFilterType extends AbstractType
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
$value = $values['value']; $value = $values['value'];
if (\strlen($value) <= 2 || empty($value)) { if (\strlen($value) <= 2 || empty($value)) {
return; return false;
} }
$expression = $filterQuery->getExpr()->like($field, $filterQuery->getExpr()->lower($filterQuery->getExpr()->literal('%' . $value . '%'))); $expression = $filterQuery->getExpr()->like($field, $filterQuery->getExpr()->lower($filterQuery->getExpr()->literal('%' . $value . '%')));
@ -105,7 +105,7 @@ class EntryFilterType extends AbstractType
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
$value = $values['value']; $value = $values['value'];
if (false === \array_key_exists($value, Response::$statusTexts)) { if (false === \array_key_exists($value, Response::$statusTexts)) {
return; return false;
} }
$paramName = sprintf('%s', str_replace('.', '_', $field)); $paramName = sprintf('%s', str_replace('.', '_', $field));
@ -129,7 +129,7 @@ class EntryFilterType extends AbstractType
'data' => $options['filter_unread'], 'data' => $options['filter_unread'],
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
if (false === $values['value']) { if (false === $values['value']) {
return; return false;
} }
$expression = $filterQuery->getExpr()->eq('e.isArchived', 'false'); $expression = $filterQuery->getExpr()->eq('e.isArchived', 'false');
@ -137,10 +137,22 @@ class EntryFilterType extends AbstractType
return $filterQuery->createCondition($expression); return $filterQuery->createCondition($expression);
}, },
]) ])
->add('isAnnotated', CheckboxFilterType::class, [
'label' => 'entry.filters.annotated_label',
'data' => $options['filter_annotated'],
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
if (false === $values['value']) {
return false;
}
$qb = $filterQuery->getQueryBuilder();
$qb->innerJoin('e.annotations', 'a');
},
])
->add('previewPicture', CheckboxFilterType::class, [ ->add('previewPicture', CheckboxFilterType::class, [
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
if (false === $values['value']) { if (false === $values['value']) {
return; return false;
} }
$expression = $filterQuery->getExpr()->isNotNull($field); $expression = $filterQuery->getExpr()->isNotNull($field);
@ -152,7 +164,7 @@ class EntryFilterType extends AbstractType
->add('isPublic', CheckboxFilterType::class, [ ->add('isPublic', CheckboxFilterType::class, [
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
if (false === $values['value']) { if (false === $values['value']) {
return; return false;
} }
// is_public isn't a real field // is_public isn't a real field
@ -183,6 +195,7 @@ class EntryFilterType extends AbstractType
'filter_archived' => false, 'filter_archived' => false,
'filter_starred' => false, 'filter_starred' => false,
'filter_unread' => false, 'filter_unread' => false,
'filter_annotated' => false,
]); ]);
} }
} }

View file

@ -142,6 +142,21 @@ class EntryRepository extends EntityRepository
return $this->sortQueryBuilder($this->getRawBuilderForUntaggedByUser($userId)); return $this->sortQueryBuilder($this->getRawBuilderForUntaggedByUser($userId));
} }
/**
* Retrieve entries with annotations for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getBuilderForAnnotationsByUser($userId)
{
return $this
->getSortedQueryBuilderByUser($userId)
->innerJoin('e.annotations', 'a')
;
}
/** /**
* Retrieve untagged entries for a user. * Retrieve untagged entries for a user.
* *
@ -579,6 +594,10 @@ class EntryRepository extends EntityRepository
$qb->leftJoin('e.tags', 't'); $qb->leftJoin('e.tags', 't');
$qb->andWhere('t.id is null'); $qb->andWhere('t.id is null');
break; break;
case 'annotated':
$qb->leftJoin('e.annotations', 'a');
$qb->andWhere('a.id is not null');
break;
} }
$ids = $qb->getQuery()->getArrayResult(); $ids = $qb->getQuery()->getArrayResult();

View file

@ -19,6 +19,7 @@ menu:
starred: Starred starred: Starred
archive: Archive archive: Archive
all_articles: All entries all_articles: All entries
with_annotations: With annotations
config: Config config: Config
tags: Tags tags: Tags
internal_settings: Internal Settings internal_settings: Internal Settings
@ -220,6 +221,7 @@ entry:
starred: Starred entries starred: Starred entries
archived: Archived entries archived: Archived entries
filtered: Filtered entries filtered: Filtered entries
with_annotations: Entries with annotations
filtered_tags: 'Filtered by tags:' filtered_tags: 'Filtered by tags:'
filtered_search: 'Filtered by search:' filtered_search: 'Filtered by search:'
untagged: Untagged entries untagged: Untagged entries
@ -245,6 +247,7 @@ entry:
archived_label: Archived archived_label: Archived
starred_label: Starred starred_label: Starred
unread_label: Unread unread_label: Unread
annotated_label: Annotated
preview_picture_label: Has a preview picture preview_picture_label: Has a preview picture
preview_picture_help: Preview picture preview_picture_help: Preview picture
is_public_label: Has a public link is_public_label: Has a public link

View file

@ -32,6 +32,7 @@ menu:
site_credentials: اعتبارنامه‌های وب‌گاه site_credentials: اعتبارنامه‌های وب‌گاه
users_management: مدیریت کاربران users_management: مدیریت کاربران
developer: مدیریت کارخواه‌های API developer: مدیریت کارخواه‌های API
quickstart: "Quickstart"
top: top:
add_new_entry: افزودن مقالهٔ تازه add_new_entry: افزودن مقالهٔ تازه
search: جستجو search: جستجو

View file

@ -19,6 +19,7 @@ menu:
starred: Favoris starred: Favoris
archive: Lus archive: Lus
all_articles: Tous les articles all_articles: Tous les articles
with_annotations: Avec annotations
config: Configuration config: Configuration
tags: Étiquettes tags: Étiquettes
internal_settings: Configuration interne internal_settings: Configuration interne
@ -220,6 +221,7 @@ entry:
starred: Articles favoris starred: Articles favoris
archived: Articles lus archived: Articles lus
filtered: Articles filtrés filtered: Articles filtrés
with_annotations: Articles avec annotations
filtered_tags: 'Articles filtrés par étiquettes :' filtered_tags: 'Articles filtrés par étiquettes :'
filtered_search: 'Articles filtrés par recherche :' filtered_search: 'Articles filtrés par recherche :'
untagged: Article sans étiquette untagged: Article sans étiquette
@ -243,6 +245,7 @@ entry:
archived_label: Lus archived_label: Lus
starred_label: Favoris starred_label: Favoris
unread_label: Non lus unread_label: Non lus
annotated_label: Annotés
preview_picture_label: A une photo preview_picture_label: A une photo
preview_picture_help: Photo preview_picture_help: Photo
is_public_label: A un lien public is_public_label: A un lien public

View file

@ -14,6 +14,8 @@
{{ 'entry.page_titles.untagged'|trans }} {{ 'entry.page_titles.untagged'|trans }}
{% elseif currentRoute == 'same_domain' %} {% elseif currentRoute == 'same_domain' %}
{{ 'entry.page_titles.same_domain'|trans }} {{ 'entry.page_titles.same_domain'|trans }}
{% elseif currentRoute == 'annotated' %}
{{ 'entry.page_titles.with_annotations'|trans }}
{% else %} {% else %}
{{ 'entry.page_titles.unread'|trans }} {{ 'entry.page_titles.unread'|trans }}
{% endif %} {% endif %}

View file

@ -131,6 +131,11 @@
{{ form_label(form.isUnread) }} {{ form_label(form.isUnread) }}
</div> </div>
<div class="input-field col s12 with-checkbox">
{{ form_widget(form.isAnnotated) }}
{{ form_label(form.isAnnotated) }}
</div>
<div class="col s12"> <div class="col s12">
<label>{{ 'entry.filters.preview_picture_help'|trans }}</label> <label>{{ 'entry.filters.preview_picture_help'|trans }}</label>
</div> </div>

View file

@ -40,6 +40,8 @@
{% set activeRoute = null %} {% set activeRoute = null %}
{% if currentRoute == 'all' or currentRouteFromQueryParams == 'all' %} {% if currentRoute == 'all' or currentRouteFromQueryParams == 'all' %}
{% set activeRoute = 'all' %} {% set activeRoute = 'all' %}
{% elseif currentRoute == 'annotated' or currentRouteFromQueryParams == 'annotated' %}
{% set activeRoute = 'annotated' %}
{% elseif currentRoute == 'archive' or currentRouteFromQueryParams == 'archive' %} {% elseif currentRoute == 'archive' or currentRouteFromQueryParams == 'archive' %}
{% set activeRoute = 'archive' %} {% set activeRoute = 'archive' %}
{% elseif currentRoute == 'starred' or currentRouteFromQueryParams == 'starred' %} {% elseif currentRoute == 'starred' or currentRouteFromQueryParams == 'starred' %}
@ -59,6 +61,9 @@
<li class="bold {% if activeRoute == 'archive' %}active{% endif %}"> <li class="bold {% if activeRoute == 'archive' %}active{% endif %}">
<a class="waves-effect" href="{{ path('archive') }}">{{ 'menu.left.archive'|trans }} <span class="numberItems grey-text">{{ count_entries('archive') }}</span></a> <a class="waves-effect" href="{{ path('archive') }}">{{ 'menu.left.archive'|trans }} <span class="numberItems grey-text">{{ count_entries('archive') }}</span></a>
</li> </li>
<li class="bold {% if activeRoute == 'annotated' %}active{% endif %}">
<a class="waves-effect" href="{{ path('annotated') }}">{{ 'menu.left.with_annotations'|trans }} <span class="numberItems grey-text">{{ count_entries('annotated') }}</span></a>
</li>
<li class="bold {% if activeRoute == 'all' %}active{% endif %}"> <li class="bold {% if activeRoute == 'all' %}active{% endif %}">
<a class="waves-effect" href="{{ path('all') }}">{{ 'menu.left.all_articles'|trans }} <span class="numberItems grey-text">{{ count_entries('all') }}</span></a> <a class="waves-effect" href="{{ path('all') }}">{{ 'menu.left.all_articles'|trans }} <span class="numberItems grey-text">{{ count_entries('all') }}</span></a>
</li> </li>

View file

@ -95,6 +95,9 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
case 'unread': case 'unread':
$qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId()); $qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId());
break; break;
case 'annotated':
$qb = $this->entryRepository->getBuilderForAnnotationsByUser($user->getId());
break;
case 'all': case 'all':
$qb = $this->entryRepository->getBuilderForAllByUser($user->getId()); $qb = $this->entryRepository->getBuilderForAllByUser($user->getId());
break; break;

View file

@ -3,6 +3,7 @@
namespace Tests\Wallabag\CoreBundle\Controller; namespace Tests\Wallabag\CoreBundle\Controller;
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase; use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
use Wallabag\AnnotationBundle\Entity\Annotation;
use Wallabag\CoreBundle\Entity\Config; use Wallabag\CoreBundle\Entity\Config;
use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\SiteCredential; use Wallabag\CoreBundle\Entity\SiteCredential;
@ -411,6 +412,16 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertSame(200, $client->getResponse()->getStatusCode()); $this->assertSame(200, $client->getResponse()->getStatusCode());
} }
public function testWithAnnotations()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/annotated/list');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertCount(2, $crawler->filter('li.entry'));
}
public function testRangeException() public function testRangeException()
{ {
$this->logInAs('admin'); $this->logInAs('admin');
@ -878,6 +889,44 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertCount(0, $crawler->filter('li.entry')); $this->assertCount(0, $crawler->filter('li.entry'));
} }
public function testFilterOnAnnotatedStatus()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/all/list');
$form = $crawler->filter('button[id=submit-filter]')->form();
$data = [
'entry_filter[isAnnotated]' => true,
];
$crawler = $client->submit($form, $data);
$this->assertCount(2, $crawler->filter('li.entry'));
$entry = new Entry($this->getLoggedInUser());
$entry->setUrl($this->url);
$em = $this->getClient()->getContainer()->get('doctrine.orm.entity_manager');
$user = $em
->getRepository('WallabagUserBundle:User')
->findOneByUserName('admin');
$annotation = new Annotation($user);
$annotation->setEntry($entry);
$annotation->setText('This is my annotation /o/');
$annotation->setQuote('content');
$this->getEntityManager()->persist($entry);
$this->getEntityManager()->flush();
$crawler = $client->submit($form, $data);
$this->assertCount(3, $crawler->filter('li.entry'));
}
public function testPaginationWithFilter() public function testPaginationWithFilter()
{ {
$this->logInAs('admin'); $this->logInAs('admin');
@ -1590,6 +1639,10 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertSame(302, $client->getResponse()->getStatusCode()); $this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('/view/', $client->getResponse()->getTargetUrl(), 'Untagged random'); $this->assertStringContainsString('/view/', $client->getResponse()->getTargetUrl(), 'Untagged random');
$client->request('GET', '/annotated/random');
$this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('/view/', $client->getResponse()->getTargetUrl(), 'With annotations random');
$client->request('GET', '/all/random'); $client->request('GET', '/all/random');
$this->assertSame(302, $client->getResponse()->getStatusCode()); $this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('/view/', $client->getResponse()->getTargetUrl(), 'All random'); $this->assertStringContainsString('/view/', $client->getResponse()->getTargetUrl(), 'All random');

View file

@ -120,7 +120,7 @@ class DeliciousControllerTest extends WallabagCoreTestCase
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content); $this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content);
$tags = $content->getTags(); $tags = $content->getTagsLabel();
$this->assertContains('osx', $tags, 'It includes the "osx" tag'); $this->assertContains('osx', $tags, 'It includes the "osx" tag');
$this->assertGreaterThanOrEqual(4, \count($tags)); $this->assertGreaterThanOrEqual(4, \count($tags));