forked from potsda.mn/mobilizon
Merge branch 'allow-to-remove-pictures' into 'master'
Allow to remove pictures and show user media size usage Closes #281 See merge request framasoft/mobilizon!721
This commit is contained in:
commit
a368c9542b
|
@ -19,7 +19,6 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
|||
code_reloader: true,
|
||||
check_origin: false,
|
||||
watchers: [
|
||||
# yarn: ["run", "dev", cd: Path.expand("../js", __DIR__)]
|
||||
node: [
|
||||
"node_modules/webpack/bin/webpack.js",
|
||||
"--mode",
|
||||
|
@ -53,8 +52,8 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
|||
patterns: [
|
||||
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
||||
~r{priv/gettext/.*(po)$},
|
||||
~r{lib/mobilizon_web/views/.*(ex)$},
|
||||
~r{lib/mobilizon_web/templates/.*(eex)$}
|
||||
~r{lib/web/(live|views)/.*(ex)$},
|
||||
~r{lib/web/templates/.*(eex)$}
|
||||
]
|
||||
]
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="root">
|
||||
<figure class="image" v-if="actualImageSrc">
|
||||
<img :src="actualImageSrc" />
|
||||
<figure class="image" v-if="imageSrc">
|
||||
<img :src="imageSrc" />
|
||||
</figure>
|
||||
<figure class="image is-128x128" v-else>
|
||||
<div class="image-placeholder">
|
||||
|
@ -9,12 +9,19 @@
|
|||
</div>
|
||||
</figure>
|
||||
|
||||
<b-upload @input="onFileChanged" :accept="accept">
|
||||
<a class="button is-primary">
|
||||
<b-icon icon="upload"></b-icon>
|
||||
<span>{{ $t("Click to upload") }}</span>
|
||||
</a>
|
||||
</b-upload>
|
||||
<div class="action-buttons">
|
||||
<b-field class="file is-primary">
|
||||
<b-upload @input="onFileChanged" :accept="accept" class="file-label">
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" icon="upload" />
|
||||
<span>{{ $t("Click to upload") }}</span>
|
||||
</span>
|
||||
</b-upload>
|
||||
</b-field>
|
||||
<b-button type="is-text" v-if="imageSrc" @click="removeOrClearPicture">
|
||||
{{ $t("Clear") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -45,16 +52,22 @@ figure.image {
|
|||
color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { IPicture } from "@/types/picture.model";
|
||||
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class PictureUpload extends Vue {
|
||||
@Model("change", { type: File }) readonly pictureFile!: File;
|
||||
|
||||
@Prop({ type: String, required: false }) defaultImageSrc!: string;
|
||||
@Prop({ type: Object, required: false }) defaultImage!: IPicture;
|
||||
|
||||
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" })
|
||||
accept!: string;
|
||||
|
@ -70,24 +83,40 @@ export default class PictureUpload extends Vue {
|
|||
})
|
||||
textFallback!: string;
|
||||
|
||||
imageSrc: string | null = null;
|
||||
imageSrc: string | null = this.defaultImage ? this.defaultImage.url : null;
|
||||
|
||||
file!: File | null;
|
||||
|
||||
mounted(): void {
|
||||
this.updatePreview(this.pictureFile);
|
||||
if (this.pictureFile) {
|
||||
this.updatePreview(this.pictureFile);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("pictureFile")
|
||||
onPictureFileChanged(val: File): void {
|
||||
console.log("onPictureFileChanged", val);
|
||||
this.updatePreview(val);
|
||||
}
|
||||
|
||||
onFileChanged(file: File): void {
|
||||
@Watch("defaultImage")
|
||||
onDefaultImageChange(defaultImage: IPicture): void {
|
||||
console.log("onDefaultImageChange", defaultImage);
|
||||
this.imageSrc = defaultImage ? defaultImage.url : null;
|
||||
}
|
||||
|
||||
onFileChanged(file: File | null): void {
|
||||
this.$emit("change", file);
|
||||
|
||||
this.updatePreview(file);
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
private updatePreview(file?: File) {
|
||||
async removeOrClearPicture(): Promise<void> {
|
||||
this.onFileChanged(null);
|
||||
}
|
||||
|
||||
private updatePreview(file?: File | null) {
|
||||
if (file) {
|
||||
this.imageSrc = URL.createObjectURL(file);
|
||||
return;
|
||||
|
@ -95,9 +124,5 @@ export default class PictureUpload extends Vue {
|
|||
|
||||
this.imageSrc = null;
|
||||
}
|
||||
|
||||
get actualImageSrc(): string | null {
|
||||
return this.imageSrc || this.defaultImageSrc;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -10,6 +10,7 @@ export const FETCH_PERSON = gql`
|
|||
summary
|
||||
preferredUsername
|
||||
suspended
|
||||
mediaSize
|
||||
avatar {
|
||||
id
|
||||
name
|
||||
|
@ -51,6 +52,7 @@ export const GET_PERSON = gql`
|
|||
summary
|
||||
preferredUsername
|
||||
suspended
|
||||
mediaSize
|
||||
avatar {
|
||||
id
|
||||
name
|
||||
|
|
|
@ -84,6 +84,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
|
|||
id
|
||||
url
|
||||
}
|
||||
mediaSize
|
||||
organizedEvents(
|
||||
afterDatetime: $afterDateTime
|
||||
beforeDatetime: $beforeDateTime
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import gql from "graphql-tag";
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export const UPLOAD_PICTURE = gql`
|
||||
mutation UploadPicture($file: Upload!, $alt: String, $name: String!) {
|
||||
uploadPicture(file: $file, alt: $alt, name: $name) {
|
||||
|
@ -9,3 +8,11 @@ export const UPLOAD_PICTURE = gql`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const REMOVE_PICTURE = gql`
|
||||
mutation RemovePicture($id: ID!) {
|
||||
removePicture(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -200,6 +200,7 @@ export const GET_USER = gql`
|
|||
currentSignInAt
|
||||
locale
|
||||
disabled
|
||||
mediaSize
|
||||
defaultActor {
|
||||
id
|
||||
}
|
||||
|
|
|
@ -799,5 +799,6 @@
|
|||
"Mobilizon uses a system of profiles to compartiment your activities. You will be able to create as many profiles as you want.": "Mobilizon uses a system of profiles to compartiment your activities. You will be able to create as many profiles as you want.",
|
||||
"Mobilizon is a federated software, meaning you can interact - depending on your admin's federation settings - with content from other instances, such as joining groups or events that were created elsewhere.": "Mobilizon is a federated software, meaning you can interact - depending on your admin federation settings - with content from other instances, such as joining groups or events that were created elsewhere.",
|
||||
"This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.": "This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.",
|
||||
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:"
|
||||
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:",
|
||||
"Uploaded media size": "Uploaded media size"
|
||||
}
|
||||
|
|
|
@ -887,5 +887,6 @@
|
|||
"Mobilizon uses a system of profiles to compartiment your activities. You will be able to create as many profiles as you want.": "Mobilizon utilise un système de profils pour compartimenter vos activités. Vous pourrez créer autant de profils que vous voulez.",
|
||||
"Mobilizon is a federated software, meaning you can interact - depending on your admin's federation settings - with content from other instances, such as joining groups or events that were created elsewhere.": "Mobilizon est un logiciel fédéré, ce qui signifie que vous pouvez interagir - en fonction des paramètres de fédération de votre administrateur·ice - avec du contenu d'autres instances, comme par exemple rejoindre des groupes ou des événements ayant été créés ailleurs.",
|
||||
"This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.": "Cette instance, <b>{instanceName} ({domain})</b>, héberge votre profil, donc notez bien son nom.",
|
||||
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :"
|
||||
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :",
|
||||
"Uploaded media size": "Taille des médias téléversés"
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface IActor {
|
|||
url: string;
|
||||
name: string;
|
||||
domain: string | null;
|
||||
mediaSize: number;
|
||||
summary: string;
|
||||
preferredUsername: string;
|
||||
suspended: boolean;
|
||||
|
@ -30,6 +31,8 @@ export class Actor implements IActor {
|
|||
|
||||
domain: string | null = null;
|
||||
|
||||
mediaSize = 0;
|
||||
|
||||
name = "";
|
||||
|
||||
preferredUsername = "";
|
||||
|
|
|
@ -39,6 +39,7 @@ export interface IUser extends ICurrentUser {
|
|||
actors: IPerson[];
|
||||
disabled: boolean;
|
||||
participations: Paginate<IParticipant>;
|
||||
mediaSize: number;
|
||||
drafts: IEvent[];
|
||||
settings: IUserSettings;
|
||||
locale: string;
|
||||
|
|
|
@ -69,7 +69,7 @@ interface IEventEditJSON {
|
|||
visibility: EventVisibility;
|
||||
joinOptions: EventJoinOptions;
|
||||
draft: boolean;
|
||||
picture: IPicture | { pictureId: string } | null;
|
||||
picture?: IPicture | { pictureId: string } | null;
|
||||
attributedToId: string | null;
|
||||
onlineAddress?: string;
|
||||
phoneAddress?: string;
|
||||
|
@ -234,7 +234,6 @@ export class EventModel implements IEvent {
|
|||
joinOptions: this.joinOptions,
|
||||
draft: this.draft,
|
||||
tags: this.tags.map((t) => t.title),
|
||||
picture: this.picture,
|
||||
onlineAddress: this.onlineAddress,
|
||||
phoneAddress: this.phoneAddress,
|
||||
physicalAddress: this.physicalAddress,
|
||||
|
|
|
@ -18,4 +18,17 @@ function localeShortWeekDayNames(): string[] {
|
|||
return weekDayNames;
|
||||
}
|
||||
|
||||
export { localeMonthNames, localeShortWeekDayNames };
|
||||
// https://stackoverflow.com/a/18650828/10204399
|
||||
function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export { localeMonthNames, localeShortWeekDayNames, formatBytes };
|
||||
|
|
|
@ -9,7 +9,7 @@ export async function buildFileFromIPicture(obj: IPicture | null | undefined): P
|
|||
return new File([blob], obj.name);
|
||||
}
|
||||
|
||||
export function buildFileVariable<T>(file: File | null, name: string, alt?: string): Record<string, unknown> {
|
||||
export function buildFileVariable(file: File | null, name: string, alt?: string): Record<string, unknown> {
|
||||
if (!file) return {};
|
||||
|
||||
return {
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<span v-else>{{ $t("I create an identity") }}</span>
|
||||
</h1>
|
||||
|
||||
<picture-upload v-model="avatarFile" :defaultImageSrc="avatarUrl" class="picture-upload" />
|
||||
<picture-upload v-model="avatarFile" :defaultImage="identity.avatar" class="picture-upload" />
|
||||
|
||||
<b-field horizontal :label="$t('Display name')">
|
||||
<b-input
|
||||
|
@ -124,6 +124,7 @@ h1 {
|
|||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { IPicture } from "@/types/picture.model";
|
||||
import {
|
||||
CREATE_PERSON,
|
||||
CURRENT_ACTOR_CLIENT,
|
||||
|
@ -136,7 +137,7 @@ import { IPerson, Person } from "../../../types/actor";
|
|||
import PictureUpload from "../../../components/PictureUpload.vue";
|
||||
import { MOBILIZON_INSTANCE_HOST } from "../../../api/_entrypoint";
|
||||
import RouteName from "../../../router/name";
|
||||
import { buildFileVariable } from "../../../utils/image";
|
||||
import { buildFileFromIPicture, buildFileVariable } from "../../../utils/image";
|
||||
import { changeIdentity } from "../../../utils/auth";
|
||||
import identityEditionMixin from "../../../mixins/identityEdition";
|
||||
|
||||
|
@ -186,13 +187,6 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
|||
) as string;
|
||||
}
|
||||
|
||||
get avatarUrl(): string | null {
|
||||
if (this.identity && this.identity.avatar && this.identity.avatar.url) {
|
||||
return this.identity.avatar.url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Watch("isUpdate")
|
||||
async isUpdateChanged(): Promise<void> {
|
||||
this.resetFields();
|
||||
|
@ -286,7 +280,6 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
|||
}
|
||||
},
|
||||
});
|
||||
this.avatarFile = null;
|
||||
|
||||
this.$notifier.success(
|
||||
this.$t("Identity {displayName} updated", {
|
||||
|
|
|
@ -198,6 +198,7 @@
|
|||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { GET_GROUP, REFRESH_PROFILE } from "@/graphql/group";
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import { SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
||||
import { IGroup, MemberRole } from "../../types/actor";
|
||||
import { usernameWithDomain, IActor } from "../../types/actor/actor.model";
|
||||
|
@ -258,6 +259,10 @@ export default class AdminGroupProfile extends Vue {
|
|||
key: this.$t("Domain") as string,
|
||||
value: (this.group.domain ? this.group.domain : this.$t("Local")) as string,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Uploaded media size") as string,
|
||||
value: formatBytes(this.group.mediaSize),
|
||||
},
|
||||
];
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -126,11 +126,11 @@
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import { GET_PERSON, SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||
|
||||
const EVENTS_PER_PAGE = 10;
|
||||
|
@ -171,9 +171,9 @@ export default class AdminProfile extends Vue {
|
|||
|
||||
participationsPage = 1;
|
||||
|
||||
get metadata(): Array<object> {
|
||||
get metadata(): Array<Record<string, unknown>> {
|
||||
if (!this.person) return [];
|
||||
const res: object[] = [
|
||||
const res: Record<string, unknown>[] = [
|
||||
{
|
||||
key: this.$t("Status") as string,
|
||||
value: this.person.suspended ? this.$t("Suspended") : this.$t("Active"),
|
||||
|
@ -182,6 +182,10 @@ export default class AdminProfile extends Vue {
|
|||
key: this.$t("Domain") as string,
|
||||
value: this.person.domain ? this.person.domain : this.$t("Local"),
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Uploaded media size"),
|
||||
value: formatBytes(this.person.mediaSize),
|
||||
},
|
||||
];
|
||||
if (!this.person.domain && this.person.user) {
|
||||
res.push({
|
||||
|
@ -193,7 +197,7 @@ export default class AdminProfile extends Vue {
|
|||
return res;
|
||||
}
|
||||
|
||||
async suspendProfile() {
|
||||
async suspendProfile(): Promise<void> {
|
||||
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||
mutation: SUSPEND_PROFILE,
|
||||
variables: {
|
||||
|
@ -229,7 +233,7 @@ export default class AdminProfile extends Vue {
|
|||
});
|
||||
}
|
||||
|
||||
async unsuspendProfile() {
|
||||
async unsuspendProfile(): Promise<void> {
|
||||
const profileID = this.id;
|
||||
this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
||||
mutation: UNSUSPEND_PROFILE,
|
||||
|
@ -249,7 +253,7 @@ export default class AdminProfile extends Vue {
|
|||
});
|
||||
}
|
||||
|
||||
async onOrganizedEventsPageChange(page: number) {
|
||||
async onOrganizedEventsPageChange(page: number): Promise<void> {
|
||||
this.organizedEventsPage = page;
|
||||
await this.$apollo.queries.person.fetchMore({
|
||||
variables: {
|
||||
|
@ -274,7 +278,7 @@ export default class AdminProfile extends Vue {
|
|||
});
|
||||
}
|
||||
|
||||
async onParticipationsPageChange(page: number) {
|
||||
async onParticipationsPageChange(page: number): Promise<void> {
|
||||
this.participationsPage = page;
|
||||
await this.$apollo.queries.person.fetchMore({
|
||||
variables: {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</nav>
|
||||
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr v-for="{ key, value, link, elements } in metadata" :key="key">
|
||||
<tr v-for="{ key, value, link, elements, type } in metadata" :key="key">
|
||||
<td>{{ key }}</td>
|
||||
<td v-if="elements && elements.length > 0">
|
||||
<ul v-for="{ value, link: elementLink, active } in elements" :key="value">
|
||||
|
@ -46,6 +46,9 @@
|
|||
{{ value }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td v-else-if="type == 'code'">
|
||||
<code>{{ value }}</code>
|
||||
</td>
|
||||
<td v-else>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -60,6 +63,7 @@
|
|||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import { GET_USER, SUSPEND_USER } from "../../graphql/user";
|
||||
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
@ -139,11 +143,16 @@ export default class AdminUserProfile extends Vue {
|
|||
{
|
||||
key: this.$i18n.t("Last IP adress"),
|
||||
value: this.user.currentSignInIp || this.$t("Unknown"),
|
||||
type: "code",
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Participations"),
|
||||
value: this.user.participations.total,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Uploaded media size"),
|
||||
value: formatBytes(this.user.mediaSize),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,11 @@
|
|||
|
||||
<form ref="form">
|
||||
<subtitle>{{ $t("General information") }}</subtitle>
|
||||
<picture-upload v-model="pictureFile" :textFallback="$t('Headline picture')" />
|
||||
<picture-upload
|
||||
v-model="pictureFile"
|
||||
:textFallback="$t('Headline picture')"
|
||||
:defaultImage="event.picture"
|
||||
/>
|
||||
|
||||
<b-field :label="$t('Title')" :type="checkTitleLength[0]" :message="checkTitleLength[1]">
|
||||
<b-input size="is-large" aria-required="true" required v-model="event.title" />
|
||||
|
@ -676,6 +680,7 @@ export default class EditEvent extends Vue {
|
|||
__typename: "Person",
|
||||
id: organizerActor.id,
|
||||
participations: {
|
||||
__typename: "PaginatedParticipantList",
|
||||
total: 1,
|
||||
elements: [
|
||||
{
|
||||
|
@ -763,11 +768,13 @@ export default class EditEvent extends Vue {
|
|||
res.endsOn = null;
|
||||
}
|
||||
|
||||
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
||||
res = { ...res, ...pictureObj };
|
||||
if (this.pictureFile) {
|
||||
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
||||
res = { ...res, ...pictureObj };
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.event.picture) {
|
||||
if (this.event.picture && this.pictureFile) {
|
||||
const oldPictureFile = (await buildFileFromIPicture(this.event.picture)) as File;
|
||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="container section" v-if="isCurrentActorAGroupAdmin">
|
||||
<section class="container section" v-if="group && isCurrentActorAGroupAdmin">
|
||||
<form @submit.prevent="updateGroup">
|
||||
<b-field :label="$t('Group name')">
|
||||
<b-input v-model="group.name" />
|
||||
|
@ -43,7 +43,7 @@
|
|||
<picture-upload
|
||||
:textFallback="$t('Avatar')"
|
||||
v-model="avatarFile"
|
||||
:defaultImageSrc="group.avatar ? group.avatar.url : null"
|
||||
:defaultImage="group.avatar"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
|||
<picture-upload
|
||||
:textFallback="$t('Banner')"
|
||||
v-model="bannerFile"
|
||||
:defaultImageSrc="group.banner ? group.banner.url : null"
|
||||
:defaultImage="group.banner"
|
||||
/>
|
||||
</b-field>
|
||||
<p class="label">{{ $t("Group visibility") }}</p>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<picture-upload
|
||||
v-model="pictureFile"
|
||||
:textFallback="$t('Headline picture')"
|
||||
:defaultImageSrc="post.picture ? post.picture.url : null"
|
||||
:defaultImage="post.picture"
|
||||
/>
|
||||
|
||||
<b-field
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.{Media, Users}
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
@doc """
|
||||
|
@ -37,8 +38,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
|||
size: file.size
|
||||
}}
|
||||
|
||||
_error ->
|
||||
{:error, dgettext("errors", "Picture with ID %{id} was not found", id: picture_id)}
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -46,7 +47,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
|||
def upload_picture(
|
||||
_parent,
|
||||
%{file: %Plug.Upload{} = file} = args,
|
||||
%{context: %{current_user: user}}
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:ok, %{name: _name, url: url, content_type: content_type, size: size}} <-
|
||||
|
@ -75,7 +76,74 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
|||
end
|
||||
end
|
||||
|
||||
def upload_picture(_parent, _args, _resolution) do
|
||||
{:error, dgettext("errors", "You need to login to upload a picture")}
|
||||
def upload_picture(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@doc """
|
||||
Remove a picture that the user owns
|
||||
"""
|
||||
@spec remove_picture(map(), map(), map()) ::
|
||||
{:ok, Picture.t()}
|
||||
| {:error, :unauthorized}
|
||||
| {:error, :unauthenticated}
|
||||
| {:error, :not_found}
|
||||
def remove_picture(_parent, %{id: picture_id}, %{context: %{current_user: %User{} = user}}) do
|
||||
with {:picture, %Picture{actor_id: actor_id} = picture} <-
|
||||
{:picture, Media.get_picture(picture_id)},
|
||||
{:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id) do
|
||||
Media.delete_picture(picture)
|
||||
else
|
||||
{:picture, nil} -> {:error, :not_found}
|
||||
{:is_owned, _} -> {:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_picture(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@doc """
|
||||
Return the total media size for an actor
|
||||
"""
|
||||
@spec actor_size(map(), map(), map()) ::
|
||||
{:ok, integer()} | {:error, :unauthorized} | {:error, :unauthenticated}
|
||||
def actor_size(%Actor{id: actor_id}, _args, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
}) do
|
||||
if can_get_actor_size?(user, actor_id) do
|
||||
{:ok, Media.media_size_for_actor(actor_id)}
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
def actor_size(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@doc """
|
||||
Return the total media size for a local user
|
||||
"""
|
||||
@spec user_size(map(), map(), map()) ::
|
||||
{:ok, integer()} | {:error, :unauthorized} | {:error, :unauthenticated}
|
||||
def user_size(%User{id: user_id}, _args, %{
|
||||
context: %{current_user: %User{} = logged_user}
|
||||
}) do
|
||||
if can_get_user_size?(logged_user, user_id) do
|
||||
{:ok, Media.media_size_for_user(user_id)}
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
def user_size(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@spec can_get_user_size?(User.t(), integer()) :: boolean()
|
||||
defp can_get_actor_size?(%User{role: role} = user, actor_id) do
|
||||
role in [:moderator, :administrator] || owns_actor?(User.owns_actor(user, actor_id))
|
||||
end
|
||||
|
||||
@spec owns_actor?({:is_owned, Actor.t() | nil}) :: boolean()
|
||||
defp owns_actor?({:is_owned, %Actor{} = _actor}), do: true
|
||||
defp owns_actor?({:is_owned, _}), do: false
|
||||
|
||||
@spec can_get_user_size?(User.t(), integer()) :: boolean()
|
||||
defp can_get_user_size?(%User{role: role, id: logged_user_id}, user_id) do
|
||||
user_id == logged_user_id || role in [:moderator, :administrator]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -525,6 +525,28 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
end
|
||||
end
|
||||
|
||||
def user_medias(%User{id: user_id}, %{page: page, limit: limit}, %{
|
||||
context: %{current_user: %User{id: logged_in_user_id}}
|
||||
})
|
||||
when user_id == logged_in_user_id do
|
||||
%{elements: elements, total: total} = Mobilizon.Media.pictures_for_user(user_id, page, limit)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
elements:
|
||||
Enum.map(elements, fn element ->
|
||||
%{
|
||||
name: element.file.name,
|
||||
url: element.file.url,
|
||||
id: element.id,
|
||||
content_type: element.file.content_type,
|
||||
size: element.file.size
|
||||
}
|
||||
end),
|
||||
total: total
|
||||
}}
|
||||
end
|
||||
|
||||
@spec update_user_login_information(User.t(), map()) ::
|
||||
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp update_user_login_information(
|
||||
|
|
|
@ -37,6 +37,8 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
|
|||
field(:followersCount, :integer, description: "Number of followers for this actor")
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
field(:media_size, :integer, description: "The total size of the media from this actor")
|
||||
|
||||
resolve_type(fn
|
||||
%Actor{type: :Person}, _ ->
|
||||
:person
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
|
|||
Schema representation for Group.
|
||||
"""
|
||||
|
||||
alias Mobilizon.GraphQL.Resolvers.Picture
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
@desc """
|
||||
|
@ -34,5 +35,10 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
|
|||
field(:followers, list_of(:follower), description: "List of followers")
|
||||
field(:followersCount, :integer, description: "Number of followers for this actor")
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.actor_size/3,
|
||||
description: "The total size of the media from this actor"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
|||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.GraphQL.Resolvers.{Discussion, Group, Member, Post, Resource, Todos}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Discussion, Group, Member, Picture, Post, Resource, Todos}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.Actors.MemberType)
|
||||
|
@ -52,6 +52,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
|||
field(:followersCount, :integer, description: "Number of followers for this actor")
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.actor_size/3,
|
||||
description: "The total size of the media from this actor"
|
||||
)
|
||||
|
||||
# This one should have a privacy setting
|
||||
field :organized_events, :paginated_event_list do
|
||||
arg(:after_datetime, :datetime,
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
|||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.GraphQL.Resolvers.Person
|
||||
alias Mobilizon.GraphQL.Resolvers.{Person, Picture}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.Events.FeedTokenType)
|
||||
|
@ -49,6 +49,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
|||
field(:followersCount, :integer, description: "Number of followers for this actor")
|
||||
field(:followingCount, :integer, description: "Number of actors following this actor")
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.actor_size/3,
|
||||
description: "The total size of the media from this actor"
|
||||
)
|
||||
|
||||
field(:feed_tokens, list_of(:feed_token),
|
||||
resolve: dataloader(Events),
|
||||
description: "A list of the feed tokens for this person"
|
||||
|
|
|
@ -16,6 +16,14 @@ defmodule Mobilizon.GraphQL.Schema.PictureType do
|
|||
field(:size, :integer, description: "The picture's size")
|
||||
end
|
||||
|
||||
@desc """
|
||||
A paginated list of pictures
|
||||
"""
|
||||
object :paginated_picture_list do
|
||||
field(:elements, list_of(:picture), description: "The list of pictures")
|
||||
field(:total, :integer, description: "The total number of pictures in the list")
|
||||
end
|
||||
|
||||
@desc "An attached picture or a link to a picture"
|
||||
input_object :picture_input do
|
||||
# Either a full picture object
|
||||
|
@ -35,7 +43,7 @@ defmodule Mobilizon.GraphQL.Schema.PictureType do
|
|||
object :picture_queries do
|
||||
@desc "Get a picture"
|
||||
field :picture, :picture do
|
||||
arg(:id, non_null(:string), description: "The picture ID")
|
||||
arg(:id, non_null(:id), description: "The picture ID")
|
||||
resolve(&Picture.picture/3)
|
||||
end
|
||||
end
|
||||
|
@ -48,5 +56,13 @@ defmodule Mobilizon.GraphQL.Schema.PictureType do
|
|||
arg(:file, non_null(:upload), description: "The picture file")
|
||||
resolve(&Picture.upload_picture/3)
|
||||
end
|
||||
|
||||
@desc """
|
||||
Remove a picture
|
||||
"""
|
||||
field :remove_picture, :deleted_object do
|
||||
arg(:id, non_null(:id), description: "The picture's ID")
|
||||
resolve(&Picture.remove_picture/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,7 +26,7 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
|
|||
)
|
||||
|
||||
field(:picture, :picture,
|
||||
description: "The event's picture",
|
||||
description: "The posts's picture",
|
||||
resolve: &Picture.picture/3
|
||||
)
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
|||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.GraphQL.Resolvers.User
|
||||
alias Mobilizon.GraphQL.Resolvers.{Picture, User}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.SortType)
|
||||
|
@ -110,6 +110,21 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
|||
field(:current_sign_in_ip, :string,
|
||||
description: "The IP adress the user's currently signed-in with"
|
||||
)
|
||||
|
||||
field(:media, :paginated_picture_list, description: "The user's media objects") do
|
||||
arg(:page, :integer,
|
||||
default_value: 1,
|
||||
description: "The page in the paginated user media list"
|
||||
)
|
||||
|
||||
arg(:limit, :integer, default_value: 10, description: "The limit of user media per page")
|
||||
resolve(&User.user_medias/3)
|
||||
end
|
||||
|
||||
field(:media_size, :integer,
|
||||
resolve: &Picture.user_size/3,
|
||||
description: "The total size of all the media from this user (from all their actors)"
|
||||
)
|
||||
end
|
||||
|
||||
@desc "The list of roles an user can have"
|
||||
|
|
|
@ -7,8 +7,10 @@ defmodule Mobilizon.Media do
|
|||
|
||||
alias Ecto.Multi
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.{File, Picture}
|
||||
alias Mobilizon.Storage.Repo
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Web.Upload
|
||||
|
||||
|
@ -35,6 +37,52 @@ defmodule Mobilizon.Media do
|
|||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
List the paginated picture for an actor
|
||||
"""
|
||||
@spec pictures_for_actor(integer | String.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def pictures_for_actor(actor_id, page, limit) do
|
||||
actor_id
|
||||
|> pictures_for_actor_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
List the paginated picture for user
|
||||
"""
|
||||
@spec pictures_for_user(integer | String.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def pictures_for_user(user_id, page, limit) do
|
||||
user_id
|
||||
|> pictures_for_user_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the sum of media size used by the user
|
||||
"""
|
||||
@spec media_size_for_actor(integer | String.t()) :: integer()
|
||||
def media_size_for_actor(actor_id) do
|
||||
actor_id
|
||||
|> pictures_for_actor_query()
|
||||
|> select([:file])
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.file.size)
|
||||
|> Enum.sum()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the sum of media size used by the user
|
||||
"""
|
||||
@spec media_size_for_user(integer | String.t()) :: integer()
|
||||
def media_size_for_user(user_id) do
|
||||
user_id
|
||||
|> pictures_for_user_query()
|
||||
|> select([:file])
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.file.size)
|
||||
|> Enum.sum()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a picture.
|
||||
"""
|
||||
|
@ -84,4 +132,19 @@ defmodule Mobilizon.Media do
|
|||
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
|
||||
)
|
||||
end
|
||||
|
||||
@spec pictures_for_actor_query(integer() | String.t()) :: Ecto.Query.t()
|
||||
defp pictures_for_actor_query(actor_id) do
|
||||
Picture
|
||||
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
||||
|> where([_p, a], a.id == ^actor_id)
|
||||
end
|
||||
|
||||
@spec pictures_for_user_query(integer() | String.t()) :: Ecto.Query.t()
|
||||
defp pictures_for_user_query(user_id) do
|
||||
Picture
|
||||
|> join(:inner, [p], a in Actor, on: p.actor_id == a.id)
|
||||
|> join(:inner, [_p, a], u in User, on: a.user_id == u.id)
|
||||
|> where([_p, _a, u], u.id == ^user_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -609,22 +609,22 @@ msgstr "Vous n'avez pas la permission de supprimer ce jeton"
|
|||
#, elixir-format
|
||||
#: lib/graphql/resolvers/admin.ex:52
|
||||
msgid "You need to be logged-in and a moderator to list action logs"
|
||||
msgstr "Vous devez être connecté·e pour rejoindre un groupe"
|
||||
msgstr "Vous devez être connecté·e et une modérateur·ice pour lister les journaux de modération"
|
||||
|
||||
#, elixir-format
|
||||
#: lib/graphql/resolvers/report.ex:26
|
||||
msgid "You need to be logged-in and a moderator to list reports"
|
||||
msgstr "Vous devez être connecté·e pour rejoindre un groupe"
|
||||
msgstr "Vous devez être connecté·e et une modérateur·ice pour lister les signalements"
|
||||
|
||||
#, elixir-format
|
||||
#: lib/graphql/resolvers/report.ex:101
|
||||
msgid "You need to be logged-in and a moderator to update a report"
|
||||
msgstr "Vous devez être connecté·e pour supprimer un groupe"
|
||||
msgstr "Vous devez être connecté·e et une modérateur·ice pour modifier un signalement"
|
||||
|
||||
#, elixir-format
|
||||
#: lib/graphql/resolvers/report.ex:41
|
||||
msgid "You need to be logged-in and a moderator to view a report"
|
||||
msgstr "Vous devez être connecté·e pour rejoindre un groupe"
|
||||
msgstr "Vous devez être connecté·e pour et une modérateur·ice pour visionner un signalement"
|
||||
|
||||
#, elixir-format
|
||||
#: lib/graphql/resolvers/admin.ex:236
|
||||
|
@ -689,7 +689,7 @@ msgstr "Vous devez être connecté·e pour supprimer un groupe"
|
|||
#, elixir-format
|
||||
#: lib/graphql/resolvers/participant.ex:105
|
||||
msgid "You need to be logged-in to join an event"
|
||||
msgstr "Vous devez être connecté·e pour rejoindre un groupe"
|
||||
msgstr "Vous devez être connecté·e pour rejoindre un événement"
|
||||
|
||||
#, elixir-format
|
||||
#: lib/graphql/resolvers/participant.ex:204
|
||||
|
|
31
priv/repo/migrations/20201120161229_fix_picture_deletion.exs
Normal file
31
priv/repo/migrations/20201120161229_fix_picture_deletion.exs
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule Mobilizon.Storage.Repo.Migrations.FixPictureDeletion do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
drop_if_exists(constraint(:posts, "posts_picture_id_fkey"))
|
||||
|
||||
alter table(:posts) do
|
||||
modify(:picture_id, references(:pictures, on_delete: :nilify_all))
|
||||
end
|
||||
|
||||
drop_if_exists(constraint(:events, "events_picture_id_fkey"))
|
||||
|
||||
alter table(:events) do
|
||||
modify(:picture_id, references(:pictures, on_delete: :nilify_all))
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop_if_exists(constraint(:posts, "posts_picture_id_fkey"))
|
||||
|
||||
alter table(:posts) do
|
||||
modify(:picture_id, references(:pictures, on_delete: :delete_all))
|
||||
end
|
||||
|
||||
drop_if_exists(constraint(:events, "events_picture_id_fkey"))
|
||||
|
||||
alter table(:events) do
|
||||
modify(:picture_id, references(:pictures, on_delete: :delete_all))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,6 +10,9 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
|||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
@default_picture_details %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
@default_picture_path "test/fixtures/picture.png"
|
||||
|
||||
setup %{conn: conn} do
|
||||
user = insert(:user)
|
||||
actor = insert(:actor, user: user)
|
||||
|
@ -17,53 +20,59 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
|||
{:ok, conn: conn, user: user, actor: actor}
|
||||
end
|
||||
|
||||
@picture_query """
|
||||
query Picture($id: ID!) {
|
||||
picture(id: $id) {
|
||||
id
|
||||
name,
|
||||
alt,
|
||||
url,
|
||||
content_type,
|
||||
size
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@upload_picture_mutation """
|
||||
mutation UploadPicture($name: String!, $alt: String, $file: Upload!) {
|
||||
uploadPicture(
|
||||
name: $name
|
||||
alt: $alt
|
||||
file: $file
|
||||
) {
|
||||
url
|
||||
name
|
||||
content_type
|
||||
size
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
describe "Resolver: Get picture" do
|
||||
test "picture/3 returns the information on a picture", context do
|
||||
test "picture/3 returns the information on a picture", %{conn: conn} do
|
||||
%Picture{id: id} = picture = insert(:picture)
|
||||
|
||||
query = """
|
||||
{
|
||||
picture(id: "#{id}") {
|
||||
name,
|
||||
alt,
|
||||
url,
|
||||
content_type,
|
||||
size
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
context.conn
|
||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "picture"))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: id})
|
||||
|
||||
assert json_response(res, 200)["data"]["picture"]["name"] == picture.file.name
|
||||
assert res["data"]["picture"]["name"] == picture.file.name
|
||||
|
||||
assert json_response(res, 200)["data"]["picture"]["content_type"] ==
|
||||
assert res["data"]["picture"]["content_type"] ==
|
||||
picture.file.content_type
|
||||
|
||||
assert json_response(res, 200)["data"]["picture"]["size"] == 13_120
|
||||
assert res["data"]["picture"]["size"] == 13_120
|
||||
|
||||
assert json_response(res, 200)["data"]["picture"]["url"] =~ Endpoint.url()
|
||||
assert res["data"]["picture"]["url"] =~ Endpoint.url()
|
||||
end
|
||||
|
||||
test "picture/3 returns nothing on a non-existent picture", context do
|
||||
query = """
|
||||
{
|
||||
picture(id: "3") {
|
||||
name,
|
||||
alt,
|
||||
url
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "picture/3 returns nothing on a non-existent picture", %{conn: conn} do
|
||||
res =
|
||||
context.conn
|
||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "picture"))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: 3})
|
||||
|
||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
||||
"Picture with ID 3 was not found"
|
||||
assert hd(res["errors"])["message"] == "Resource not found"
|
||||
assert hd(res["errors"])["status_code"] == 404
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -71,22 +80,9 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
|||
test "upload_picture/3 uploads a new picture", %{conn: conn, user: user} do
|
||||
picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
|
||||
mutation = """
|
||||
mutation { uploadPicture(
|
||||
name: "#{picture.name}",
|
||||
alt: "#{picture.alt}",
|
||||
file: "#{picture.file}"
|
||||
) {
|
||||
url,
|
||||
name,
|
||||
content_type,
|
||||
size
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
map = %{
|
||||
"query" => mutation,
|
||||
"query" => @upload_picture_mutation,
|
||||
"variables" => picture,
|
||||
picture.file => %Plug.Upload{
|
||||
path: "test/fixtures/picture.png",
|
||||
filename: picture.file
|
||||
|
@ -101,30 +97,20 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
|||
"/api",
|
||||
map
|
||||
)
|
||||
|> json_response(200)
|
||||
|
||||
assert json_response(res, 200)["data"]["uploadPicture"]["name"] == picture.name
|
||||
assert json_response(res, 200)["data"]["uploadPicture"]["content_type"] == "image/png"
|
||||
assert json_response(res, 200)["data"]["uploadPicture"]["size"] == 10_097
|
||||
assert json_response(res, 200)["data"]["uploadPicture"]["url"]
|
||||
assert res["data"]["uploadPicture"]["name"] == picture.name
|
||||
assert res["data"]["uploadPicture"]["content_type"] == "image/png"
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
assert res["data"]["uploadPicture"]["url"]
|
||||
end
|
||||
|
||||
test "upload_picture/3 forbids uploading if no auth", %{conn: conn} do
|
||||
picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
||||
|
||||
mutation = """
|
||||
mutation { uploadPicture(
|
||||
name: "#{picture.name}",
|
||||
alt: "#{picture.alt}",
|
||||
file: "#{picture.file}"
|
||||
) {
|
||||
url,
|
||||
name
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
map = %{
|
||||
"query" => mutation,
|
||||
"query" => @upload_picture_mutation,
|
||||
"variables" => picture,
|
||||
picture.file => %Plug.Upload{
|
||||
path: "test/fixtures/picture.png",
|
||||
filename: picture.file
|
||||
|
@ -138,9 +124,368 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
|||
"/api",
|
||||
map
|
||||
)
|
||||
|> json_response(200)
|
||||
|
||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
||||
"You need to login to upload a picture"
|
||||
assert hd(res["errors"])["message"] == "You need to be logged in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Resolver: Remove picture" do
|
||||
@remove_picture_mutation """
|
||||
mutation RemovePicture($id: ID!) {
|
||||
removePicture(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "Removes a previously uploaded picture", %{conn: conn, user: user, actor: actor} do
|
||||
%Picture{id: picture_id} = insert(:picture, actor: actor)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @remove_picture_mutation,
|
||||
variables: %{id: picture_id}
|
||||
)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
assert res["data"]["removePicture"]["id"] == to_string(picture_id)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: picture_id})
|
||||
|
||||
assert hd(res["errors"])["message"] == "Resource not found"
|
||||
assert hd(res["errors"])["status_code"] == 404
|
||||
end
|
||||
|
||||
test "Removes nothing if picture is not found", %{conn: conn, user: user} do
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @remove_picture_mutation,
|
||||
variables: %{id: 400}
|
||||
)
|
||||
|
||||
assert hd(res["errors"])["message"] == "Resource not found"
|
||||
assert hd(res["errors"])["status_code"] == 404
|
||||
end
|
||||
|
||||
test "Removes nothing if picture if not logged-in", %{conn: conn, actor: actor} do
|
||||
%Picture{id: picture_id} = insert(:picture, actor: actor)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @remove_picture_mutation,
|
||||
variables: %{id: picture_id}
|
||||
)
|
||||
|
||||
assert hd(res["errors"])["message"] == "You need to be logged in"
|
||||
assert hd(res["errors"])["status_code"] == 401
|
||||
end
|
||||
end
|
||||
|
||||
describe "Resolver: Get actor media size" do
|
||||
@actor_media_size_query """
|
||||
query LoggedPerson {
|
||||
loggedPerson {
|
||||
id
|
||||
mediaSize
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "with own actor", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
insert(:actor, user: user)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @actor_media_size_query)
|
||||
|
||||
assert res["data"]["loggedPerson"]["mediaSize"] == 0
|
||||
|
||||
res = upload_picture(conn, user)
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @actor_media_size_query)
|
||||
|
||||
assert res["data"]["loggedPerson"]["mediaSize"] == 10_097
|
||||
|
||||
res =
|
||||
upload_picture(
|
||||
conn,
|
||||
user,
|
||||
"test/fixtures/image.jpg",
|
||||
Map.put(@default_picture_details, :file, "image.jpg")
|
||||
)
|
||||
|
||||
assert res["data"]["uploadPicture"]["size"] == 13_227
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @actor_media_size_query)
|
||||
|
||||
assert res["data"]["loggedPerson"]["mediaSize"] == 23_324
|
||||
end
|
||||
|
||||
@list_actors_query """
|
||||
query ListPersons($preferredUsername: String) {
|
||||
persons(preferredUsername: $preferredUsername) {
|
||||
total,
|
||||
elements {
|
||||
id
|
||||
mediaSize
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "as a moderator", %{conn: conn} do
|
||||
moderator = insert(:user, role: :moderator)
|
||||
user = insert(:user)
|
||||
actor = insert(:actor, user: user)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(moderator)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @list_actors_query,
|
||||
variables: %{preferredUsername: actor.preferred_username}
|
||||
)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
assert hd(res["data"]["persons"]["elements"])["mediaSize"] == 0
|
||||
|
||||
upload_picture(conn, user)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(moderator)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @list_actors_query,
|
||||
variables: %{preferredUsername: actor.preferred_username}
|
||||
)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
assert hd(res["data"]["persons"]["elements"])["mediaSize"] == 10_097
|
||||
end
|
||||
|
||||
@event_organizer_media_query """
|
||||
query Event($uuid: UUID!) {
|
||||
event(uuid: $uuid) {
|
||||
id
|
||||
organizerActor {
|
||||
id
|
||||
mediaSize
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "as a different user", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
event = insert(:event)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @event_organizer_media_query,
|
||||
variables: %{uuid: event.uuid}
|
||||
)
|
||||
|
||||
assert hd(res["errors"])["message"] == "unauthorized"
|
||||
end
|
||||
|
||||
test "without being logged-in", %{conn: conn} do
|
||||
event = insert(:event)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @event_organizer_media_query,
|
||||
variables: %{uuid: event.uuid}
|
||||
)
|
||||
|
||||
assert hd(res["errors"])["message"] == "unauthenticated"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Resolver: Get user media size" do
|
||||
@user_media_size_query """
|
||||
query LoggedUser {
|
||||
loggedUser {
|
||||
id
|
||||
mediaSize
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@change_default_actor_mutation """
|
||||
mutation ChangeDefaultActor($preferredUsername: String!) {
|
||||
changeDefaultActor(preferredUsername: $preferredUsername) {
|
||||
defaultActor {
|
||||
id
|
||||
preferredUsername
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "with own user", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
insert(:actor, user: user)
|
||||
actor_2 = insert(:actor, user: user)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @user_media_size_query)
|
||||
|
||||
assert res["errors"] == nil
|
||||
assert res["data"]["loggedUser"]["mediaSize"] == 0
|
||||
|
||||
res = upload_picture(conn, user)
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @user_media_size_query)
|
||||
|
||||
assert res["data"]["loggedUser"]["mediaSize"] == 10_097
|
||||
|
||||
res =
|
||||
upload_picture(
|
||||
conn,
|
||||
user,
|
||||
"test/fixtures/image.jpg",
|
||||
Map.put(@default_picture_details, :file, "image.jpg")
|
||||
)
|
||||
|
||||
assert res["data"]["uploadPicture"]["size"] == 13_227
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @user_media_size_query)
|
||||
|
||||
assert res["data"]["loggedUser"]["mediaSize"] == 23_324
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @change_default_actor_mutation,
|
||||
variables: %{preferredUsername: actor_2.preferred_username}
|
||||
)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
|
||||
res =
|
||||
upload_picture(
|
||||
conn,
|
||||
user,
|
||||
"test/fixtures/image.jpg",
|
||||
Map.put(@default_picture_details, :file, "image.jpg")
|
||||
)
|
||||
|
||||
assert res["data"]["uploadPicture"]["size"] == 13_227
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @user_media_size_query)
|
||||
|
||||
assert res["data"]["loggedUser"]["mediaSize"] == 36_551
|
||||
end
|
||||
|
||||
@list_users_query """
|
||||
query ListUsers($email: String) {
|
||||
users(email: $email) {
|
||||
total,
|
||||
elements {
|
||||
id
|
||||
mediaSize
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "as a moderator", %{conn: conn} do
|
||||
moderator = insert(:user, role: :moderator)
|
||||
user = insert(:user)
|
||||
insert(:actor, user: user)
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(moderator)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @list_users_query,
|
||||
variables: %{email: user.email}
|
||||
)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
assert hd(res["data"]["users"]["elements"])["mediaSize"] == 0
|
||||
|
||||
res = upload_picture(conn, user)
|
||||
assert is_nil(res["errors"])
|
||||
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(moderator)
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @list_users_query,
|
||||
variables: %{email: user.email}
|
||||
)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
assert hd(res["data"]["users"]["elements"])["mediaSize"] == 10_097
|
||||
end
|
||||
|
||||
test "without being logged-in", %{conn: conn} do
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(query: @user_media_size_query)
|
||||
|
||||
assert hd(res["errors"])["message"] == "You need to be logged-in to view current user"
|
||||
end
|
||||
end
|
||||
|
||||
@spec upload_picture(Plug.Conn.t(), Mobilizon.Users.User.t(), String.t(), map()) :: map()
|
||||
defp upload_picture(
|
||||
conn,
|
||||
user,
|
||||
picture_path \\ @default_picture_path,
|
||||
picture_details \\ @default_picture_details
|
||||
) do
|
||||
map = %{
|
||||
"query" => @upload_picture_mutation,
|
||||
"variables" => picture_details,
|
||||
picture_details.file => %Plug.Upload{
|
||||
path: picture_path,
|
||||
filename: picture_details.file
|
||||
}
|
||||
}
|
||||
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> put_req_header("content-type", "multipart/form-data")
|
||||
|> post(
|
||||
"/api",
|
||||
map
|
||||
)
|
||||
|> json_response(200)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue