639 lines
23 KiB
PHP
Executable File
639 lines
23 KiB
PHP
Executable File
<?php
|
|
foreach (glob(dirname(__FILE__).'/Tag/*.php') as $trait) require $trait;
|
|
|
|
class Tag extends Rails\ActiveRecord\Base
|
|
{
|
|
use TagTypeMethods, TagApiMethods, Tag\CacheMethods;
|
|
|
|
use Moebooru\Versioning\VersioningTrait;
|
|
|
|
public $type_name;
|
|
|
|
static public function init_versioning($v)
|
|
{
|
|
$v->versioned_attributes([
|
|
'tag_type',
|
|
'is_ambiguous' => ['default' => false]
|
|
])
|
|
->versioning_group_by(['action' => 'edit']);
|
|
}
|
|
|
|
static public function find_or_create_by_name($name)
|
|
{
|
|
# Reserve ` as a field separator for tag/summary.
|
|
$name = str_replace(array(' ', '`'), array('_', ''), ltrim((strtolower($name)), '-~'));
|
|
|
|
$ambiguous = false;
|
|
$tag_type = CONFIG()->tag_types['General'];
|
|
|
|
if (strpos($name, 'ambiguous:') === 0) {
|
|
$name = str_replace('ambiguous:', '', $name);
|
|
$ambiguous = true;
|
|
}
|
|
|
|
if (is_int(strpos($name, ':'))) {
|
|
list($type, $tag_name) = explode(':', $name);
|
|
|
|
if (isset(CONFIG()->tag_types[$type])) {
|
|
$tag_type = CONFIG()->tag_types[$type];
|
|
$name = $tag_name;
|
|
}
|
|
}
|
|
|
|
$tag = Tag::where(['name' => $name])->first();
|
|
|
|
if ($tag) {
|
|
if ($tag_type)
|
|
$tag->updateAttribute('tag_type', $tag_type);
|
|
if ($ambiguous)
|
|
$tag->updateAttribute('is_ambiguous', $ambiguous);
|
|
|
|
return $tag;
|
|
} else {
|
|
return self::create(array('name' => $name, 'tag_type' => $tag_type, 'is_ambiguous' => $ambiguous));
|
|
}
|
|
}
|
|
|
|
/* } Parse methods { */
|
|
static public function scan_query($query)
|
|
{
|
|
return array_unique(array_filter(explode(' ', strtolower($query))));
|
|
}
|
|
|
|
static public function scan_tags($tags)
|
|
{
|
|
return array_unique(explode(' ', str_replace(array('%', ','), '', strtolower($tags))));
|
|
}
|
|
|
|
static public function select_ambiguous($tags)
|
|
{
|
|
if ($tags)
|
|
return self::connection()->selectValues("SELECT name FROM tags WHERE name IN (?) AND is_ambiguous = TRUE ORDER BY name", $tags);
|
|
}
|
|
|
|
static public function find_suggestions($query)
|
|
{
|
|
if (substr_count($query, '_') === 1)
|
|
# Contains only one underscore
|
|
$search_for = implode('_', array_reverse(explode('_', $query)));
|
|
else
|
|
$search_for = "%" . $query . "%";
|
|
|
|
$tags = Tag::where("name LIKE ? AND name <> ?", $search_for, $query)->order("post_count DESC")->limit(6)->select("name")->pluck('name');
|
|
sort($tags);
|
|
return $tags;
|
|
}
|
|
|
|
static public function parse_cast($x, $type)
|
|
{
|
|
if ($type == 'int')
|
|
return (int)$x;
|
|
elseif ($type == 'float')
|
|
return (float)$x;
|
|
elseif ($type == 'date') {
|
|
return $x;
|
|
}
|
|
}
|
|
|
|
static public function parse_helper($range, $type = 'int')
|
|
{
|
|
# "1", "0.5", "5.", ".5":
|
|
# (-?(\d+(\.\d*)?|\d*\.\d+))
|
|
// case range
|
|
if (preg_match('/^(.+?)\.\.(.+)/', $range, $m))
|
|
return array('between', self::parse_cast($m[1], $type), self::parse_cast($m[2], $type));
|
|
|
|
elseif (preg_match('/^<=(.+)|^\.\.(.+)/', $range, $m))
|
|
return array('lte', self::parse_cast($m[1], $type));
|
|
|
|
elseif (preg_match('/^<(.+)/', $range, $m))
|
|
return array('lt', self::parse_cast($m[1], $type));
|
|
|
|
elseif (preg_match('/^>=(.+)|^(.+)\.\.$/', $range, $m))
|
|
return array('gte', self::parse_cast($m[1], $type));
|
|
|
|
elseif (preg_match('/^>(.+)/', $range, $m))
|
|
return array('gt', self::parse_cast($m[1], $type));
|
|
|
|
elseif (preg_match('/^(.+?)|(.+)/', $range, $m)) {
|
|
$items = explode(',', $range);
|
|
foreach ($items as &$val) {
|
|
$val = self::parse_cast($val, $type);
|
|
unset($val);
|
|
}
|
|
return array('in', $items);
|
|
|
|
} else
|
|
return array('eq', self::parse_cast($range, $type));
|
|
}
|
|
|
|
static public function parse_query($query, $options = array())
|
|
{
|
|
$q = array('include' => array(), 'exclude' => array(), 'related' => array());
|
|
|
|
foreach (self::scan_query($query) as $token) {
|
|
if (preg_match('/^([qse])$/', $token, $m)) {
|
|
$q['rating'] = $m[1];
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^(unlocked|deleted|ext|user|sub|vote|-vote|fav|md5|-rating|rating|width|height|mpixels|score|source|id|date|pool|-pool|parent|order|change|holds|pending|shown|limit):(.+)$/', $token, $m))
|
|
{
|
|
if ($m[1] == "user")
|
|
$q['user'] = $m[2];
|
|
elseif ($m[1] == "vote") {
|
|
list($vote, $user) = explode(':', $m[2]);
|
|
if ($user = User::where(['name' => $user])->first())
|
|
$user_id = $user->id;
|
|
else
|
|
$user_id = null;
|
|
$q['vote'] = array(self::parse_helper($vote), $user_id);
|
|
} elseif ($m[1] == "-vote") {
|
|
|
|
if ($user = User::where(['name' => $m[2]])->first())
|
|
$user_id = $user->id;
|
|
else
|
|
$user_id = null;
|
|
$q['vote_negated'] = $user_id;
|
|
// $q['vote_negated'] = User.find_by_name_nocase($m[2]).id rescue nil
|
|
if (!$q['vote_negated'])
|
|
$q['error'] = "no user named ".$m[2];
|
|
} elseif ($m[1] == "fav")
|
|
$q['fav'] = $m[2];
|
|
elseif ($m[1] == "sub")
|
|
$q['subscriptions'] = $m[2];
|
|
elseif ($m[1] == "md5")
|
|
$q['md5'] = $m[2];
|
|
elseif ($m[1] == "-rating")
|
|
$q['rating_negated'] = $m[2];
|
|
elseif ($m[1] == "rating")
|
|
$q['rating'] = $m[2];
|
|
elseif ($m[1] == "id")
|
|
$q['post_id'] = self::parse_helper($m[2]);
|
|
elseif ($m[1] == "width")
|
|
$q['width'] = self::parse_helper($m[2]);
|
|
elseif ($m[1] == "height")
|
|
$q['height'] = self::parse_helper($m[2]);
|
|
elseif ($m[1] == "mpixels")
|
|
$q['mpixels'] = self::parse_helper($m[2], 'float');
|
|
elseif ($m[1] == "score")
|
|
$q['score'] = self::parse_helper($m[2]);
|
|
elseif ($m[1] == "source")
|
|
$q['source'] = $m[2].'%';
|
|
elseif ($m[1] == "date")
|
|
$q['date'] = self::parse_helper($m[2], 'date');
|
|
elseif ($m[1] == "pool") {
|
|
$q['pool'] = $m[2];
|
|
if (preg_match('/^(\d+)$/', $q['pool']))
|
|
$q['pool'] = (int)$q['pool'];
|
|
} elseif ($m[1] == "-pool") {
|
|
$pool = $m[2];
|
|
|
|
if (preg_match('/^(\d+)$/', $pool))
|
|
$pool = (int)$pool;
|
|
|
|
$q['exclude_pools'][] = $pool;
|
|
} elseif ($m[1] == "parent")
|
|
$q['parent_id'] = $m[2] == "none" ? false : (int)$m[2];
|
|
elseif ($m[1] == "order")
|
|
$q['order'] = $m[2];
|
|
elseif ($m[1] == "unlocked")
|
|
$m[2] == "rating" && $q['unlocked_rating'] = true;
|
|
elseif ($m[1] == "deleted") {
|
|
# This naming is slightly odd, to retain API compatibility with Danbooru's "deleted:true"
|
|
# search flag.
|
|
if ($m[2] == "true")
|
|
$q['show_deleted_only'] = true;
|
|
elseif ($m[2] == "all")
|
|
$q['show_deleted_only'] = false; # all posts, deleted or not
|
|
} elseif ($m[1] == "ext")
|
|
$q['ext'] = $m[2];
|
|
elseif ($m[1] == "change")
|
|
$q['change'] = self::parse_helper($m[2]);
|
|
elseif ($m[1] == "shown")
|
|
$q['shown_in_index'] = ($m[2] == "true");
|
|
elseif ($m[1] == "holds") {
|
|
if ($m[2] == "true" or $m[2] == "only")
|
|
$q['show_holds'] = 'only';
|
|
elseif ($m[2] == "all")
|
|
$q['show_holds'] = 'yes'; # all posts, held or not
|
|
elseif ($m[2] == "false")
|
|
$q['show_holds'] = 'hide';
|
|
} elseif ($m[1] == "pending") {
|
|
if ($m[2] == "true" or $m[2] == "only")
|
|
$q['show_pending'] = 'only';
|
|
elseif ($m[2] == "all")
|
|
$q['show_pending'] = 'yes'; # all posts, pending or not
|
|
elseif ($m[2] == "false")
|
|
$q['show_pending'] = 'hide';
|
|
} elseif ($m[1] == "limit")
|
|
$q['limit'] = $m[2];
|
|
} elseif (substr($token, 0, 1) == '-' && strlen($token) > 1) {
|
|
$q['exclude'][] = substr($token, 1);
|
|
} elseif (substr($token, 0, 1) == '~' && strlen($token) > 1) {
|
|
$q['include'][] = substr($token, 1);
|
|
} elseif (is_int(strpos($token, '*'))) {
|
|
$matches = Tag::where("name LIKE ?", str_replace('*', '%', $token))->select("name, post_count")->limit(25)->order("post_count DESC")->pluck('name');
|
|
!$matches && $matches = array('~no_matches~');
|
|
$q['include'] = array_merge($q['include'], $matches);
|
|
} else
|
|
$q['related'][] = $token;
|
|
}
|
|
|
|
if (empty($options['skip_aliasing'])) {
|
|
$q['exclude'] && $q['exclude'] = TagAlias::to_aliased($q['exclude']);
|
|
$q['include'] && $q['include'] = TagAlias::to_aliased($q['include']);
|
|
$q['related'] && $q['related'] = TagAlias::to_aliased($q['related']);
|
|
}
|
|
|
|
return $q;
|
|
}
|
|
/* } { */
|
|
|
|
static public function compact_tags($tags, $max_len)
|
|
{
|
|
if (strlen($tags) < $max_len)
|
|
return $tags;
|
|
|
|
$split_tags = explode(' ', $tags);
|
|
|
|
# Put long tags first, so we don't remove every tag because of one very long one.
|
|
usort($split_tags, function($a, $b) {
|
|
$a_len = strlen($a);
|
|
$b_len = strlen($b);
|
|
if ($a_len == $b_len)
|
|
return 0;
|
|
return $a_len > $b_len ? 1 : -1;
|
|
});
|
|
|
|
# Tag types that we're allowed to remove:
|
|
$length = strlen($tags);
|
|
foreach (array_keys($split_tags) as $i) {
|
|
$length -= strlen($split_tags[$i]) + 1;
|
|
$split_tags[$i] = null;
|
|
if ($length <= $max_len)
|
|
break;
|
|
}
|
|
|
|
sort($split_tags);
|
|
return trim(implode(' ', $split_tags));
|
|
}
|
|
|
|
static public function count_by_period($start, $stop, array $options = array())
|
|
{
|
|
$options['limit'] = !isset($options['limit']) || !ctype_digit((string)$options['limit']) ? 50 : (int)$options['limit'];
|
|
!isset($options['exclude_types']) && $options['exclude_types'] = array();
|
|
$sql = 'SELECT
|
|
(SELECT name FROM tags WHERE id = pt.tag_id) AS name,
|
|
COUNT(pt.tag_id) AS post_count
|
|
FROM posts p, posts_tags pt, tags t
|
|
WHERE p.created_at BETWEEN ? AND ? AND
|
|
p.id = pt.post_id AND
|
|
pt.tag_id = t.id AND
|
|
t.tag_type IN (?)
|
|
GROUP BY pt.tag_id
|
|
ORDER BY post_count DESC
|
|
LIMIT '.$options['limit'];
|
|
|
|
$tag_types_to_show = array_diff(Tag::tag_type_indexes(), $options['exclude_types']);
|
|
|
|
$counts = self::connection()->select($sql, $start, $stop, $tag_types_to_show);
|
|
return $counts;
|
|
}
|
|
|
|
static public function type_name_helper($tag_name) # :nodoc:
|
|
{
|
|
$tag = Tag::where("name = ?", $tag_name)->select("tag_type")->first();
|
|
|
|
if (!$tag)
|
|
return "general";
|
|
else
|
|
return self::_type_map($tag->tag_type);
|
|
}
|
|
|
|
# Find the tag type name of a tag.
|
|
#
|
|
# === Parameters
|
|
# * :tag_name<String>:: The tag name to search for
|
|
static public function type_name($tag_name)
|
|
{
|
|
return Rails::cache()->fetch(['tag_type' => $tag_name], ['expires_in' => '1 day'], function() use ($tag_name) {
|
|
return self::type_name_helper(str_replace(' ', '_', $tag_name));
|
|
});
|
|
}
|
|
|
|
static public function tag_type_indexes()
|
|
{
|
|
$all = array_unique(array_values(CONFIG()->tag_types));
|
|
sort($all);
|
|
return $all;
|
|
}
|
|
|
|
static public function tag_list_order($tag_type)
|
|
{
|
|
switch ($tag_type) {
|
|
case "artist" : return 0;
|
|
case "circle" : return 1;
|
|
case "copyright": return 2;
|
|
case "character": return 3;
|
|
case "general" : return 5;
|
|
case "faults" : return 6;
|
|
default : return 4;
|
|
}
|
|
}
|
|
|
|
static public function type_name_from_value($type_value)
|
|
{
|
|
$type = array_search($type_value, CONFIG()->tag_types);
|
|
if ($type)
|
|
return strtolower($type);
|
|
}
|
|
|
|
static public function batch_get_tag_types_for_posts($posts)
|
|
{
|
|
$tags = array();
|
|
foreach ($posts as $post) {
|
|
$tags = array_merge($tags, $post->tags());
|
|
}
|
|
return self::batch_get_tag_types($tags);
|
|
}
|
|
|
|
# Get all tag types for the given tags.
|
|
static public function batch_get_tag_types($post_tags)
|
|
{
|
|
$post_tags_key = [];
|
|
|
|
foreach ($post_tags as $t) {
|
|
$post_tags_key[] = ['tag_type' => $t];
|
|
}
|
|
# Without this, the following splat will eat the last argument because
|
|
# it'll be considered an option instead of key (being a hash).
|
|
$post_tags_key[] = [];
|
|
|
|
$results = [];
|
|
$cached = call_user_func_array([Rails::cache(), 'readMulti'], $post_tags_key);
|
|
foreach ($cached as $cache_key => $value) {
|
|
# The if cache_key is required since there's small chance read_multi
|
|
# returning nil key on certain key names.
|
|
// if ($cache_key) {
|
|
$results[substr($cache_key, 9)] = $value;
|
|
// }
|
|
}
|
|
foreach( array_diff( $post_tags, array_keys($results) ) as $tag ) {
|
|
$results[$tag] = self::type_name($tag);
|
|
}
|
|
return $results;
|
|
}
|
|
|
|
# Returns tags (with type specified by input) related by input tag
|
|
# In array of hashes.
|
|
# Hash in format { 'name' => tag_name, 'post_count' => tag_post_count }
|
|
static public function calculate_related_by_type($tag, $type, $limit = 25)
|
|
{
|
|
return Rails::cache()->fetch(['category' => 'reltags_by_type', 'type' => $type, 'tag' => $tag], ['expires_in' => '1 hour'], function() use ($tag, $type, $limit) {
|
|
|
|
$sql = "
|
|
SELECT tags.name, tags.post_count
|
|
FROM tags
|
|
JOIN posts_tags pt ON tags.id = pt.tag_id
|
|
JOIN posts ON pt.post_id = posts.id
|
|
INNER JOIN posts_tags pti ON posts.id = pti.post_id JOIN tags ti ON pti.tag_id = ti.id
|
|
WHERE
|
|
posts.status != 'deleted'
|
|
AND ti.name IN (?)
|
|
AND tags.tag_type = ?
|
|
GROUP BY tags.name
|
|
LIMIT " . (int)$limit;
|
|
|
|
$rows = self::connection()->select($sql, $tag, $type);
|
|
$reduced = [];
|
|
foreach ($rows as $row) {
|
|
$reduced[] = ['name' => $row['name'], 'post_count' => $row['post_count']];
|
|
}
|
|
return $reduced;
|
|
});
|
|
}
|
|
|
|
static public function find_related($tags)
|
|
{
|
|
if (is_array($tags)) {
|
|
if (count($tags) == 1)
|
|
# Replace tags array into its first element
|
|
# to be searched again later below.
|
|
$tags = current($tags);
|
|
else
|
|
return self::calculate_related($tags);
|
|
}
|
|
|
|
if ($tags) {
|
|
$t = self::where(['name' => $tags])->first();
|
|
if ($t)
|
|
return $t->related();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
// static public function find_related($tags)
|
|
// {
|
|
// if (is_array($tags) && count($tags) > 1)
|
|
// return self::calculate_related($tags);
|
|
// elseif ($tags = current($tags)) {
|
|
// $t = Tag::find_by_name($tags);
|
|
// if ($t)
|
|
// return $t->related();
|
|
// }
|
|
// return array();
|
|
// }
|
|
|
|
static public function calculate_related($tags)
|
|
{
|
|
if (!$tags)
|
|
return array();
|
|
|
|
!is_array($tags) && $tags = array($tags);
|
|
|
|
return Rails::cache()->fetch(['category' => 'reltags', 'tags' => implode(',', $tags)], ['expires_in' => '1 hour'], function() use ($tags) {
|
|
$from = array("posts_tags pt0");
|
|
$cond = array("pt0.post_id = pt1.post_id");
|
|
$sql = "SELECT ";
|
|
|
|
# Ignore deleted posts in pt0, so the count excludes them.
|
|
$cond[] = "(SELECT TRUE FROM posts p0 WHERE p0.id = pt0.post_id AND p0.status <> 'deleted')";
|
|
|
|
foreach (range(1, count($tags)) as $i)
|
|
$from[] = "posts_tags pt" . $i;
|
|
if (count($tags) > 1) {
|
|
foreach (range(2, count($tags)) as $i)
|
|
$cond[] = "pt1.post_id = pt".$i.".post_id";
|
|
}
|
|
foreach (range(1, count($tags)) as $i)
|
|
$cond[] = "pt".$i.".tag_id = (SELECT id FROM tags WHERE name = ?)";
|
|
|
|
$sql .= "(SELECT name FROM tags WHERE id = pt0.tag_id) AS tag, COUNT(*) AS tag_count";
|
|
$sql .= " FROM " . implode(', ', $from);
|
|
$sql .= " WHERE " . implode(' AND ', $cond);
|
|
$sql .= " GROUP BY pt0.tag_id";
|
|
$sql .= " ORDER BY tag_count DESC LIMIT 25";
|
|
array_unshift($tags, $sql);
|
|
$tags = self::connection()->query($tags);
|
|
|
|
if (!$tags)
|
|
return array();
|
|
|
|
$calc = array();
|
|
foreach ($tags as $tag)
|
|
$calc[] = array($tag['tag'], $tag['tag_count']);
|
|
|
|
return $calc;
|
|
});
|
|
}
|
|
|
|
# Return the cache version of the summary.
|
|
static public function get_summary_version()
|
|
{
|
|
return Rails::cache()->fetch('$tag_version', function() { return 0; });
|
|
}
|
|
|
|
# Create a compact list of all active tags, sorted by post_count.
|
|
#
|
|
# "1`tagme` 2`fixme` 3`fixed`alias` "
|
|
#
|
|
# Each tag is bounded by backticks, so "`tagme`" can be used to match a whole tag.
|
|
#
|
|
# This is returned as a preencoded JSON string, so the entire block can be cached.
|
|
static public function get_json_summary()
|
|
{
|
|
$summary_version = self::get_summary_version();
|
|
$key = 'tag_summary/' . $summary_version;
|
|
|
|
$data = Rails::cache()->fetch($key, ['expires_in' => '1 hour'], function() {
|
|
$data = Tag::get_json_summary_no_cache();
|
|
return json_encode($data);
|
|
});
|
|
|
|
return $data;
|
|
}
|
|
|
|
static public function get_json_summary_no_cache()
|
|
{
|
|
$version = Tag::get_summary_version();
|
|
|
|
$tags = self::connection()->select("SELECT
|
|
t.id, t.name, t.tag_type, ta.name AS alias
|
|
FROM tags t LEFT JOIN tag_aliases ta ON (t.id = ta.alias_id AND NOT ta.is_pending)
|
|
WHERE t.post_count > 0
|
|
ORDER BY t.id, ta.name");
|
|
|
|
$tags_with_type = array();
|
|
$current_tag = "";
|
|
$last_tag_id = null;
|
|
foreach($tags as $tag) {
|
|
$id = $tag["id"];
|
|
if ($id != $last_tag_id) {
|
|
if (!empty($last_tag_id))
|
|
$tags_with_type[] = $current_tag;
|
|
|
|
$last_tag_id = $id;
|
|
$current_tag = $tag['tag_type'].'`'.$tag['name'].'`';
|
|
}
|
|
|
|
if (!empty($tag["alias"]))
|
|
$current_tag .= $tag["alias"] . "`";
|
|
}
|
|
if (!empty($last_tag_id))
|
|
$tags_with_type[] = $current_tag;
|
|
|
|
$tags_string = implode(' ', $tags_with_type) . " ";
|
|
return array('version' => $version, 'data' => $tags_string);
|
|
}
|
|
|
|
static public function purge_tags()
|
|
{
|
|
$sql =
|
|
"DELETE FROM tags " .
|
|
"WHERE post_count = 0 AND " .
|
|
"id NOT IN (SELECT alias_id FROM tag_aliases UNION SELECT predicate_id FROM tag_implications UNION SELECT consequent_id FROM tag_implications)";
|
|
self::connection()->executeSql($sql);
|
|
}
|
|
|
|
static public function recalculate_post_count()
|
|
{
|
|
$sql = "UPDATE tags SET post_count = (SELECT COUNT(*) FROM posts_tags pt, posts p WHERE pt.tag_id = tags.id AND pt.post_id = p.id AND p.status <> 'deleted')";
|
|
self::connection()->executeSql($sql);
|
|
}
|
|
|
|
static public function mass_edit($start_tags, $result_tags, $updater_id, $updater_ip_addr)
|
|
{
|
|
foreach (Post::find_by_tags($start_tags) as $p) {
|
|
$start = TagAlias::to_aliased(Tag::scan_tags($start_tags));
|
|
$result = TagAlias::to_aliased(Tag::scan_tags($result_tags));
|
|
$tags = array_merge(array_diff($p->tags(), $start), $result);
|
|
$tags = implode(' ', $tags);
|
|
$p->updateAttributes(array('updater_user_id' => $updater_id, 'updater_ip_addr' => $updater_ip_addr, 'tags' => $tags));
|
|
}
|
|
}
|
|
|
|
static private function _type_map($tag_type)
|
|
{
|
|
foreach (CONFIG()->tag_types as $type_name => $id) {
|
|
if ($id == $tag_type)
|
|
return strtolower($type_name);
|
|
}
|
|
}
|
|
|
|
public function related()
|
|
{
|
|
if (time() > $this->cached_related_expires_on) {
|
|
$length = ceil($this->post_count / 3);
|
|
$length < 12 && $length = 12;
|
|
$length > 8760 && $length = 8760;
|
|
|
|
self::connection()->executeSql("UPDATE tags SET cached_related = ?, cached_related_expires_on = ? WHERE id = ?", implode(",", Rails\Toolbox\ArrayTools::flatten($this->calculate_related($this->name))), date('Y-m-d H:i:s', strtotime('+'.$length.' hours')), $this->id);
|
|
$this->reload();
|
|
}
|
|
|
|
$related = explode(',', $this->cached_related);
|
|
$i = 0;
|
|
$groups = array();
|
|
foreach ($related as $rel) {
|
|
$group[] = $rel;
|
|
if ($i &1) {
|
|
$groups[] = $group;
|
|
$group = array();
|
|
}
|
|
$i++;
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
protected function init()
|
|
{
|
|
$this->tag_type = (int)$this->tag_type;
|
|
$this->type_name = self::type_name_from_value($this->tag_type);
|
|
$this->is_ambiguous = (bool)$this->is_ambiguous;
|
|
}
|
|
|
|
protected function validations()
|
|
{
|
|
return [
|
|
'name' => [
|
|
'length' => ['minimum' => 1]
|
|
]
|
|
];
|
|
}
|
|
|
|
protected function callbacks()
|
|
{
|
|
return [
|
|
'after_save' => [
|
|
'update_cache'
|
|
],
|
|
'after_create' => [
|
|
'update_cache_on_create'
|
|
]
|
|
];
|
|
}
|
|
}
|