%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009 Marc Worrell
%% Date: 2009-04-09
%%
%% @doc Model for predicates
%% Copyright 2009 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(m_predicate).
-author("Marc Worrell <marc@worrell.nl").
-behaviour(zotonic_model).
%% interface functions
-export([
m_get/3,
is_predicate/2,
is_used/2,
id_to_name/2,
name_to_id/2,
objects/2,
subjects/2,
all/1,
get/2,
insert/2,
flush/1,
update_noflush/4,
object_category/2,
subject_category/2,
for_subject/2
]).
-include_lib("zotonic.hrl").
%% @doc Fetch the value for the key from a model source
-spec m_get( list(), zotonic_model:opt_msg(), z:context() ) -> zotonic_model:return().
m_get([], _Msg, Context) ->
{ok, {all(Context), []}};
m_get([ <<"all">> | Rest ], _Msg, Context) ->
{ok, {all(Context), Rest}};
m_get([ <<"is_used">>, Pred | Rest ], _Msg, Context) ->
{ok, {is_used(Pred, Context), Rest}};
m_get([ <<"object_category">>, Key | Rest ], _Msg, Context) ->
{ok, {object_category(Key, Context), Rest}};
m_get([ <<"subject_category">>, Key | Rest ], _Msg, Context) ->
{ok, {subject_category(Key, Context), Rest}};
m_get([ <<"is_valid_object_subcategory">>, Predicate, Category | Rest ], _Msg, Context) ->
IsValid = is_valid_object_category(Predicate, Category, true, Context),
{ok, {IsValid, Rest}};
m_get([ <<"is_valid_object_category">>, Predicate, Category | Rest ], _Msg, Context) ->
IsValid = is_valid_object_category(Predicate, Category, false, Context),
{ok, {IsValid, Rest}};
m_get([ <<"is_valid_subbject_subcategory">>, Predicate, Category | Rest ], _Msg, Context) ->
IsValid = is_valid_subject_category(Predicate, Category, true, Context),
{ok, {IsValid, Rest}};
m_get([ <<"is_valid_subbject_category">>, Predicate, Category | Rest ], _Msg, Context) ->
IsValid = is_valid_subject_category(Predicate, Category, false, Context),
{ok, {IsValid, Rest}};
m_get([ Key | Rest ], _Msg, Context) ->
{ok, {get(Key, Context), Rest}};
m_get(_Vs, _Msg, _Context) ->
{error, unknown_path}.
%% @doc Test if the property is the name of a predicate
%% @spec is_predicate(Pred, Context) -> bool()
is_predicate(Id, Context) when is_integer(Id) ->
case m_rsc:p_no_acl(Id, category_id, Context) of
undefined -> false;
CatId -> m_category:is_a(CatId, predicate, Context)
end;
is_predicate(Pred, Context) ->
case m_rsc:name_to_id(Pred, Context) of
{ok, Id} -> is_predicate(Id, Context);
_ -> false
end.
%% @doc Check if a predicate is actually in use for an existing edge.
is_used(Predicate, Context) ->
Id = m_rsc:rid(Predicate, Context),
z_db:q1("select id from edge where predicate_id = $1 limit 1", [Id], Context) =/= undefined.
is_valid_object_category(Predicate, Category, IsSubcats, Context) ->
CatId = m_rsc:rid(Category, Context),
ValidCats = object_category(Predicate, Context),
case lists:member({CatId}, ValidCats) of
true ->
true;
false when ValidCats =:= [] ->
true;
false when IsSubcats ->
IsA = m_category:is_a(CatId, Context),
lists:any(
fun(IsACat) ->
IsACatId = m_rsc:rid(IsACat, Context),
lists:member({IsACatId}, ValidCats)
end,
IsA);
false ->
false
end.
is_valid_subject_category(Predicate, Category, IsSubcats, Context) ->
CatId = m_rsc:rid(Category, Context),
ValidCats = subject_category(Predicate, Context),
case lists:member({CatId}, ValidCats) of
true ->
true;
false when ValidCats =:= [] ->
true;
false ->
IsA = m_category:is_a(CatId, Context),
case lists:any(
fun(IsACat) ->
IsACatId = m_rsc:rid(IsACat, Context),
lists:member({IsACatId}, ValidCats)
end,
IsA)
of
true ->
true;
false when IsSubcats ->
% Check subcategories
SubCats = m_category:tree_flat(CatId, Context),
SubCatIds = lists:map(
fun(C) -> proplists:get_value(id, C) end,
SubCats),
lists:any(
fun(CId) -> lists:member({CId}, ValidCats) end,
SubCatIds);
false ->
false
end
end.
%% @doc Lookup the name of a predicate with an id
-spec id_to_name(m_rsc:resource_id(), z:context()) -> {ok, atom()} | {error, {unknown_predicate, term()}}.
id_to_name(Id, Context) when is_integer(Id) ->
F = fun() ->
{L, R} = cat_bounds(Context),
case z_db:q1("
select r.name
from rsc r
join hierarchy c
on r.category_id = c.id and c.name = '$category'
where r.id = $1
and $2 <= c.nr
and c.nr <= $3", [Id, L, R], Context) of
undefined -> {error, {unknown_predicate, Id}};
Name -> {ok, z_convert:to_atom(Name)}
end
end,
z_depcache:memo(F, {predicate_name, Id}, ?DAY, [predicate], Context).
%% @doc Return the id of the predicate
-spec name_to_id( m_rsc:resource_name(), z:context() ) -> {ok, m_rsc:resource_id()} | {error, {unknown_predicate, term()}}.
name_to_id(Name, Context) ->
case m_rsc:name_to_id(Name, Context) of
{ok, Id} ->
case is_predicate(Id, Context) of
true -> {ok, Id};
false -> {error, {unkown_predicate, Id}}
end;
_ -> {error, {unknown_predicate, Name}}
end.
%% @doc Return the definition of the predicate
-spec get( atom() | m_rsc:resource_id() | string() | binary(), z:context() ) -> list() | undefined.
get(PredId, Context) when is_integer(PredId) ->
case id_to_name(PredId, Context) of
{error, _} -> undefined;
{ok, Name} -> get(Name, Context)
end;
get(Pred, Context) when is_list(Pred) orelse is_binary(Pred) ->
get(z_convert:to_atom(z_string:to_lower(Pred)), Context);
get(Pred, Context) ->
case z_depcache:get(predicate, Pred, Context) of
{ok, undefined} ->
undefined;
{ok, Value} ->
Value;
undefined ->
proplists:get_value(Pred, all(Context))
end.
%% @doc Return the category ids that are valid as objects
objects(Id, Context) ->
Objects = z_db:q("
select category_id
from predicate_category
where predicate_id = $1
and is_subject = false",
[Id], Context),
[R || {R} <- Objects].
%% @doc Return the category ids that are valid as subjects
subjects(Id, Context) ->
Subjects = z_db:q("
select category_id
from predicate_category
where predicate_id = $1
and is_subject = true",
[Id], Context),
[R || {R} <- Subjects].
%% @doc Return the list of all predicates
%% @spec all(Context) -> PropList
all(Context) ->
F = fun() ->
{L, R} = cat_bounds(Context),
Preds = z_db:assoc_props("
select *
from rsc r
join hierarchy c
on r.category_id = c.id and c.name = '$category'
where $1 <= c.nr
and c.nr <= $2
order by r.name", [L, R], Context),
FSetPred = fun(Pred) ->
Id = proplists:get_value(id, Pred),
Atom = case proplists:get_value(name, Pred) of
undefined -> undefined;
B -> list_to_atom(binary_to_list(B))
end,
{Atom, [{pred, Atom}, {subject, subjects(Id, Context)}, {object, objects(Id, Context)} | Pred]}
end,
[FSetPred(Pred) || Pred <- Preds]
end,
z_depcache:memo(F, predicate, ?DAY, Context).
%% @doc Insert a new predicate, sets some defaults.
-spec insert(binary()|list(), z:context()) -> {ok, integer()} | {error, any()}.
insert(Title, Context) ->
Name = z_string:to_name(Title),
Uri = "http://zotonic.net/predicate/" ++ Name,
Props = #{
<<"title">> => Title,
<<"name">> => Name,
<<"uri">> => Uri,
<<"category">> => predicate,
<<"group">> => admins,
<<"is_published">> => true
},
case m_rsc:insert(Props, Context) of
{ok, Id} ->
flush(Context),
{ok, Id};
{error, Reason} ->
{error, Reason}
end.
%% @doc Flush all cached data about predicates.
flush(Context) ->
z_depcache:flush(predicate, Context).
%% @doc Reset the list of valid subjects and objects.
-spec update_noflush(integer(), list(), list(), z:context()) -> ok.
update_noflush(Id, Subjects, Objects, Context) ->
SubjectIds0 = [m_rsc:rid(N, Context) || N <- Subjects, N /= [], N /= <<>>],
ObjectIds0 = [m_rsc:rid(N, Context) || N <- Objects, N /= [], N /= <<>>],
SubjectIds = [N || N <- SubjectIds0, N =/= undefined],
ObjectIds = [ N || N <- ObjectIds0, N =/= undefined ],
ok = z_db:transaction(
fun(Ctx) ->
update_predicate_category(Id, true, SubjectIds, Ctx),
update_predicate_category(Id, false, ObjectIds, Ctx),
ok
end,
Context).
update_predicate_category(Id, IsSubject, CatIds, Context) ->
OldIdsR = z_db:q("select category_id from predicate_category where predicate_id = $1 and is_subject = $2",
[Id, IsSubject], Context),
OldIds = [N || {N} <- OldIdsR],
% Delete the ones that are not there anymore
[z_db:q("delete from predicate_category where predicate_id = $1 and category_id = $2 and is_subject = $3",
[Id, OldId, IsSubject], Context)
|| OldId <- OldIds, not lists:member(OldId, CatIds)
],
[z_db:insert(predicate_category, [{predicate_id, Id}, {category_id, NewId}, {is_subject, IsSubject}],
Context)
|| NewId <- CatIds, not lists:member(NewId, OldIds)
],
ok.
%% @doc Return all the valid categories for objects.
%% Return the empty list when there is no constraint.
%% Note that the resulting array is a bit strangely formatted
%% [{id}, {id2}, ...], this is compatible with the category name lookup and
%% prevents mixups with strings (lists of integers).
%% @spec object_category(Id, Context) -> List
object_category(Id, Context) ->
F = fun() ->
case name_to_id(Id, Context) of
{ok, PredId} ->
z_db:q(
"select category_id from predicate_category where predicate_id = $1 and "
++ "is_subject = false",
[PredId], Context);
_ ->
[]
end
end,
z_depcache:memo(F, {object_category, Id}, ?WEEK, [predicate], Context).
%% @doc Return all the valid categories for subjects.
%% Return the empty list when there is no constraint.
%% Note that the resulting array is a bit strangely formatted [{id}, {id2}, ...],
%% this is compatible with the category name lookup and prevents mixups with
%% strings (lists of integers).
%% @spec subject_category(Id, Context) -> List
subject_category(Id, Context) ->
F = fun() ->
case name_to_id(Id, Context) of
{ok, PredId} ->
z_db:q("select category_id from predicate_category where predicate_id = $1 "
"and is_subject = true",
[PredId], Context);
_ ->
[]
end
end,
z_depcache:memo(F, {subject_category, Id}, ?WEEK, [predicate], Context).
%% @doc Return the list of predicates that are valid for the given resource id.
%% Append all predicates that have no restrictions.
for_subject(Id, Context) ->
F = fun() ->
{L, R} = cat_bounds(Context),
ValidIds = z_db:q("
select p.predicate_id
from predicate_category p,
hierarchy pc,
rsc r,
hierarchy rc
where p.category_id = pc.id
and pc.name = '$category'
and r.category_id = rc.id
and rc.name = '$category'
and rc.nr >= pc.lft
and rc.nr <= pc.rght
and r.id = $1
and is_subject = true
", [Id], Context),
Valid = [ValidId || {ValidId} <- ValidIds],
NoRestrictionIds = z_db:q("
select r.id
from rsc r left join predicate_category p on p.predicate_id = r.id and p.is_subject = true
join hierarchy c on (r.category_id = c.id and c.name = '$category')
where p.predicate_id is null
and $1 <= c.nr and c.nr <= $2
", [L, R], Context),
NoRestriction = [NoRestrictionId || {NoRestrictionId} <- NoRestrictionIds],
Valid ++ NoRestriction
end,
z_depcache:memo(F, {predicate_for_subject, Id}, ?WEEK, [predicate, category, Id], Context).
%% @doc Return the id of the predicate category
-spec cat_id(#context{}) -> integer().
cat_id(Context) ->
{ok, Id} = m_category:name_to_id(predicate, Context),
Id.
-spec cat_bounds(#context{}) -> {integer(), integer()}.
cat_bounds(Context) -> m_category:get_range(cat_id(Context), Context).