diff options
author | Yorhel <git@yorhel.nl> | 2020-07-31 14:00:24 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2020-08-07 12:36:58 +0200 |
commit | 9d9e077476c9028462e32f054bb6b5b1201e32a4 (patch) | |
tree | 05131636a3368ac8ddc19ab2e2b1999a6ab086cb | |
parent | afe7f9874ee797a974fc66abc3a9c735e7b22ab3 (diff) |
reviews: Add submit/edit forms
-rw-r--r-- | elm/Reviews/Edit.elm | 147 | ||||
-rw-r--r-- | lib/VNWeb/Prelude.pm | 1 | ||||
-rw-r--r-- | lib/VNWeb/Reviews/Edit.pm | 94 | ||||
-rw-r--r-- | lib/VNWeb/VN/Page.pm | 18 | ||||
-rw-r--r-- | lib/VNWeb/Validation.pm | 11 |
5 files changed, 267 insertions, 4 deletions
diff --git a/elm/Reviews/Edit.elm b/elm/Reviews/Edit.elm new file mode 100644 index 00000000..c66a5445 --- /dev/null +++ b/elm/Reviews/Edit.elm @@ -0,0 +1,147 @@ +module Reviews.Edit 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 Lib.Util exposing (..) +import Lib.RDate as RDate +import Gen.Api as GApi +import Gen.ReviewsEdit as GRE + + +maxChars = 700 + +main : Program GRE.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 : Maybe String + , vid : Int + , vntitle : String + , rid : Maybe Int + , spoiler : Bool + , full : Bool + , summary : TP.Model + , text : TP.Model + , releases : List GRE.RecvReleases + } + + +init : GRE.Recv -> Model +init d = + { state = Api.Normal + , id = d.id + , vid = d.vid + , vntitle = d.vntitle + , rid = d.rid + , spoiler = d.spoiler + , full = d.text /= "" + , summary = TP.bbcode d.summary + , text = TP.bbcode d.text + , releases = d.releases + } + + +encode : Model -> GRE.Send +encode m = + { id = m.id + , vid = m.vid + , rid = m.rid + , spoiler = m.spoiler + , summary = m.summary.data + , text = if m.full then m.text.data else "" + } + + +type Msg + = Release (Maybe Int) + | Full Bool + | Spoiler Bool + | Summary TP.Msg + | Text TP.Msg + | Submit + | Submitted GApi.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Release i -> ({ model | rid = i }, Cmd.none) + Full b -> ({ model | full = b }, Cmd.none) + Spoiler b -> ({ model | spoiler = b }, Cmd.none) + Summary m -> let (nm,nc) = TP.update m model.summary in ({ model | summary = nm }, Cmd.map Summary nc) + Text m -> let (nm,nc) = TP.update m model.text in ({ model | text = nm }, Cmd.map Text nc) + + Submit -> ({ model | state = Api.Loading }, GRE.send (encode model) Submitted) + Submitted (GApi.Redirect s) -> (model, load s) + Submitted r -> ({ model | state = Api.Error r }, Cmd.none) + + +showrel r = "[" ++ (RDate.format (RDate.expand r.released)) ++ " " ++ (String.join "," r.lang) ++ "] " ++ r.title ++ " (r" ++ String.fromInt r.id ++ ")" + +view : Model -> Html Msg +view model = + form_ Submit (model.state == Api.Loading) + [ div [ class "mainbox" ] + [ h1 [] [ text <| if model.id == Nothing then "Submit a review" else "Edit review" ] + , table [ class "formtable" ] + [ formField "Subject" [ a [ href <| "/v"++String.fromInt model.vid ] [ text model.vntitle ] ] + , formField "" + [ inputSelect "" model.rid Release [style "width" "500px" ] <| + (Nothing, "No release selected") + :: List.map (\r -> (Just r.id, showrel r)) model.releases + ++ if model.rid == Nothing || List.any (\r -> Just r.id == model.rid) model.releases then [] else [(model.rid, "Deleted or moved release: r"++Maybe.withDefault "" (Maybe.map String.fromInt model.rid))] + , br [] [] + , text "You do not have to select a release, but indicating which release your review is based on gives more context." + ] + , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ] + , formField "Review type" + [ label [] [ inputRadio "type" (model.full == False) (\_ -> Full False), b [] [ text " Short review" ] + , text <| " - Recommendation-style, maximum " ++ String.fromInt maxChars ++ " characters." ] + , br [] [] + , label [] [ inputRadio "type" (model.full == True ) (\_ -> Full True ), b [] [ text " Full review" ] + , text " - Longer, more detailed." ] + , if not model.full && model.text.data /= "" + then span [] [ br [] [], b [ class "standout" ] [ text "Warning: " ], text "existing content from the \"Full review\" mode will be lost when saving this form." ] + else text "" + , br [] [] + , b [ class "grayedout" ] [ text "You can always switch between review types later." ] + ] + , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ] + , formField "" + [ label [] [ inputCheck "" model.spoiler Spoiler, text " This review contains spoilers." ] + , br [] [] + , b [ class "grayedout" ] [ text "You do not have to check this option if all spoilers in your review are marked with [spoiler] tags." ] + ] + , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ] + , formField (if model.full then "sum::Summary" else "sum::Review") + [ TP.view "sum" model.summary Summary 700 ([rows 5, cols 50] ++ GRE.valSummary) + [ a [ href "/d9#3" ] [ text "BBCode formatting supported" ] ] + , div [ style "width" "700px", style "text-align" "right" ] + [ let + len = String.length model.summary.data + lbl = String.fromInt len ++ "/" ++ String.fromInt maxChars + in if len > maxChars then b [ class "standout" ] [ text lbl ] else text lbl + ] + ] + , if not model.full then text "" else + formField "text::Full review" + [ TP.view "text" model.text Text 700 ([rows 15, cols 50, required True] ++ GRE.valText) + [ a [ href "/d9#3" ] [ text "BBCode formatting supported" ] ] + ] + ] + ] + , div [ class "mainbox" ] + [ fieldset [ class "submit" ] [ submitButton "Submit" model.state (String.length model.summary.data <= maxChars) ] ] + ] diff --git a/lib/VNWeb/Prelude.pm b/lib/VNWeb/Prelude.pm index 79012c8b..e202974c 100644 --- a/lib/VNWeb/Prelude.pm +++ b/lib/VNWeb/Prelude.pm @@ -93,6 +93,7 @@ our %RE = ( did => qr{d$id}, tid => qr{(?<id>t$num)}, gid => qr{g$id}, + wid => qr{(?<id>w$num)}, imgid=> qr{(?<id>(?:ch|cv|sf)$num)}, vrev => qr{v$id$rev?}, rrev => qr{r$id$rev?}, diff --git a/lib/VNWeb/Reviews/Edit.pm b/lib/VNWeb/Reviews/Edit.pm new file mode 100644 index 00000000..02423334 --- /dev/null +++ b/lib/VNWeb/Reviews/Edit.pm @@ -0,0 +1,94 @@ +package VNWeb::Reviews::Edit; + +use VNWeb::Prelude; + + +my $FORM = { + id => { vndbid => 'w', required => 0 }, + vid => { id => 1 }, + vntitle => { _when => 'out' }, + rid => { id => 1, required => 0 }, + spoiler => { anybool => 1 }, + summary => { maxlength => 700 }, + text => { maxlength => 100_000, required => 0, default => '' }, + + releases => { _when => 'out', $VNWeb::Elm::apis{Releases}[0]->%* }, +}; + +my $FORM_IN = form_compile in => $FORM; +my $FORM_OUT = form_compile out => $FORM; + + +sub _releases { + my($id) = @_; + my $r = tuwf->dbAlli(' + SELECT rv.vid, r.id, r.title, r.original, r.released, r.type as rtype, r.reso_x, r.reso_y + FROM releases r + JOIN releases_vn rv ON rv.id = r.id + WHERE NOT r.hidden AND rv.vid =', \$id, ' + ORDER BY r.released, r.title, r.id' + ); + enrich_flatten lang => id => id => sub { sql('SELECT id, lang FROM releases_lang WHERE id IN', $_, 'ORDER BY lang') }, $r; + enrich_flatten platforms => id => id => sub { sql('SELECT id, platform FROM releases_platforms WHERE id IN', $_, 'ORDER BY platform') }, $r; + $r +} + + +TUWF::get qr{/$RE{vid}/addreview}, sub { + my $v = tuwf->dbRowi('SELECT id, title FROM vn WHERE NOT hidden AND id =', \tuwf->capture('id')); + return tuwf->resNotFound if !$v->{id}; + + my $id = tuwf->dbVali('SELECT id FROM reviews WHERE vid =', \$v->{id}, 'AND uid =', \auth->uid); + return tuwf->resRedirect("/$id/edit") if $id; + return tuwf->resDenied if !can_edit w => {}; + + framework_ title => "Write review for $v->{title}", sub { + elm_ 'Reviews.Edit' => $FORM_OUT, { elm_empty($FORM_OUT)->%*, vid => $v->{id}, vntitle => $v->{title}, releases => _releases $v->{id} }; + }; +}; + + +TUWF::get qr{/$RE{wid}/edit}, sub { + my $e = tuwf->dbRowi( + 'SELECT r.id, r.uid, r.vid, r.rid, r.summary, r.text, r.spoiler, v.title AS vntitle + FROM reviews r JOIN vn v ON v.id = r.vid WHERE r.id =', \tuwf->capture('id') + ); + return tuwf->resNotFound if !$e->{id}; + return tuwf->resDenied if !can_edit w => $e; + + $e->{releases} = _releases $e->{vid}; + + # TODO: Option to delete the review. + + framework_ title => "Edit review for $e->{vntitle}", sub { + elm_ 'Reviews.Edit' => $FORM_OUT, $e; + }; +}; + + + +elm_api ReviewsEdit => $FORM_OUT, $FORM_IN, sub { + my($data) = @_; + my $id = delete $data->{id}; + + my $review = $id ? tuwf->dbRowi('SELECT id, uid FROM reviews WHERE id =', \$id) : {}; + return elm_Unauth if !can_edit w => $review; + + validate_dbid 'SELECT id FROM vn WHERE id IN', $data->{vid}; + validate_dbid 'SELECT id FROM releases WHERE id IN', $data->{rid} if defined $data->{rid}; + + if($id) { + $data->{lastmod} = sql 'NOW()'; + tuwf->dbExeci('UPDATE reviews SET', $data, 'WHERE id =', \$id) if $id; + + } else { + return elm_Unauth if tuwf->dbVali('SELECT 1 FROM reviews WHERE vid =', \$data->{vid}, 'AND uid =', \auth->uid); + $data->{uid} = auth->uid; + $id = tuwf->dbVali('INSERT INTO reviews', $data, 'RETURNING id'); + } + + elm_Redirect "/$id" +}; + + +1; diff --git a/lib/VNWeb/VN/Page.pm b/lib/VNWeb/VN/Page.pm index 4ff6dbdd..859f4cf9 100644 --- a/lib/VNWeb/VN/Page.pm +++ b/lib/VNWeb/VN/Page.pm @@ -55,6 +55,14 @@ sub og { } +# The voting and review options are hidden if nothing has been released yet. +sub canvote { + my($v) = @_; + my $minreleased = min grep $_, map $_->{released}, $v->{releases}->@*; + $minreleased && $minreleased <= strftime('%Y%m%d', gmtime) +} + + sub rev_ { my($v) = @_; revision_ v => $v, \&enrich_item, @@ -290,9 +298,6 @@ sub infobox_useroptions_ { my($v) = @_; return if !auth; - # Voting option is hidden if nothing has been released yet - my $minreleased = min grep $_, map $_->{released}, $v->{releases}->@*; - my $labels = tuwf->dbAlli(' SELECT l.id, l.label, l.private, uvl.vid IS NOT NULL as assigned FROM ulist_labels l @@ -308,7 +313,7 @@ sub infobox_useroptions_ { uid => 1*auth->uid, vid => 1*$v->{id}, onlist => $lst->{vid}?\1:\0, - canvote => $minreleased && $minreleased <= strftime('%Y%m%d', gmtime) ? \1 : \0, + canvote => canvote($v)?\1:\0, vote => fmtvote($lst->{vote}).'', notes => $lst->{notes}||'', labels => [ map +{ id => 1*$_->{id}, label => $_->{label}, private => $_->{private}?\1:\0 }, @$labels ], @@ -390,6 +395,11 @@ sub tabs_ { } }; ul_ sub { + if(auth && canvote $v) { + my $id = tuwf->dbVali('SELECT id FROM reviews WHERE vid =', \$v->{id}, 'AND uid =', \auth->uid); + li_ sub { a_ href => "/v$v->{id}/addreview", 'add review' } if !$id && can_edit w => {}; + li_ sub { a_ href => "/$id/edit", 'edit review' } if $id; + } if(auth->permEdit) { li_ sub { a_ href => "/v$v->{id}/add", 'add release' }; li_ sub { a_ href => "/v$v->{id}/addchar", 'add character' }; diff --git a/lib/VNWeb/Validation.pm b/lib/VNWeb/Validation.pm index e122a8be..341d3947 100644 --- a/lib/VNWeb/Validation.pm +++ b/lib/VNWeb/Validation.pm @@ -173,6 +173,11 @@ sub validate_dbid { # Otherwise, checks if the user can edit the post. # Requires the 'user_id', 'date' and 'hidden' fields. # +# w: +# If no 'id' field, checks if the user can submit a new review. +# Otherwise, checks if the user can edit the review. +# Requires the 'uid' field. +# # 'dbentry_type's: # If no 'id' field, checks whether the user can create a new entry. # Otherwise, requires 'entry_hidden' and 'entry_locked' fields. @@ -199,6 +204,12 @@ sub can_edit { } } + if($type eq 'w') { + return 1 if auth->permBoardmod; + return auth->permReview if !$entry->{id}; + return auth && auth->uid == $entry->{uid}; + } + die "Can't do authorization test when entry_hidden/entry_locked fields aren't present" if $entry->{id} && (!exists $entry->{entry_hidden} || !exists $entry->{entry_locked}); |