Merge branch 'attach-picture-entities' into 'master'

Attach actor to pictures entity

Closes #129

See merge request framasoft/mobilizon!147
This commit is contained in:
Thomas Citharel 2019-06-03 15:54:37 +02:00
commit 4434459e59
15 changed files with 645 additions and 785 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="editor"> <div class="editor" id="tiptab-editor" :data-actor-id="loggedPerson && loggedPerson.id">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
<div class="menubar bar-is-hidden" :class="{ 'is-focused': focused }"> <div class="menubar bar-is-hidden" :class="{ 'is-focused': focused }">
@ -172,16 +172,25 @@ import {
} from 'tiptap-extensions'; } from 'tiptap-extensions';
import tippy, { Instance } from 'tippy.js'; import tippy, { Instance } from 'tippy.js';
import { SEARCH_PERSONS } from '@/graphql/search'; import { SEARCH_PERSONS } from '@/graphql/search';
import { IActor } from '@/types/actor'; import { IActor, IPerson } from '@/types/actor';
import Image from '@/components/Editor/Image'; import Image from '@/components/Editor/Image';
import { UPLOAD_PICTURE } from '@/graphql/upload'; import { UPLOAD_PICTURE } from '@/graphql/upload';
import { listenFileUpload } from '@/utils/upload'; import { listenFileUpload } from '@/utils/upload';
import { LOGGED_PERSON } from '@/graphql/actor';
@Component({ @Component({
components: { EditorContent, EditorMenuBar, EditorMenuBubble }, components: { EditorContent, EditorMenuBar, EditorMenuBubble },
apollo: {
loggedPerson: {
query: LOGGED_PERSON,
},
},
}) })
export default class CreateEvent extends Vue { export default class CreateEvent extends Vue {
@Prop({ required: true }) value!: String; @Prop({ required: true }) value!: String;
loggedPerson!: IPerson;
editor: Editor = null; editor: Editor = null;
/** /**
@ -429,6 +438,7 @@ export default class CreateEvent extends Vue {
variables: { variables: {
file: image, file: image,
name: image.name, name: image.name,
actorId: this.loggedPerson.id,
}, },
}); });
if (data.uploadPicture && data.uploadPicture.url) { if (data.uploadPicture && data.uploadPicture.url) {

View File

@ -71,11 +71,14 @@ export default class Image extends Node {
const { schema } = view.state; const { schema } = view.state;
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }); const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
const client = apolloProvider.defaultClient as ApolloClient<InMemoryCache>; const client = apolloProvider.defaultClient as ApolloClient<InMemoryCache>;
const editorElem = document.getElementById('tiptab-editor');
const actorId = editorElem && editorElem.dataset.actorId;
for (const image of images) { for (const image of images) {
const { data } = await client.mutate({ const { data } = await client.mutate({
mutation: UPLOAD_PICTURE, mutation: UPLOAD_PICTURE,
variables: { variables: {
actorId,
file: image, file: image,
name: image.name, name: image.name,
}, },

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
export const UPLOAD_PICTURE = gql` export const UPLOAD_PICTURE = gql`
mutation UploadPicture($file: Upload!, $alt: String, $name: String!){ mutation UploadPicture($file: Upload!, $alt: String, $name: String!, $actorId: ID!){
uploadPicture(file: $file, alt: $alt, name: $name) { uploadPicture(file: $file, alt: $alt, name: $name, actorId: $actorId) {
url, url,
id id
} }

View File

@ -5,9 +5,11 @@ defmodule Mobilizon.Media.Picture do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Media.File alias Mobilizon.Media.File
alias Mobilizon.Actors.Actor
schema "pictures" do schema "pictures" do
embeds_one(:file, File, on_replace: :update) embeds_one(:file, File, on_replace: :update)
belongs_to(:actor, Actor)
timestamps() timestamps()
end end
@ -15,7 +17,7 @@ defmodule Mobilizon.Media.Picture do
@doc false @doc false
def changeset(picture, attrs) do def changeset(picture, attrs) do
picture picture
|> cast(attrs, []) |> cast(attrs, [:actor_id])
|> cast_embed(:file) |> cast_embed(:file)
end end
end end

View File

@ -149,6 +149,18 @@ defmodule Mobilizon.Users.User do
def is_confirmed(%User{confirmed_at: nil} = _user), do: {:error, :unconfirmed} def is_confirmed(%User{confirmed_at: nil} = _user), do: {:error, :unconfirmed}
def is_confirmed(%User{} = user), do: {:ok, user} def is_confirmed(%User{} = user), do: {:ok, user}
@doc """
Returns whether an user owns an actor
"""
@spec owns_actor(struct(), String.t()) :: {:is_owned, false} | {:is_owned, true, Actor.t()}
def owns_actor(%User{} = user, actor_id) when is_binary(actor_id) do
case Integer.parse(actor_id) do
{actor_id, ""} -> owns_actor(user, actor_id)
_ -> {:is_owned, false}
end
end
@spec owns_actor(struct(), integer()) :: {:is_owned, false} | {:is_owned, true, Actor.t()}
def owns_actor(%User{actors: actors}, actor_id) do def owns_actor(%User{actors: actors}, actor_id) do
case Enum.find(actors, fn a -> a.id == actor_id end) do case Enum.find(actors, fn a -> a.id == actor_id end) do
nil -> {:is_owned, false} nil -> {:is_owned, false}

View File

@ -4,6 +4,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
""" """
alias Mobilizon.Media alias Mobilizon.Media
alias Mobilizon.Media.Picture alias Mobilizon.Media.Picture
alias Mobilizon.Users.User
@doc """ @doc """
Get picture for an event's pic Get picture for an event's pic
@ -43,16 +44,21 @@ defmodule MobilizonWeb.Resolvers.Picture do
end end
@spec upload_picture(map(), map(), map()) :: {:ok, Picture.t()} | {:error, any()} @spec upload_picture(map(), map(), map()) :: {:ok, Picture.t()} | {:error, any()}
def upload_picture(_parent, %{file: %Plug.Upload{} = file} = args, %{ def upload_picture(_parent, %{file: %Plug.Upload{} = file, actor_id: actor_id} = args, %{
context: %{ context: %{
current_user: _user current_user: user
} }
}) do }) do
with {:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(file), with {:is_owned, true, _actor} <- User.owns_actor(user, actor_id),
{:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(file),
args <- Map.put(args, :url, url), args <- Map.put(args, :url, url),
{:ok, picture = %Picture{}} <- Media.create_picture(%{"file" => args}) do {:ok, picture = %Picture{}} <-
Media.create_picture(%{"file" => args, "actor_id" => actor_id}) do
{:ok, %{name: picture.file.name, url: picture.file.url, id: picture.id}} {:ok, %{name: picture.file.name, url: picture.file.url, id: picture.id}}
else else
{:is_owned, false} ->
{:error, "Actor id is not owned by authenticated user"}
err -> err ->
{:error, err} {:error, err}
end end

View File

@ -31,7 +31,7 @@ defmodule MobilizonWeb.Router do
end end
pipeline :browser do pipeline :browser do
plug(Plug.Static, at: "/", from: "priv/static") plug(Plug.Static, at: "/", from: "priv/static/js")
plug(:accepts, ["html"]) plug(:accepts, ["html"])
plug(:fetch_session) plug(:fetch_session)
plug(:fetch_flash) plug(:fetch_flash)

View File

@ -26,6 +26,7 @@ defmodule MobilizonWeb.Schema.PictureType do
field(:name, non_null(:string)) field(:name, non_null(:string))
field(:alt, :string) field(:alt, :string)
field(:file, non_null(:upload)) field(:file, non_null(:upload))
field(:actor_id, :id)
end end
object :picture_queries do object :picture_queries do
@ -42,6 +43,7 @@ defmodule MobilizonWeb.Schema.PictureType do
arg(:name, non_null(:string)) arg(:name, non_null(:string))
arg(:alt, :string) arg(:alt, :string)
arg(:file, non_null(:upload)) arg(:file, non_null(:upload))
arg(:actor_id, non_null(:id))
resolve(&Picture.upload_picture/3) resolve(&Picture.upload_picture/3)
end end
end end

View File

@ -176,6 +176,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
# Repo.one(query) # Repo.one(query)
# end # end
@doc """
Save picture data from %Plug.Upload{} and return AS Link data.
"""
def make_picture_data(%Plug.Upload{} = picture) do def make_picture_data(%Plug.Upload{} = picture) do
with {:ok, picture} <- MobilizonWeb.Upload.store(picture) do with {:ok, picture} <- MobilizonWeb.Upload.store(picture) do
picture picture
@ -184,6 +187,10 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
end end
end end
@doc """
Convert a picture model into an AS Link representation
"""
# TODO: Move me to Mobilizon.Service.ActivityPub.Converters
def make_picture_data(%Picture{file: file} = _picture) do def make_picture_data(%Picture{file: file} = _picture) do
%{ %{
"type" => "Document", "type" => "Document",
@ -198,6 +205,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
} }
end end
@doc """
Save picture data from raw data and return AS Link data.
"""
def make_picture_data(%{picture: picture}) do def make_picture_data(%{picture: picture}) do
with {:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(picture.file), with {:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(picture.file),
{:ok, %Picture{file: _file} = pic} <- {:ok, %Picture{file: _file} = pic} <-
@ -205,7 +215,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"file" => %{ "file" => %{
"url" => url, "url" => url,
"name" => picture.name "name" => picture.name
} },
"actor_id" => picture.actor_id
}) do }) do
make_picture_data(pic) make_picture_data(pic)
end end

View File

@ -0,0 +1,9 @@
defmodule :"Elixir.Mobilizon.Repo.Migrations.Attach-pictures-to-actors" do
use Ecto.Migration
def change do
alter table(:pictures) do
add(:actor_id, references(:actors, on_delete: :delete_all), null: false)
end
end
end

File diff suppressed because it is too large Load Diff

View File

@ -22,17 +22,22 @@ defmodule Mobilizon.MediaTest do
test "get_picture!/1 returns the picture with given id" do test "get_picture!/1 returns the picture with given id" do
picture = insert(:picture) picture = insert(:picture)
assert Media.get_picture!(picture.id) == picture assert Media.get_picture!(picture.id).id == picture.id
end end
test "create_picture/1 with valid data creates a picture" do test "create_picture/1 with valid data creates a picture" do
assert {:ok, %Picture{} = picture} = Media.create_picture(@valid_attrs) assert {:ok, %Picture{} = picture} =
Media.create_picture(Map.put(@valid_attrs, :actor_id, insert(:actor).id))
assert picture.file.name == "something old" assert picture.file.name == "something old"
end end
test "update_picture/2 with valid data updates the picture" do test "update_picture/2 with valid data updates the picture" do
picture = insert(:picture) picture = insert(:picture)
assert {:ok, %Picture{} = picture} = Media.update_picture(picture, @update_attrs)
assert {:ok, %Picture{} = picture} =
Media.update_picture(picture, Map.put(@update_attrs, :actor_id, insert(:actor).id))
assert picture.file.name == "something new" assert picture.file.name == "something new"
end end

View File

@ -103,7 +103,8 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
picture: { picture: {
name: "picture for my event", name: "picture for my event",
alt: "A very sunny landscape", alt: "A very sunny landscape",
file: "event.jpg" file: "event.jpg",
actor_id: #{actor.id}
} }
} }
) { ) {
@ -148,7 +149,8 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
mutation { uploadPicture( mutation { uploadPicture(
name: "#{picture.name}", name: "#{picture.name}",
alt: "#{picture.alt}", alt: "#{picture.alt}",
file: "#{picture.file}" file: "#{picture.file}",
actor_id: #{actor.id}
) { ) {
id, id,
url, url,

View File

@ -7,8 +7,9 @@ defmodule MobilizonWeb.Resolvers.PictureResolverTest do
setup %{conn: conn} do setup %{conn: conn} do
user = insert(:user) user = insert(:user)
actor = insert(:actor, user: user)
{:ok, conn: conn, user: user} {:ok, conn: conn, user: user, actor: actor}
end end
describe "Resolver: Get picture" do describe "Resolver: Get picture" do
@ -56,14 +57,15 @@ defmodule MobilizonWeb.Resolvers.PictureResolverTest do
end end
describe "Resolver: Upload picture" do describe "Resolver: Upload picture" do
test "upload_picture/3 uploads a new picture", %{conn: conn, user: user} do test "upload_picture/3 uploads a new picture", %{conn: conn, user: user, actor: actor} do
picture = %{name: "my pic", alt: "represents something", file: "picture.png"} picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
mutation = """ mutation = """
mutation { uploadPicture( mutation { uploadPicture(
name: "#{picture.name}", name: "#{picture.name}",
alt: "#{picture.alt}", alt: "#{picture.alt}",
file: "#{picture.file}" file: "#{picture.file}",
actor_id: #{actor.id}
) { ) {
url, url,
name name
@ -92,14 +94,15 @@ defmodule MobilizonWeb.Resolvers.PictureResolverTest do
assert json_response(res, 200)["data"]["uploadPicture"]["url"] assert json_response(res, 200)["data"]["uploadPicture"]["url"]
end end
test "upload_picture/3 forbids uploading if no auth", %{conn: conn} do test "upload_picture/3 forbids uploading if no auth", %{conn: conn, actor: actor} do
picture = %{name: "my pic", alt: "represents something", file: "picture.png"} picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
mutation = """ mutation = """
mutation { uploadPicture( mutation { uploadPicture(
name: "#{picture.name}", name: "#{picture.name}",
alt: "#{picture.alt}", alt: "#{picture.alt}",
file: "#{picture.file}" file: "#{picture.file}",
actor_id: #{actor.id}
) { ) {
url, url,
name name

View File

@ -180,7 +180,8 @@ defmodule Mobilizon.Factory do
def picture_factory do def picture_factory do
%Mobilizon.Media.Picture{ %Mobilizon.Media.Picture{
file: build(:file) file: build(:file),
actor: build(:actor)
} }
end end
end end