Merge branch 'feature/edit-event' into 'master'

Improve create event and prepare update event

See merge request framasoft/mobilizon!176
This commit is contained in:
Thomas Citharel 2019-09-05 14:09:09 +02:00
commit ba6d684786
27 changed files with 856 additions and 173 deletions

View File

@ -81,11 +81,12 @@ import { Modal } from 'buefy/dist/components/dialog';
export default class AddressAutoComplete extends Vue {
@Prop({ required: false, default: () => [] }) initialData!: IAddress[];
@Prop({ required: false }) value!: IAddress;
data: IAddress[] = this.initialData;
selected: IAddress|null = new Address();
isFetching: boolean = false;
queryText: string = '';
queryText: string = this.value && this.value.description || '';
addressModalActive: boolean = false;
async getAsyncData(query) {

View File

@ -1,33 +1,51 @@
<template>
<b-field label="Enter some tags">
<b-taginput
v-model="tags"
v-model="tagsStrings"
:data="filteredTags"
autocomplete
:allow-new="true"
:field="path"
icon="label"
placeholder="Add a tag"
@typing="getFilteredTags">
@typing="getFilteredTags"
>
</b-taginput>
</b-field>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { get } from 'lodash';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { get, differenceBy } from 'lodash';
import { ITag } from '@/types/tag.model';
@Component
@Component({
computed: {
tagsStrings: {
get() {
return this.$props.data.map((tag: ITag) => tag.title);
},
set(tagStrings) {
const tagEntities = tagStrings.map((tag) => {
if (TagInput.isTag(tag)) {
return tag;
}
return { title: tag, slug: tag } as ITag;
});
this.$emit('input', tagEntities);
},
},
},
})
export default class TagInput extends Vue {
@Prop({ required: false, default: () => [] }) data!: object[];
@Prop({ required: false, default: () => [] }) data!: ITag[];
@Prop({ required: true, default: 'value' }) path!: string;
@Prop({ required: true }) value!: string;
@Prop({ required: true }) value!: ITag[];
filteredTags: object[] = [];
tags: object[] = [];
filteredTags: ITag[] = [];
getFilteredTags(text) {
this.filteredTags = this.data.filter((option) => {
this.filteredTags = differenceBy(this.data, this.value, 'id').filter((option) => {
return get(option, this.path)
.toString()
.toLowerCase()
@ -35,18 +53,6 @@ export default class TagInput extends Vue {
});
}
@Watch('tags')
onTagsChanged (tags) {
const tagEntities = tags.map((tag) => {
if (TagInput.isTag(tag)) {
return tag;
}
return { title: tag, slug: tag } as ITag;
});
console.log('tags changed', tagEntities);
this.$emit('input', tagEntities);
}
static isTag(x: any): x is ITag {
return x.slug !== undefined;
}

View File

@ -52,6 +52,7 @@ export const FETCH_EVENT = gql`
domain,
name,
url,
id,
},
# attributedTo {
# avatar {
@ -64,6 +65,7 @@ export const FETCH_EVENT = gql`
${participantQuery}
},
tags {
id,
slug,
title
},
@ -82,6 +84,25 @@ export const FETCH_EVENT = gql`
domain,
name,
}
},
options {
maximumAttendeeCapacity,
remainingAttendeeCapacity,
showRemainingAttendeeCapacity,
offers {
price,
priceCurrency,
url
},
participationConditions {
title,
content,
url
},
attendees,
program,
commentModeration,
showParticipationPrice
}
}
}
@ -144,6 +165,7 @@ export const CREATE_EVENT = gql`
$organizerActorId: ID!,
$category: String,
$beginsOn: DateTime!,
$endsOn: DateTime,
$picture: PictureInput,
$tags: [String],
$options: EventOptionsInput,
@ -154,6 +176,7 @@ export const CREATE_EVENT = gql`
title: $title,
description: $description,
beginsOn: $beginsOn,
endsOn: $endsOn,
organizerActorId: $organizerActorId,
category: $category,
options: $options,
@ -173,13 +196,32 @@ export const CREATE_EVENT = gql`
`;
export const EDIT_EVENT = gql`
mutation EditEvent(
mutation updateEvent(
$id: ID!,
$title: String!,
$description: String!,
$organizerActorId: Int!,
$category: String
$organizerActorId: ID!,
$category: String,
$beginsOn: DateTime!,
$endsOn: DateTime,
$picture: PictureInput,
$tags: [String],
$options: EventOptionsInput,
$physicalAddress: AddressInput,
$visibility: EventVisibility
) {
EditEvent(title: $title, description: $description, organizerActorId: $organizerActorId, category: $category) {
updateEvent(eventId: $id,
title: $title,
description: $description,
beginsOn: $beginsOn,
endsOn: $endsOn,
organizerActorId: $organizerActorId,
category: $category,
options: $options,
picture: $picture,
tags: $tags,
physicalAddress: $physicalAddress,
visibility: $visibility) {
uuid
}
}

View File

@ -4,9 +4,9 @@ import { ITag } from '@/types/tag.model';
import { IPicture } from '@/types/picture.model';
export enum EventStatus {
TENTATIVE,
CONFIRMED,
CANCELLED,
TENTATIVE = 'TENTATIVE',
CONFIRMED = 'CONFIRMED',
CANCELLED = 'CANCELLED',
}
export enum EventVisibility {
@ -17,9 +17,9 @@ export enum EventVisibility {
}
export enum EventJoinOptions {
FREE,
RESTRICTED,
INVITE,
FREE = 'FREE',
RESTRICTED = 'RESTRICTED',
INVITE = 'INVITE',
}
export enum EventVisibilityJoinOptions {

View File

@ -177,7 +177,7 @@
<script lang="ts">
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, IEvent, CommentModeration } from '@/types/event.model';
import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration } from '@/types/event.model';
import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue';
@ -207,6 +207,7 @@ export default class EditEvent extends Vue {
eventId!: string | undefined;
loggedPerson = new Person();
tags: ITag[] = [];
event = new EventModel();
pictureFile: File | null = null;
@ -223,7 +224,7 @@ export default class EditEvent extends Vue {
@Watch('$route.params.eventId', { immediate: true })
async onEventIdParamChanged (val: string) {
if (this.isUpdate !== true) return;
if (!this.isUpdate) return;
this.eventId = val;
@ -231,6 +232,7 @@ export default class EditEvent extends Vue {
this.event = await this.getEvent();
this.pictureFile = await buildFileFromIPicture(this.event.picture);
this.limitedPlaces = this.event.options.maximumAttendeeCapacity != null;
}
}
@ -241,7 +243,6 @@ export default class EditEvent extends Vue {
this.event.beginsOn = now;
this.event.endsOn = end;
console.log('eventvisibilityjoinoptions', this.eventVisibilityJoinOptions);
}
createOrUpdate(e: Event) {
@ -261,7 +262,7 @@ export default class EditEvent extends Vue {
console.log('Event created', data);
this.$router.push({
await this.$router.push({
name: 'Event',
params: { uuid: data.createEvent.uuid },
});
@ -277,7 +278,7 @@ export default class EditEvent extends Vue {
variables: this.buildVariables(),
});
this.$router.push({
await this.$router.push({
name: 'Event',
params: { uuid: this.eventId as string },
});
@ -297,6 +298,8 @@ export default class EditEvent extends Vue {
};
const res = Object.assign({}, this.event, obj);
delete this.event.options['__typename'];
if (this.event.physicalAddress) {
delete this.event.physicalAddress['__typename'];
}

View File

@ -57,7 +57,7 @@
<p class="control">
<router-link
class="button"
:to="{ name: 'EditEvent', params: {uuid: event.uuid}}"
:to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
>
<translate>Edit</translate>
</router-link>

View File

@ -110,6 +110,24 @@ defmodule Mobilizon.Actors.Actor do
|> unique_constraint(:url, name: :actors_url_index)
end
@doc false
def update_changeset(%Actor{} = actor, attrs) do
actor
|> Ecto.Changeset.cast(attrs, [
:name,
:summary,
:keys,
:manually_approves_followers,
:suspended,
:user_id
])
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> validate_required([:preferred_username, :keys, :suspended, :url])
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|> unique_constraint(:url, name: :actors_url_index)
end
@doc """
Changeset for person registration
"""

View File

@ -124,7 +124,7 @@ defmodule Mobilizon.Actors do
"""
def update_actor(%Actor{} = actor, attrs) do
actor
|> Actor.changeset(attrs)
|> Actor.update_changeset(attrs)
|> delete_files_if_media_changed()
|> Repo.update()
end

View File

@ -54,10 +54,10 @@ defmodule Mobilizon.Events.Event do
field(:online_address, :string)
field(:phone_address, :string)
field(:category, :string)
embeds_one(:options, Mobilizon.Events.EventOptions)
embeds_one(:options, Mobilizon.Events.EventOptions, on_replace: :update)
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
many_to_many(:tags, Tag, join_through: "events_tags")
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant)
has_many(:tracks, Track)
has_many(:sessions, Session)
@ -98,6 +98,38 @@ defmodule Mobilizon.Events.Event do
])
end
@doc false
def update_changeset(%Event{} = event, attrs) do
event
|> Ecto.Changeset.cast(attrs, [
:title,
:slug,
:description,
:begins_on,
:ends_on,
:category,
:status,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
])
|> cast_embed(:options)
|> put_tags(attrs)
|> validate_required([
:title,
:begins_on,
:organizer_actor_id,
:url,
:uuid
])
end
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
defp put_tags(changeset, _), do: changeset
def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id)
when organizer_actor_id == actor_id do
{:event_can_be_managed, true}

View File

@ -227,11 +227,12 @@ defmodule Mobilizon.Events do
:tracks,
:tags,
:participants,
:physical_address
:physical_address,
:picture
])}
err ->
{:error, err}
_err ->
{:error, :event_not_found}
end
end
@ -435,7 +436,8 @@ defmodule Mobilizon.Events do
"""
def update_event(%Event{} = event, attrs) do
event
|> Event.changeset(attrs)
|> Repo.preload(:tags)
|> Event.update_changeset(attrs)
|> Repo.update()
end

View File

@ -2,8 +2,7 @@ defmodule MobilizonWeb.API.Events do
@moduledoc """
API for Events
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils
@ -12,28 +11,23 @@ defmodule MobilizonWeb.API.Events do
Create an event
"""
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
def create_event(
%{
begins_on: begins_on,
description: description,
options: options,
organizer_actor_id: organizer_actor_id,
def create_event(%{organizer_actor: organizer_actor} = args) do
with %{
title: title,
physical_address: physical_address,
picture: picture,
content_html: content_html,
tags: tags,
title: title
} = args
)
when is_map(options) do
with %Actor{url: url} = actor <-
Actors.get_local_actor_with_everything(organizer_actor_id),
physical_address <- Map.get(args, :physical_address, nil),
title <- String.trim(title),
visibility <- Map.get(args, :visibility, :public),
picture <- Map.get(args, :picture, nil),
{content_html, tags, to, cc} <-
Utils.prepare_content(actor, description, visibility, tags, nil),
to: to,
cc: cc,
begins_on: begins_on,
ends_on: ends_on,
category: category,
options: options
} <- prepare_args(args),
event <-
ActivityPubUtils.make_event_data(
url,
organizer_actor.url,
%{to: to, cc: cc},
title,
content_html,
@ -41,17 +35,104 @@ defmodule MobilizonWeb.API.Events do
tags,
%{
begins_on: begins_on,
ends_on: ends_on,
physical_address: physical_address,
category: Map.get(args, :category),
category: category,
options: options
}
) do
ActivityPub.create(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: actor,
actor: organizer_actor,
object: event,
local: true
})
end
end
@doc """
Update an event
"""
@spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any()
def update_event(
%{
organizer_actor: organizer_actor
} = args,
%Event{} = event
) do
with args <- Map.put(args, :tags, Map.get(args, :tags, [])),
%{
title: title,
physical_address: physical_address,
picture: picture,
content_html: content_html,
tags: tags,
to: to,
cc: cc,
begins_on: begins_on,
ends_on: ends_on,
category: category,
options: options
} <-
prepare_args(Map.merge(event, args)),
event <-
ActivityPubUtils.make_event_data(
organizer_actor.url,
%{to: to, cc: cc},
title,
content_html,
picture,
tags,
%{
begins_on: begins_on,
ends_on: ends_on,
physical_address: physical_address,
category: category,
options: options
},
event.uuid,
event.url
) do
ActivityPub.update(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: organizer_actor.url,
cc: [],
object: event,
local: true
})
end
end
defp prepare_args(
%{
organizer_actor: organizer_actor,
title: title,
description: description,
options: options,
tags: tags,
begins_on: begins_on,
category: category
} = args
) do
with physical_address <- Map.get(args, :physical_address, nil),
title <- String.trim(title),
visibility <- Map.get(args, :visibility, :public),
picture <- Map.get(args, :picture, nil),
{content_html, tags, to, cc} <-
Utils.prepare_content(organizer_actor, description, visibility, tags, nil) do
%{
title: title,
physical_address: physical_address,
picture: picture,
content_html: content_html,
tags: tags,
to: to,
cc: cc,
begins_on: begins_on,
ends_on: Map.get(args, :ends_on, nil),
category: category,
options: options
}
end
end
end

View File

@ -18,13 +18,13 @@ defmodule MobilizonWeb.API.Groups do
preferred_username: title,
summary: summary,
creator_actor_id: creator_actor_id,
avatar: avatar,
banner: banner
avatar: _avatar,
banner: _banner
} = args
) do
with {:is_owned, true, actor} <- User.owns_actor(user, creator_actor_id),
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
title <- String.trim(title),
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
visibility <- Map.get(args, :visibility, :public),
{content_html, tags, to, cc} <-
Utils.prepare_content(actor, summary, visibility, [], nil),

View File

@ -58,7 +58,8 @@ defmodule MobilizonWeb.Resolvers.Event do
) do
# We get the organizer's next public event
events =
[Events.get_actor_upcoming_public_event(organizer_actor, uuid)] |> Enum.filter(&is_map/1)
[Events.get_actor_upcoming_public_event(organizer_actor, uuid)]
|> Enum.filter(&is_map/1)
# We find similar events with the same tags
# uniq_by : It's possible event_from_same_actor is inside events_from_tags
@ -150,7 +151,17 @@ defmodule MobilizonWeb.Resolvers.Event do
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_full(event_id)},
{:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
{
:ok,
%{
event: %{
id: event_id
},
actor: %{
id: actor_id
}
}
}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
@ -173,12 +184,35 @@ defmodule MobilizonWeb.Resolvers.Event do
@doc """
Create an event
"""
def create_event(_parent, args, %{context: %{current_user: _user}} = _resolution) do
with {:ok, args} <- save_attached_picture(args),
def create_event(
_parent,
%{organizer_actor_id: organizer_actor_id} = args,
%{
context: %{
current_user: user
}
} = _resolution
) do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:is_owned, true, organizer_actor} <- User.owns_actor(user, organizer_actor_id),
{:ok, args} <- save_attached_picture(args),
{:ok, args} <- save_physical_address(args),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <-
MobilizonWeb.API.Events.create_event(args) do
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{
:ok,
%Activity{
data: %{
"object" => %{"type" => "Event"} = _object
}
},
%Event{} = event
} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event}
else
{:is_owned, false} ->
{:error, "Organizer actor id is not owned by the user"}
end
end
@ -186,19 +220,72 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to create events"}
end
@doc """
Update an event
"""
def update_event(
_parent,
%{event_id: event_id} = args,
%{
context: %{
current_user: user
}
} = _resolution
) do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:ok, %Event{} = event} <- Mobilizon.Events.get_event_full(event_id),
{:is_owned, true, organizer_actor} <- User.owns_actor(user, event.organizer_actor_id),
{:ok, args} <- save_attached_picture(args),
{:ok, args} <- save_physical_address(args),
args <- Map.put(args, :organizer_actor, organizer_actor),
{
:ok,
%Activity{
data: %{
"object" => %{"type" => "Event"} = _object
}
},
%Event{} = event
} <-
MobilizonWeb.API.Events.update_event(args, event) do
{:ok, event}
else
{:error, :event_not_found} ->
{:error, "Event not found"}
{:is_owned, _} ->
{:error, "User doesn't own actor"}
end
end
def update_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to update an event"}
end
# If we have an attached picture, just transmit it. It will be handled by
# Mobilizon.Service.ActivityPub.Utils.make_picture_data/1
# However, we need to pass it's actor ID
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(
%{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args
%{
picture: %{
picture: %{file: %Plug.Upload{} = _picture} = all_pic
}
} = args
) do
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor_id))}
end
# Otherwise if we use a previously uploaded picture we need to fetch it from database
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
defp save_attached_picture(
%{
picture: %{
picture_id: picture_id
}
} = args
) do
with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
{:ok, Map.put(args, :picture, picture)}
end
@ -208,7 +295,13 @@ defmodule MobilizonWeb.Resolvers.Event do
defp save_attached_picture(args), do: {:ok, args}
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args)
defp save_physical_address(
%{
physical_address: %{
url: physical_address_url
}
} = args
)
when not is_nil(physical_address_url) do
with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
args <- Map.put(args, :physical_address, address.url) do
@ -230,9 +323,15 @@ defmodule MobilizonWeb.Resolvers.Event do
@doc """
Delete an event
"""
def delete_event(_parent, %{event_id: event_id, actor_id: actor_id}, %{
context: %{current_user: user}
}) do
def delete_event(
_parent,
%{event_id: event_id, actor_id: actor_id},
%{
context: %{
current_user: user
}
}
) do
with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id),
{:is_owned, true, _} <- User.owns_actor(user, actor_id),
{:event_can_be_managed, true} <- Event.can_event_be_managed_by(event, actor_id),

View File

@ -229,11 +229,42 @@ defmodule MobilizonWeb.Schema.EventType do
arg(:organizer_actor_id, non_null(:id))
arg(:category, :string, default_value: "meeting")
arg(:physical_address, :address_input)
arg(:options, :event_options_input, default_value: %{})
arg(:options, :event_options_input)
resolve(&Event.create_event/3)
end
@desc "Update an event"
field :update_event, type: :event do
arg(:event_id, non_null(:id))
arg(:title, :string)
arg(:description, :string)
arg(:begins_on, :datetime)
arg(:ends_on, :datetime)
arg(:state, :integer)
arg(:status, :integer)
arg(:public, :boolean)
arg(:visibility, :event_visibility)
arg(:organizer_actor_id, :id)
arg(:tags, list_of(:string), description: "The list of tags associated to the event")
arg(:picture, :picture_input,
description:
"The picture for the event, either as an object or directly the ID of an existing Picture"
)
arg(:publish_at, :datetime)
arg(:online_address, :string)
arg(:phone_address, :string)
arg(:category, :string)
arg(:physical_address, :address_input)
arg(:options, :event_options_input)
resolve(&Event.update_event/3)
end
@desc "Delete an event"
field :delete_event, :deleted_object do
arg(:event_id, non_null(:integer))

View File

@ -39,24 +39,16 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """
Wraps an object into an activity
"""
# TODO: Rename me
@spec insert(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()}
def insert(map, local \\ true) when is_map(map) do
with map <- lazy_put_activity_defaults(map),
{:ok, object} <- insert_full_object(map) do
activity = %Activity{
@spec create_activity(map(), boolean()) :: {:ok, %Activity{}}
def create_activity(map, local \\ true) when is_map(map) do
with map <- lazy_put_activity_defaults(map) do
{:ok,
%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: get_recipients(map)
}
# Notification.create_notifications(activity)
# stream_out(activity)
{:ok, activity, object}
else
%Activity{} = activity -> {:ok, activity}
error -> {:error, error}
}}
end
end
@ -137,7 +129,8 @@ defmodule Mobilizon.Service.ActivityPub do
%{to: to, actor: actor, published: published, object: object},
additional
),
{:ok, activity, object} <- insert(create_data, local),
{:ok, activity} <- create_activity(create_data, local),
{:ok, object} <- insert_full_object(create_data),
:ok <- maybe_federate(activity) do
# {:ok, actor} <- Actors.increase_event_count(actor) do
{:ok, activity, object}
@ -160,7 +153,8 @@ defmodule Mobilizon.Service.ActivityPub do
"object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity"
},
{:ok, activity, object} <- insert(data, local),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
@ -177,7 +171,8 @@ defmodule Mobilizon.Service.ActivityPub do
"object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity"
},
{:ok, activity, object} <- insert(data, local),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
@ -195,7 +190,8 @@ defmodule Mobilizon.Service.ActivityPub do
"actor" => actor,
"object" => object
},
{:ok, activity, object} <- insert(data, local),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- update_object(object["id"], data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
@ -210,7 +206,8 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do
# with nil <- get_existing_like(url, object),
# like_data <- make_like_data(user, object, activity_id),
# {:ok, activity, object} <- insert(like_data, local),
# {:ok, activity} <- create_activity(like_data, local),
# {:ok, object} <- insert_full_object(data),
# {:ok, object} <- add_like_to_object(activity, object),
# :ok <- maybe_federate(activity) do
# {:ok, activity, object}
@ -228,7 +225,8 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do
# with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
# unlike_data <- make_unlike_data(actor, like_activity, activity_id),
# {:ok, unlike_activity, _object} <- insert(unlike_data, local),
# {:ok, unlike_activity} <- create_activity(unlike_data, local),
# {:ok, _object} <- insert_full_object(data),
# {:ok, _activity} <- Repo.delete(like_activity),
# {:ok, object} <- remove_like_from_object(like_activity, object),
# :ok <- maybe_federate(unlike_activity) do
@ -247,7 +245,8 @@ defmodule Mobilizon.Service.ActivityPub do
) do
with true <- is_public?(object),
announce_data <- make_announce_data(actor, object, activity_id, public),
{:ok, activity, object} <- insert(announce_data, local),
{:ok, activity} <- create_activity(announce_data, local),
{:ok, object} <- insert_full_object(announce_data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
@ -265,7 +264,8 @@ defmodule Mobilizon.Service.ActivityPub do
) do
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
{:ok, unannounce_activity} <- create_activity(unannounce_data, local),
{:ok, object} <- insert_full_object(unannounce_data),
:ok <- maybe_federate(unannounce_activity) do
{:ok, unannounce_activity, object}
else
@ -282,7 +282,8 @@ defmodule Mobilizon.Service.ActivityPub do
activity_follow_id <-
activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id),
{:ok, activity, object} <- insert(data, local),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
@ -304,12 +305,14 @@ defmodule Mobilizon.Service.ActivityPub do
follower,
"#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity"
),
{:ok, follow_activity, _object} <- insert(data, local),
{:ok, follow_activity} <- create_activity(data, local),
{:ok, _object} <- insert_full_object(data),
activity_unfollow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <-
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
{:ok, activity, object} <- insert(unfollow_data, local),
{:ok, activity} <- create_activity(unfollow_data, local),
{:ok, object} <- insert_full_object(unfollow_data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
@ -331,7 +334,8 @@ defmodule Mobilizon.Service.ActivityPub do
}
with {:ok, _} <- Events.delete_event(event),
{:ok, activity, object} <- insert(data, local),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
@ -347,7 +351,8 @@ defmodule Mobilizon.Service.ActivityPub do
}
with {:ok, _} <- Events.delete_comment(comment),
{:ok, activity, object} <- insert(data, local),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
@ -363,7 +368,8 @@ defmodule Mobilizon.Service.ActivityPub do
}
with {:ok, _} <- Actors.delete_actor(actor),
{:ok, activity, object} <- insert(data, local),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
@ -384,9 +390,10 @@ defmodule Mobilizon.Service.ActivityPub do
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity, report} <- insert(flag_data, local),
{:ok, activity} <- create_activity(flag_data, local),
{:ok, object} <- insert_full_object(flag_data),
:ok <- maybe_federate(activity) do
{:ok, activity, report}
{:ok, activity, object}
end
end
@ -403,7 +410,8 @@ defmodule Mobilizon.Service.ActivityPub do
join_data <- Convertible.model_to_as(participant),
join_data <- Map.put(join_data, "to", [event.organizer_actor.url]),
join_data <- Map.put(join_data, "cc", []),
{:ok, activity, _} <- insert(join_data, local),
{:ok, activity} <- create_activity(join_data, local),
{:ok, _object} <- insert_full_object(join_data),
:ok <- maybe_federate(activity) do
if role === :participant do
accept(
@ -443,7 +451,8 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [event.organizer_actor.url],
"cc" => []
},
{:ok, activity, _} <- insert(leave_data, local),
{:ok, activity} <- create_activity(leave_data, local),
{:ok, _object} <- insert_full_object(leave_data),
:ok <- maybe_federate(activity) do
{:ok, activity, participant}
end

