diff options
-rw-r--r-- | elm/User/Edit.elm | 70 | ||||
-rw-r--r-- | lib/VNWeb/Chars/Page.pm | 49 | ||||
-rw-r--r-- | lib/VNWeb/Chars/VNTab.pm | 1 | ||||
-rw-r--r-- | lib/VNWeb/User/Edit.pm | 13 | ||||
-rw-r--r-- | lib/VNWeb/VN/Page.pm | 18 | ||||
-rw-r--r-- | sql/perms.sql | 1 | ||||
-rw-r--r-- | sql/schema.sql | 9 | ||||
-rw-r--r-- | sql/tableattrs.sql | 2 | ||||
-rw-r--r-- | util/updates/2022-08-30-tag-trait-prefs.sql (renamed from util/updates/wip-tag-prefs.sql) | 11 |
9 files changed, 140 insertions, 34 deletions
diff --git a/elm/User/Edit.elm b/elm/User/Edit.elm index 5f32f588..d44f64cd 100644 --- a/elm/User/Edit.elm +++ b/elm/User/Edit.elm @@ -55,6 +55,7 @@ type alias Model = , mailConfirm : Bool , traitSearch : A.Model GApi.ApiTraitResult , tagpSearch : A.Model GApi.ApiTagResult + , traitpSearch: A.Model GApi.ApiTraitResult } @@ -75,6 +76,7 @@ init d = , mailConfirm = False , traitSearch = A.init "" , tagpSearch = A.init "" + , traitpSearch= A.init "" } @@ -130,6 +132,9 @@ type PrefMsg | TagPSpoil Int Int | TagPChilds Int Bool | TagPDel Int + | TraitPSpoil Int Int + | TraitPChilds Int Bool + | TraitPDel Int type PassMsg = CPass Bool @@ -147,6 +152,7 @@ type Msg | Pass PassMsg | TraitSearch (A.Msg GApi.ApiTraitResult) | TagPrefSearch (A.Msg GApi.ApiTagResult) + | TraitPrefSearch (A.Msg GApi.ApiTraitResult) | Submit | Submitted GApi.Response @@ -157,6 +163,9 @@ traitConfig = { wrap = TraitSearch, id = "traitadd", source = A.traitSource } tagpConfig : A.Config Msg GApi.ApiTagResult tagpConfig = { wrap = TagPrefSearch, id = "tagpadd", source = A.tagSource } +traitpConfig : A.Config Msg GApi.ApiTraitResult +traitpConfig = { wrap = TraitPrefSearch, id = "traitpadd", source = A.traitSource } + updateAdmin : AdminMsg -> GUE.SendAdmin -> GUE.SendAdmin updateAdmin msg model = @@ -244,6 +253,9 @@ updatePrefs msg model = TagPSpoil i s -> { model | tagprefs = modidx i (\e -> { e | spoil = s }) model.tagprefs } TagPChilds i b-> { model | tagprefs = modidx i (\e -> { e | childs = b }) model.tagprefs } TagPDel idx -> { model | tagprefs = delidx idx model.tagprefs } + TraitPSpoil i s -> { model | traitprefs = modidx i (\e -> { e | spoil = s }) model.traitprefs } + TraitPChilds i b-> { model | traitprefs = modidx i (\e -> { e | childs = b }) model.traitprefs } + TraitPDel idx -> { model | traitprefs = delidx idx model.traitprefs } updatePass : PassMsg -> PassData -> PassData updatePass msg model = @@ -297,10 +309,21 @@ update msg model = if t.hidden || List.any (\l -> l.tid == t.id) p.tagprefs then ({ model | tagpSearch = A.clear nm "" }, c) else - let np = { p | tagprefs = p.tagprefs ++ [{ tid = t.id, name = t.name, spoil = 0, childs = False }] } + let np = { p | tagprefs = p.tagprefs ++ [{ tid = t.id, name = t.name, spoil = 0, childs = True }] } in ({ model | saved = False, tagpSearch = A.clear nm "", prefs = Just np }, c) _ -> ({ model | tagpSearch = nm }, c) + TraitPrefSearch m -> + let (nm, c, res) = A.update traitpConfig m model.traitpSearch + in case (res, model.prefs) of + (Just t, Just p) -> + if t.hidden || List.any (\l -> l.tid == t.id) p.traitprefs + then ({ model | traitpSearch = A.clear nm "" }, c) + else + let np = { p | traitprefs = p.traitprefs ++ [{ tid = t.id, name = t.name, group = t.group_name, spoil = 0, childs = True }] } + in ({ model | saved = False, traitpSearch = A.clear nm "", prefs = Just np }, c) + _ -> ({ model | traitpSearch = nm }, c) + Submit -> if Maybe.withDefault False (Maybe.map (\p -> p.cpass && p.pass1 /= p.pass2) model.pass) then ({ model | passNeq = True }, Cmd.none ) @@ -468,17 +491,27 @@ view model = , formField "css::Custom CSS" [ inputTextArea "css" m.customcss (Prefs << Css) ([ rows 5, cols 60 ] ++ GUE.valPrefsCustomcss) ] ] - ttprefsform m = - [ formField "Tags" + ttspoil = + [ (-1, "Always show & highlight") + , (0, "Always show") + , (1, "Force minor spoiler") + , (2, "Force major spoiler") + , (3, "Always hide") ] + + ttprefsform m = div [] + [ p [ style "margin" "0 20px 20px 20px", style "max-width" "800px" ] + [ text "Here you can set display preferences for individual tags & traits." + , text " This feature can be used to completely hide tags/traits you'd rather not see at all" + , text " or you'd like to highlight as a possible trigger warning instead." + , br [] [] + , text "These settings are applied on visual novel and character pages, other listings on the site are unaffected." + ] + , h2 [] [ text "Tags" ] + , div [ style "margin" "5px 0 20px 20px" ] [ if List.isEmpty m.tagprefs then text "" else table [] <| List.indexedMap (\i t -> tr [] [ td [] [ a [ href <| "/" ++ t.tid ] [ text t.name ] ] - , td [] [ inputSelect "" t.spoil (Prefs << TagPSpoil i) [ style "width" "200px" ] - [ (-1, "Always show & highlight") - , (0, "Always show") - , (1, "Force minor spoiler") - , (2, "Force major spoiler") - , (3, "Always hide") ] ] + , td [] [ inputSelect "" t.spoil (Prefs << TagPSpoil i) [ style "width" "200px" ] ttspoil ] , td [] [ label [] [ inputCheck "" t.childs (Prefs << TagPChilds i), text " also apply to child tags" ] ] , td [] [ inputButton "remove" (Prefs (TagPDel i)) [] ] ] @@ -486,6 +519,21 @@ view model = , if List.length m.traits >= 500 then text "" else A.view tagpConfig model.tagpSearch [placeholder "Add tag..."] ] + , h2 [] [ text "Traits" ] + , div [ style "margin" "5px 0 20px 20px" ] + [ if List.isEmpty m.traitprefs then text "" + else table [] <| List.indexedMap (\i t -> tr [] + [ td [] + [ Maybe.withDefault (text "") <| Maybe.map (\g -> b [ class "grayedout" ] [ text <| g ++ " / " ]) t.group + , a [ href <| "/" ++ t.tid ] [ text t.name ] ] + , td [] [ inputSelect "" t.spoil (Prefs << TraitPSpoil i) [ style "width" "200px" ] ttspoil ] + , td [] [ label [] [ inputCheck "" t.childs (Prefs << TraitPChilds i), text " also apply to child traits" ] ] + , td [] [ inputButton "remove" (Prefs (TraitPDel i)) [] ] + ] + ) m.traitprefs + , if List.length m.traits >= 500 then text "" + else A.view traitpConfig model.traitpSearch [placeholder "Add trait..."] + ] ] in form_ "mainform" Submit (model.state == Api.Loading) @@ -528,9 +576,7 @@ view model = , table [ class "formtable" ] <| Maybe.withDefault [] (Maybe.map prefsform model.prefs) ] , div [ class "mainbox", classList [("hidden", model.tab /= TTPref)] ] - [ h1 [] [ text "Tags & traits" ] - , table [ class "formtable" ] <| Maybe.withDefault [] (Maybe.map ttprefsform model.prefs) - ] + [ h1 [] [ text "Tags & traits" ], Maybe.withDefault (text "") (Maybe.map ttprefsform model.prefs) ] , div [ class "mainbox" ] [ fieldset [ class "submit" ] [ submitButton "Submit" model.state (not model.passNeq) diff --git a/lib/VNWeb/Chars/Page.pm b/lib/VNWeb/Chars/Page.pm index 4829fe54..2c8dc24c 100644 --- a/lib/VNWeb/Chars/Page.pm +++ b/lib/VNWeb/Chars/Page.pm @@ -16,6 +16,18 @@ sub enrich_seiyuu { }, @chars; } +sub sql_trait_overrides { + sql '( + WITH RECURSIVE trait_overrides (tid, spoil, childs, lvl) AS ( + SELECT tid, spoil, childs, 0 FROM users_prefs_traits WHERE id =', \auth->uid, ' + UNION ALL + SELECT tp.id, x.spoil, true, lvl+1 + FROM trait_overrides x + JOIN traits_parents tp ON tp.parent = x.tid + WHERE x.childs + ) SELECT DISTINCT ON(tid) tid, spoil FROM trait_overrides ORDER BY tid, lvl + )'; +} sub enrich_item { my($c) = @_; @@ -23,9 +35,17 @@ sub enrich_item { enrich_image_obj image => $c; enrich_merge vid => 'SELECT id AS vid, title, alttitle, c_released AS vn_released FROM vnt WHERE id IN', $c->{vns}; enrich_merge rid => 'SELECT id AS rid, title AS rtitle, original AS roriginal, released AS rel_released FROM releases WHERE id IN', grep $_->{rid}, $c->{vns}->@*; - enrich_merge tid => - 'SELECT t.id AS tid, t.name, t.hidden, t.locked, t.applicable, t.sexual, coalesce(g.id, t.id) AS group, coalesce(g.name, t.name) AS groupname, coalesce(g.order,0) AS order - FROM traits t LEFT JOIN traits g ON t.group = g.id WHERE t.id IN', $c->{traits}; + + # Even with trait overrides, we'll want to see the raw data in revision diffs, + # so fetch the raw spoil as a separate column and do filtering/processing later. + enrich_merge tid => sub { sql ' + SELECT t.id AS tid, t.name, t.hidden, t.locked, t.applicable, t.sexual, x.spoil AS override + , coalesce(g.id, t.id) AS group, coalesce(g.name, t.name) AS groupname, coalesce(g.order,0) AS order + FROM traits t + LEFT JOIN traits g ON t.group = g.id + LEFT JOIN', sql_trait_overrides(), 'x ON x.tid = t.id + WHERE t.id IN', $_ + }, $c->{traits}; $c->{vns} = [ sort { $a->{vn_released} <=> $b->{vn_released} || ($a->{rel_released}||0) <=> ($b->{rel_released}||0) || $a->{title} cmp $b->{title} || idcmp($a->{vid}, $b->{vid}) || idcmp($a->{rid}||'r999999', $b->{rid}||'r999999') } $c->{vns}->@* ]; @@ -53,12 +73,14 @@ sub fetch_chars { }, $l; enrich traits => id => id => sub { sql ' - SELECT ct.id, ct.tid, ct.spoil, t.name, t.hidden, t.locked, t.sexual, coalesce(g.id, t.id) AS group, coalesce(g.name, t.name) AS groupname, coalesce(g.order,0) AS order - FROM chars_traits ct - JOIN traits t ON t.id = ct.tid - LEFT JOIN traits g ON t.group = g.id - WHERE ct.id IN', $_, ' - ORDER BY g.order NULLS FIRST, coalesce(g.name, t.name), t.name' + SELECT ct.id, ct.tid, COALESCE(x.spoil, ct.spoil) AS spoil, t.name, t.hidden, t.locked, t.sexual + , coalesce(g.id, t.id) AS group, coalesce(g.name, t.name) AS groupname, coalesce(g.order,0) AS order + FROM chars_traits ct + JOIN traits t ON t.id = ct.tid + LEFT JOIN traits g ON t.group = g.id + LEFT JOIN', sql_trait_overrides(), 'x ON x.tid = ct.tid + WHERE x.spoil IS DISTINCT FROM 1+1+1 AND ct.id IN', $_, ' + ORDER BY g.order NULLS FIRST, coalesce(g.name, t.name), t.name' }, $l; enrich_seiyuu $vid, $l; @@ -160,13 +182,16 @@ sub chartable_ { } if defined $c->{age}; my @groups; - for(grep !$_->{hidden} && $_->{spoil} <= $view->{spoilers} && (!$_->{sexual} || $view->{traits_sexual}), $c->{traits}->@*) { + for(grep !$_->{hidden} && ($_->{override}//$_->{spoil}) <= $view->{spoilers} && (!$_->{sexual} || $view->{traits_sexual}), $c->{traits}->@*) { push @groups, $_ if !@groups || $groups[$#groups]{group} ne $_->{group}; push $groups[$#groups]{traits}->@*, $_; } tr_ class => "trait_group_$_->{group}", sub { td_ class => 'key', sub { a_ href => "/$_->{group}", $_->{groupname} }; - td_ sub { join_ ', ', sub { a_ href => "/$_->{tid}", $_->{name}; spoil_ $_->{spoil} }, $_->{traits}->@* }; + td_ sub { join_ ', ', sub { + my $spoil = $_->{override}//$_->{spoil}; + a_ href => "/$_->{tid}", class => $spoil==-1?'standout':undef, $_->{name}; spoil_ $spoil + }, $_->{traits}->@* }; } for @groups; my @visvns = grep $_->{spoil} <= $view->{spoilers}, $c->{vns}->@*; @@ -246,7 +271,7 @@ TUWF::get qr{/$RE{crev}} => sub { my $max_spoil = max( $inst_maxspoil||0, - (map $_->{spoil}, grep !$_->{hidden}, $c->{traits}->@*), + (map $_->{override}//$_->{spoil}, grep !$_->{hidden} && !(($_->{override}//0) == 3), $c->{traits}->@*), (map $_->{spoil}, $c->{vns}->@*), defined $c->{spoil_gender} ? 2 : 0, $c->{desc} =~ /\[spoiler\]/i ? 2 : 0, # crude diff --git a/lib/VNWeb/Chars/VNTab.pm b/lib/VNWeb/Chars/VNTab.pm index 3c6fba75..4b4a07d0 100644 --- a/lib/VNWeb/Chars/VNTab.pm +++ b/lib/VNWeb/Chars/VNTab.pm @@ -21,6 +21,7 @@ sub chars_ { my sub opts_ { p_ class => 'mainopts', sub { + debug_ $chars; if($max_spoil) { a_ mkclass(checked => $view->{spoilers} == 0), href => '?view='.viewset(spoilers=>0,traits_sexual=>$view->{traits_sexual}).'#chars', 'Hide spoilers'; a_ mkclass(checked => $view->{spoilers} == 1), href => '?view='.viewset(spoilers=>1,traits_sexual=>$view->{traits_sexual}).'#chars', 'Show minor spoilers'; diff --git a/lib/VNWeb/User/Edit.pm b/lib/VNWeb/User/Edit.pm index 41353615..e52758ff 100644 --- a/lib/VNWeb/User/Edit.pm +++ b/lib/VNWeb/User/Edit.pm @@ -62,12 +62,19 @@ my $FORM = { title_langs => { langpref => 1 }, alttitle_langs => { langpref => 1 }, - tagprefs => { sort_keys => 'tag', maxlength => 500, aoh => { + tagprefs => { sort_keys => 'tid', maxlength => 500, aoh => { tid => { vndbid => 'g' }, spoil => { int => 1, range => [ -1, 3 ] }, childs => { anybool => 1 }, name => {}, } }, + traitprefs => { sort_keys => 'tid', maxlength => 500, aoh => { + tid => { vndbid => 'i' }, + spoil => { int => 1, range => [ -1, 3 ] }, + childs => { anybool => 1 }, + name => {}, + group => { required => 0 }, + } }, # Supporter options nodistract_noads => { anybool => 1 }, @@ -120,6 +127,7 @@ TUWF::get qr{/$RE{uid}/edit}, sub { $u->{prefs}{alttitle_langs} = langpref_parse($u->{prefs}{alttitle_langs}) // $DEFAULT_ALTTITLE_LANGS; $u->{prefs}{traits} = tuwf->dbAlli('SELECT u.tid, t.name, g.name AS "group" FROM users_traits u JOIN traits t ON t.id = u.tid LEFT JOIN traits g ON g.id = t.group WHERE u.id =', \$u->{id}, 'ORDER BY g.order, t.name'); $u->{prefs}{tagprefs} = tuwf->dbAlli('SELECT u.tid, u.spoil, u.childs, t.name FROM users_prefs_tags u JOIN tags t ON t.id = u.tid WHERE u.id =', \$u->{id}, 'ORDER BY t.name'); + $u->{prefs}{traitprefs} = tuwf->dbAlli('SELECT u.tid, u.spoil, u.childs, t.name, g.name as "group" FROM users_prefs_traits u JOIN traits t ON t.id = u.tid LEFT JOIN traits g ON g.id = t.group WHERE u.id =', \$u->{id}, 'ORDER BY g.order, t.name'); } $u->{admin} = auth->permDbmod || auth->permUsermod || auth->permTagmod || auth->permBoardmod ? @@ -172,6 +180,9 @@ elm_api UserEdit => $FORM_OUT, $FORM_IN, sub { tuwf->dbExeci('DELETE FROM users_prefs_tags WHERE id =', \$data->{id}); tuwf->dbExeci('INSERT INTO users_prefs_tags', { id => $data->{id}, tid => $_->{tid}, spoil => $_->{spoil}, childs => $_->{childs} }) for $p->{tagprefs}->@*; + + tuwf->dbExeci('DELETE FROM users_prefs_traits WHERE id =', \$data->{id}); + tuwf->dbExeci('INSERT INTO users_prefs_traits', { id => $data->{id}, tid => $_->{tid}, spoil => $_->{spoil}, childs => $_->{childs} }) for $p->{traitprefs}->@*; } if(auth->permUsermod) { diff --git a/lib/VNWeb/VN/Page.pm b/lib/VNWeb/VN/Page.pm index 68b31c54..ec54dbe7 100644 --- a/lib/VNWeb/VN/Page.pm +++ b/lib/VNWeb/VN/Page.pm @@ -47,19 +47,19 @@ sub enrich_vn { WHERE tv.vid =', \$v->{id}, ' ORDER BY rating DESC, t.name' ) : tuwf->dbAlli(' - WITH RECURSIVE tag_prefs (tid, spoil, childs) AS ( - SELECT tid, spoil, childs FROM users_prefs_tags WHERE id =', \auth->uid, ' - ), tag_overrides (tid, spoil, childs) AS ( - SELECT tid, spoil, childs FROM tag_prefs + WITH RECURSIVE tag_overrides (tid, spoil, childs, lvl) AS ( + SELECT tid, spoil, childs, 0 FROM users_prefs_tags WHERE id =', \auth->uid, ' UNION ALL - SELECT tp.id, x.spoil, true + SELECT tp.id, x.spoil, true, lvl+1 FROM tag_overrides x - JOIN tags_parents tp ON tp.parent = x.tid AND tp.main - WHERE x.childs AND NOT EXISTS(SELECT 1 FROM tag_prefs y WHERE y.tid = tp.id) + JOIN tags_parents tp ON tp.parent = x.tid + WHERE x.childs + ), tag_overrides_grouped (tid, spoil) AS ( + SELECT DISTINCT ON(tid) tid, spoil FROM tag_overrides ORDER BY tid, lvl ) SELECT t.id, t.name, t.cat, tv.rating, COALESCE(x.spoil, tv.spoiler) AS spoiler, tv.lie, x.tid IS NOT NULL AS override FROM tags t JOIN tags_vn_direct tv ON t.id = tv.tag - LEFT JOIN tag_overrides x ON x.tid = tv.tag + LEFT JOIN tag_overrides_grouped x ON x.tid = tv.tag WHERE tv.vid =', \$v->{id}, 'AND x.spoil IS DISTINCT FROM 1+1+1 ORDER BY rating DESC, t.name' ); @@ -923,7 +923,7 @@ sub tags_ { standout => $t->{spoiler} == -1, lie => $view->{spoilers} > 1 && $t->{lie}, parent => !$t->{rating}), $t->{name}; - spoil_ max 0, $t->{spoiler}; + spoil_ $t->{spoiler}; } if $lvl; if($t->{childs}) { diff --git a/sql/perms.sql b/sql/perms.sql index 2a09c83a..f13e9d54 100644 --- a/sql/perms.sql +++ b/sql/perms.sql @@ -83,6 +83,7 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON ulist_vns_labels TO vndb_site; GRANT SELECT, INSERT, UPDATE ON users TO vndb_site; GRANT SELECT, INSERT, UPDATE ON users_prefs TO vndb_site; GRANT SELECT, INSERT, UPDATE, DELETE ON users_prefs_tags TO vndb_site; +GRANT SELECT, INSERT, UPDATE, DELETE ON users_prefs_traits TO vndb_site; GRANT SELECT (id, perm_usermod), INSERT (id, mail, ip) ON users_shadow TO vndb_site; GRANT SELECT, INSERT ON users_username_hist TO vndb_site; GRANT SELECT, INSERT, UPDATE, DELETE ON users_traits TO vndb_site; diff --git a/sql/schema.sql b/sql/schema.sql index 8f9cc30f..47cda09b 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -1107,6 +1107,15 @@ CREATE TABLE users_prefs_tags ( PRIMARY KEY(id, tid) ); +-- users_prefs_traits +CREATE TABLE users_prefs_traits ( + id vndbid NOT NULL, + tid vndbid NOT NULL, + spoil smallint NOT NULL, + childs boolean NOT NULL, + PRIMARY KEY(id, tid) +); + -- Additional fields for the 'users' table, but with some protected columns. -- (Separated from the users table to simplify permission management) CREATE TABLE users_shadow ( diff --git a/sql/tableattrs.sql b/sql/tableattrs.sql index 794d0c55..a94fed5f 100644 --- a/sql/tableattrs.sql +++ b/sql/tableattrs.sql @@ -144,6 +144,8 @@ ALTER TABLE ulist_vns_labels ADD CONSTRAINT ulist_vns_labels_uid_vid_fke ALTER TABLE users_prefs ADD CONSTRAINT users_prefs_id_fkey FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE users_prefs_tags ADD CONSTRAINT users_prefs_tags_id_fkey FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE users_prefs_tags ADD CONSTRAINT users_prefs_tags_tag_fkey FOREIGN KEY (tag) REFERENCES tags (id) ON DELETE CASCADE; +ALTER TABLE users_prefs_traits ADD CONSTRAINT users_prefs_traits_id_fkey FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; +ALTER TABLE users_prefs_traits ADD CONSTRAINT users_prefs_traits_tid_fkey FOREIGN KEY (tid) REFERENCES traits (id) ON DELETE CASCADE; ALTER TABLE users_shadow ADD CONSTRAINT users_shadow_id_fkey FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE users_traits ADD CONSTRAINT users_traits_id_fkey FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE users_traits ADD CONSTRAINT users_traits_tid_fkey FOREIGN KEY (tid) REFERENCES traits (id); diff --git a/util/updates/wip-tag-prefs.sql b/util/updates/2022-08-30-tag-trait-prefs.sql index c5ee7312..db2bec02 100644 --- a/util/updates/wip-tag-prefs.sql +++ b/util/updates/2022-08-30-tag-trait-prefs.sql @@ -9,4 +9,15 @@ CREATE TABLE users_prefs_tags ( ALTER TABLE users_prefs_tags ADD CONSTRAINT users_prefs_tags_id_fkey FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE users_prefs_tags ADD CONSTRAINT users_prefs_tags_tid_fkey FOREIGN KEY (tid) REFERENCES tags (id) ON DELETE CASCADE; +CREATE TABLE users_prefs_traits ( + id vndbid NOT NULL, + tid vndbid NOT NULL, + spoil smallint NOT NULL, + childs boolean NOT NULL, + PRIMARY KEY(id, tid) +); + +ALTER TABLE users_prefs_traits ADD CONSTRAINT users_prefs_traits_id_fkey FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; +ALTER TABLE users_prefs_traits ADD CONSTRAINT users_prefs_traits_tid_fkey FOREIGN KEY (tid) REFERENCES traits (id) ON DELETE CASCADE; + \i sql/perms.sql |