diff options
author | Yorhel <git@yorhel.nl> | 2020-08-16 08:58:50 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2020-08-16 09:00:38 +0200 |
commit | 487c9a4a3fa4109559aa2a25184224b992705eef (patch) | |
tree | 9b6e3bf2f63c3bfabf135e5aac71fda5d5d3a554 | |
parent | f32a4b4f3038a4eed239b9b857aefd70281e076f (diff) |
Discussions: Split post editing out of Discussions::Edit + support editing review comments
This split simplifies Discussions::Edit a little bit and allows
Discussions::PostEdit to be generic enough to handle editing review
comments as well.
-rw-r--r-- | elm/Discussions/Edit.elm | 30 | ||||
-rw-r--r-- | elm/Discussions/PostEdit.elm | 108 | ||||
-rw-r--r-- | lib/VNWeb/Discussions/Edit.pm | 93 | ||||
-rw-r--r-- | lib/VNWeb/Discussions/PostEdit.pm | 88 | ||||
-rw-r--r-- | lib/VNWeb/Reviews/Page.pm | 2 |
5 files changed, 244 insertions, 77 deletions
diff --git a/elm/Discussions/Edit.elm b/elm/Discussions/Edit.elm index 9a98733b..6008cdef 100644 --- a/elm/Discussions/Edit.elm +++ b/elm/Discussions/Edit.elm @@ -26,7 +26,6 @@ main = Browser.element type alias Model = { state : Api.State , tid : Maybe String - , num : Maybe Int , can_mod : Bool , can_private : Bool , locked : Bool @@ -50,7 +49,6 @@ init d = , can_mod = d.can_mod , can_private = d.can_private , tid = d.tid - , num = d.num , locked = d.locked , hidden = d.hidden , private = d.private @@ -73,7 +71,6 @@ searchConfig = { wrap = BoardSearch, id = "boardadd", source = A.boardSource } encode : Model -> GDE.Send encode m = { tid = m.tid - , num = m.num , locked = m.locked , hidden = m.hidden , private = m.private @@ -148,8 +145,6 @@ update msg model = view : Model -> Html Msg view model = let - thread = model.tid == Nothing || model.num == Just 1 - board n bd = li [] <| [ text "[" @@ -184,7 +179,7 @@ view model = else text "" ] - poll () = + poll = [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ] , formField "" [ label [] [ inputCheck "" model.pollEnabled PollEnabled, text " Add poll" ] ] ] ++ @@ -211,26 +206,22 @@ view model = in form_ Submit (model.state == Api.Loading) [ div [ class "mainbox" ] - [ h1 [] [ text <| if model.tid == Nothing then "Create new thread" else "Edit post" ] + [ h1 [] [ text <| if model.tid == Nothing then "Create new thread" else "Edit thread" ] , table [ class "formtable" ] <| - [ if thread - then formField "title::Thread title" [ inputText "title" (Maybe.withDefault "" model.title) Title (style "width" "400px" :: required True :: GDE.valTitle) ] - else formField "Topic" [ a [ href <| "/" ++ Maybe.withDefault "" model.tid ] [ text (Maybe.withDefault "" model.title) ] ] - , if thread && model.can_mod + [ formField "title::Thread title" [ inputText "title" (Maybe.withDefault "" model.title) Title (style "width" "400px" :: required True :: GDE.valTitle) ] + , if model.can_mod then formField "" [ label [] [ inputCheck "" model.locked Locked, text " Locked" ] ] else text "" , if model.can_mod then formField "" [ label [] [ inputCheck "" model.hidden Hidden, text " Hidden" ] ] else text "" - , if thread && model.can_private + , if model.can_private then formField "" [ label [] [ inputCheck "" model.private Private, text " Private" ] ] else text "" , if model.tid /= Nothing && model.can_mod then formField "" [ label [] [ inputCheck "" model.nolastmod Nolastmod, text " Don't update last modification timestamp" ] ] else text "" - , if thread - then formField "boardadd::Boards" (boards ()) - else text "" + , formField "boardadd::Boards" (boards ()) , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ] , formField "msg::Message" [ TP.view "msg" model.msg Content 700 ([rows 12, cols 50] ++ GDE.valMsg) @@ -239,15 +230,10 @@ view model = ] ] ] - ++ (if thread then poll () else []) + ++ poll ++ (if not model.can_mod || model.tid == Nothing then [] else [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "DANGER ZONE" ] ] - , formField "" - [ inputCheck "" model.delete Delete - , text <| " Permanently delete this " ++ if thread then "thread and all replies." else "post." - , text <| if thread then "" else " This causes all replies after this one to be renumbered." - , text <| " This action can not be reverted, only do this with obvious spam!" - ] + , formField "" [ inputCheck "" model.delete Delete, text " Permanently delete this thread and all replies. This action can not be reverted, only do this with obvious spam!" ] ]) ] , div [ class "mainbox" ] diff --git a/elm/Discussions/PostEdit.elm b/elm/Discussions/PostEdit.elm new file mode 100644 index 00000000..0eb787d2 --- /dev/null +++ b/elm/Discussions/PostEdit.elm @@ -0,0 +1,108 @@ +module Discussions.PostEdit exposing (main) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Browser +import Browser.Navigation exposing (load) +import Lib.Html exposing (..) +import Lib.TextPreview as TP +import Lib.Api as Api +import Gen.Api as GApi +import Gen.DiscussionsPostEdit as GPE + + +main : Program GPE.Recv Model Msg +main = Browser.element + { init = \e -> (init e, Cmd.none) + , view = view + , update = update + , subscriptions = always Sub.none + } + + +type alias Model = + { state : Api.State + , id : String + , num : Int + , can_mod : Bool + , hidden : Bool + , nolastmod : Bool + , delete : Bool + , msg : TP.Model + } + + +init : GPE.Recv -> Model +init d = + { state = Api.Normal + , id = d.id + , num = d.num + , can_mod = d.can_mod + , hidden = d.hidden + , nolastmod = False + , delete = False + , msg = TP.bbcode d.msg + } + +encode : Model -> GPE.Send +encode m = + { id = m.id + , num = m.num + , hidden = m.hidden + , nolastmod = m.nolastmod + , delete = m.delete + , msg = m.msg.data + } + + +type Msg + = Hidden Bool + | Nolastmod Bool + | Delete Bool + | Content TP.Msg + | Submit + | Submitted GApi.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Hidden b -> ({ model | hidden = b }, Cmd.none) + Nolastmod b -> ({ model | nolastmod=b }, Cmd.none) + Delete b -> ({ model | delete = b }, Cmd.none) + Content m -> let (nm,nc) = TP.update m model.msg in ({ model | msg = nm }, Cmd.map Content nc) + + Submit -> ({ model | state = Api.Loading }, GPE.send (encode model) Submitted) + Submitted (GApi.Redirect s) -> (model, load s) + Submitted r -> ({ model | state = Api.Error r }, Cmd.none) + + +view : Model -> Html Msg +view model = + form_ Submit (model.state == Api.Loading) + [ div [ class "mainbox" ] + [ h1 [] [ text "Edit post" ] + , table [ class "formtable" ] <| + [ formField "Post" [ a [ href <| "/" ++ model.id ++ "." ++ String.fromInt model.num ] [ text <| "#" ++ String.fromInt model.num ++ " on " ++ model.id ] ] + , if model.can_mod + then formField "" [ label [] [ inputCheck "" model.hidden Hidden, text " Hidden" ] ] + else text "" + , if model.can_mod + then formField "" [ label [] [ inputCheck "" model.nolastmod Nolastmod, text " Don't update last modification timestamp" ] ] + else text "" + , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ] + , formField "msg::Message" + [ TP.view "msg" model.msg Content 700 ([rows 12, cols 50] ++ GPE.valMsg) + [ b [ class "standout" ] [ text " (English please!) " ] + , a [ href "/d9#3" ] [ text "Formatting" ] + ] + ] + ] + ++ (if not model.can_mod then [] else + [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "DANGER ZONE" ] ] + , formField "" [ inputCheck "" model.delete Delete, text " Permanently delete this post. This action can not be reverted, only do this with obvious spam!" ] + ]) + ] + , div [ class "mainbox" ] + [ fieldset [ class "submit" ] [ submitButton "Submit" model.state True ] ] + ] diff --git a/lib/VNWeb/Discussions/Edit.pm b/lib/VNWeb/Discussions/Edit.pm index ec1218c0..0ddf5376 100644 --- a/lib/VNWeb/Discussions/Edit.pm +++ b/lib/VNWeb/Discussions/Edit.pm @@ -6,9 +6,7 @@ use VNWeb::Discussions::Lib; my $FORM = { tid => { required => 0, vndbid => 't' }, # Thread ID, only when editing a post - num => { required => 0, id => 1 }, # Post number, only when editing - # Only when num = 1 || tid = undef title => { required => 0, maxlength => 50 }, boards => { required => 0, sort_keys => [ 'boardtype', 'iid' ], aoh => { btype => { enum => \%BOARD_TYPE }, @@ -23,9 +21,9 @@ my $FORM = { can_mod => { anybool => 1, _when => 'out' }, can_private => { anybool => 1, _when => 'out' }, - locked => { anybool => 1 }, # When can_mod && (num = 1 || tid = undef) + locked => { anybool => 1 }, # When can_mod hidden => { anybool => 1 }, # When can_mod - private => { anybool => 1 }, # When can_private && (num = 1 || tid = undef) + private => { anybool => 1 }, # When can_private nolastmod => { anybool => 1, _when => 'in' }, # When can_mod delete => { anybool => 1 }, # When can_mod @@ -39,48 +37,39 @@ my $FORM_IN = form_compile in => $FORM; elm_api DiscussionsEdit => $FORM_OUT, $FORM_IN, sub { my($data) = @_; my $tid = $data->{tid}; - my $num = $data->{num} || 1; my $t = !$tid ? {} : tuwf->dbRowi(' - SELECT t.id, tp.num, t.poll_question, t.poll_max_options, tp.hidden, tp.uid AS user_id,', sql_totime('tp.date'), 'AS date + SELECT t.id, t.poll_question, t.poll_max_options, t.hidden, tp.uid AS user_id,', sql_totime('tp.date'), 'AS date FROM threads t - JOIN threads_posts tp ON tp.tid = t.id AND tp.num =', \$num, - 'WHERE t.id =', \$tid, + JOIN threads_posts tp ON tp.tid = t.id AND tp.num = 1 + WHERE t.id =', \$tid, 'AND', sql_visible_threads()); return tuwf->resNotFound if $tid && !$t->{id}; return elm_Unauth if !can_edit t => $t; if($tid && $data->{delete} && auth->permBoardmod) { - auth->audit($t->{user_id}, 'post delete', "deleted $tid.$num"); - if($num == 1) { - tuwf->dbExeci('DELETE FROM threads WHERE id =', \$tid); - tuwf->dbExeci(q{DELETE FROM notifications WHERE ltype = 't' AND iid = vndbid_num(}, \$tid, ')'); - return elm_Redirect '/t'; - } else { - tuwf->dbExeci('DELETE FROM threads_posts WHERE tid =', \$tid, 'AND num =', \$num); - tuwf->dbExeci(q{DELETE FROM notifications WHERE ltype = 't' AND iid = vndbid_num(}, \$tid, ') AND subid =', \$num); - return elm_Redirect "/$tid"; - } - } - auth->audit($t->{user_id}, 'post edit', "edited $tid.$num") if $tid && $t->{user_id} != auth->uid; - - my $pollchanged = !$data->{tid} && $data->{poll}; - if($num == 1) { - die "Invalid title" if !length $data->{title}; - die "Invalid boards" if !$data->{boards} || grep +(!$BOARD_TYPE{$_->{btype}}{dbitem})^(!$_->{iid}), $data->{boards}->@*; - - validate_dbid 'SELECT id FROM vn WHERE id IN', map $_->{btype} eq 'v' ? $_->{iid} : (), $data->{boards}->@*; - validate_dbid 'SELECT id FROM producers WHERE id IN', map $_->{btype} eq 'p' ? $_->{iid} : (), $data->{boards}->@*; - # Do not validate user boards here, it's possible to have threads assigned to deleted users. - - die "Invalid max_options" if $data->{poll} && $data->{poll}{max_options} > $data->{poll}{options}->@*; - $pollchanged = 1 if $tid && $data->{poll} && ( - $data->{poll}{question} ne ($t->{poll_question}||'') - || $data->{poll}{max_options} != $t->{poll_max_options} - || join("\n", $data->{poll}{options}->@*) ne - join("\n", map $_->{option}, tuwf->dbAlli('SELECT option FROM threads_poll_options WHERE tid =', \$tid, 'ORDER BY id')->@*) - ) + auth->audit($t->{user_id}, 'post delete', "deleted $tid.1"); + tuwf->dbExeci('DELETE FROM threads WHERE id =', \$tid); + tuwf->dbExeci(q{DELETE FROM notifications WHERE ltype = 't' AND iid = vndbid_num(}, \$tid, ')'); + return elm_Redirect '/t'; } + auth->audit($t->{user_id}, 'post edit', "edited $tid.1") if $tid && $t->{user_id} != auth->uid; + + + die "Invalid title" if !length $data->{title}; + die "Invalid boards" if !$data->{boards} || grep +(!$BOARD_TYPE{$_->{btype}}{dbitem})^(!$_->{iid}), $data->{boards}->@*; + + validate_dbid 'SELECT id FROM vn WHERE id IN', map $_->{btype} eq 'v' ? $_->{iid} : (), $data->{boards}->@*; + validate_dbid 'SELECT id FROM producers WHERE id IN', map $_->{btype} eq 'p' ? $_->{iid} : (), $data->{boards}->@*; + # Do not validate user boards here, it's possible to have threads assigned to deleted users. + + die "Invalid max_options" if $data->{poll} && $data->{poll}{max_options} > $data->{poll}{options}->@*; + my $pollchanged = (!$tid && $data->{poll}) || ($tid && $data->{poll} && ( + $data->{poll}{question} ne ($t->{poll_question}||'') + || $data->{poll}{max_options} != $t->{poll_max_options} + || join("\n", $data->{poll}{options}->@*) ne + join("\n", map $_->{option}, tuwf->dbAlli('SELECT option FROM threads_poll_options WHERE tid =', \$tid, 'ORDER BY id')->@*) + )); my $thread = { title => $data->{title}, @@ -94,13 +83,11 @@ elm_api DiscussionsEdit => $FORM_OUT, $FORM_IN, sub { private => $data->{private} ) : (), }; - tuwf->dbExeci('UPDATE threads SET', $thread, 'WHERE id =', \$tid) if $tid && $num == 1; + tuwf->dbExeci('UPDATE threads SET', $thread, 'WHERE id =', \$tid) if $tid; $tid = tuwf->dbVali('INSERT INTO threads', $thread, 'RETURNING id') if !$tid; - if($num == 1) { - tuwf->dbExeci('DELETE FROM threads_boards WHERE tid =', \$tid); - tuwf->dbExeci('INSERT INTO threads_boards', { tid => $tid, type => $_->{btype}, iid => $_->{iid}//0 }) for $data->{boards}->@*; - } + tuwf->dbExeci('DELETE FROM threads_boards WHERE tid =', \$tid); + tuwf->dbExeci('INSERT INTO threads_boards', { tid => $tid, type => $_->{btype}, iid => $_->{iid}//0 }) for $data->{boards}->@*; if($pollchanged) { tuwf->dbExeci('DELETE FROM threads_poll_options WHERE tid =', \$tid); @@ -109,30 +96,29 @@ elm_api DiscussionsEdit => $FORM_OUT, $FORM_IN, sub { my $post = { tid => $tid, - num => $num, + num => 1, msg => bb_subst_links($data->{msg}), $data->{tid} ? () : (uid => auth->uid), - auth->permBoardmod && $num != 1 ? (hidden => $data->{hidden}) : (), !$data->{tid} || (auth->permBoardmod && $data->{nolastmod}) ? () : (edited => sql 'NOW()') }; tuwf->dbExeci('INSERT INTO threads_posts', $post) if !$data->{tid}; - tuwf->dbExeci('UPDATE threads_posts SET', $post, 'WHERE', { tid => $tid, num => $num }) if $data->{tid}; + tuwf->dbExeci('UPDATE threads_posts SET', $post, 'WHERE', { tid => $tid, num => 1 }) if $data->{tid}; - elm_Redirect "/$tid.$num"; + elm_Redirect "/$tid.1"; }; -TUWF::get qr{(?:/t/(?<board>$BOARD_RE)/new|/$RE{postid}/edit)}, sub { +TUWF::get qr{(?:/t/(?<board>$BOARD_RE)/new|/$RE{tid}\.1/edit)}, sub { my($board_type, $board_id) = (tuwf->capture('board')||'') =~ /^([^0-9]+)([0-9]*)$/; - my($tid, $num) = (tuwf->capture('id'), tuwf->capture('num')); + my $tid = tuwf->capture('id'); $board_type = 'ge' if $board_type && $board_type eq 'an' && !auth->permBoardmod; my $t = !$tid ? {} : tuwf->dbRowi(' - SELECT t.id, tp.tid, tp.num, t.title, t.locked, t.private, t.hidden AS thread_hidden, t.poll_question, t.poll_max_options, tp.hidden, tp.msg, tp.uid AS user_id,', sql_totime('tp.date'), 'AS date + SELECT t.id, tp.tid, t.title, t.locked, t.private, t.hidden, t.poll_question, t.poll_max_options, tp.msg, tp.uid AS user_id,', sql_totime('tp.date'), 'AS date FROM threads t - JOIN threads_posts tp ON tp.tid = t.id AND tp.num =', \$num, - 'WHERE t.id =', \$tid, + JOIN threads_posts tp ON tp.tid = t.id AND tp.num = 1 + WHERE t.id =', \$tid, 'AND', sql_visible_threads()); return tuwf->resNotFound if $tid && !$t->{id}; return tuwf->resDenied if !can_edit t => $t; @@ -159,16 +145,15 @@ TUWF::get qr{(?:/t/(?<board>$BOARD_RE)/new|/$RE{postid}/edit)}, sub { $t->{can_mod} = auth->permBoardmod; $t->{can_private} = auth->isMod; - $t->{hidden} = $tid && $num == 1 ? $t->{thread_hidden}//0 : $t->{hidden}//0; + $t->{hidden} //= 0; $t->{msg} //= ''; $t->{title} //= tuwf->reqGet('title'); $t->{tid} //= undef; - $t->{num} //= undef; $t->{private} //= auth->isMod && tuwf->reqGet('priv') ? 1 : 0; $t->{locked} //= 0; $t->{delete} = 0; - framework_ title => $tid ? 'Edit post' : 'Create new thread', sub { + framework_ title => $tid ? 'Edit thread' : 'Create new thread', sub { elm_ 'Discussions.Edit' => $FORM_OUT, $t; }; }; diff --git a/lib/VNWeb/Discussions/PostEdit.pm b/lib/VNWeb/Discussions/PostEdit.pm new file mode 100644 index 00000000..05df61f4 --- /dev/null +++ b/lib/VNWeb/Discussions/PostEdit.pm @@ -0,0 +1,88 @@ +package VNWeb::Discussions::PostEdit; +# Also used for editing review comments, which follow the exact same format. + +use VNWeb::Prelude; +use VNWeb::Discussions::Lib; + + +my $FORM = { + id => { vndbid => ['t','w'] }, + num => { id => 1 }, + + can_mod => { anybool => 1, _when => 'out' }, + hidden => { anybool => 1 }, # When can_mod + nolastmod => { anybool => 1, _when => 'in' }, # When can_mod + delete => { anybool => 1 }, # When can_mod + + msg => { maxlength => 32768 }, +}; + +my $FORM_OUT = form_compile out => $FORM; +my $FORM_IN = form_compile in => $FORM; + + +sub _info { + my($id,$num) = @_; + tuwf->dbRowi(' + SELECT t.id, tp.num, tp.hidden, tp.msg, tp.uid AS user_id,', sql_totime('tp.date'), 'AS date + FROM threads t + JOIN threads_posts tp ON tp.tid = t.id AND tp.num =', \$num, ' + WHERE t.id =', \$id, 'AND', sql_visible_threads(),' + UNION ALL + SELECT id, num, hidden, msg, uid AS user_id,', sql_totime('date'), 'AS date + FROM reviews_posts WHERE id =', \$id, 'AND num =', \$num + ); +} + + +elm_api DiscussionsPostEdit => $FORM_OUT, $FORM_IN, sub { + my($data) = @_; + my $id = $data->{id}; + my $num = $data->{num}; + + my $t = _info $id, $num; + return tuwf->resNotFound if !$t->{id}; + return elm_Unauth if !can_edit t => $t; + + if($data->{delete} && auth->permBoardmod) { + auth->audit($t->{user_id}, 'post delete', "deleted $id.$num"); + tuwf->dbExeci('DELETE FROM threads_posts WHERE tid =', \$id, 'AND num =', \$num); + tuwf->dbExeci('DELETE FROM reviews_posts WHERE id =', \$id, 'AND num =', \$num); + tuwf->dbExeci(q{DELETE FROM notifications WHERE ltype = 't' AND iid = vndbid_num(}, \$id, ') AND subid =', \$num); + return elm_Redirect "/$id"; + } + auth->audit($t->{user_id}, 'post edit', "edited $id.$num") if $t->{user_id} != auth->uid; + + my $post = { + tid => $id, + num => $num, + msg => bb_subst_links($data->{msg}), + auth->permBoardmod ? (hidden => $data->{hidden}) : (), + (auth->permBoardmod && $data->{nolastmod}) ? () : (edited => sql 'NOW()') + }; + tuwf->dbExeci('UPDATE threads_posts SET', $post, 'WHERE', { tid => $id, num => $num }); + $post->{id} = delete $post->{tid}; + tuwf->dbExeci('UPDATE reviews_posts SET', $post, 'WHERE', { id => $id, num => $num }); + + elm_Redirect "/$id.$num"; +}; + + +TUWF::get qr{/(?:$RE{tid}|$RE{wid})\.$RE{num}/edit}, sub { + my($id, $num) = (tuwf->capture('id'), tuwf->capture('num')); + tuwf->pass if $id =~ /^t/ && $num == 1; # t#.1 goes to Discussions::Edit. + + my $t = _info $id, $num; + return tuwf->resNotFound if $id && !$t->{id}; + return tuwf->resDenied if !can_edit t => $t; + + $t->{can_mod} = auth->permBoardmod; + $t->{delete} = 0; + + framework_ title => 'Edit post', sub { + elm_ 'Discussions.PostEdit' => $FORM_OUT, $t; + }; +}; + + +1; diff --git a/lib/VNWeb/Reviews/Page.pm b/lib/VNWeb/Reviews/Page.pm index 56e8ca8f..69af9054 100644 --- a/lib/VNWeb/Reviews/Page.pm +++ b/lib/VNWeb/Reviews/Page.pm @@ -114,7 +114,7 @@ TUWF::get qr{/$RE{wid}(?:(?<sep>[\./])$RE{num})?}, sub { h1_ class => 'boxtitle', 'Comments'; # XXX: How does this look with pagination? VNWeb::Discussions::Thread::posts_($w, $posts, $page); } - # TODO: "Add comment" form + fix post editing and reporting. + # TODO: "Add comment" form + fix post reporting. }; }; |