[PLUGIN][TagBasedFiltering] Expand to allow filtering by actor tags

This commit is contained in:
Hugo Sales 2021-12-05 17:50:15 +00:00
parent e29e1cc87c
commit 9f445632b2
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
9 changed files with 117 additions and 41 deletions

View File

@ -28,12 +28,14 @@ use App\Core\Controller;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Entity\Actor;
use App\Entity\ActorTag;
use App\Entity\ActorTagBlock;
use App\Entity\Language; use App\Entity\Language;
use App\Entity\Note; use App\Entity\Note;
use App\Entity\NoteTag; use App\Entity\NoteTag;
use App\Entity\NoteTagBlock; use App\Entity\NoteTagBlock;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\NotImplementedException;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use Component\Tag\Tag; use Component\Tag\Tag;
use Functional as F; use Functional as F;
@ -46,23 +48,24 @@ use Symfony\Component\HttpFoundation\Request;
class TagBasedFiltering extends Controller class TagBasedFiltering extends Controller
{ {
/** private function editBlocked(
* Edit the user's list of blocked note tags, with the option of adding the notes from the note $note_id, if given Request $request,
*/ ?int $id,
public function editBlockedNoteTags(Request $request, ?int $note_id) string $type_name,
{ callable $calculate_target,
$user = Common::ensureLoggedIn(); callable $calculate_blocks,
$note = !\is_null($note_id) ? Note::getById($note_id) : null; callable $calculate_tags,
$note_tag_blocks = NoteTagBlock::getByActorId($user->getId()); string $new_label,
$note_tags = !\is_null($note_id) ? NoteTag::getByNoteId($note_id) : []; string $existing_label,
$blockable_note_tags = F\reject( string $block_class,
$note_tags, ) {
fn (NoteTag $nt) => NoteTagBlock::checkBlocksNoteTag($nt, $note_tag_blocks), $user = Common::ensureLoggedIn();
); $target = $calculate_target();
$tag_blocks = $calculate_blocks($user);
$blockable_tags = $calculate_tags($tag_blocks);
$new_tags_form_definition = []; $new_tags_form_definition = [];
foreach ($blockable_tags as $nt) {
foreach ($blockable_note_tags as $nt) {
$canon = $nt->getCanonical(); $canon = $nt->getCanonical();
$new_tags_form_definition[] = ["{$canon}:new-tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']]; $new_tags_form_definition[] = ["{$canon}:new-tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']];
$new_tags_form_definition[] = ["{$canon}:use-canon", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]]; $new_tags_form_definition[] = ["{$canon}:use-canon", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]];
@ -70,7 +73,7 @@ class TagBasedFiltering extends Controller
} }
$existing_tags_form_definition = []; $existing_tags_form_definition = [];
foreach ($note_tag_blocks as $ntb) { foreach ($tag_blocks as $ntb) {
$canon = $ntb->getCanonical(); $canon = $ntb->getCanonical();
$existing_tags_form_definition[] = ["{$canon}:old-tag", TextType::class, ['data' => '#' . $ntb->getTag(), 'label' => ' ', 'disabled' => true]]; $existing_tags_form_definition[] = ["{$canon}:old-tag", TextType::class, ['data' => '#' . $ntb->getTag(), 'label' => ' ', 'disabled' => true]];
$existing_tags_form_definition[] = ["{$canon}:toggle-canon", SubmitType::class, ['label' => $ntb->getUseCanonical() ? _m('Set non-canonical') : _m('Set canonical')]]; $existing_tags_form_definition[] = ["{$canon}:toggle-canon", SubmitType::class, ['label' => $ntb->getUseCanonical() ? _m('Set non-canonical') : _m('Set canonical')]];
@ -78,7 +81,7 @@ class TagBasedFiltering extends Controller
} }
$new_tags_form = null; $new_tags_form = null;
if (!empty($new_tags_form_definition)) { if (!empty($new_tags_form_definition) && $user->getId() !== $target->getActorId()) {
$new_tags_form = Form::create($new_tags_form_definition); $new_tags_form = Form::create($new_tags_form_definition);
$new_tags_form->handleRequest($request); $new_tags_form->handleRequest($request);
if ($new_tags_form->isSubmitted() && $new_tags_form->isValid()) { if ($new_tags_form->isSubmitted() && $new_tags_form->isValid()) {
@ -89,11 +92,12 @@ class TagBasedFiltering extends Controller
/** @var SubmitButton $button */ /** @var SubmitButton $button */
$button = $new_tags_form->get($id); $button = $new_tags_form->get($id);
if ($button->isClicked()) { if ($button->isClicked()) {
Cache::delete(NoteTagBlock::cacheKey($user->getId())); Cache::delete($block_class::cacheKey($user->getId()));
Cache::delete(TagFilerPlugin::cacheKeys($user->getId())['note']); Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]);
$new_tag = Tag::ensureValid($data[$canon . ':new-tag']); $new_tag = Tag::ensureValid($data[$canon . ':new-tag']);
$canonical_tag = Tag::canonicalTag($new_tag, Language::getByNote($note)->getLocale()); $language = $target instanceof Note ? Language::getByNote($target)->getLocale() : $user->getActor()->getTopLanguage()->getLocale();
DB::persist(NoteTagBlock::create([ $canonical_tag = Tag::canonicalTag($new_tag, $language);
DB::persist($block_class::create([
'blocker' => $user->getId(), 'blocker' => $user->getId(),
'tag' => $new_tag, 'tag' => $new_tag,
'canonical' => $canonical_tag, 'canonical' => $canonical_tag,
@ -119,17 +123,17 @@ class TagBasedFiltering extends Controller
/** @var SubmitButton $button */ /** @var SubmitButton $button */
$button = $existing_tags_form->get($id); $button = $existing_tags_form->get($id);
if ($button->isClicked()) { if ($button->isClicked()) {
Cache::delete(NoteTagBlock::cacheKey($user->getId())); Cache::delete($block_class::cacheKey($user->getId()));
Cache::delete(TagFilerPlugin::cacheKeys($user->getId())['note']); Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]);
switch ($type) { switch ($type) {
case 'toggle-canon': case 'toggle-canon':
$ntb = DB::getReference('note_tag_block', ['blocker' => $user->getId(), 'canonical' => $canon]); $ntb = DB::getReference($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]);
$ntb->setUseCanonical(!$ntb->getUseCanonical()); $ntb->setUseCanonical(!$ntb->getUseCanonical());
DB::flush(); DB::flush();
throw new RedirectException; throw new RedirectException;
case 'remove': case 'remove':
$old_tag = $data[$canon . ':old-tag']; $old_tag = $data[$canon . ':old-tag'];
DB::removeBy('note_tag_block', ['blocker' => $user->getId(), 'canonical' => $canon]); DB::removeBy($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]);
throw new RedirectException; throw new RedirectException;
} }
} }
@ -140,14 +144,50 @@ class TagBasedFiltering extends Controller
return [ return [
'_template' => 'tag-based-filtering/edit-tags.html.twig', '_template' => 'tag-based-filtering/edit-tags.html.twig',
'note' => !\is_null($note_id) ? Note::getById($note_id) : null, $type_name => $target,
'new_tags_form' => $new_tags_form?->createView(), 'new_tags_form' => $new_tags_form?->createView(),
'existing_tags_form' => $existing_tags_form?->createView(), 'existing_tags_form' => $existing_tags_form?->createView(),
'new_label' => $new_label,
'existing_label' => $existing_label,
]; ];
} }
public function editBlockedActorTags(Request $request, ?int $note_id) /**
* Edit the user's list of blocked note tags, with the option of adding the notes from the note $note_id, if given
*/
public function editBlockedNoteTags(Request $request, ?int $note_id)
{ {
throw new NotImplementedException; return $this->editBlocked(
request: $request,
id: $note_id,
type_name: 'note',
calculate_target: fn () => !\is_null($note_id) ? Note::getById($note_id) : null,
calculate_blocks: fn ($user) => NoteTagBlock::getByActorId($user->getId()),
calculate_tags: fn ($tag_blocks) => F\reject(
!\is_null($note_id) ? NoteTag::getByNoteId($note_id) : [],
fn (NoteTag $nt) => NoteTagBlock::checkBlocksNoteTag($nt, $tag_blocks),
),
new_label: _m('Tags in the note above:'),
existing_label: _m('Tags you already blocked:'),
block_class: NoteTagBlock::class,
);
}
public function editBlockedActorTags(Request $request, ?int $actor_id)
{
return $this->editBlocked(
request: $request,
id: $actor_id,
type_name: 'actor',
calculate_target: fn () => !\is_null($actor_id) ? Actor::getById($actor_id) : null,
calculate_blocks: fn ($user) => ActorTagBlock::getByActorId($user->getId()),
calculate_tags: fn ($tag_blocks) => F\reject(
!\is_null($actor_id) ? ActorTag::getByActorId($actor_id) : [],
fn (ActorTag $nt) => ActorTagBlock::checkBlocksActorTag($nt, $tag_blocks),
),
new_label: _m('Tags of the account above:'),
existing_label: _m('Tags you already blocked:'),
block_class: ActorTagBlock::class,
);
} }
} }

View File

@ -48,13 +48,13 @@ class TagBasedFiltering extends Plugin
if (!\is_int($actor_id)) { if (!\is_int($actor_id)) {
$actor_id = $actor_id->getId(); $actor_id = $actor_id->getId();
} }
return ['note' => "filtered-tags-{$actor_id}"]; return ['note' => "blocked-note-tags-{$actor_id}", 'actor' => "blocked-actor-tags-{$actor_id}"];
} }
public function onAddRoute(RouteLoader $r) public function onAddRoute(RouteLoader $r)
{ {
$r->connect(id: self::NOTE_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-note-tags/{note_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedNoteTags']); $r->connect(id: self::NOTE_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-note-tags/{note_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedNoteTags']);
$r->connect(id: self::ACTOR_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-actor-tags/{note_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedActorTags']); $r->connect(id: self::ACTOR_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-actor-tags/{actor_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedActorTags']);
return Event::next; return Event::next;
} }
@ -69,7 +69,7 @@ class TagBasedFiltering extends Plugin
$actions[] = [ $actions[] = [
'title' => _m('Block people tags'), 'title' => _m('Block people tags'),
'classes' => '', 'classes' => '',
'url' => Router::url(self::ACTOR_TAG_FILTER_ROUTE, ['note_id' => $note->getId()]), 'url' => Router::url(self::ACTOR_TAG_FILTER_ROUTE, ['actor_id' => $note->getActor()->getId()]),
]; ];
} }

