Added auto-tagger extension
This commit is contained in:
parent
ac80ca8443
commit
b4bde94516
7 changed files with 474 additions and 0 deletions
|
@ -8,6 +8,7 @@ abstract class Permissions
|
|||
|
||||
public const MANAGE_EXTENSION_LIST = "manage_extension_list";
|
||||
public const MANAGE_ALIAS_LIST = "manage_alias_list";
|
||||
public const MANAGE_AUTO_TAG = "manage_auto_tag";
|
||||
public const MASS_TAG_EDIT = "mass_tag_edit";
|
||||
|
||||
public const VIEW_IP = "view_ip"; # view IP addresses associated with things
|
||||
|
|
|
@ -77,6 +77,7 @@ new UserClass("base", null, [
|
|||
|
||||
Permissions::MANAGE_EXTENSION_LIST => false,
|
||||
Permissions::MANAGE_ALIAS_LIST => false,
|
||||
Permissions::MANAGE_AUTO_TAG => false,
|
||||
Permissions::MASS_TAG_EDIT => false,
|
||||
|
||||
Permissions::VIEW_IP => false, # view IP addresses associated with things
|
||||
|
@ -209,6 +210,7 @@ new UserClass("admin", "base", [
|
|||
Permissions::REPLACE_IMAGE => true,
|
||||
Permissions::MANAGE_EXTENSION_LIST => true,
|
||||
Permissions::MANAGE_ALIAS_LIST => true,
|
||||
Permissions::MANAGE_AUTO_TAG => true,
|
||||
Permissions::EDIT_IMAGE_TAG => true,
|
||||
Permissions::EDIT_IMAGE_SOURCE => true,
|
||||
Permissions::EDIT_IMAGE_OWNER => true,
|
||||
|
|
7
ext/auto_tagger/config.php
Normal file
7
ext/auto_tagger/config.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
abstract class AutoTaggerConfig
|
||||
{
|
||||
public const VERSION = "ext_auto_tagger_ver";
|
||||
public const ITEMS_PER_PAGE = "auto_tagger_items_per_page";
|
||||
}
|
12
ext/auto_tagger/info.php
Normal file
12
ext/auto_tagger/info.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class AutoTaggerInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "auto_tagger";
|
||||
|
||||
public $key = self::KEY;
|
||||
public $name = "Auto-Tagger";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Provides several automatic tagging functions";
|
||||
}
|
331
ext/auto_tagger/main.php
Normal file
331
ext/auto_tagger/main.php
Normal file
|
@ -0,0 +1,331 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
use MicroCRUD\ActionColumn;
|
||||
use MicroCRUD\TextColumn;
|
||||
use MicroCRUD\Table;
|
||||
|
||||
class AutoTaggerTable extends Table
|
||||
{
|
||||
public function __construct(\FFSPHP\PDO $db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
$this->table = "auto_tagger";
|
||||
$this->base_query = "SELECT * FROM auto_tag";
|
||||
$this->primary_key = "tag";
|
||||
$this->size = 100;
|
||||
$this->limit = 1000000;
|
||||
$this->set_columns([
|
||||
new TextColumn("tag", "Tag"),
|
||||
new TextColumn("additional_tags", "Additional Tags"),
|
||||
new ActionColumn("tag"),
|
||||
]);
|
||||
$this->order_by = ["tag"];
|
||||
$this->table_attrs = ["class" => "zebra"];
|
||||
}
|
||||
}
|
||||
|
||||
class AddAutoTagEvent extends Event
|
||||
{
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var string */
|
||||
public $additional_tags;
|
||||
|
||||
public function __construct(string $tag, string $additional_tags)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tag = trim($tag);
|
||||
$this->additional_tags = trim($additional_tags);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteAutoTagEvent extends Event
|
||||
{
|
||||
public $tag;
|
||||
|
||||
public function __construct(string $tag)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tag = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
class AddAutoTagException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
class AutoTagger extends Extension
|
||||
{
|
||||
/** @var AutoTaggerTheme */
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $config, $database, $page, $user;
|
||||
|
||||
if ($event->page_matches("auto_tag")) {
|
||||
if ($event->get_arg(0) == "add") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["c_tag"=>"string", "c_additional_tags"=>"string"]);
|
||||
try {
|
||||
send_event(new AddAutoTagEvent($input['c_tag'], $input['c_additional_tags']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("auto_tag/list"));
|
||||
} catch (AddAutoTagException $ex) {
|
||||
$this->theme->display_error(500, "Error adding auto-tag", $ex->getMessage());
|
||||
}
|
||||
}
|
||||
} elseif ($event->get_arg(0) == "remove") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$user->ensure_authed();
|
||||
$input = validate_input(["d_tag"=>"string"]);
|
||||
send_event(new DeleteAutoTagEvent($input['d_tag']));
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("auto_tag/list"));
|
||||
}
|
||||
} elseif ($event->get_arg(0) == "list") {
|
||||
$t = new AutoTaggerTable($database->raw_db());
|
||||
$t->token = $user->get_auth_token();
|
||||
$t->inputs = $_GET;
|
||||
$t->size = $config->get_int(AutoTaggerConfig::ITEMS_PER_PAGE, 30);
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$t->create_url = make_link("auto_tag/add");
|
||||
$t->delete_url = make_link("auto_tag/remove");
|
||||
}
|
||||
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
|
||||
} elseif ($event->get_arg(0) == "export") {
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_type("text/csv");
|
||||
$page->set_filename("auto_tag.csv");
|
||||
$page->set_data($this->get_auto_tag_csv($database));
|
||||
} elseif ($event->get_arg(0) == "import") {
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
if (count($_FILES) > 0) {
|
||||
$tmp = $_FILES['auto_tag_file']['tmp_name'];
|
||||
$contents = file_get_contents($tmp);
|
||||
$count = $this->add_auto_tag_csv($database, $contents);
|
||||
log_info(AutoTaggerInfo::KEY, "Imported $count auto-tag definitions from file from file", "Imported $count auto-tag definitions");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("auto_tag/list"));
|
||||
} else {
|
||||
$this->theme->display_error(400, "No File Specified", "You have to upload a file");
|
||||
}
|
||||
} else {
|
||||
$this->theme->display_error(401, "Admins Only", "Only admins can edit the auto-tag list");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
if ($event->parent=="tags") {
|
||||
$event->add_nav_link("auto_tag", new Link('auto_tag/list'), "Auto-Tag", NavLink::is_active(["auto_tag"]));
|
||||
}
|
||||
}
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
// Create the database tables
|
||||
if ($this->get_version(AutoTaggerConfig::VERSION) < 1) {
|
||||
$database->create_table("auto_tag", "
|
||||
tag VARCHAR(128) NOT NULL PRIMARY KEY,
|
||||
additional_tags VARCHAR(2000) NOT NULL
|
||||
");
|
||||
|
||||
if ($database->get_driver_name() == DatabaseDriver::PGSQL) {
|
||||
$database->execute('CREATE INDEX auto_tag_lower_tag_idx ON auto_tag ((lower(tag)))');
|
||||
}
|
||||
$this->set_version(AutoTaggerConfig::VERSION, 1);
|
||||
|
||||
log_info(AutoTaggerInfo::KEY, "extension installed");
|
||||
}
|
||||
}
|
||||
|
||||
public function onTagSet(TagSetEvent $event)
|
||||
{
|
||||
$results = $this->apply_auto_tags($event->tags);
|
||||
if (!empty($results)) {
|
||||
$event->tags = $results;
|
||||
}
|
||||
}
|
||||
|
||||
public function onAddAutoTag(AddAutoTagEvent $event)
|
||||
{
|
||||
global $page;
|
||||
$this->add_auto_tag($event->tag, $event->additional_tags);
|
||||
$page->flash("Added Auto-Tag");
|
||||
}
|
||||
|
||||
public function onDeleteAutoTag(DeleteAutoTagEvent $event)
|
||||
{
|
||||
$this->remove_auto_tag($event->tag);
|
||||
}
|
||||
|
||||
public function onUserBlockBuilding(UserBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
if ($user->can(Permissions::MANAGE_AUTO_TAG)) {
|
||||
$event->add_link("Auto-Tag Editor", make_link("auto_tag/list"));
|
||||
}
|
||||
}
|
||||
|
||||
private function get_auto_tag_csv(Database $database): string
|
||||
{
|
||||
$csv = "";
|
||||
$pairs = $database->get_pairs("SELECT tag, additional_tags FROM auto_tag ORDER BY tag");
|
||||
foreach ($pairs as $old => $new) {
|
||||
$csv .= "\"$old\",\"$new\"\n";
|
||||
}
|
||||
return $csv;
|
||||
}
|
||||
|
||||
private function add_auto_tag_csv(Database $database, string $csv): int
|
||||
{
|
||||
$csv = str_replace("\r", "\n", $csv);
|
||||
$i = 0;
|
||||
foreach (explode("\n", $csv) as $line) {
|
||||
$parts = str_getcsv($line);
|
||||
if (count($parts) == 2) {
|
||||
try {
|
||||
send_event(new AddAutoTagEvent($parts[0], $parts[1]));
|
||||
$i++;
|
||||
} catch (AddAutoTagException $ex) {
|
||||
$this->theme->display_error(500, "Error adding auto-tags", $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return $i;
|
||||
}
|
||||
|
||||
private function add_auto_tag(string $tag, string $additional_tags)
|
||||
{
|
||||
global $database;
|
||||
if ($database->exists("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag])) {
|
||||
throw new AutoTaggerException("Auto-Tag is already set for that tag");
|
||||
} else {
|
||||
$tag = Tag::sanitize($tag);
|
||||
$additional_tags = Tag::explode($additional_tags);
|
||||
|
||||
$database->execute(
|
||||
"INSERT INTO auto_tag(tag, additional_tags) VALUES(:tag, :additional_tags)",
|
||||
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
|
||||
);
|
||||
|
||||
log_info(
|
||||
AutoTaggerInfo::KEY,
|
||||
"Added auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}"
|
||||
);
|
||||
|
||||
// Now we apply it to existing items
|
||||
$this->apply_new_auto_tag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
private function update_auto_tag(string $tag, string $additional_tags): bool
|
||||
{
|
||||
global $database;
|
||||
$result = $database->get_row("SELECT * FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag"=>$tag]);
|
||||
|
||||
if ($result===null) {
|
||||
throw new AutoTaggerException("Auto-tag not set for $tag, can't update");
|
||||
} else {
|
||||
$additional_tags = Tag::explode($additional_tags);
|
||||
$current_additional_tags = Tag::explode($result["additional_tags"]);
|
||||
|
||||
if (!Tag::compare($additional_tags, $current_additional_tags)) {
|
||||
$database->execute(
|
||||
"UPDATE auto_tag SET additional_tags = :additional_tags WHERE LOWER(tag)=LOWER(:tag)",
|
||||
["tag"=>$tag, "additional_tags"=>Tag::implode($additional_tags)]
|
||||
);
|
||||
|
||||
log_info(
|
||||
AutoTaggerInfo::KEY,
|
||||
"Updated auto-tag for {$tag} -> {".implode(" ", $additional_tags)."}",
|
||||
"Updated Auto-Tag"
|
||||
);
|
||||
|
||||
// Now we apply it to existing items
|
||||
$this->apply_new_auto_tag($tag);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function apply_new_auto_tag(string $tag)
|
||||
{
|
||||
global $database;
|
||||
$tag_id = $database->get_one("SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)", ["tag"=>$tag]);
|
||||
if (!empty($tag_id)) {
|
||||
$image_ids = $database->get_col_iterable("SELECT image_id FROM image_tags WHERE tag_id = :tag_id", ["tag_id"=>$tag_id]);
|
||||
foreach ($image_ids as $image_id) {
|
||||
$image = Image::by_id($image_id);
|
||||
$event = new TagSetEvent($image, $image->get_tag_array());
|
||||
send_event($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function remove_auto_tag(String $tag)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$database->execute("DELETE FROM auto_tag WHERE LOWER(tag)=LOWER(:tag)", ["tag" => $tag]);
|
||||
}
|
||||
|
||||
/**
|
||||
* #param string[] $tags_mixed
|
||||
*/
|
||||
private function apply_auto_tags(array $tags_mixed): ?array
|
||||
{
|
||||
global $database;
|
||||
|
||||
while (true) {
|
||||
$new_tags = [];
|
||||
foreach ($tags_mixed as $tag) {
|
||||
$additional_tags = $database->get_one(
|
||||
"SELECT additional_tags FROM auto_tag WHERE LOWER(tag) = LOWER(:input)",
|
||||
["input" => $tag]
|
||||
);
|
||||
|
||||
if (!empty($additional_tags)) {
|
||||
$additional_tags = explode(" ", $additional_tags);
|
||||
$new_tags = array_merge(
|
||||
$new_tags,
|
||||
array_udiff($additional_tags, $tags_mixed, 'strcasecmp')
|
||||
);
|
||||
}
|
||||
}
|
||||
if (empty($new_tags)) {
|
||||
break;
|
||||
}
|
||||
$tags_mixed = array_merge($tags_mixed, $new_tags);
|
||||
}
|
||||
|
||||
$results = array_intersect_key(
|
||||
$tags_mixed,
|
||||
array_unique(array_map('strtolower', $tags_mixed))
|
||||
);
|
||||
|
||||
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the priority for this extension.
|
||||
*
|
||||
*/
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 30;
|
||||
}
|
||||
}
|
85
ext/auto_tagger/test.php
Normal file
85
ext/auto_tagger/test.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php declare(strict_types=1);
|
||||
class AutoTaggerTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testAutoTagList()
|
||||
{
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_response(200);
|
||||
$this->assert_title("Alias List");
|
||||
}
|
||||
|
||||
public function testAliasListReadOnly()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("Add");
|
||||
|
||||
$this->log_out();
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("Add");
|
||||
}
|
||||
|
||||
public function testAliasOneToOne()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_no_text("test1");
|
||||
|
||||
send_event(new AddAliasEvent("test1", "test2"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_text("test1");
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_text('"test1","test2"');
|
||||
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
|
||||
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
|
||||
$this->assert_title("Image $image_id: test2");
|
||||
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
|
||||
$this->assert_response(302);
|
||||
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works
|
||||
$this->assert_response(302);
|
||||
$this->delete_image($image_id);
|
||||
|
||||
send_event(new DeleteAliasEvent("test1"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("test1");
|
||||
}
|
||||
|
||||
public function testAliasOneToMany()
|
||||
{
|
||||
$this->log_in_as_admin();
|
||||
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_no_text("multi");
|
||||
|
||||
send_event(new AddAliasEvent("onetag", "multi tag"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_text("multi");
|
||||
$this->assert_text("tag");
|
||||
$this->get_page("alias/export/aliases.csv");
|
||||
$this->assert_text('"onetag","multi tag"');
|
||||
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag");
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
|
||||
$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
|
||||
$this->assert_title("multi tag");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->get_page("post/list/multi/1");
|
||||
$this->assert_title("multi");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->get_page("post/list/multi tag/1");
|
||||
$this->assert_title("multi tag");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->delete_image($image_id_1);
|
||||
$this->delete_image($image_id_2);
|
||||
|
||||
send_event(new DeleteAliasEvent("onetag"));
|
||||
$this->get_page('alias/list');
|
||||
$this->assert_title("Alias List");
|
||||
$this->assert_no_text("test1");
|
||||
}
|
||||
}
|
36
ext/auto_tagger/theme.php
Normal file
36
ext/auto_tagger/theme.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
class AutoTaggerTheme extends Themelet
|
||||
{
|
||||
/**
|
||||
* Show a page of auto-tag definitions.
|
||||
*
|
||||
* Note: $can_manage = whether things like "add new alias" should be shown
|
||||
*/
|
||||
public function display_auto_tagtable($table, $paginator): void
|
||||
{
|
||||
global $page, $user;
|
||||
|
||||
$can_manage = $user->can(Permissions::MANAGE_AUTO_TAG);
|
||||
$html = "
|
||||
$table
|
||||
$paginator
|
||||
<p><a href='".make_link("auto_tag/export/auto_tag.csv")."' download='auto_tag.csv'>Download as CSV</a></p>
|
||||
";
|
||||
|
||||
$bulk_html = "
|
||||
".make_form(make_link("auto_tag/import"), 'post', true)."
|
||||
<input type='file' name='auto_tag_file'>
|
||||
<input type='submit' value='Upload List'>
|
||||
</form>
|
||||
";
|
||||
|
||||
$page->set_title("Auto-Tag List");
|
||||
$page->set_heading("Auto-Tag List");
|
||||
$page->add_block(new NavBlock());
|
||||
$page->add_block(new Block("Auto-Tag", $html));
|
||||
if ($can_manage) {
|
||||
$page->add_block(new Block("Bulk Upload", $bulk_html, "main", 51));
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue