diff options
author | Yorhel <git@yorhel.nl> | 2020-01-28 09:23:45 +0100 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2020-01-28 16:42:44 +0100 |
commit | 915ea70075d6ffced641ac52cf567acc747aa7eb (patch) | |
tree | 1f0955db45b2f6048b35165eddf7a1e3e3f1e781 /lib | |
parent | 495e03c42b378bfd5ee9bb0c431bac45c5e2b330 (diff) |
v2rw: Convert the VN tagmod interface
This also changes the voting interface a little bit:
- Spoiler options are a bit more concise
- Mouse-over a button indicates what it does
- The -1 and -2 options are not available anymore
- Downvoted tags are hidden by default
- Moderators can now vote-and-overrule in a single go
Diffstat (limited to 'lib')
-rw-r--r-- | lib/VNDB/DB/Tags.pm | 34 | ||||
-rw-r--r-- | lib/VNDB/Handler/Tags.pm | 161 | ||||
-rw-r--r-- | lib/VNWeb/DB.pm | 2 | ||||
-rw-r--r-- | lib/VNWeb/Elm.pm | 16 | ||||
-rw-r--r-- | lib/VNWeb/Prelude.pm | 1 | ||||
-rw-r--r-- | lib/VNWeb/Tags/Elm.pm | 24 | ||||
-rw-r--r-- | lib/VNWeb/VN/Tagmod.pm | 90 |
7 files changed, 129 insertions, 199 deletions
diff --git a/lib/VNDB/DB/Tags.pm b/lib/VNDB/DB/Tags.pm index 875ff6e9..ed3ea9fe 100644 --- a/lib/VNDB/DB/Tags.pm +++ b/lib/VNDB/DB/Tags.pm @@ -5,7 +5,7 @@ use strict; use warnings; use Exporter 'import'; -our @EXPORT = qw|dbTagGet dbTTTree dbTagEdit dbTagAdd dbTagMerge dbTagLinks dbTagLinkEdit dbTagStats dbTagWipeVotes|; +our @EXPORT = qw|dbTagGet dbTTTree dbTagEdit dbTagAdd dbTagMerge dbTagLinks dbTagStats dbTagWipeVotes|; # %options->{ id noid name search state searchable applicable page results what sort reverse } @@ -211,38 +211,6 @@ sub dbTagLinks { } -# Change a user's tags for a VN entry -sub dbTagLinkEdit { - my($self, $uid, $vid, $insert, $update, $delete, $overrule) = @_; - - # overrule - # 1. set ignore flag for everyone except $uid - $self->dbExec('UPDATE tags_vn SET ignore = ? WHERE tag = ? AND vid = ? AND uid <> ?', - $overrule->{$_}?1:0, $_, $vid, $uid) for(keys %$overrule); - # 2. make sure $uid isn't ignored when others are set to ignore - # (this happens when a mod takes over an other mods' overrule) - $self->dbExec('UPDATE tags_vn SET ignore = false WHERE tag = ? AND vid = ? AND uid = ?', - $_, $vid, $uid) for(grep $overrule->{$_}, keys %$overrule); - - # delete - $self->dbExec('DELETE FROM tags_vn WHERE vid = ? AND uid = ? AND tag IN(!l)', - $vid, $uid, [ keys %$delete ]) if keys %$delete; - - # insert - my $val = join ',', map '(?,?,?,?,?,?)', keys %$insert; - $self->dbExec("INSERT INTO tags_vn (tag, vid, uid, vote, spoiler, ignore) VALUES $val", map - +($_, $vid, $uid, $insert->{$_}[0], $insert->{$_}[1]<0?undef:$insert->{$_}[1], $insert->{$_}[2]?1:0), - keys %$insert) if keys %$insert; - - # update - $self->dbExec('UPDATE tags_vn SET vote = ?, spoiler = ?, date = NOW() WHERE tag = ? AND vid = ? AND uid = ?', - $update->{$_}[0], $update->{$_}[1]<0?undef:$update->{$_}[1], $_, $vid, $uid) for (keys %$update); - - # Update cache - $self->dbExec('SELECT tag_vn_calc(?)', $vid); -} - - # Fetch all tags related to a VN # Argument: %options->{ vid minrating state results what page sort reverse } # sort: name, rating diff --git a/lib/VNDB/Handler/Tags.pm b/lib/VNDB/Handler/Tags.pm index 5acc948f..c44529cf 100644 --- a/lib/VNDB/Handler/Tags.pm +++ b/lib/VNDB/Handler/Tags.pm @@ -15,7 +15,6 @@ TUWF::register( qr{g([1-9]\d*)/(add)}, \&tagedit, qr{g/new}, \&tagedit, qr{g/list}, \&taglist, - qr{v([1-9]\d*)/tagmod}, \&vntagmod, qr{u([1-9]\d*)/tags}, \&usertags, qr{g}, \&tagindex, qr{g/debug}, \&fulltree, @@ -379,166 +378,6 @@ sub taglist { } -sub vntagmod { - my($self, $vid) = @_; - - my $v = $self->dbVNGet(id => $vid)->[0]; - return $self->resNotFound if !$v || $v->{hidden}; - - return $self->htmlDenied if !$self->authCan('tag'); - - my $tags = $self->dbTagStats(vid => $vid, results => 9999); - my $my = $self->dbTagLinks(vid => $vid, uid => $self->authInfo->{id}); - - if($self->reqMethod eq 'POST') { - return if !$self->authCheckCode; - my $frm = $self->formValidate( - { post => 'taglinks', required => 0, default => '', maxlength => 10240, regex => [ qr/^[1-9][0-9]*,-?[1-3],-?[0-2]( [1-9][0-9]*,-?[1-3],-?[0-2])*$/, 'meh' ] }, - { post => 'overrule', required => 0, multi => 1, template => 'id' }, - ); - return $self->resNotFound if $frm->{_err}; - - # convert some data in a more convenient structure for faster lookup - my %tags = map +($_->{id} => $_), @$tags; - my %old = map +($_->{tag} => $_), @$my; - my %new = map { my($tag, $vote, $spoiler) = split /,/; ($tag => [ $vote, $spoiler ]) } split / /, $frm->{taglinks}; - my %over = !$self->authCan('tagmod') || !$frm->{overrule}[0] ? () : (map $new{$_} ? ($_ => 1) : (), @{$frm->{overrule}}); - - # hashes which need to be filled, indicating what should be changed to the DB - my %delete; # tag => 1 - my %update; # tag => [ vote, spoiler ] (ignore flag is untouched) - my %insert; # tag => [ vote, spoiler, ignore ] - my %overrule; # tag => 0/1 - - # remove tags in the deleted state - delete $new{$_->{id}} for(keys %new ? @{$self->dbTagGet(id => [ keys %new ], state => 1)} : ()); - # and not-applicable tags - delete $new{$_->{id}} for(keys %new ? @{$self->dbTagGet(id => [ keys %new ], applicable => 0)} : ()); - - for my $t (keys %old, keys %new) { - my $prev_over = $old{$t} && !$old{$t}{ignore} && $tags{$t}{overruled}; - - # overrule checkbox has changed? make sure to (de-)overrule the tag votes - $overrule{$t} = $over{$t}?1:0 if (!$prev_over && $over{$t}) || ($prev_over && !$over{$t}); - - # tag deleted? - if($old{$t} && !$new{$t}) { - $delete{$t} = 1; - next; - } - - # and insert or update the vote - if(!$old{$t} && $new{$t}) { - # determine whether this vote is going to be ignored or not - my $ign = $tags{$t}{overruled} && !$prev_over && !$over{$t}; - $insert{$t} = [ $new{$t}[0], $new{$t}[1], $ign ]; - } elsif($old{$t}{vote} != $new{$t}[0] || (defined $old{$t}{spoiler} ? $old{$t}{spoiler} : -1) != $new{$t}[1]) { - $update{$t} = [ $new{$t}[0], $new{$t}[1] ]; - } - } - - $self->dbTagLinkEdit($self->authInfo->{id}, $vid, \%insert, \%update, \%delete, \%overrule); - - # need to re-fetch the tags and tag links, as these have been modified - $tags = $self->dbTagStats(vid => $vid, results => 9999); - $my = $self->dbTagLinks(vid => $vid, uid => $self->authInfo->{id}); - } - - - my $title = "Add/remove tags for $v->{title}"; - $self->htmlHeader(title => $title, noindex => 1); - $self->htmlMainTabs('v', $v, 'tagmod'); - div class => 'mainbox'; - h1 $title; - div class => 'notice'; - h2 'Tagging'; - ul; - li; txt 'Make sure you have read the '; a href => '/d10', 'guidelines'; txt '!'; end; - li 'Don\'t forget to hit the submit button on the bottom of the page to make your changes permanent.'; - end; - end; - end 'div'; - $self->htmlForm({ action => "/v$vid/tagmod", nosubmit => 1 }, tagmod => [ 'Tags', - [ hidden => short => 'taglinks', value => '' ], - [ static => nolabel => 1, content => sub { - table class => 'tgl stripe'; - thead; - Tr; - td ''; - td colspan => $self->authCan('tagmod') ? 3 : 2, class => 'tc_you', 'You'; - td colspan => 3, class => 'tc_others', 'Others'; - end; - Tr; - td class => 'tc_tagname', 'Tag'; - td class => 'tc_myvote', 'Rating'; - td class => 'tc_myover', 'O' if $self->authCan('tagmod'); - td class => 'tc_myspoil', 'Spoiler'; - td class => 'tc_allvote', 'Rating'; - td class => 'tc_allspoil', 'Spoiler'; - td class => 'tc_allwho', ''; - end; - end 'thead'; - tfoot; Tr; - td colspan => 6; - input type => 'submit', class => 'submit', value => 'Save changes', style => 'float: right'; - input id => 'tagmod_tag', type => 'text', class => 'text', value => ''; - input id => 'tagmod_add', type => 'button', class => 'submit', value => 'Add tag'; - br; - p; - txt 'Check the '; a href => '/g', 'tag list'; txt ' to browse all available tags.'; - br; - txt 'Can\'t find what you\'re looking for? '; a href => '/g/new', 'Request a new tag'; txt '.'; - end; - end; - end; end 'tfoot'; - tbody id => 'tagtable'; - _tagmod_list($self, $vid, $tags, $my); - end 'tbody'; - end 'table'; - } ], - ]); - $self->htmlFooter; -} - -sub _tagmod_list { - my($self, $vid, $tags, $my) = @_; - - my %my = map +($_->{tag} => $_), @$my; - - for my $cat (keys %TAG_CATEGORY) { - my @tags = grep $_->{cat} eq $cat, @$tags; - next if !@tags; - Tr class => 'tagmod_cat'; - td colspan => 7, $TAG_CATEGORY{$cat}; - end; - for my $t (@tags) { - my $m = $my{$t->{id}}; - Tr id => "tgl_$t->{id}"; - td class => 'tc_tagname'; a href => "/g$t->{id}", $t->{name}; end; - td class => 'tc_myvote', $m->{vote}||0; - if($self->authCan('tagmod')) { - td class => 'tc_myover'; - input type => 'checkbox', name => 'overrule', value => $t->{id}, - $m->{vote} && !$m->{ignore} && $t->{overruled} ? (checked => 'checked') : () - if $t->{cnt} > 1; - end; - } - td class => 'tc_myspoil', defined $m->{spoiler} ? $m->{spoiler} : -1; - td class => 'tc_allvote'; - VNWeb::Tags::Lib::tagscore_($t->{rating}); - i $t->{overruled} ? (class => 'grayedout') : (), " ($t->{cnt})"; - b class => 'standout', style => 'font-weight: bold', title => 'Tag overruled. All votes other than that of the moderator who overruled it will be ignored.', ' !' if $t->{overruled}; - end; - td class => 'tc_allspoil', sprintf '%.2f', $t->{spoiler}; - td class => 'tc_allwho'; - a href => "/g/links?v=$vid;t=$t->{id}", 'Who?'; - end; - end; - } - } -} - - sub tagindex { my $self = shift; diff --git a/lib/VNWeb/DB.pm b/lib/VNWeb/DB.pm index 2e7cf7f6..30018c96 100644 --- a/lib/VNWeb/DB.pm +++ b/lib/VNWeb/DB.pm @@ -185,7 +185,7 @@ sub enrich_merge { _enrich sub { my($data, $array) = @_; my %ids = map +(delete($_->{$key}), $_), @$data; - %$_ = (%$_, $ids{ $_->{$key} }->%*) for @$array; + %$_ = (%$_, ($ids{ $_->{$key} }||{})->%*) for @$array; }, $key, $sql, @array; } diff --git a/lib/VNWeb/Elm.pm b/lib/VNWeb/Elm.pm index 0c8b42eb..d98d1967 100644 --- a/lib/VNWeb/Elm.pm +++ b/lib/VNWeb/Elm.pm @@ -1,7 +1,8 @@ # This module is responsible for generating elm/Gen/*. # -# It exports an `elm_form` function to generate type definitions, a JSON -# encoder and HTML5 validation attributes to simplify and synchronize forms. +# It exports an `elm_api` function to create an API endpoint, type definitions, +# a JSON encoder and HTML5 validation attributes to simplify and synchronize +# forms. # # It also exports an `elm_Response` function for each possible API response # (see %apis below). @@ -47,7 +48,7 @@ my %apis = ( DoubleIP => [], # Account with same IP already exists BadCurPass => [], # Current password is incorrect when changing password MailChange => [], # A confirmation mail has been sent to change a user's email address - Releases => [ { aoh => { # Response to /r/get.json + Releases => [ { aoh => { # Response to 'Release' id => { id => 1 }, title => {}, original => { required => 0, default => '' }, @@ -56,11 +57,18 @@ my %apis = ( lang => { type => 'array', values => {} }, platforms=> { type => 'array', values => {} }, } } ], - BoardResult => [ { aoh => { # Response to /t/boards.json + BoardResult => [ { aoh => { # Response to 'Boards' btype => {}, iid => { required => 0, default => 0, id => 1 }, title => { required => 0 }, } } ], + TagResult => [ { aoh => { # Response to 'Tags' + id => { id => 1 }, + name => {}, + searchable => { anybool => 1 }, + applicable => { anybool => 1 }, + state => { int => 1 }, + } } ], ); diff --git a/lib/VNWeb/Prelude.pm b/lib/VNWeb/Prelude.pm index e44703a6..aa6d9291 100644 --- a/lib/VNWeb/Prelude.pm +++ b/lib/VNWeb/Prelude.pm @@ -89,6 +89,7 @@ our %RE = ( iid => qr{i$id}, did => qr{d$id}, tid => qr{t$id}, + gid => qr{g$id}, vrev => qr{v$id$rev?}, rrev => qr{r$id$rev?}, prev => qr{p$id$rev?}, diff --git a/lib/VNWeb/Tags/Elm.pm b/lib/VNWeb/Tags/Elm.pm new file mode 100644 index 00000000..0f816bad --- /dev/null +++ b/lib/VNWeb/Tags/Elm.pm @@ -0,0 +1,24 @@ +package VNWeb::Tags::Elm; + +use VNWeb::Prelude; + +elm_api Tags => undef, { search => {} }, sub { + my $q = shift->{search}; + my $qs = $q =~ s/[%_]//gr; + + elm_TagResult tuwf->dbPagei({ results => 15, page => 1 }, + 'SELECT t.id, t.name, t.searchable, t.applicable, t.state + FROM (', + sql_join('UNION ALL', + $q =~ /^$RE{gid}$/ ? sql('SELECT 1, id FROM tags WHERE id =', \"$+{id}") : (), + sql('SELECT 1+substr_score(lower(name),', \$qs, '), id FROM tags WHERE name ILIKE', \"%$qs%"), + sql('SELECT 10+substr_score(lower(alias),', \$qs, '), tag FROM tags_aliases WHERE alias ILIKE', \"%$qs%"), + ), ') x (prio, id) + JOIN tags t ON t.id = x.id + WHERE t.state <> 1 + GROUP BY t.id, t.name, t.searchable, t.applicable, t.state + ORDER BY MIN(x.prio), t.name + ') +}; + +1; diff --git a/lib/VNWeb/VN/Tagmod.pm b/lib/VNWeb/VN/Tagmod.pm new file mode 100644 index 00000000..8a2e2509 --- /dev/null +++ b/lib/VNWeb/VN/Tagmod.pm @@ -0,0 +1,90 @@ +package VNWeb::VN::Tagmod; + +use VNWeb::Prelude; +use VNWeb::Tags::Lib; + + +my $FORM = { + id => { id => 1 }, + title => { _when => 'out' }, + tags => { aoh => { + id => { id => 1 }, + vote => { int => 1, enum => [ -3..3 ] }, + spoil => { required => 0, uint => 1, enum => [ 0..2 ] }, + overrule => { anybool => 1 }, + cat => { _when => 'out' }, + name => { _when => 'out' }, + rating => { _when => 'out', num => 1 }, + count => { _when => 'out', uint => 1 }, + spoiler => { _when => 'out', num => 1 }, + overruled => { _when => 'out', anybool => 1 }, + } }, + mod => { _when => 'out', anybool => 1 }, +}; + +my $FORM_IN = form_compile in => $FORM; +my $FORM_OUT = form_compile out => $FORM; + +elm_api Tagmod => $FORM_OUT, $FORM_IN, sub { + my($id, $tags) = $_[0]->@{'id', 'tags'}; + return elm_Unauth if !auth->permTag; + + $tags = [ grep $_->{vote}, @$tags ]; + $_->{overrule} = 0 for auth->permTagmod ? () : @$tags; + + # Weed out invalid/deleted/non-applicable tags + enrich_merge id => 'SELECT id, 1 as exists FROM tags WHERE state <> 1 AND applicable AND id IN', $tags; + $tags = [ grep $_->{exists}, @$tags ]; + + # Find out if any of these tags are being overruled + enrich_merge id => sub { sql 'SELECT tag AS id, bool_or(ignore) as overruled FROM tags_vn WHERE vid =', \$id, 'AND tag IN', $_, 'GROUP BY tag' }, $tags; + + # Delete tag votes not in $tags + tuwf->dbExeci('DELETE FROM tags_vn WHERE uid =', \auth->uid, 'AND vid =', \$id, @$tags ? ('AND tag NOT IN', [ map $_->{id}, @$tags ]) : ()); + + # Add & update tags + for(@$tags) { + my $row = { uid => auth->uid, vid => $id, tag => $_->{id}, vote => $_->{vote}, spoiler => $_->{spoil}, ignore => ($_->{overruled} && !$_->{overrule})?1:0 }; + tuwf->dbExeci('INSERT INTO tags_vn', $row, 'ON CONFLICT (uid, vid, tag) DO UPDATE SET', $row); + tuwf->dbExeci('UPDATE tags_vn SET ignore = TRUE WHERE uid <>', \auth->uid, 'AND vid =', \$id, 'AND tag =', \$_->{id}) if $_->{overrule}; + } + + # Make sure to reset the ignore flag when a moderator removes an overruled vote. + # (i.e. look for tags where *all* votes are on ignore) + tuwf->dbExeci('UPDATE tags_vn tv SET ignore = FALSE WHERE NOT EXISTS(SELECT 1 FROM tags_vn tvi WHERE tvi.tag = tv.tag AND tvi.vid = tv.vid AND NOT tvi.ignore) AND vid =', \$id) if auth->permTagmod; + + tuwf->dbExeci(select => sql_func tag_vn_calc => \$id); + elm_Success +}; + + +TUWF::get qr{/$RE{vid}/tagmod}, sub { + my $v = tuwf->dbRowi('SELECT id, title, hidden AS entry_hidden, locked AS entry_locked FROM vn WHERE id =', \tuwf->capture('id')); + return tuwf->resNotFound if !$v->{id} || (!auth->permDbmod && $v->{entry_hidden}); + return tuwf->resDenied if !auth->permTag; + + my $tags = tuwf->dbAlli(' + SELECT t.id, t.name, t.cat, count(*) as count + , avg(CASE WHEN tv.ignore THEN NULL ELSE tv.vote END) as rating + , coalesce(avg(CASE WHEN tv.ignore THEN NULL ELSE tv.spoiler END), t.defaultspoil) as spoiler + , bool_or(tv.ignore) as overruled + FROM tags t + JOIN tags_vn tv ON tv.tag = t.id + WHERE tv.vid =', \$v->{id}, ' + GROUP BY t.id, t.name, t.cat + ORDER BY t.name' + ); + enrich_merge id => sub { sql 'SELECT tag AS id, vote, spoiler AS spoil, ignore FROM tags_vn WHERE', { uid => auth->uid, vid => $v->{id} } }, $tags; + + for(@$tags) { + $_->{vote} //= 0; + $_->{spoil} //= undef; + $_->{overrule} = $_->{vote} && !$_->{ignore} && $_->{overruled}; + } + + framework_ title => "Edit tags for $v->{title}", type => 'v', dbobj => $v, tab => 'tagmod', sub { + elm_ 'Tagmod' => $FORM_OUT, { id => $v->{id}, title => $v->{title}, tags => $tags, mod => auth->permTagmod }; + }; +}; + +1; |