View File

@ -15,12 +15,30 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
@impl Converter
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
avatar =
object["icon"]["url"] &&
%{
"name" => object["icon"]["name"] || "avatar",
"url" => object["icon"]["url"]
}
banner =
object["image"]["url"] &&
%{
"name" => object["image"]["name"] || "banner",
"url" => object["image"]["url"]
}
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferred_username"],
"preferred_username" => object["preferredUsername"],
"summary" => object["summary"],
"url" => object["url"],
"name" => object["name"]
"name" => object["name"],
"avatar" => avatar,
"banner" => banner,
"keys" => object["publicKey"]["publicKeyPem"],
"manually_approves_followers" => object["manuallyApprovesFollowers"]
}
end

View File

@ -57,6 +57,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
"organizer_actor_id" => actor_id,
"picture_id" => picture_id,
"begins_on" => object["startTime"],
"ends_on" => object["endTime"],
"category" => object["category"],
"url" => object["id"],
"uuid" => object["uuid"],
@ -173,7 +174,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
"startTime" => event.begins_on |> date_to_string(),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(),
"id" => event.url
"id" => event.url,
"url" => event.url
}
res =

View File

@ -295,19 +295,15 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
data
)
when object_type in ["Person", "Application", "Service", "Organization"] do
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
case Actors.get_actor_by_url(object["id"]) do
{:ok, %Actor{url: url}} ->
{:ok, new_actor_data} = ActivityPub.actor_data_from_actor_object(object)
Actors.insert_or_update_actor(new_actor_data)
{:ok, %Actor{url: actor_url}} ->
ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: url
actor: actor_url
})
e ->
@ -316,6 +312,28 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => actor} =
_update
) do
with {:ok, %{"actor" => existing_organizer_actor_url} = _existing_event_data} <-
fetch_obj_helper_as_activity_streams(object),
{:ok, %Actor{url: actor_url}} <- actor |> Utils.get_url() |> Actors.get_actor_by_url(),
true <- Utils.get_url(existing_organizer_actor_url) == actor_url do
ActivityPub.update(%{
local: false,
to: object["to"] || [],
cc: object["cc"] || [],
object: object,
actor: actor_url
})
else
e ->
Logger.debug(inspect(e))
:error
end
end
def handle_incoming(
%{
"type" => "Undo",

View File

@ -29,6 +29,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
@actor_types ["Group", "Person", "Application"]
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_url(%{"id" => id}), do: id
@ -119,7 +121,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => "Event"} = object_data})
def insert_full_object(%{"object" => %{"type" => "Event"} = object_data, "type" => "Create"})
when is_map(object_data) do
with {:ok, object_data} <-
Converters.Event.as_to_model_data(object_data),
@ -128,7 +130,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
end
end
def insert_full_object(%{"object" => %{"type" => "Group"} = object_data})
def insert_full_object(%{"object" => %{"type" => "Group"} = object_data, "type" => "Create"})
when is_map(object_data) do
with object_data <-
Map.put(object_data, "preferred_username", object_data["preferredUsername"]),
@ -140,7 +142,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => "Note"} = object_data})
def insert_full_object(%{"object" => %{"type" => "Note"} = object_data, "type" => "Create"})
when is_map(object_data) do
with data <- Converters.Comment.as_to_model_data(object_data),
{:ok, %Comment{} = comment} <- Events.create_comment(data) do
@ -177,6 +179,39 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
def insert_full_object(_), do: {:ok, nil}
@doc """
Update an object
"""
@spec update_object(struct(), map()) :: {:ok, struct()} | any()
def update_object(object, object_data)
def update_object(event_url, %{
"object" => %{"type" => "Event"} = object_data,
"type" => "Update"
})
when is_map(object_data) do
with {:event_not_found, %Event{} = event} <-
{:event_not_found, Events.get_event_by_url(event_url)},
{:ok, object_data} <- Converters.Event.as_to_model_data(object_data),
{:ok, %Event{} = event} <- Events.update_event(event, object_data) do
{:ok, event}
end
end
def update_object(actor_url, %{
"object" => %{"type" => type_actor} = object_data,
"type" => "Update"
})
when is_map(object_data) and type_actor in @actor_types do
with {:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor_url),
object_data <- Converters.Actor.as_to_model_data(object_data),
{:ok, %Actor{} = actor} <- Actors.update_actor(actor, object_data) do
{:ok, actor}
end
end
def update_object(_, _), do: {:ok, nil}
#### Like-related helpers
# @doc """
@ -264,7 +299,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
String.t(),
map(),
list(),
map()
map(),
String.t()
) :: map()
def make_event_data(
actor,
@ -273,10 +309,12 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
content_html,
picture \\ nil,
tags \\ [],
metadata \\ %{}
metadata \\ %{},
uuid \\ nil,
url \\ nil
) do
Logger.debug("Making event data")
uuid = Ecto.UUID.generate()
uuid = uuid || Ecto.UUID.generate()
res = %{
"type" => "Event",
@ -285,9 +323,10 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"content" => content_html,
"name" => title,
"startTime" => metadata.begins_on,
"endTime" => metadata.ends_on,
"category" => metadata.category,
"actor" => actor,
"id" => Routes.page_url(Endpoint, :event, uuid),
"id" => url || Routes.page_url(Endpoint, :event, uuid),
"uuid" => uuid,
"tag" =>
tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end)
@ -505,7 +544,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
activity_id,
public
)
when type in ["Group", "Person", "Application"] do
when type in @actor_types do
do_make_announce_data(actor_url, actor_followers_url, url, url, activity_id, public)
end

View File

@ -1,5 +1,5 @@
# source: http://localhost:4000/api
# timestamp: Mon Sep 02 2019 16:41:17 GMT+0200 (GMT+02:00)
# timestamp: Thu Sep 05 2019 13:00:10 GMT+0200 (GMT+02:00)
schema {
query: RootQueryType
@ -440,7 +440,7 @@ enum EventVisibility {
"""Visible only to people members of the group or followers of the person"""
PRIVATE
"""Publically listed and federated. Can be shared."""
"""Publicly listed and federated. Can be shared."""
PUBLIC
"""Visible only to people with the link - or invited"""
@ -823,7 +823,7 @@ type RootMutationType {
"""Create an event"""
createEvent(
beginsOn: DateTime!
category: String
category: String = "meeting"
description: String!
endsOn: DateTime
onlineAddress: String
@ -852,11 +852,6 @@ type RootMutationType {
"""Create a group"""
createGroup(
"""
The actor's username which will be the admin (otherwise user's default one)
"""
adminActorUsername: String
"""
The avatar for the group, either as an object or directly the ID of an existing Picture
"""
@ -867,14 +862,17 @@ type RootMutationType {
"""
banner: PictureInput
"""The summary for the group"""
description: String = ""
"""The identity that creates the group"""
creatorActorId: Int!
"""The displayed name for the group"""
name: String
"""The name for the group"""
preferredUsername: String!
"""The summary for the group"""
summary: String = ""
): Group
"""Create a new person for user"""
@ -969,6 +967,34 @@ type RootMutationType {
"""Send a link through email to reset user password"""
sendResetPassword(email: String!, locale: String = "en"): String
"""Update an event"""
updateEvent(
beginsOn: DateTime
category: String
description: String
endsOn: DateTime
eventId: ID!
onlineAddress: String
options: EventOptionsInput
organizerActorId: ID
phoneAddress: String
physicalAddress: AddressInput
"""
The picture for the event, either as an object or directly the ID of an existing Picture
"""
picture: PictureInput
public: Boolean
publishAt: DateTime
state: Int
status: Int
"""The list of tags associated to the event"""
tags: [String]
title: String
visibility: EventVisibility
): Event
"""Update an identity"""
updatePerson(
"""

View File

@ -1,24 +1,24 @@
{
"type": "Update",
"object": {
"url": "http://mastodon.example.org/@gargron",
"url": "https://framapiaf.org/@framasoft",
"type": "Person",
"summary": "<p>Some bio</p>",
"publicKey": {
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n",
"owner": "http://mastodon.example.org/users/gargron",
"id": "http://mastodon.example.org/users/gargron#main-key"
"owner": "https://framapiaf.org/users/framasoft",
"id": "https://framapiaf.org/users/framasoft#main-key"
},
"preferredUsername": "gargron",
"outbox": "http://mastodon.example.org/users/gargron/outbox",
"name": "gargle",
"preferredUsername": "framasoft",
"outbox": "https://framapiaf.org/users/framasoft/outbox",
"name": "nextsoft",
"manuallyApprovesFollowers": false,
"inbox": "http://mastodon.example.org/users/gargron/inbox",
"id": "http://mastodon.example.org/users/gargron",
"following": "http://mastodon.example.org/users/gargron/following",
"followers": "http://mastodon.example.org/users/gargron/followers",
"inbox": "https://framapiaf.org/users/framasoft/inbox",
"id": "https://framapiaf.org/users/framasoft",
"following": "https://framapiaf.org/users/framasoft/following",
"followers": "https://framapiaf.org/users/framasoft/followers",
"endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox"
"sharedInbox": "https://framapiaf.org/inbox"
},
"icon":{
"type":"Image",
@ -31,8 +31,8 @@
"url":"https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png"
}
},
"id": "http://mastodon.example.org/users/gargron#updates/1519563538",
"actor": "http://mastodon.example.org/users/gargron",
"id": "https://framapiaf.org/users/gargron#updates/1519563538",
"actor": "https://framapiaf.org/users/framasoft",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",

View File

@ -270,10 +270,8 @@ defmodule Mobilizon.ActorsTest do
assert %Actor{} = actor
assert actor.summary == "some updated description"
assert actor.name == "some updated name"
assert actor.domain == "some updated domain"
assert actor.keys == "some updated keys"
refute actor.suspended
assert actor.preferred_username == "some updated username"
end
test "update_actor/2 with valid data updates the actor and it's media files", %{
@ -310,10 +308,8 @@ defmodule Mobilizon.ActorsTest do
assert %Actor{} = actor
assert actor.summary == "some updated description"
assert actor.name == "some updated name"
assert actor.domain == "some updated domain"
assert actor.keys == "some updated keys"
refute actor.suspended
assert actor.preferred_username == "some updated username"
refute File.exists?(
Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <>

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
import Mobilizon.Factory
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors
alias Mobilizon.Service.HTTPSignatures.Signature
@ -137,11 +138,14 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
end
describe "update" do
@updated_actor_summary "This is an updated actor"
test "it creates an update activity with the new actor data" do
actor = insert(:actor)
actor_data = MobilizonWeb.ActivityPub.ActorView.render("actor.json", %{actor: actor})
actor_data = Map.put(actor_data, "summary", @updated_actor_summary)
{:ok, update, _} =
{:ok, update, updated_actor} =
ActivityPub.update(%{
actor: actor_data["url"],
to: [actor.url <> "/followers"],
@ -153,6 +157,42 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
assert update.data["to"] == [actor.url <> "/followers"]
assert update.data["object"]["id"] == actor_data["id"]
assert update.data["object"]["type"] == actor_data["type"]
assert update.data["object"]["summary"] == @updated_actor_summary
refute updated_actor.summary == actor.summary
{:ok, %Actor{} = database_actor} = Mobilizon.Actors.get_actor_by_url(actor.url)
assert database_actor.summary == @updated_actor_summary
assert database_actor.preferred_username == actor.preferred_username
end
@updated_start_time DateTime.utc_now() |> DateTime.truncate(:second)
test "it creates an update activity with the new event data" do
actor = insert(:actor)
event = insert(:event, organizer_actor: actor)
event_data = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event)
event_data = Map.put(event_data, "startTime", @updated_start_time)
{:ok, update, updated_event} =
ActivityPub.update(%{
actor: actor.url,
to: [actor.url <> "/followers"],
cc: [],
object: event_data
})
assert update.data["actor"] == actor.url
assert update.data["to"] == [actor.url <> "/followers"]
assert update.data["object"]["id"] == event_data["id"]
assert update.data["object"]["type"] == event_data["type"]
assert update.data["object"]["startTime"] == @updated_start_time
refute updated_event.begins_on == event.begins_on
%Event{} = database_event = Mobilizon.Events.get_event_by_url(event.url)
assert database_event.begins_on == @updated_start_time
assert database_event.title == event.title
end
end
end

View File

@ -17,7 +17,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.ActorTest do
actor =
ActorConverter.as_to_model_data(%{
"type" => "Person",
"preferred_username" => "test_account"
"preferredUsername" => "test_account"
})
assert actor["type"] == :Person

View File

@ -330,7 +330,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert data["object"] == comment.url
end
test "it works for incoming update activities" do
test "it works for incoming update activities on actors" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
@ -349,11 +349,37 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data)
{:ok, %Actor{} = actor} = Actors.get_actor_by_url(data["actor"])
assert actor.name == "gargle"
assert actor.name == "nextsoft"
assert actor.summary == "<p>Some bio</p>"
end
test "it works for incoming update activities on events" do
data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("name", "My updated event")
|> Map.put("id", data["object"]["id"])
|> Map.put("type", "Event")
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data)
%Event{} = event = Events.get_event_by_url(data["object"]["id"])
assert event.title == "My updated event"
assert event.description == data["object"]["content"]
end
# test "it works for incoming update activities which lock the account" do
# data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()

View File

@ -58,6 +58,40 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
json_response(res, 200)["errors"]
end
test "create_event/3 should check the organizer_actor_id is owned by the user", %{
conn: conn,
user: user
} do
another_actor = insert(:actor)
begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
mutation = """
mutation {
createEvent(
title: "come to my event",
description: "it will be fine",
begins_on: "#{begins_on}",
organizer_actor_id: "#{another_actor.id}",
category: "birthday"
) {
title,
uuid
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["data"]["createEvent"] == nil
assert hd(json_response(res, 200)["errors"])["message"] ==
"Organizer actor id is not owned by the user"
end
test "create_event/3 creates an event", %{conn: conn, actor: actor, user: user} do
mutation = """
mutation {
@ -384,6 +418,166 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"]
end
test "update_event/3 should check the event exists", %{conn: conn, actor: _actor, user: user} do
mutation = """
mutation {
updateEvent(
event_id: 45,
title: "my event updated"
) {
title,
uuid,
tags {
title,
slug
}
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] == "Event not found"
end
test "update_event/3 should check the user is an administrator", %{
conn: conn,
actor: _actor,
user: user
} do
event = insert(:event)
mutation = """
mutation {
updateEvent(
title: "my event updated",
event_id: #{event.id}
) {
title,
uuid,
tags {
title,
slug
}
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] == "User doesn't own actor"
end
test "update_event/3 updates an event", %{conn: conn, actor: actor, user: user} do
event = insert(:event, organizer_actor: actor)
begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
mutation = """
mutation {
updateEvent(
title: "my event updated",
description: "description updated",
begins_on: "#{begins_on}",
event_id: #{event.id},
organizer_actor_id: "#{actor.id}",
category: "birthday",
tags: ["tag1_updated", "tag2_updated"]
) {
title,
uuid,
url,
tags {
title,
slug
}
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["updateEvent"]["title"] == "my event updated"
assert json_response(res, 200)["data"]["updateEvent"]["uuid"] == event.uuid
assert json_response(res, 200)["data"]["updateEvent"]["url"] == event.url
assert json_response(res, 200)["data"]["updateEvent"]["tags"] == [
%{"slug" => "tag1-updated", "title" => "tag1_updated"},
%{"slug" => "tag2-updated", "title" => "tag2_updated"}
]
end
test "update_event/3 updates an event with a new picture", %{
conn: conn,
actor: actor,
user: user
} do
event = insert(:event, organizer_actor: actor)
begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
mutation = """
mutation {
updateEvent(
title: "my event updated",
description: "description updated",
begins_on: "#{begins_on}",
event_id: #{event.id},
organizer_actor_id: "#{actor.id}",
category: "birthday",
picture: {
picture: {
name: "picture for my event",
alt: "A very sunny landscape",
file: "event.jpg",
actor_id: "#{actor.id}"
}
}
) {
title,
uuid,
url,
picture {
name,
url
}
}
}
"""
map = %{
"query" => mutation,
"event.jpg" => %Plug.Upload{
path: "test/fixtures/picture.png",
filename: "event.jpg"
}
}
res =
conn
|> auth_conn(user)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api", map)
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["updateEvent"]["title"] == "my event updated"
assert json_response(res, 200)["data"]["updateEvent"]["uuid"] == event.uuid
assert json_response(res, 200)["data"]["updateEvent"]["url"] == event.url
assert json_response(res, 200)["data"]["updateEvent"]["picture"]["name"] ==
"picture for my event"
end
test "list_events/3 returns events", context do
event = insert(:event)

View File

@ -67,8 +67,8 @@ defmodule Mobilizon.Factory do
def tag_factory do
%Mobilizon.Events.Tag{
title: "MyTag",
slug: sequence("MyTag")
title: sequence("MyTag"),
slug: sequence("my-tag")
}
end