View File

@ -2,15 +2,22 @@
{% import '/cards/note/view.html.twig' as noteView %} {% import '/cards/note/view.html.twig' as noteView %}
{% block body %} {% block body %}
{% if new_tags_form is not null %} {% if note is defined or actor is defined %}
<div class="section-widget"> <div class="section-widget-padded">
{{ noteView.macro_note(note, {}) }} {% if note is defined %}
{{ noteView.macro_note(note, {}) }}
{% elseif actor is defined %}
{% include 'cards/profile/view.html.twig' with {'actor': actor} only %}
{% endif %}
</div> </div>
<p>{% trans %}Tags in the note above:{% endtrans %}</p> {% endif %}
{% if new_tags_form is not null %}
<p>{{ new_label }}</p>
{{ form(new_tags_form) }} {{ form(new_tags_form) }}
<hr>
{% endif %} {% endif %}
{% if existing_tags_form is not null %} {% if existing_tags_form is not null %}
<p>{% trans %}Tags you already blocked:{% endtrans %}</p> <p>{{ existing_label }}</p>
{{ form(existing_tags_form) }} {{ form(existing_tags_form) }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -263,6 +263,7 @@ class UserPanel extends Controller
$extra_step = function ($data, $extra_args) use ($user, $actor) { $extra_step = function ($data, $extra_args) use ($user, $actor) {
$user->setNicknameSanitizedAndCached($data['nickname'], $actor->getId()); $user->setNicknameSanitizedAndCached($data['nickname'], $actor->getId());
}; };
return Form::handle($form_definition, $request, $actor, $extra, $extra_step, [['self_tags' => $extra['self_tags']]]); return Form::handle($form_definition, $request, $actor, $extra, $extra_step, [['self_tags' => $extra['self_tags']]]);
} }

View File

@ -58,7 +58,7 @@ use Functional as F;
* @method static void persist(object $entity) // Tells the EntityManager to make an instance managed and persistent. * @method static void persist(object $entity) // Tells the EntityManager to make an instance managed and persistent.
* @method static bool contains(object $entity) // Determines whether an entity instance is managed in this EntityManager. * @method static bool contains(object $entity) // Determines whether an entity instance is managed in this EntityManager.
* @method static void flush() // Flushes the in-memory state of persisted objects to the database. * @method static void flush() // Flushes the in-memory state of persisted objects to the database.
* @method mixed wrapInTransaction(callable $func) // Executes a function in a transaction. Warning: suppresses exceptions * @method mixed wrapInTransaction(callable $func) // Executes a function in a transaction. Warning: suppresses exceptions
*/ */
class DB class DB
{ {
@ -263,9 +263,14 @@ class DB
} }
} }
public static function removeBy(string $table, array $criteria) public static function removeBy(string $table, array $criteria): void
{ {
self::remove(self::getReference($table, $criteria)); $class = self::$table_map[$table];
if (empty(array_intersect(self::getPKForClass($class), array_keys($criteria)))) {
self::remove(self::findOneBy($class, $criteria));
} else {
self::remove(self::getReference($table, $criteria));
}
} }
public static function count(string $table, array $criteria) public static function count(string $table, array $criteria)

View File

@ -270,6 +270,14 @@ class Actor extends Entity
return Cache::get('actor-fullname-id-' . $id, fn () => self::getById($id)->getFullname()); return Cache::get('actor-fullname-id-' . $id, fn () => self::getById($id)->getFullname());
} }
/**
* For consistency with Note
*/
public function getActorId(): int
{
return $this->getId();
}
/** /**
* Tags attributed to self, shortcut function for increased legibility * Tags attributed to self, shortcut function for increased legibility
* *

View File

@ -165,7 +165,7 @@ class ActorCircle extends Entity
'name' => 'actor_circle', 'name' => 'actor_circle',
'description' => 'a actor can have lists of actors, to separate their feed', 'description' => 'a actor can have lists of actors, to separate their feed',
'fields' => [ 'fields' => [
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'],
'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'not null' => true, 'description' => 'user making the tag'], 'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'not null' => true, 'description' => 'user making the tag'],
'tag' => ['type' => 'varchar', 'length' => 64, 'foreign key' => true, 'target' => 'ActorTag.canonical', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor tag'], // Join with ActorTag // // so, Doctrine doesn't like that the target is not unique, even though the pair is 'tag' => ['type' => 'varchar', 'length' => 64, 'foreign key' => true, 'target' => 'ActorTag.canonical', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor tag'], // Join with ActorTag // // so, Doctrine doesn't like that the target is not unique, even though the pair is
'description' => ['type' => 'text', 'description' => 'description of the people tag'], 'description' => ['type' => 'text', 'description' => 'description of the people tag'],

View File

@ -21,6 +21,7 @@ declare(strict_types = 1);
namespace App\Entity; namespace App\Entity;
use App\Core\Cache;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router\Router; use App\Core\Router\Router;
@ -109,6 +110,19 @@ class ActorTag extends Entity
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
// }}} Autocode // }}} Autocode
public static function cacheKey(int|Actor $actor_id)
{
if (!\is_int($actor_id)) {
$actor_id = $actor_id->getId();
}
return "actor-tags-{$actor_id}";
}
public static function getByActorId(int $actor_id): array
{
return Cache::getList(self::cacheKey($actor_id), fn () => DB::dql('select at from actor_tag at join actor a with a.id = at.tagger where a.id = :id', ['id' => $actor_id]));
}
public function getUrl(?Actor $actor = null): string public function getUrl(?Actor $actor = null): string
{ {
$params = ['tag' => $this->getCanonical()]; $params = ['tag' => $this->getCanonical()];

View File

@ -26,6 +26,7 @@ use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use Component\Tag\Tag; use Component\Tag\Tag;
use DateTimeInterface; use DateTimeInterface;
use Functional as F;
/** /**
* Entity for User's Note Tag block * Entity for User's Note Tag block