mirror of
https://github.com/wallabag/wallabag.git
synced 2024-11-29 04:21:06 +00:00
Merge pull request #5794 from wallabag/2.5.0
Merge branch 2.5.0 in master
This commit is contained in:
commit
5809d7b072
40 changed files with 1160 additions and 355 deletions
|
@ -287,7 +287,7 @@ a.original:not(.waves-effect) {
|
|||
flex-basis: 5em;
|
||||
align-self: flex-end;
|
||||
float: right;
|
||||
max-width: 6em;
|
||||
max-width: 8em;
|
||||
}
|
||||
|
||||
.tags {
|
||||
|
|
|
@ -248,6 +248,11 @@ old_sound_rabbit_mq:
|
|||
exchange_options:
|
||||
name: 'wallabag.import.pinboard'
|
||||
type: topic
|
||||
import_delicious:
|
||||
connection: default
|
||||
exchange_options:
|
||||
name: 'wallabag.import.delicious'
|
||||
type: topic
|
||||
import_instapaper:
|
||||
connection: default
|
||||
exchange_options:
|
||||
|
@ -315,6 +320,15 @@ old_sound_rabbit_mq:
|
|||
name: 'wallabag.import.pinboard'
|
||||
callback: wallabag_import.consumer.amqp.pinboard
|
||||
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
|
||||
import_delicious:
|
||||
connection: default
|
||||
exchange_options:
|
||||
name: 'wallabag.import.delicious'
|
||||
type: topic
|
||||
queue_options:
|
||||
name: 'wallabag.import.delicious'
|
||||
callback: wallabag_import.consumer.amqp.delicious
|
||||
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
|
||||
import_wallabag_v1:
|
||||
connection: default
|
||||
exchange_options:
|
||||
|
|
|
@ -69,11 +69,11 @@ security:
|
|||
- { path: ^/logout, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_2FA_IN_PROGRESS] }
|
||||
- { path: ^/register, 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: /tags/(.*).xml$, 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: ^/settings, roles: ROLE_SUPER_ADMIN }
|
||||
- { path: ^/annotations, roles: ROLE_USER }
|
||||
|
|
|
@ -277,12 +277,26 @@ class EntryController extends Controller
|
|||
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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
|
@ -517,6 +531,20 @@ class EntryController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List the entries with the same domain as the current one.
|
||||
*
|
||||
* @param int $page
|
||||
*
|
||||
* @Route("/domain/{id}/{page}", requirements={"id" = ".+"}, defaults={"page" = 1}, name="same_domain")
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getSameDomainEntries(Request $request, $page = 1)
|
||||
{
|
||||
return $this->showEntries('same-domain', $request, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Global method to retrieve entries depending on the given type
|
||||
* It returns the response to be send.
|
||||
|
@ -549,10 +577,16 @@ class EntryController extends Controller
|
|||
$qb = $repository->getBuilderForArchiveByUser($this->getUser()->getId());
|
||||
$formOptions['filter_archived'] = true;
|
||||
break;
|
||||
case 'annotated':
|
||||
$qb = $repository->getBuilderForAnnotationsByUser($this->getUser()->getId());
|
||||
break;
|
||||
case 'unread':
|
||||
$qb = $repository->getBuilderForUnreadByUser($this->getUser()->getId());
|
||||
$formOptions['filter_unread'] = true;
|
||||
break;
|
||||
case 'same-domain':
|
||||
$qb = $repository->getBuilderForSameDomainByUser($this->getUser()->getId(), $request->get('id'));
|
||||
break;
|
||||
case 'all':
|
||||
$qb = $repository->getBuilderForAllByUser($this->getUser()->getId());
|
||||
break;
|
||||
|
|
|
@ -47,7 +47,7 @@ class ExportController extends Controller
|
|||
*
|
||||
* @Route("/export/{category}.{format}", name="export_entries", requirements={
|
||||
* "format": "epub|mobi|pdf|json|xml|txt|csv",
|
||||
* "category": "all|unread|starred|archive|tag_entries|untagged|search"
|
||||
* "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain"
|
||||
* })
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
|
@ -80,6 +80,13 @@ class ExportController extends Controller
|
|||
->getResult();
|
||||
|
||||
$title = 'Search ' . $searchTerm;
|
||||
} elseif ('annotated' === $category) {
|
||||
$entries = $repository->getBuilderForAnnotationsByUser(
|
||||
$this->getUser()->getId()
|
||||
)->getQuery()
|
||||
->getResult();
|
||||
|
||||
$title = 'With annotations';
|
||||
} else {
|
||||
$entries = $repository
|
||||
->$methodBuilder($this->getUser()->getId())
|
||||
|
|
|
@ -93,7 +93,7 @@ class EntryFilterType extends AbstractType
|
|||
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
|
||||
$value = $values['value'];
|
||||
if (\strlen($value) <= 2 || empty($value)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
$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) {
|
||||
$value = $values['value'];
|
||||
if (false === \array_key_exists($value, Response::$statusTexts)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$paramName = sprintf('%s', str_replace('.', '_', $field));
|
||||
|
@ -129,7 +129,7 @@ class EntryFilterType extends AbstractType
|
|||
'data' => $options['filter_unread'],
|
||||
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
|
||||
if (false === $values['value']) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$expression = $filterQuery->getExpr()->eq('e.isArchived', 'false');
|
||||
|
@ -137,10 +137,22 @@ class EntryFilterType extends AbstractType
|
|||
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, [
|
||||
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
|
||||
if (false === $values['value']) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$expression = $filterQuery->getExpr()->isNotNull($field);
|
||||
|
@ -152,7 +164,7 @@ class EntryFilterType extends AbstractType
|
|||
->add('isPublic', CheckboxFilterType::class, [
|
||||
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
|
||||
if (false === $values['value']) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// is_public isn't a real field
|
||||
|
@ -183,6 +195,7 @@ class EntryFilterType extends AbstractType
|
|||
'filter_archived' => false,
|
||||
'filter_starred' => false,
|
||||
'filter_unread' => false,
|
||||
'filter_annotated' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,34 @@ class EntryRepository extends EntityRepository
|
|||
return $this
|
||||
->getSortedQueryBuilderByUser($userId)
|
||||
->andWhere('e.isArchived = false')
|
||||
;
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves entries with the same domain.
|
||||
*
|
||||
* @param int $userId
|
||||
* @param int $entryId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getBuilderForSameDomainByUser($userId, $entryId)
|
||||
{
|
||||
$queryBuilder = $this->createQueryBuilder('e');
|
||||
|
||||
return $this
|
||||
->getSortedQueryBuilderByUser($userId)
|
||||
->andWhere('e.id <> :entryId')->setParameter('entryId', $entryId)
|
||||
->andWhere(
|
||||
$queryBuilder->expr()->in(
|
||||
'e.domainName',
|
||||
$this
|
||||
->createQueryBuilder('e2')
|
||||
->select('e2.domainName')
|
||||
->where('e2.id = :entryId')->setParameter('entryId', $entryId)
|
||||
->getDQL()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,6 +142,21 @@ class EntryRepository extends EntityRepository
|
|||
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.
|
||||
*
|
||||
|
@ -552,6 +594,10 @@ class EntryRepository extends EntityRepository
|
|||
$qb->leftJoin('e.tags', 't');
|
||||
$qb->andWhere('t.id is null');
|
||||
break;
|
||||
case 'annotated':
|
||||
$qb->leftJoin('e.annotations', 'a');
|
||||
$qb->andWhere('a.id is not null');
|
||||
break;
|
||||
}
|
||||
|
||||
$ids = $qb->getQuery()->getArrayResult();
|
||||
|
|
|
@ -19,6 +19,7 @@ menu:
|
|||
starred: Starred
|
||||
archive: Archive
|
||||
all_articles: All entries
|
||||
with_annotations: With annotations
|
||||
config: Config
|
||||
tags: Tags
|
||||
internal_settings: Internal Settings
|
||||
|
@ -220,10 +221,12 @@ entry:
|
|||
starred: Starred entries
|
||||
archived: Archived entries
|
||||
filtered: Filtered entries
|
||||
with_annotations: Entries with annotations
|
||||
filtered_tags: 'Filtered by tags:'
|
||||
filtered_search: 'Filtered by search:'
|
||||
untagged: Untagged entries
|
||||
all: All entries
|
||||
same_domain: Same domain
|
||||
list:
|
||||
number_on_the_page: '{0} There are no entries.|{1} There is one entry.|]1,Inf[ There are %count% entries.'
|
||||
reading_time: estimated reading time
|
||||
|
@ -237,6 +240,7 @@ entry:
|
|||
toogle_as_star: Toggle starred
|
||||
delete: Delete
|
||||
export_title: Export
|
||||
show_same_domain: Show articles with the same domain
|
||||
assign_search_tag: Assign this search as a tag to each result
|
||||
filters:
|
||||
title: Filters
|
||||
|
@ -244,6 +248,7 @@ entry:
|
|||
archived_label: Archived
|
||||
starred_label: Starred
|
||||
unread_label: Unread
|
||||
annotated_label: Annotated
|
||||
preview_picture_label: Has a preview picture
|
||||
preview_picture_help: Preview picture
|
||||
is_public_label: Has a public link
|
||||
|
@ -481,12 +486,12 @@ import:
|
|||
description: Pocket import isn't configured.
|
||||
admin_message: You need to define %keyurls%a pocket_consumer_key%keyurle%.
|
||||
user_message: Your server admin needs to define an API Key for Pocket.
|
||||
authorize_message: You can import your data from your Pocket account. You just have to click on the below button and authorize the application to connect to getpocket.com.
|
||||
authorize_message: You can import your data from your Pocket account. You just have to click on the button below and authorize the application to connect to getpocket.com.
|
||||
connect_to_pocket: Connect to Pocket and import data
|
||||
wallabag_v1:
|
||||
page_title: Import > Wallabag v1
|
||||
description: This importer will import all your wallabag v1 articles. On your config page, click on "JSON export" in the "Export your wallabag data" section. You will have a "wallabag-export-1-xxxx-xx-xx.json" file.
|
||||
how_to: Please select your wallabag export and click on the below button to upload and import it.
|
||||
how_to: Please select your wallabag export and click on the button below to upload and import it.
|
||||
wallabag_v2:
|
||||
page_title: Import > Wallabag v2
|
||||
description: This importer will import all your wallabag v2 articles. Go to All articles, then, on the export sidebar, click on "JSON". You will have a "All articles.json" file.
|
||||
|
@ -496,7 +501,7 @@ import:
|
|||
readability:
|
||||
page_title: Import > Readability
|
||||
description: This importer will import all your Readability articles. On the tools (https://www.readability.com/tools/) page, click on "Export your data" in the "Data Export" section. You will received an email to download a json (which does not end with .json in fact).
|
||||
how_to: Please select your Readability export and click on the below button to upload and import it.
|
||||
how_to: Please select your Readability export and click on the button below to upload and import it.
|
||||
worker:
|
||||
enabled: 'Import is made asynchronously. Once the import task is started, an external worker will handle jobs one at a time. The current service is:'
|
||||
download_images_warning: You enabled downloading images for your articles. Combined with classic import it can take ages to proceed (or maybe failed). We <strong>strongly recommend</strong> to enable asynchronous import to avoid errors.
|
||||
|
@ -511,11 +516,15 @@ import:
|
|||
instapaper:
|
||||
page_title: Import > Instapaper
|
||||
description: This importer will import all your Instapaper articles. On the settings (https://www.instapaper.com/user) page, click on "Download .CSV file" in the "Export" section. A CSV file will be downloaded (like "instapaper-export.csv").
|
||||
how_to: Please select your Instapaper export and click on the below button to upload and import it.
|
||||
how_to: Please select your Instapaper export and click on the button below to upload and import it.
|
||||
pinboard:
|
||||
page_title: Import > Pinboard
|
||||
description: This importer will import all your Pinboard articles. On the backup (https://pinboard.in/settings/backup) page, click on "JSON" in the "Bookmarks" section. A JSON file will be downloaded (like "pinboard_export").
|
||||
how_to: Please select your Pinboard export and click on the below button to upload and import it.
|
||||
how_to: Please select your Pinboard export and click on the button below to upload and import it.
|
||||
delicious:
|
||||
page_title: Import > del.icio.us
|
||||
description: This importer will import all your Delicious bookmarks. Since 2021, you can export again your data from it using the export page (https://del.icio.us/export). Choose the "JSON" format and download it (like "delicious_export.2021.02.06_21.10.json").
|
||||
how_to: Please select your Delicious export and click on the button below to upload and import it.
|
||||
developer:
|
||||
page_title: API clients management
|
||||
welcome_message: Welcome to the wallabag API
|
||||
|
|
|
@ -32,6 +32,7 @@ menu:
|
|||
site_credentials: اعتبارنامههای وبگاه
|
||||
users_management: مدیریت کاربران
|
||||
developer: مدیریت کارخواههای API
|
||||
quickstart: "Quickstart"
|
||||
top:
|
||||
add_new_entry: افزودن مقالهٔ تازه
|
||||
search: جستجو
|
||||
|
|
|
@ -19,6 +19,7 @@ menu:
|
|||
starred: Favoris
|
||||
archive: Lus
|
||||
all_articles: Tous les articles
|
||||
with_annotations: Avec annotations
|
||||
config: Configuration
|
||||
tags: Étiquettes
|
||||
internal_settings: Configuration interne
|
||||
|
@ -220,6 +221,7 @@ entry:
|
|||
starred: Articles favoris
|
||||
archived: Articles lus
|
||||
filtered: Articles filtrés
|
||||
with_annotations: Articles avec annotations
|
||||
filtered_tags: 'Articles filtrés par étiquettes :'
|
||||
filtered_search: 'Articles filtrés par recherche :'
|
||||
untagged: Article sans étiquette
|
||||
|
@ -243,6 +245,7 @@ entry:
|
|||
archived_label: Lus
|
||||
starred_label: Favoris
|
||||
unread_label: Non lus
|
||||
annotated_label: Annotés
|
||||
preview_picture_label: A une photo
|
||||
preview_picture_help: Photo
|
||||
is_public_label: A un lien public
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
{{ 'entry.page_titles.filtered_tags'|trans }} {{ filter }}
|
||||
{% elseif currentRoute == 'untagged' %}
|
||||
{{ 'entry.page_titles.untagged'|trans }}
|
||||
{% elseif currentRoute == 'same_domain' %}
|
||||
{{ 'entry.page_titles.same_domain'|trans }}
|
||||
{% elseif currentRoute == 'annotated' %}
|
||||
{{ 'entry.page_titles.with_annotations'|trans }}
|
||||
{% else %}
|
||||
{{ 'entry.page_titles.unread'|trans }}
|
||||
{% endif %}
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
</div>
|
||||
|
||||
<ul class="tools right">
|
||||
<li>
|
||||
<a title="{{ 'entry.list.show_same_domain'|trans }}" class="tool grey-text" href="{{ path('same_domain', { 'id': entry.id }) }}" data-action="same_domain" data-entry-id="{{ entry.id }}"><i class="material-icons">language</i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', { 'id': entry.id }) }}" data-action="archived" data-entry-id="{{ entry.id }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i></a>
|
||||
</li>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
{% include "@WallabagCore/themes/material/Entry/Card/_content.html.twig" with {'entry': entry, 'withMetadata': true, 'subClass': 'metadata'} only %}
|
||||
<ul class="tools-list hide-on-small-only">
|
||||
<li>
|
||||
<a title="{{ 'entry.list.show_same_domain'|trans }}" class="tool grey-text" href="{{ path('same_domain', { 'id': entry.id }) }}"><i class="material-icons">language</i></a>
|
||||
<a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i></a>
|
||||
<a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool grey-text" href="{{ path('star_entry', { 'id': entry.id }) }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i></a>
|
||||
<a title="{{ 'entry.list.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" class="tool grey-text delete" href="{{ path('delete_entry', { 'id': entry.id }) }}"><i class="material-icons">delete</i></a>
|
||||
|
|
|
@ -134,6 +134,11 @@
|
|||
{{ form_label(form.isUnread) }}
|
||||
</div>
|
||||
|
||||
<div class="input-field col s12 with-checkbox">
|
||||
{{ form_widget(form.isAnnotated) }}
|
||||
{{ form_label(form.isAnnotated) }}
|
||||
</div>
|
||||
|
||||
<div class="col s12">
|
||||
<label>{{ 'entry.filters.preview_picture_help'|trans }}</label>
|
||||
</div>
|
||||
|
|
|
@ -40,6 +40,8 @@
|
|||
{% set activeRoute = null %}
|
||||
{% if currentRoute == 'all' or currentRouteFromQueryParams == 'all' %}
|
||||
{% set activeRoute = 'all' %}
|
||||
{% elseif currentRoute == 'annotated' or currentRouteFromQueryParams == 'annotated' %}
|
||||
{% set activeRoute = 'annotated' %}
|
||||
{% elseif currentRoute == 'archive' or currentRouteFromQueryParams == 'archive' %}
|
||||
{% set activeRoute = 'archive' %}
|
||||
{% elseif currentRoute == 'starred' or currentRouteFromQueryParams == 'starred' %}
|
||||
|
@ -59,6 +61,9 @@
|
|||
<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>
|
||||
</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 %}">
|
||||
<a class="waves-effect" href="{{ path('all') }}">{{ 'menu.left.all_articles'|trans }} <span class="numberItems grey-text">{{ count_entries('all') }}</span></a>
|
||||
</li>
|
||||
|
|
|
@ -95,6 +95,9 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
|
|||
case 'unread':
|
||||
$qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId());
|
||||
break;
|
||||
case 'annotated':
|
||||
$qb = $this->entryRepository->getBuilderForAnnotationsByUser($user->getId());
|
||||
break;
|
||||
case 'all':
|
||||
$qb = $this->entryRepository->getBuilderForAllByUser($user->getId());
|
||||
break;
|
||||
|
|
|
@ -19,7 +19,7 @@ class ImportCommand extends ContainerAwareCommand
|
|||
->setDescription('Import entries from a JSON export')
|
||||
->addArgument('username', InputArgument::REQUIRED, 'User to populate')
|
||||
->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file')
|
||||
->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, readability, firefox or chrome', 'v1')
|
||||
->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, delicious, readability, firefox or chrome', 'v1')
|
||||
->addOption('markAsRead', null, InputOption::VALUE_OPTIONAL, 'Mark all entries as read', false)
|
||||
->addOption('useUserId', null, InputOption::VALUE_NONE, 'Use user id instead of username to find account')
|
||||
->addOption('disableContentUpdate', null, InputOption::VALUE_NONE, 'Disable fetching updated content from URL')
|
||||
|
@ -77,6 +77,9 @@ class ImportCommand extends ContainerAwareCommand
|
|||
case 'pinboard':
|
||||
$import = $this->getContainer()->get('wallabag_import.pinboard.import');
|
||||
break;
|
||||
case 'delicious':
|
||||
$import = $this->getContainer()->get('wallabag_import.delicious.import');
|
||||
break;
|
||||
default:
|
||||
$import = $this->getContainer()->get('wallabag_import.wallabag_v1.import');
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class RedisWorkerCommand extends ContainerAwareCommand
|
|||
$this
|
||||
->setName('wallabag:import:redis-worker')
|
||||
->setDescription('Launch Redis worker')
|
||||
->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, firefox, chrome or instapaper')
|
||||
->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, delicious, firefox, chrome or instapaper')
|
||||
->addOption('maxIterations', '', InputOption::VALUE_OPTIONAL, 'Number of iterations before stoping', false)
|
||||
;
|
||||
}
|
||||
|
|
77
src/Wallabag/ImportBundle/Controller/DeliciousController.php
Normal file
77
src/Wallabag/ImportBundle/Controller/DeliciousController.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\ImportBundle\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Wallabag\ImportBundle\Form\Type\UploadImportType;
|
||||
|
||||
class DeliciousController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/delicious", name="import_delicious")
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
$form = $this->createForm(UploadImportType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
$delicious = $this->get('wallabag_import.delicious.import');
|
||||
$delicious->setUser($this->getUser());
|
||||
|
||||
if ($this->get('craue_config')->get('import_with_rabbitmq')) {
|
||||
$delicious->setProducer($this->get('old_sound_rabbit_mq.import_delicious_producer'));
|
||||
} elseif ($this->get('craue_config')->get('import_with_redis')) {
|
||||
$delicious->setProducer($this->get('wallabag_import.producer.redis.delicious'));
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$file = $form->get('file')->getData();
|
||||
$markAsRead = $form->get('mark_as_read')->getData();
|
||||
$name = 'delicious_' . $this->getUser()->getId() . '.json';
|
||||
|
||||
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
|
||||
$res = $delicious
|
||||
->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
|
||||
->setMarkAsRead($markAsRead)
|
||||
->import();
|
||||
|
||||
$message = 'flashes.import.notice.failed';
|
||||
|
||||
if (true === $res) {
|
||||
$summary = $delicious->getSummary();
|
||||
$message = $this->get('translator')->trans('flashes.import.notice.summary', [
|
||||
'%imported%' => $summary['imported'],
|
||||
'%skipped%' => $summary['skipped'],
|
||||
]);
|
||||
|
||||
if (0 < $summary['queued']) {
|
||||
$message = $this->get('translator')->trans('flashes.import.notice.summary_with_queue', [
|
||||
'%queued%' => $summary['queued'],
|
||||
]);
|
||||
}
|
||||
|
||||
unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
|
||||
}
|
||||
|
||||
$this->get('session')->getFlashBag()->add(
|
||||
'notice',
|
||||
$message
|
||||
);
|
||||
|
||||
return $this->redirect($this->generateUrl('homepage'));
|
||||
}
|
||||
|
||||
$this->get('session')->getFlashBag()->add(
|
||||
'notice',
|
||||
'flashes.import.notice.failed_on_file'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('WallabagImportBundle:Delicious:index.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'import' => $delicious,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ class ImportController extends Controller
|
|||
+ $this->getTotalMessageInRabbitQueue('chrome')
|
||||
+ $this->getTotalMessageInRabbitQueue('instapaper')
|
||||
+ $this->getTotalMessageInRabbitQueue('pinboard')
|
||||
+ $this->getTotalMessageInRabbitQueue('delicious')
|
||||
+ $this->getTotalMessageInRabbitQueue('elcurator')
|
||||
;
|
||||
} catch (\Exception $e) {
|
||||
|
@ -60,6 +61,7 @@ class ImportController extends Controller
|
|||
+ $redis->llen('wallabag.import.chrome')
|
||||
+ $redis->llen('wallabag.import.instapaper')
|
||||
+ $redis->llen('wallabag.import.pinboard')
|
||||
+ $redis->llen('wallabag.import.delicious')
|
||||
+ $redis->llen('wallabag.import.elcurator')
|
||||
;
|
||||
} catch (\Exception $e) {
|
||||
|
|
151
src/Wallabag/ImportBundle/Import/DeliciousImport.php
Normal file
151
src/Wallabag/ImportBundle/Import/DeliciousImport.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\ImportBundle\Import;
|
||||
|
||||
use Wallabag\CoreBundle\Entity\Entry;
|
||||
|
||||
class DeliciousImport extends AbstractImport
|
||||
{
|
||||
private $filepath;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'Delicious';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return 'import_delicious';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return 'import.delicious.description';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file path to the json file.
|
||||
*
|
||||
* @param string $filepath
|
||||
*/
|
||||
public function setFilepath($filepath)
|
||||
{
|
||||
$this->filepath = $filepath;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function import()
|
||||
{
|
||||
if (!$this->user) {
|
||||
$this->logger->error('DeliciousImport: user is not defined');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
|
||||
$this->logger->error('DeliciousImport: unable to read file', ['filepath' => $this->filepath]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents($this->filepath), true);
|
||||
|
||||
if (empty($data)) {
|
||||
$this->logger->error('DeliciousImport: no entries in imported file');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->producer) {
|
||||
$this->parseEntriesForProducer($data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->parseEntries($data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateEntry(array $importedEntry)
|
||||
{
|
||||
if (empty($importedEntry['url'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parseEntry(array $importedEntry)
|
||||
{
|
||||
$existingEntry = $this->em
|
||||
->getRepository('WallabagCoreBundle:Entry')
|
||||
->findByUrlAndUserId($importedEntry['url'], $this->user->getId());
|
||||
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'title' => $importedEntry['title'],
|
||||
'url' => $importedEntry['url'],
|
||||
'is_archived' => $this->markAsRead,
|
||||
'is_starred' => false,
|
||||
'created_at' => $importedEntry['created'],
|
||||
'tags' => $importedEntry['tags'],
|
||||
];
|
||||
|
||||
$entry = new Entry($this->user);
|
||||
$entry->setUrl($data['url']);
|
||||
$entry->setTitle($data['title']);
|
||||
|
||||
// update entry with content (in case fetching failed, the given entry will be return)
|
||||
$this->fetchContent($entry, $data['url'], $data);
|
||||
|
||||
if (!empty($data['tags'])) {
|
||||
$this->tagsAssigner->assignTagsToEntry(
|
||||
$entry,
|
||||
$data['tags'],
|
||||
$this->em->getUnitOfWork()->getScheduledEntityInsertions()
|
||||
);
|
||||
}
|
||||
|
||||
$entry->updateArchived($data['is_archived']);
|
||||
$entry->setStarred($data['is_starred']);
|
||||
$entry->setCreatedAt(\DateTime::createFromFormat('U', $data['created_at']));
|
||||
|
||||
$this->em->persist($entry);
|
||||
++$this->importedEntries;
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setEntryAsRead(array $importedEntry)
|
||||
{
|
||||
return $importedEntry;
|
||||
}
|
||||
}
|
|
@ -32,6 +32,14 @@ services:
|
|||
- "@wallabag_import.pinboard.import"
|
||||
- "@event_dispatcher"
|
||||
- "@logger"
|
||||
wallabag_import.consumer.amqp.delicious:
|
||||
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
|
||||
arguments:
|
||||
- "@doctrine.orm.entity_manager"
|
||||
- "@wallabag_user.user_repository"
|
||||
- "@wallabag_import.delicious.import"
|
||||
- "@event_dispatcher"
|
||||
- "@logger"
|
||||
wallabag_import.consumer.amqp.wallabag_v1:
|
||||
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
|
||||
arguments:
|
||||
|
|
|
@ -63,6 +63,27 @@ services:
|
|||
- "@event_dispatcher"
|
||||
- "@logger"
|
||||
|
||||
# delicious
|
||||
wallabag_import.queue.redis.delicious:
|
||||
class: Simpleue\Queue\RedisQueue
|
||||
arguments:
|
||||
- "@wallabag_core.redis.client"
|
||||
- "wallabag.import.delicious"
|
||||
|
||||
wallabag_import.producer.redis.delicious:
|
||||
class: Wallabag\ImportBundle\Redis\Producer
|
||||
arguments:
|
||||
- "@wallabag_import.queue.redis.delicious"
|
||||
|
||||
wallabag_import.consumer.redis.delicious:
|
||||
class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer
|
||||
arguments:
|
||||
- "@doctrine.orm.entity_manager"
|
||||
- "@wallabag_user.user_repository"
|
||||
- "@wallabag_import.delicious.import"
|
||||
- "@event_dispatcher"
|
||||
- "@logger"
|
||||
|
||||
# pocket
|
||||
wallabag_import.queue.redis.pocket:
|
||||
class: Simpleue\Queue\RedisQueue
|
||||
|
|
|
@ -96,6 +96,18 @@ services:
|
|||
tags:
|
||||
- { name: wallabag_import.import, alias: pinboard }
|
||||
|
||||
wallabag_import.delicious.import:
|
||||
class: Wallabag\ImportBundle\Import\DeliciousImport
|
||||
arguments:
|
||||
- "@doctrine.orm.entity_manager"
|
||||
- "@wallabag_core.content_proxy"
|
||||
- "@wallabag_core.tags_assigner"
|
||||
- "@event_dispatcher"
|
||||
calls:
|
||||
- [ setLogger, [ "@logger" ]]
|
||||
tags:
|
||||
- { name: wallabag_import.import, alias: delicious }
|
||||
|
||||
wallabag_import.firefox.import:
|
||||
class: Wallabag\ImportBundle\Import\FirefoxImport
|
||||
arguments:
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
{% extends "WallabagCoreBundle::layout.html.twig" %}
|
||||
|
||||
{% block title %}{{ 'import.delicious.page_title'|trans }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<div class="card-panel settings">
|
||||
{% include 'WallabagImportBundle:Import:_information.html.twig' %}
|
||||
|
||||
<div class="row">
|
||||
<blockquote>{{ import.description|trans }}</blockquote>
|
||||
<p>{{ 'import.delicious.how_to'|trans }}</p>
|
||||
|
||||
<div class="col s12">
|
||||
{{ form_start(form, {'method': 'POST'}) }}
|
||||
{{ form_errors(form) }}
|
||||
<div class="row">
|
||||
<div class="file-field input-field col s12">
|
||||
{{ form_errors(form.file) }}
|
||||
<div class="btn">
|
||||
<span>{{ form.file.vars.label|trans }}</span>
|
||||
{{ form_widget(form.file) }}
|
||||
</div>
|
||||
<div class="file-path-wrapper">
|
||||
<input class="file-path validate" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-field col s6 with-checkbox">
|
||||
<h6>{{ 'import.form.mark_as_read_title'|trans }}</h6>
|
||||
{{ form_widget(form.mark_as_read) }}
|
||||
{{ form_label(form.mark_as_read) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_widget(form.save, { 'attr': {'class': 'btn waves-effect waves-light'} }) }}
|
||||
|
||||
{{ form_rest(form) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Wallabag\CoreBundle\Controller;
|
||||
|
||||
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
|
||||
use Wallabag\AnnotationBundle\Entity\Annotation;
|
||||
use Wallabag\CoreBundle\Entity\Config;
|
||||
use Wallabag\CoreBundle\Entity\Entry;
|
||||
use Wallabag\CoreBundle\Entity\SiteCredential;
|
||||
|
@ -448,6 +449,16 @@ class EntryControllerTest extends WallabagCoreTestCase
|
|||
$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('ol.entries > li'));
|
||||
}
|
||||
|
||||
public function testRangeException()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
|
@ -915,6 +926,44 @@ class EntryControllerTest extends WallabagCoreTestCase
|
|||
$this->assertCount(0, $crawler->filter($this->entryDataTestAttribute));
|
||||
}
|
||||
|
||||
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('ol.entries > li'));
|
||||
|
||||
$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('ol.entries > li'));
|
||||
}
|
||||
|
||||
public function testPaginationWithFilter()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
|
@ -1627,6 +1676,10 @@ class EntryControllerTest extends WallabagCoreTestCase
|
|||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
$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');
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('/view/', $client->getResponse()->getTargetUrl(), 'All random');
|
||||
|
@ -1708,4 +1761,14 @@ class EntryControllerTest extends WallabagCoreTestCase
|
|||
$client->request('GET', '/delete/' . $entry2->getId());
|
||||
$this->assertSame(404, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testGetSameDomainEntries()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
|
||||
$crawler = $client->request('GET', '/domain/1');
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
$this->assertCount(4, $crawler->filter('ol.entries > li'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Wallabag\ImportBundle\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
|
||||
|
||||
class DeliciousControllerTest extends WallabagCoreTestCase
|
||||
{
|
||||
public function testImportDelicious()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
|
||||
$crawler = $client->request('GET', '/import/delicious');
|
||||
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
|
||||
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
|
||||
}
|
||||
|
||||
public function testImportDeliciousWithRabbitEnabled()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
|
||||
$client->getContainer()->get('craue_config')->set('import_with_rabbitmq', 1);
|
||||
|
||||
$crawler = $client->request('GET', '/import/delicious');
|
||||
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
|
||||
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
|
||||
|
||||
$client->getContainer()->get('craue_config')->set('import_with_rabbitmq', 0);
|
||||
}
|
||||
|
||||
public function testImportDeliciousBadFile()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
|
||||
$crawler = $client->request('GET', '/import/delicious');
|
||||
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
|
||||
|
||||
$data = [
|
||||
'upload_import_file[file]' => '',
|
||||
];
|
||||
|
||||
$client->submit($form, $data);
|
||||
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testImportDeliciousWithRedisEnabled()
|
||||
{
|
||||
$this->checkRedis();
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
$client->getContainer()->get('craue_config')->set('import_with_redis', 1);
|
||||
|
||||
$crawler = $client->request('GET', '/import/delicious');
|
||||
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
|
||||
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
|
||||
|
||||
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
|
||||
|
||||
$file = new UploadedFile(__DIR__ . '/../fixtures/delicious_export.2021.02.06_21.10.json', 'delicious.json');
|
||||
|
||||
$data = [
|
||||
'upload_import_file[file]' => $file,
|
||||
];
|
||||
|
||||
$client->submit($form, $data);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
|
||||
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
|
||||
|
||||
$this->assertNotEmpty($client->getContainer()->get('wallabag_core.redis.client')->lpop('wallabag.import.delicious'));
|
||||
|
||||
$client->getContainer()->get('craue_config')->set('import_with_redis', 0);
|
||||
}
|
||||
|
||||
public function testImportDeliciousWithFile()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
|
||||
$crawler = $client->request('GET', '/import/delicious');
|
||||
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
|
||||
|
||||
$file = new UploadedFile(__DIR__ . '/../fixtures/delicious_export.2021.02.06_21.10.json', 'delicious.json');
|
||||
|
||||
$data = [
|
||||
'upload_import_file[file]' => $file,
|
||||
];
|
||||
|
||||
$client->submit($form, $data);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$content = $client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagCoreBundle:Entry')
|
||||
->findByUrlAndUserId(
|
||||
'https://feross.org/spoofmac/',
|
||||
$this->getLoggedInUserId()
|
||||
);
|
||||
|
||||
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
|
||||
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
|
||||
|
||||
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content);
|
||||
|
||||
$tags = $content->getTagsLabel();
|
||||
$this->assertContains('osx', $tags, 'It includes the "osx" tag');
|
||||
$this->assertGreaterThanOrEqual(4, \count($tags));
|
||||
|
||||
$this->assertInstanceOf(\DateTime::class, $content->getCreatedAt());
|
||||
$this->assertSame('2013-01-17', $content->getCreatedAt()->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testImportDeliciousWithFileAndMarkAllAsRead()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
|
||||
$crawler = $client->request('GET', '/import/delicious');
|
||||
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
|
||||
|
||||
$file = new UploadedFile(__DIR__ . '/../fixtures/delicious_export.2021.02.06_21.10.json', 'delicious-read.json');
|
||||
|
||||
$data = [
|
||||
'upload_import_file[file]' => $file,
|
||||
'upload_import_file[mark_as_read]' => 1,
|
||||
];
|
||||
|
||||
$client->submit($form, $data);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$content1 = $client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagCoreBundle:Entry')
|
||||
->findByUrlAndUserId(
|
||||
'https://stackoverflow.com/review/',
|
||||
$this->getLoggedInUserId()
|
||||
);
|
||||
|
||||
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content1);
|
||||
|
||||
$content2 = $client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagCoreBundle:Entry')
|
||||
->findByUrlAndUserId(
|
||||
'https://addyosmani.com/basket.js/',
|
||||
$this->getLoggedInUserId()
|
||||
);
|
||||
|
||||
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content2);
|
||||
|
||||
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
|
||||
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
|
||||
}
|
||||
|
||||
public function testImportDeliciousWithEmptyFile()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getClient();
|
||||
|
||||
$crawler = $client->request('GET', '/import/delicious');
|
||||
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
|
||||
|
||||
$file = new UploadedFile(__DIR__ . '/../fixtures/test.txt', 'test.txt');
|
||||
|
||||
$data = [
|
||||
'upload_import_file[file]' => $file,
|
||||
];
|
||||
|
||||
$client->submit($form, $data);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
|
||||
$this->assertStringContainsString('flashes.import.notice.failed', $body[0]);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,6 @@ class ImportControllerTest extends WallabagCoreTestCase
|
|||
$crawler = $client->request('GET', '/import/');
|
||||
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
$this->assertSame(9, $crawler->filter('blockquote')->count());
|
||||
$this->assertSame(10, $crawler->filter('blockquote')->count());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
[
|
||||
{
|
||||
"title": "basket.js - a simple script loader that caches scripts with localStorage",
|
||||
"tags": [
|
||||
"basket",
|
||||
"javascript",
|
||||
"loader",
|
||||
"localStorage"
|
||||
],
|
||||
"url": "https://addyosmani.com/basket.js/",
|
||||
"description": "\"A simple (proof-of-concept) script loader that caches scripts with localStorage\"",
|
||||
"created": "1358531607",
|
||||
"others": 9,
|
||||
"owner": "maciej",
|
||||
"private": "0"
|
||||
},
|
||||
{
|
||||
"title": "Review - Stack Overflow",
|
||||
"tags": [
|
||||
""
|
||||
],
|
||||
"url": "https://stackoverflow.com/review/",
|
||||
"description": "",
|
||||
"created": "1358457437",
|
||||
"others": 84,
|
||||
"owner": "maciej",
|
||||
"private": "0"
|
||||
},
|
||||
{
|
||||
"title": "Announcing SpoofMAC - Spoof your MAC address in Mac OS X",
|
||||
"tags": [
|
||||
"MAC_address",
|
||||
"osx",
|
||||
"mac",
|
||||
"spoof"
|
||||
],
|
||||
"url": "https://feross.org/spoofmac/",
|
||||
"description": "",
|
||||
"created": "1358425796",
|
||||
"others": 6,
|
||||
"owner": "maciej",
|
||||
"private": "0"
|
||||
}
|
||||
]
|
|
@ -1,13 +1,13 @@
|
|||
[
|
||||
{
|
||||
"created_at": "2015-09-09 11:10:32 UTC",
|
||||
"title": "Qualité de code - Intégration de php-git-hooks dans Symfony2 - Experts Symfony et Drupal - Lexik",
|
||||
"url": "https://devblog.lexik.fr/git/qualite-de-code-integration-de-php-git-hooks-dans-symfony2-2842",
|
||||
"description": null,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2"
|
||||
],
|
||||
"is_saved": true
|
||||
}
|
||||
{
|
||||
"created_at": "2015-09-09 11:10:32 UTC",
|
||||
"title": "Qualité de code - Intégration de php-git-hooks dans Symfony2 - Experts Symfony et Drupal - Lexik",
|
||||
"url": "https://devblog.lexik.fr/git/qualite-de-code-integration-de-php-git-hooks-dans-symfony2-2842",
|
||||
"description": null,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2"
|
||||
],
|
||||
"is_saved": true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,63 +1,63 @@
|
|||
{
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"index": 0,
|
||||
"dateAdded": 1388166091504000,
|
||||
"lastModified": 1472897622350000,
|
||||
"id": 1,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
"guid": "root________",
|
||||
"title": "",
|
||||
"index": 0,
|
||||
"dateAdded": 1388166091504000,
|
||||
"lastModified": 1472897622350000,
|
||||
"id": 1,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "placesRoot",
|
||||
"children": [
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Barre personnelle",
|
||||
"index": 1,
|
||||
"dateAdded": 1388166091504000,
|
||||
"lastModified": 1472897622263000,
|
||||
"id": 3,
|
||||
"annos": [
|
||||
{
|
||||
"guid": "toolbar_____",
|
||||
"title": "Barre personnelle",
|
||||
"index": 1,
|
||||
"dateAdded": 1388166091504000,
|
||||
"lastModified": 1472897622263000,
|
||||
"id": 3,
|
||||
"annos": [
|
||||
{
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"value": "Ajoutez des marque-pages dans ce dossier pour les voir apparaître sur votre barre personnelle"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "tard77lzbC5H",
|
||||
"title": "Orange offre un meilleur réseau mobile que Bouygues et SFR, Free derrière - L'Express L'Expansion",
|
||||
"index": 1,
|
||||
"dateAdded": 1388166091644000,
|
||||
"lastModified": 1388166091644000,
|
||||
"tags":"test,tag",
|
||||
"id": 4,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://lexpansion.lexpress.fr/high-tech/orange-offre-un-meilleur-reseau-mobile-que-bouygues-et-sfr-free-derriere_1811554.html"
|
||||
},
|
||||
{
|
||||
"guid": "E385l9vZ_LVn",
|
||||
"title": "Le journaliste et cinéaste Claude Lanzmann est mort",
|
||||
"index": 1,
|
||||
"dateAdded": 1388166091544000,
|
||||
"lastModified": 1388166091545000,
|
||||
"id": 5,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://www.lemonde.fr/disparitions/article/2018/07/05/le-journaliste-et-cineaste-claude-lanzmann-est-mort_5326313_3382.html"
|
||||
}
|
||||
]
|
||||
"name": "bookmarkProperties/description",
|
||||
"flags": 0,
|
||||
"expires": 4,
|
||||
"value": "Ajoutez des marque-pages dans ce dossier pour les voir apparaître sur votre barre personnelle"
|
||||
}
|
||||
],
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "toolbarFolder",
|
||||
"children": [
|
||||
{
|
||||
"guid": "tard77lzbC5H",
|
||||
"title": "Orange offre un meilleur réseau mobile que Bouygues et SFR, Free derrière - L'Express L'Expansion",
|
||||
"index": 1,
|
||||
"dateAdded": 1388166091644000,
|
||||
"lastModified": 1388166091644000,
|
||||
"tags": "test,tag",
|
||||
"id": 4,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "http://lexpansion.lexpress.fr/high-tech/orange-offre-un-meilleur-reseau-mobile-que-bouygues-et-sfr-free-derriere_1811554.html"
|
||||
},
|
||||
{
|
||||
"guid": "unfiled_____",
|
||||
"title": "Autres marque-pages",
|
||||
"index": 3,
|
||||
"dateAdded": 1388166091504000,
|
||||
"lastModified": 1388166091542000,
|
||||
"id": 6,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder"
|
||||
"guid": "E385l9vZ_LVn",
|
||||
"title": "Le journaliste et cinéaste Claude Lanzmann est mort",
|
||||
"index": 1,
|
||||
"dateAdded": 1388166091544000,
|
||||
"lastModified": 1388166091545000,
|
||||
"id": 5,
|
||||
"type": "text/x-moz-place",
|
||||
"uri": "https://www.lemonde.fr/disparitions/article/2018/07/05/le-journaliste-et-cineaste-claude-lanzmann-est-mort_5326313_3382.html"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "unfiled_____",
|
||||
"title": "Autres marque-pages",
|
||||
"index": 3,
|
||||
"dateAdded": 1388166091504000,
|
||||
"lastModified": 1388166091542000,
|
||||
"id": 6,
|
||||
"type": "text/x-moz-place-container",
|
||||
"root": "unfiledBookmarksFolder"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,35 @@
|
|||
[{"href":"https:\/\/developers.google.com\/web\/updates\/2016\/07\/infinite-scroller","description":"Complexities of an Infinite Scroller","extended":"TL;DR: Re-use your DOM elements and remove the ones that are far away from the viewport. Use placeholders to account for delayed data","meta":"21ff61c6f648901168f9e6119f53df7d","hash":"e69b65724cca1c585b446d4c47865d76","time":"2016-10-31T15:57:56Z","shared":"yes","toread":"no","tags":"infinite dom performance scroll"},
|
||||
{"href":"https:\/\/ma.ttias.be\/varnish-explained\/","description":"Varnish (explained) for PHP developers","extended":"A few months ago, I gave a presentation at LaraconEU in Amsterdam titled \"Varnish for PHP developers\". The generic title of that presentation is actually Varnish Explained and this is a write-up of that presentation, the video and the slides.","meta":"d32ad9fac2ed29da4aec12c562e9afb1","hash":"21dd6bdda8ad62666a2c9e79f6e80f98","time":"2016-10-26T06:43:03Z","shared":"yes","toread":"no","tags":"varnish PHP"},
|
||||
{"href":"https:\/\/ilia.ws\/files\/nginx_torontophpug.pdf","description":"Nginx Tricks for PHP Developers","extended":"","meta":"9adbb5c4ca6760e335b920800d88c70a","hash":"0189bb08f8bd0122c6544bed4624c546","time":"2016-10-05T07:11:27Z","shared":"yes","toread":"no","tags":"nginx PHP best_practice"}]
|
||||
[
|
||||
{
|
||||
"href": "https://developers.google.com/web/updates/2016/07/infinite-scroller",
|
||||
"description": "Complexities of an Infinite Scroller",
|
||||
"extended": "TL;DR: Re-use your DOM elements and remove the ones that are far away from the viewport. Use placeholders to account for delayed data",
|
||||
"meta": "21ff61c6f648901168f9e6119f53df7d",
|
||||
"hash": "e69b65724cca1c585b446d4c47865d76",
|
||||
"time": "2016-10-31T15:57:56Z",
|
||||
"shared": "yes",
|
||||
"toread": "no",
|
||||
"tags": "infinite dom performance scroll"
|
||||
},
|
||||
{
|
||||
"href": "https://ma.ttias.be/varnish-explained/",
|
||||
"description": "Varnish (explained) for PHP developers",
|
||||
"extended": "A few months ago, I gave a presentation at LaraconEU in Amsterdam titled \"Varnish for PHP developers\". The generic title of that presentation is actually Varnish Explained and this is a write-up of that presentation, the video and the slides.",
|
||||
"meta": "d32ad9fac2ed29da4aec12c562e9afb1",
|
||||
"hash": "21dd6bdda8ad62666a2c9e79f6e80f98",
|
||||
"time": "2016-10-26T06:43:03Z",
|
||||
"shared": "yes",
|
||||
"toread": "no",
|
||||
"tags": "varnish PHP"
|
||||
},
|
||||
{
|
||||
"href": "https://ilia.ws/files/nginx_torontophpug.pdf",
|
||||
"description": "Nginx Tricks for PHP Developers",
|
||||
"extended": "",
|
||||
"meta": "9adbb5c4ca6760e335b920800d88c70a",
|
||||
"hash": "0189bb08f8bd0122c6544bed4624c546",
|
||||
"time": "2016-10-05T07:11:27Z",
|
||||
"shared": "yes",
|
||||
"toread": "no",
|
||||
"tags": "nginx PHP best_practice"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
{
|
||||
"bookmarks": [
|
||||
{
|
||||
"article__excerpt": "This is a guest post from Moritz Beller from the Delft University of Technology in The Netherlands. His team produced amazing research on several million Travis CI builds, creating invaluable…",
|
||||
"favorite": false,
|
||||
"date_archived": "2016-08-02T06:49:30",
|
||||
"article__url": "https://blog.travis-ci.com/2016-07-28-what-we-learned-from-analyzing-2-million-travis-builds/",
|
||||
"date_added": "2016-08-01T05:24:16",
|
||||
"date_favorited": null,
|
||||
"article__title": "Travis",
|
||||
"archive": true
|
||||
},
|
||||
{
|
||||
"article__excerpt": "The GraphQL Type system describes the capabilities of a GraphQL server and is used to determine if a query is valid. The type system also describes the input types of query variables to determine if…",
|
||||
"favorite": false,
|
||||
"date_archived": "2016-07-19T06:48:31",
|
||||
"article__url": "https://facebook.github.io/graphql/October2016/",
|
||||
"date_added": "2016-06-24T17:50:16",
|
||||
"date_favorited": null,
|
||||
"article__title": "GraphQL",
|
||||
"archive": true
|
||||
}
|
||||
],
|
||||
"recommendations": []
|
||||
"bookmarks": [
|
||||
{
|
||||
"article__excerpt": "This is a guest post from Moritz Beller from the Delft University of Technology in The Netherlands. His team produced amazing research on several million Travis CI builds, creating invaluable…",
|
||||
"favorite": false,
|
||||
"date_archived": "2016-08-02T06:49:30",
|
||||
"article__url": "https://blog.travis-ci.com/2016-07-28-what-we-learned-from-analyzing-2-million-travis-builds/",
|
||||
"date_added": "2016-08-01T05:24:16",
|
||||
"date_favorited": null,
|
||||
"article__title": "Travis",
|
||||
"archive": true
|
||||
},
|
||||
{
|
||||
"article__excerpt": "The GraphQL Type system describes the capabilities of a GraphQL server and is used to determine if a query is valid. The type system also describes the input types of query variables to determine if…",
|
||||
"favorite": false,
|
||||
"date_archived": "2016-07-19T06:48:31",
|
||||
"article__url": "https://facebook.github.io/graphql/October2016/",
|
||||
"date_added": "2016-06-24T17:50:16",
|
||||
"date_favorited": null,
|
||||
"article__title": "GraphQL",
|
||||
"archive": true
|
||||
}
|
||||
],
|
||||
"recommendations": []
|
||||
}
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
{
|
||||
"bookmarks": [
|
||||
{
|
||||
"article__excerpt": "When Twitter started it had so much promise to change the way we communicate. But now it has been ruined by the amount of garbage and hate we have to wade through. It’s like that polluted…",
|
||||
"favorite": false,
|
||||
"date_archived": null,
|
||||
"article__url": "https://venngage.com/blog/hashtags-are-worthless/",
|
||||
"date_added": "2016-08-25T12:05:00",
|
||||
"date_favorited": null,
|
||||
"article__title": "We Looked At 167,943 Tweets & Found Out Hashtags Are Worthless",
|
||||
"archive": false
|
||||
},
|
||||
{
|
||||
"article__title": "No title found",
|
||||
"article__url": "http://news.nationalgeographic.com/2016/02/160211-albatrosses-mothers-babies-animals-science/&sf20739758=1",
|
||||
"archive": false,
|
||||
"date_added": "2016-09-08T11:55:58+0200",
|
||||
"favorite": true
|
||||
},
|
||||
{
|
||||
"archive": 0,
|
||||
"date_added": "2016-09-08T11:55:58+0200",
|
||||
"favorite": 0,
|
||||
"article__title": "Bordeaux: Poche, chocolatine… Une association traduit aux étudiants étrangers les mots du Sud-Ouest",
|
||||
"article__url": "https://www.20minutes.fr/bordeaux/2120479-20170823-bordeaux-poche-chocolatine-association-traduit-etudiants-etrangers-mots-sud-ouest"
|
||||
}
|
||||
],
|
||||
"recommendations": []
|
||||
"bookmarks": [
|
||||
{
|
||||
"article__excerpt": "When Twitter started it had so much promise to change the way we communicate. But now it has been ruined by the amount of garbage and hate we have to wade through. It’s like that polluted…",
|
||||
"favorite": false,
|
||||
"date_archived": null,
|
||||
"article__url": "https://venngage.com/blog/hashtags-are-worthless/",
|
||||
"date_added": "2016-08-25T12:05:00",
|
||||
"date_favorited": null,
|
||||
"article__title": "We Looked At 167,943 Tweets & Found Out Hashtags Are Worthless",
|
||||
"archive": false
|
||||
},
|
||||
{
|
||||
"article__title": "No title found",
|
||||
"article__url": "http://news.nationalgeographic.com/2016/02/160211-albatrosses-mothers-babies-animals-science/&sf20739758=1",
|
||||
"archive": false,
|
||||
"date_added": "2016-09-08T11:55:58+0200",
|
||||
"favorite": true
|
||||
},
|
||||
{
|
||||
"archive": 0,
|
||||
"date_added": "2016-09-08T11:55:58+0200",
|
||||
"favorite": 0,
|
||||
"article__title": "Bordeaux: Poche, chocolatine… Une association traduit aux étudiants étrangers les mots du Sud-Ouest",
|
||||
"article__url": "https://www.20minutes.fr/bordeaux/2120479-20170823-bordeaux-poche-chocolatine-association-traduit-etudiants-etrangers-mots-sud-ouest"
|
||||
}
|
||||
],
|
||||
"recommendations": []
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue