diff --git a/js/src/components/Event/Date.vue b/js/src/components/Event/Date.vue new file mode 100644 index 000000000..c8ae190c6 --- /dev/null +++ b/js/src/components/Event/Date.vue @@ -0,0 +1,54 @@ +<template> + <span class="container"> + <span class="month">{{ month }}</span> + <span class="day">{{ day }}</span> + </span> +</template> +<script lang="ts"> +import { Component, Prop, Vue } from 'vue-property-decorator'; + +@Component +export default class DateComponent extends Vue { + @Prop({ required: true }) date!: string; + + get dateObj() { + return new Date(this.$props.date); + } + + get month() { + return this.dateObj.toLocaleString(undefined, { month: 'short' }); + } + + get day() { + return this.dateObj.toLocaleString(undefined, { day: 'numeric' }); + } +} +</script> + +<style lang="scss" scoped> + .container { + display: inline-flex; + padding: 2px 0; + width: 40px; + background: #fff; + + span { + flex: 0; + flex-direction: column; + text-align: center; + + &.month { + color: #fa3e3e; + padding: 2px 0; + font-size: 12px; + line-height: 12px; + } + + &.day { + font-weight: 300; + font-size: 20px; + line-height: 20px; + } + } + } +</style> diff --git a/js/src/components/Event/EventCard.vue b/js/src/components/Event/EventCard.vue index 9023e1679..d1bfd38f4 100644 --- a/js/src/components/Event/EventCard.vue +++ b/js/src/components/Event/EventCard.vue @@ -1,8 +1,8 @@ <template> <div class="card"> <div class="card-image" v-if="!event.image"> - <figure class="image is-4by3"> - <img src="https://picsum.photos/g/400/200/"> + <figure class="image is-16by9"> + <img src="https://picsum.photos/g/400/225/?random"> </figure> </div> <div class="card-content"> @@ -10,12 +10,17 @@ <router-link :to="{ name: 'Event', params:{ uuid: event.uuid } }"> <h2 class="title">{{ event.title }}</h2> </router-link> - <span>{{ event.beginsOn | formatDay }}</span> + <DateComponent v-if="!options.hideDate" :date="event.beginsOn" /> </div> - <div v-if="!hideDetails"> - <div v-if="event.participants.length === 1"> + <div v-if="!options.hideDetails"> + <div v-if="event.participants.length > 0 && + options.loggedPerson && + event.participants[0].actor.id === options.loggedPerson.id"> + <b-tag type="is-info"><translate>Organizer</translate></b-tag> + </div> + <div v-else-if="event.participants.length === 1"> <translate - :translate-params="{name: event.participants[0].actor.preferredUsername}" + :translate-params="{name: event.participants[0].actor.preferredUsername}" >%{name} organizes this event</translate> </div> <div v-else> @@ -35,11 +40,17 @@ <script lang="ts"> import { IEvent, ParticipantRole } from '@/types/event.model'; import { Component, Prop, Vue } from 'vue-property-decorator'; +import DateComponent from '@/components/Event/Date.vue'; -@Component +@Component({ + components: { + DateComponent, + EventCard, + }, +}) export default class EventCard extends Vue { @Prop({ required: true }) event!: IEvent; - @Prop({ default: false }) hideDetails!: boolean; + @Prop({ default() { return { hideDate: false, loggedPerson: false, hideDetails: false }; } }) options!: object; data() { return { diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue index 967995037..4c977329e 100644 --- a/js/src/components/NavBar.vue +++ b/js/src/components/NavBar.vue @@ -75,8 +75,8 @@ import { ICurrentUser } from '@/types/current-user.model' }, config: { query: CONFIG, - } - } + }, + }, }) export default class NavBar extends Vue { notifications = [ diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 5167e7ff6..9d43ad06d 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -3,6 +3,7 @@ import gql from 'graphql-tag'; export const FETCH_PERSON = gql` query($name:String!) { person(preferredUsername: $name) { + id, url, name, domain, @@ -11,9 +12,13 @@ query($name:String!) { suspended, avatarUrl, bannerUrl, + feedTokens { + token + }, organizedEvents { uuid, - title + title, + beginsOn }, } } @@ -28,6 +33,26 @@ query { } }`; +export const LOGGED_PERSON_WITH_GOING_TO_EVENTS = gql` +query { + loggedPerson { + id, + avatarUrl, + preferredUsername, + goingToEvents { + uuid, + title, + beginsOn, + participants { + actor { + id, + preferredUsername + } + } + }, + } +}`; + export const IDENTITIES = gql` query { identities { diff --git a/js/src/graphql/feed_tokens.ts b/js/src/graphql/feed_tokens.ts new file mode 100644 index 000000000..f7659b521 --- /dev/null +++ b/js/src/graphql/feed_tokens.ts @@ -0,0 +1,49 @@ +import gql from 'graphql-tag'; + +export const LOGGED_PERSON = gql` +query { + loggedPerson { + id, + avatarUrl, + preferredUsername, + } +}`; + +export const CREATE_FEED_TOKEN_ACTOR = gql` +mutation createFeedToken($actor_id: Int!) { + createFeedToken(actor_id: $actor_id) { + token, + actor { + id + }, + user { + id + } + } +}`; + +export const CREATE_FEED_TOKEN = gql` +mutation { + createFeedToken { + token, + actor { + id + }, + user { + id + } + } +}`; + +export const DELETE_FEED_TOKEN = gql` +mutation deleteFeedToken($token: String!) { + deleteFeedToken(token: $token) { + actor { + id + }, + user { + id + } + } + } +`; diff --git a/js/src/types/actor.model.ts b/js/src/types/actor.model.ts index 749e59e25..62a3c8af8 100644 --- a/js/src/types/actor.model.ts +++ b/js/src/types/actor.model.ts @@ -1,3 +1,6 @@ +import { ICurrentUser } from '@/types/current-user.model'; +import { IEvent } from '@/types/event.model'; + export interface IActor { id?: string; url: string; @@ -22,14 +25,24 @@ export class Actor implements IActor { } export interface IPerson extends IActor { - + feedTokens: IFeedToken[]; + goingToEvents: IEvent[]; } export interface IGroup extends IActor { members: IMember[]; } -export class Person extends Actor implements IPerson {} +export class Person extends Actor implements IPerson { + feedTokens: IFeedToken[] = []; + goingToEvents: IEvent[] = []; +} + +export interface IFeedToken { + token: string; + actor?: IPerson; + user: ICurrentUser; +} export enum MemberRole { PENDING, diff --git a/js/src/types/config.model.ts b/js/src/types/config.model.ts index 78ff7d284..05dc42a3c 100644 --- a/js/src/types/config.model.ts +++ b/js/src/types/config.model.ts @@ -1,5 +1,5 @@ export interface IConfig { - name: string, + name: string; - registrationsOpen: boolean, + registrationsOpen: boolean; } diff --git a/js/src/views/Account/Profile.vue b/js/src/views/Account/Profile.vue index 628fbd9b1..481e7b348 100644 --- a/js/src/views/Account/Profile.vue +++ b/js/src/views/Account/Profile.vue @@ -1,68 +1,107 @@ <template> - <section> - <div class="columns"> - <div class="column"> - <div class="card" v-if="person"> - <div class="card-image" v-if="person.bannerUrl"> - <figure class="image"> - <img :src="person.bannerUrl"> - </figure> - </div> - <div class="card-content"> - <div class="media"> - <div class="media-left"> - <figure class="image is-48x48"> - <img :src="person.avatarUrl"> - </figure> - </div> - <div class="media-content"> - <p class="title">{{ person.name }}</p> - <p class="subtitle">@{{ person.preferredUsername }}</p> - </div> - </div> + <section> + <div class="columns"> + <div class="column"> + <div class="card" v-if="person"> + <div class="card-image" v-if="person.bannerUrl"> + <figure class="image"> + <img :src="person.bannerUrl"> + </figure> + </div> + <div class="card-content"> + <div class="media"> + <div class="media-left"> + <figure class="image is-48x48"> + <img :src="person.avatarUrl"> + </figure> + </div> + <div class="media-content"> + <p class="title">{{ person.name }}</p> + <p class="subtitle">@{{ person.preferredUsername }}</p> + </div> + </div> - <div class="content"> - <p v-html="person.summary"></p> + <div class="content"> + <p v-html="person.summary"></p> + </div> + + <b-dropdown hoverable has-link aria-role="list"> + <button class="button is-info" slot="trigger"> + <translate>Public feeds</translate> + <b-icon icon="menu-down"></b-icon> + </button> + + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('atom', true)"> + <translate>Public RSS/Atom Feed</translate> + </a> + </b-dropdown-item> + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('ics', true)"> + <translate>Public iCal Feed</translate> + </a> + </b-dropdown-item> + </b-dropdown> + + <b-dropdown hoverable has-link aria-role="list" v-if="person.feedTokens.length > 0"> + <button class="button is-info" slot="trigger"> + <translate>Private feeds</translate> + <b-icon icon="menu-down"></b-icon> + </button> + + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('atom', false)"> + <translate>RSS/Atom Feed</translate> + </a> + </b-dropdown-item> + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('ics', false)"> + <translate>iCal Feed</translate> + </a> + </b-dropdown-item> + </b-dropdown> + <a class="button" v-else @click="createToken"> + <translate>Create token</translate> + </a> + </div> + <section v-if="person.organizedEvents.length > 0"> + <h2 class="subtitle"> + <translate>Organized</translate> + </h2> + <div class="columns"> + <EventCard + v-for="event in person.organizedEvents" + :event="event" + :options="{ hideDetails: true }" + :key="event.uuid" + class="column is-one-third" + /> + </div> + <div class="field is-grouped"> + <p class="control"> + <a + class="button" + @click="logoutUser()" + v-if="loggedPerson && loggedPerson.id === person.id" + > + <translate>User logout</translate> + </a> + </p> + <p class="control"> + <a + class="button" + @click="deleteProfile()" + v-if="loggedPerson && loggedPerson.id === person.id" + > + <translate>Delete</translate> + </a> + </p> + </div> + </section> + </div> </div> - </div> - <section v-if="person.organizedEvents.length > 0"> - <h2 class="subtitle"> - <translate>Organized</translate> - </h2> - <div class="columns"> - <EventCard - v-for="event in person.organizedEvents" - :event="event" - :hideDetails="true" - :key="event.uuid" - class="column is-one-third" - /> - </div> - <div class="field is-grouped"> - <p class="control"> - <a - class="button" - @click="logoutUser()" - v-if="loggedPerson && loggedPerson.id === person.id" - > - <translate>User logout</translate> - </a> - </p> - <p class="control"> - <a - class="button" - @click="deleteProfile()" - v-if="loggedPerson && loggedPerson.id === person.id" - > - <translate>Delete</translate> - </a> - </p> - </div> - </section> </div> - </div> - </div> - </section> + </section> </template> <script lang="ts"> @@ -70,6 +109,9 @@ import { FETCH_PERSON, LOGGED_PERSON } from '@/graphql/actor'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import EventCard from '@/components/Event/EventCard.vue'; import { RouteName } from '@/router'; +import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint'; +import { IPerson } from '@/types/actor.model'; +import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens'; @Component({ apollo: { @@ -92,21 +134,41 @@ import { RouteName } from '@/router'; export default class Profile extends Vue { @Prop({ type: String, required: true }) name!: string; - person = null; + person!: IPerson; - // call again the method if the route changes + // call again the method if the route changes @Watch('$route') - onRouteChange() { - // this.fetchData() + onRouteChange() { + // this.fetchData() } logoutUser() { - // TODO : implement logout + // TODO : implement logout this.$router.push({ name: RouteName.HOME }); } nl2br(text) { return text.replace(/(?:\r\n|\r|\n)/g, '<br>'); } + + feedUrls(format, isPublic = true): string { + let url = format === 'ics' ? 'webcal:' : ''; + url += `//${MOBILIZON_INSTANCE_HOST}/`; + if (isPublic === true) { + url += `@${this.person.preferredUsername}/feed/`; + } else { + url += `events/going/${this.person.feedTokens[0].token}/`; + } + return url + (format === 'ics' ? 'ics' : 'atom'); + } + + async createToken() { + const { data } = await this.$apollo.mutate({ + mutation: CREATE_FEED_TOKEN_ACTOR, + variables: { actor_id: this.person.id }, + }); + + this.person.feedTokens.push(data); + } } </script> diff --git a/js/src/views/Account/Register.vue b/js/src/views/Account/Register.vue index b872b6290..3c125d9b6 100644 --- a/js/src/views/Account/Register.vue +++ b/js/src/views/Account/Register.vue @@ -93,6 +93,8 @@ export default class Register extends Vue { avatarUrl: '', bannerUrl: '', domain: null, + feedTokens: [], + goingToEvents: [], }; errors: object = {}; validationSent: boolean = false; diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index c3acf532b..436b378ca 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -107,6 +107,7 @@ import { IEvent, IParticipant } from '@/types/event.model'; import { IPerson } from '@/types/actor.model'; import { RouteName } from '@/router'; import 'vue-simple-markdown/dist/vue-simple-markdown.css'; +import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint'; @Component({ apollo: { @@ -199,19 +200,15 @@ export default class Event extends Vue { } } - downloadIcsEvent() { - // FIXME: remove eventFetch - // eventFetch(`/events/${this.uuid}/ics`, this.$store, { responseType: 'arraybuffer' }) - // .then(response => response.text()) - // .then((response) => { - // const blob = new Blob([ response ], { type: 'text/calendar' }); - // const link = document.createElement('a'); - // link.href = window.URL.createObjectURL(blob); - // link.download = `${this.event.title}.ics`; - // document.body.appendChild(link); - // link.click(); - // document.body.removeChild(link); - // }); + async downloadIcsEvent() { + const data = await (await fetch(`${GRAPHQL_API_ENDPOINT}/events/${this.uuid}/export/ics`)).text(); + const blob = new Blob([data], { type: 'text/calendar' }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = `${this.event.title}.ics`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); } actorIsParticipant() { diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue index 29cb47aa8..b755a4811 100644 --- a/js/src/views/Group/Group.vue +++ b/js/src/views/Group/Group.vue @@ -33,7 +33,7 @@ <EventCard v-for="event in group.organizedEvents" :event="event" - :hideDetails="true" + :options="{ hideDetails: true }" :key="event.uuid" class="column is-one-third" /> diff --git a/js/src/views/Home.vue b/js/src/views/Home.vue index 25a6fb911..e3aa5848d 100644 --- a/js/src/views/Home.vue +++ b/js/src/views/Home.vue @@ -18,6 +18,50 @@ >Welcome back %{username}</translate> </h1> </section> + <section v-if="loggedPerson"> + <span class="events-nearby title">Events you're going at</span> + <b-loading :active.sync="$apollo.loading"></b-loading> + <div v-if="goingToEvents.size > 0" v-for="row in Array.from(goingToEvents.entries())"> + <!-- Iterators will be supported in v-for with VueJS 3 --> + <date-component :date="row[0]"></date-component> + <h3 class="subtitle" + v-if="isToday(row[0])" + v-translate="{count: row[1].length}" + :translate-n="row[1].length" + translate-plural="You have %{ count } events today" + > + You have one event today. + </h3> + <h3 class="subtitle" + v-else-if="isTomorrow(row[0])" + v-translate="{count: row[1].length}" + :translate-n="row[1].length" + translate-plural="You have %{ count } events tomorrow" + > + You have one event tomorrow. + </h3> + <h3 class="subtitle" + v-else + v-translate="{count: row[1].length, days: calculateDiffDays(row[0])}" + :translate-n="row[1].length" + translate-plural="You have %{ count } events in %{ days } days" + > + You have one event in %{ days } days. + </h3> + <div class="columns"> + <EventCard + v-for="event in row[1]" + :key="event.uuid" + :event="event" + :options="{loggedPerson: loggedPerson}" + class="column is-one-quarter-desktop is-half-mobile" + /> + </div> + </div> + <b-message v-else type="is-danger"> + <translate>You're not going to any event yet</translate> + </b-message> + </section> <section> <span class="events-nearby title">Events nearby you</span> <b-loading :active.sync="$apollo.loading"></b-loading> @@ -41,11 +85,13 @@ import ngeohash from 'ngeohash'; import { FETCH_EVENTS } from '@/graphql/event'; import { Component, Vue } from 'vue-property-decorator'; import EventCard from '@/components/Event/EventCard.vue'; -import { LOGGED_PERSON } from '@/graphql/actor'; -import { IPerson } from '@/types/actor.model'; +import { LOGGED_PERSON_WITH_GOING_TO_EVENTS } from '@/graphql/actor'; +import { IPerson, Person } from '@/types/actor.model'; import { ICurrentUser } from '@/types/current-user.model'; import { CURRENT_USER_CLIENT } from '@/graphql/user'; import { RouteName } from '@/router'; +import { IEvent } from '@/types/event.model'; +import DateComponent from '@/components/Event/Date.vue'; @Component({ apollo: { @@ -54,13 +100,14 @@ import { RouteName } from '@/router'; fetchPolicy: 'no-cache', // Debug me: https://github.com/apollographql/apollo-client/issues/3030 }, loggedPerson: { - query: LOGGED_PERSON, + query: LOGGED_PERSON_WITH_GOING_TO_EVENTS, }, currentUser: { query: CURRENT_USER_CLIENT, }, }, components: { + DateComponent, EventCard, }, }) @@ -69,8 +116,7 @@ export default class Home extends Vue { locations = []; city = { name: null }; country = { name: null }; - // FIXME: correctly parse local storage - loggedPerson!: IPerson; + loggedPerson: IPerson = new Person(); currentUser!: ICurrentUser; get displayed_name() { @@ -79,13 +125,47 @@ export default class Home extends Vue { : this.loggedPerson.name; } - fetchLocations() { - // FIXME: remove eventFetch - // eventFetch('/locations', this.$store) - // .then(response => (response.json())) - // .then((response) => { - // this.locations = response; - // }); + isToday(date: string) { + return (new Date(date)).toDateString() === (new Date()).toDateString(); + } + + isTomorrow(date: string) :boolean { + return this.isInDays(date, 1); + } + + isInDays(date: string, nbDays: number) :boolean { + return this.calculateDiffDays(date) === nbDays; + } + + isBefore(date: string, nbDays: number) :boolean { + return this.calculateDiffDays(date) > nbDays; + } + + // FIXME: Use me + isInLessThanSevenDays(date: string): boolean { + return this.isInDays(date, 7); + } + + calculateDiffDays(date: string): number { + const dateObj = new Date(date); + return Math.ceil((dateObj.getTime() - (new Date()).getTime()) / 1000 / 60 / 60 / 24); + } + + get goingToEvents(): Map<string, IEvent[]> { + const res = this.$data.loggedPerson.goingToEvents.filter((event) => { + return event.beginsOn != null && this.isBefore(event.beginsOn, 0) + }); + res.sort( + (a: IEvent, b: IEvent) => new Date(a.beginsOn) > new Date(b.beginsOn), + ); + const groups = res.reduce((acc: Map<string, IEvent[]>, event: IEvent) => { + const day = (new Date(event.beginsOn)).toDateString(); + const events: IEvent[] = acc.get(day) || []; + events.push(event); + acc.set(day, events); + return acc; + }, new Map()); + return groups; } geoLocalize() { @@ -144,6 +224,6 @@ export default class Home extends Vue { } .events-nearby { - margin-bottom: 25px; + margin: 25px auto; } </style> diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex index 43725edde..ce1814cbb 100644 --- a/lib/mobilizon_web/resolvers/person.ex +++ b/lib/mobilizon_web/resolvers/person.ex @@ -6,6 +6,7 @@ defmodule MobilizonWeb.Resolvers.Person do alias Mobilizon.Actors.Actor alias Mobilizon.Users.User alias Mobilizon.Users + alias Mobilizon.Events alias Mobilizon.Service.ActivityPub @doc """ @@ -83,4 +84,34 @@ defmodule MobilizonWeb.Resolvers.Person do {:error, e} end end + + @doc """ + Returns the list of events this person is going to + """ + def person_going_to_events(%Actor{id: actor_id}, _args, %{ + context: %{current_user: user} + }) do + with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), + events <- Events.list_event_participations_for_actor(actor) do + {:ok, events} + else + {:is_owned, false} -> + {:error, "Actor id is not owned by authenticated user"} + end + end + + @doc """ + Returns the list of events this person is going to + """ + def person_going_to_events(_parent, %{}, %{ + context: %{current_user: user} + }) do + with %Actor{} = actor <- Users.get_actor_for_user(user), + events <- Events.list_event_participations_for_actor(actor) do + {:ok, events} + else + {:is_owned, false} -> + {:error, "Actor id is not owned by authenticated user"} + end + end end diff --git a/lib/mobilizon_web/schema/actors/person.ex b/lib/mobilizon_web/schema/actors/person.ex index 8fe4c8ddf..c542009ac 100644 --- a/lib/mobilizon_web/schema/actors/person.ex +++ b/lib/mobilizon_web/schema/actors/person.ex @@ -53,6 +53,11 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do resolve: dataloader(Events), description: "A list of the events this actor has organized" ) + + @desc "The list of events this person goes to" + field :going_to_events, list_of(:event) do + resolve(&Resolvers.Person.person_going_to_events/3) + end end object :person_queries do diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex index ea5d89314..398e67408 100644 --- a/lib/service/export/feed.ex +++ b/lib/service/export/feed.ex @@ -88,7 +88,9 @@ defmodule Mobilizon.Service.Export.Feed do # Create an entry for the Atom feed @spec get_entry(Event.t()) :: any() defp get_entry(%Event{} = event) do - with {:ok, html, []} <- Earmark.as_html(event.description) do + description = event.description || "" + + with {:ok, html, []} <- Earmark.as_html(description) do entry = Entry.new(event.url, event.publish_at || event.inserted_at, event.title) |> Entry.link(event.url, rel: "alternate", type: "text/html") diff --git a/test/mobilizon_web/resolvers/person_resolver_test.exs b/test/mobilizon_web/resolvers/person_resolver_test.exs index d5dbf64cc..698e05f72 100644 --- a/test/mobilizon_web/resolvers/person_resolver_test.exs +++ b/test/mobilizon_web/resolvers/person_resolver_test.exs @@ -137,4 +137,86 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do MapSet.new([actor.preferred_username, "new_identity"]) end end + + test "get_current_person/3 can return the events the person is going to", context do + user = insert(:user) + actor = insert(:actor, user: user) + + query = """ + { + loggedPerson { + goingToEvents { + uuid, + title + } + } + } + """ + + res = + context.conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person")) + + assert json_response(res, 200)["data"]["loggedPerson"]["goingToEvents"] == [] + + event = insert(:event, %{organizer_actor: actor}) + insert(:participant, %{actor: actor, event: event}) + + res = + context.conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person")) + + assert json_response(res, 200)["data"]["loggedPerson"]["goingToEvents"] == [ + %{"title" => event.title, "uuid" => event.uuid} + ] + end + + test "find_person/3 can return the events an identity is going to if it's the same actor", + context do + user = insert(:user) + actor = insert(:actor, user: user) + insert(:actor, user: user) + actor_from_other_user = insert(:actor) + + query = """ + { + person(preferredUsername: "#{actor.preferred_username}") { + goingToEvents { + uuid, + title + } + } + } + """ + + res = + context.conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "person")) + + assert json_response(res, 200)["data"]["person"]["goingToEvents"] == [] + + query = """ + { + person(preferredUsername: "#{actor_from_other_user.preferred_username}") { + goingToEvents { + uuid, + title + } + } + } + """ + + res = + context.conn + |> auth_conn(user) + |> get("/api", AbsintheHelpers.query_skeleton(query, "person")) + + assert json_response(res, 200)["data"]["person"]["goingToEvents"] == nil + + assert hd(json_response(res, 200)["errors"])["message"] == + "Actor id is not owned by authenticated user" + end end