summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2020-07-31 14:00:24 +0200
committerYorhel <git@yorhel.nl>2020-08-07 12:36:58 +0200
commit9d9e077476c9028462e32f054bb6b5b1201e32a4 (patch)
tree05131636a3368ac8ddc19ab2e2b1999a6ab086cb
parentafe7f9874ee797a974fc66abc3a9c735e7b22ab3 (diff)
reviews: Add submit/edit forms
-rw-r--r--elm/Reviews/Edit.elm147
-rw-r--r--lib/VNWeb/Prelude.pm1
-rw-r--r--lib/VNWeb/Reviews/Edit.pm94
-rw-r--r--lib/VNWeb/VN/Page.pm18
-rw-r--r--lib/VNWeb/Validation.pm11
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});