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,
|
code_reloader: true,
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
watchers: [
|
watchers: [
|
||||||
# yarn: ["run", "dev", cd: Path.expand("../js", __DIR__)]
|
|
||||||
node: [
|
node: [
|
||||||
"node_modules/webpack/bin/webpack.js",
|
"node_modules/webpack/bin/webpack.js",
|
||||||
"--mode",
|
"--mode",
|
||||||
|
@ -53,8 +52,8 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
||||||
patterns: [
|
patterns: [
|
||||||
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
||||||
~r{priv/gettext/.*(po)$},
|
~r{priv/gettext/.*(po)$},
|
||||||
~r{lib/mobilizon_web/views/.*(ex)$},
|
~r{lib/web/(live|views)/.*(ex)$},
|
||||||
~r{lib/mobilizon_web/templates/.*(eex)$}
|
~r{lib/web/templates/.*(eex)$}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<figure class="image" v-if="actualImageSrc">
|
<figure class="image" v-if="imageSrc">
|
||||||
<img :src="actualImageSrc" />
|
<img :src="imageSrc" />
|
||||||
</figure>
|
</figure>
|
||||||
<figure class="image is-128x128" v-else>
|
<figure class="image is-128x128" v-else>
|
||||||
<div class="image-placeholder">
|
<div class="image-placeholder">
|
||||||
|
@ -9,12 +9,19 @@
|
||||||
</div>
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<b-upload @input="onFileChanged" :accept="accept">
|
<div class="action-buttons">
|
||||||
<a class="button is-primary">
|
<b-field class="file is-primary">
|
||||||
<b-icon icon="upload"></b-icon>
|
<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>{{ $t("Click to upload") }}</span>
|
||||||
</a>
|
</span>
|
||||||
</b-upload>
|
</b-upload>
|
||||||
|
</b-field>
|
||||||
|
<b-button type="is-text" v-if="imageSrc" @click="removeOrClearPicture">
|
||||||
|
{{ $t("Clear") }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -45,16 +52,22 @@ figure.image {
|
||||||
color: #eee;
|
color: #eee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { IPicture } from "@/types/picture.model";
|
||||||
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class PictureUpload extends Vue {
|
export default class PictureUpload extends Vue {
|
||||||
@Model("change", { type: File }) readonly pictureFile!: File;
|
@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" })
|
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" })
|
||||||
accept!: string;
|
accept!: string;
|
||||||
|
@ -70,24 +83,40 @@ export default class PictureUpload extends Vue {
|
||||||
})
|
})
|
||||||
textFallback!: string;
|
textFallback!: string;
|
||||||
|
|
||||||
imageSrc: string | null = null;
|
imageSrc: string | null = this.defaultImage ? this.defaultImage.url : null;
|
||||||
|
|
||||||
|
file!: File | null;
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
|
if (this.pictureFile) {
|
||||||
this.updatePreview(this.pictureFile);
|
this.updatePreview(this.pictureFile);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Watch("pictureFile")
|
@Watch("pictureFile")
|
||||||
onPictureFileChanged(val: File): void {
|
onPictureFileChanged(val: File): void {
|
||||||
|
console.log("onPictureFileChanged", val);
|
||||||
this.updatePreview(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.$emit("change", file);
|
||||||
|
|
||||||
this.updatePreview(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) {
|
if (file) {
|
||||||
this.imageSrc = URL.createObjectURL(file);
|
this.imageSrc = URL.createObjectURL(file);
|
||||||
return;
|
return;
|
||||||
|
@ -95,9 +124,5 @@ export default class PictureUpload extends Vue {
|
||||||
|
|
||||||
this.imageSrc = null;
|
this.imageSrc = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get actualImageSrc(): string | null {
|
|
||||||
return this.imageSrc || this.defaultImageSrc;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const FETCH_PERSON = gql`
|
||||||
summary
|
summary
|
||||||
preferredUsername
|
preferredUsername
|
||||||
suspended
|
suspended
|
||||||
|
mediaSize
|
||||||
avatar {
|
avatar {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
@ -51,6 +52,7 @@ export const GET_PERSON = gql`
|
||||||
summary
|
summary
|
||||||
preferredUsername
|
preferredUsername
|
||||||
suspended
|
suspended
|
||||||
|
mediaSize
|
||||||
avatar {
|
avatar {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|
|
@ -84,6 +84,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
mediaSize
|
||||||
organizedEvents(
|
organizedEvents(
|
||||||
afterDatetime: $afterDateTime
|
afterDatetime: $afterDateTime
|
||||||
beforeDatetime: $beforeDateTime
|
beforeDatetime: $beforeDateTime
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
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!) {
|
||||||
uploadPicture(file: $file, alt: $alt, name: $name) {
|
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
|
currentSignInAt
|
||||||
locale
|
locale
|
||||||
disabled
|
disabled
|
||||||
|
mediaSize
|
||||||
defaultActor {
|
defaultActor {
|
||||||
id
|
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 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.",
|
"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.",
|
"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 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.",
|
"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.",
|
"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;
|
url: string;
|
||||||
name: string;
|
name: string;
|
||||||
domain: string | null;
|
domain: string | null;
|
||||||
|
mediaSize: number;
|
||||||
summary: string;
|
summary: string;
|
||||||
preferredUsername: string;
|
preferredUsername: string;
|
||||||
suspended: boolean;
|
suspended: boolean;
|
||||||
|
@ -30,6 +31,8 @@ export class Actor implements IActor {
|
||||||
|
|
||||||
domain: string | null = null;
|
domain: string | null = null;
|
||||||
|
|
||||||
|
mediaSize = 0;
|
||||||
|
|
||||||
name = "";
|
name = "";
|
||||||
|
|
||||||
preferredUsername = "";
|
preferredUsername = "";
|
||||||
|
|
|
@ -39,6 +39,7 @@ export interface IUser extends ICurrentUser {
|
||||||
actors: IPerson[];
|
actors: IPerson[];
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
participations: Paginate<IParticipant>;
|
participations: Paginate<IParticipant>;
|
||||||
|
mediaSize: number;
|
||||||
drafts: IEvent[];
|
drafts: IEvent[];
|
||||||
settings: IUserSettings;
|
settings: IUserSettings;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
|
|
@ -69,7 +69,7 @@ interface IEventEditJSON {
|
||||||
visibility: EventVisibility;
|
visibility: EventVisibility;
|
||||||
joinOptions: EventJoinOptions;
|
joinOptions: EventJoinOptions;
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
picture: IPicture | { pictureId: string } | null;
|
picture?: IPicture | { pictureId: string } | null;
|
||||||
attributedToId: string | null;
|
attributedToId: string | null;
|
||||||
onlineAddress?: string;
|
onlineAddress?: string;
|
||||||
phoneAddress?: string;
|
phoneAddress?: string;
|
||||||
|
@ -234,7 +234,6 @@ export class EventModel implements IEvent {
|
||||||
joinOptions: this.joinOptions,
|
joinOptions: this.joinOptions,
|
||||||
draft: this.draft,
|
draft: this.draft,
|
||||||
tags: this.tags.map((t) => t.title),
|
tags: this.tags.map((t) => t.title),
|
||||||
picture: this.picture,
|
|
||||||
onlineAddress: this.onlineAddress,
|
onlineAddress: this.onlineAddress,
|
||||||
phoneAddress: this.phoneAddress,
|
phoneAddress: this.phoneAddress,
|
||||||
physicalAddress: this.physicalAddress,
|
physicalAddress: this.physicalAddress,
|
||||||
|
|
|
@ -18,4 +18,17 @@ function localeShortWeekDayNames(): string[] {
|
||||||
return weekDayNames;
|
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);
|
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 {};
|
if (!file) return {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<span v-else>{{ $t("I create an identity") }}</span>
|
<span v-else>{{ $t("I create an identity") }}</span>
|
||||||
</h1>
|
</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-field horizontal :label="$t('Display name')">
|
||||||
<b-input
|
<b-input
|
||||||
|
@ -124,6 +124,7 @@ h1 {
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||||
import { mixins } from "vue-class-component";
|
import { mixins } from "vue-class-component";
|
||||||
|
import { IPicture } from "@/types/picture.model";
|
||||||
import {
|
import {
|
||||||
CREATE_PERSON,
|
CREATE_PERSON,
|
||||||
CURRENT_ACTOR_CLIENT,
|
CURRENT_ACTOR_CLIENT,
|
||||||
|
@ -136,7 +137,7 @@ import { IPerson, Person } from "../../../types/actor";
|
||||||
import PictureUpload from "../../../components/PictureUpload.vue";
|
import PictureUpload from "../../../components/PictureUpload.vue";
|
||||||
import { MOBILIZON_INSTANCE_HOST } from "../../../api/_entrypoint";
|
import { MOBILIZON_INSTANCE_HOST } from "../../../api/_entrypoint";
|
||||||
import RouteName from "../../../router/name";
|
import RouteName from "../../../router/name";
|
||||||
import { buildFileVariable } from "../../../utils/image";
|
import { buildFileFromIPicture, buildFileVariable } from "../../../utils/image";
|
||||||
import { changeIdentity } from "../../../utils/auth";
|
import { changeIdentity } from "../../../utils/auth";
|
||||||
import identityEditionMixin from "../../../mixins/identityEdition";
|
import identityEditionMixin from "../../../mixins/identityEdition";
|
||||||
|
|
||||||
|
@ -186,13 +187,6 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||||
) as string;
|
) 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")
|
@Watch("isUpdate")
|
||||||
async isUpdateChanged(): Promise<void> {
|
async isUpdateChanged(): Promise<void> {
|
||||||
this.resetFields();
|
this.resetFields();
|
||||||
|
@ -286,7 +280,6 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.avatarFile = null;
|
|
||||||
|
|
||||||
this.$notifier.success(
|
this.$notifier.success(
|
||||||
this.$t("Identity {displayName} updated", {
|
this.$t("Identity {displayName} updated", {
|
||||||
|
|
|
@ -198,6 +198,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
import { GET_GROUP, REFRESH_PROFILE } from "@/graphql/group";
|
import { GET_GROUP, REFRESH_PROFILE } from "@/graphql/group";
|
||||||
|
import { formatBytes } from "@/utils/datetime";
|
||||||
import { SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
import { SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
||||||
import { IGroup, MemberRole } from "../../types/actor";
|
import { IGroup, MemberRole } from "../../types/actor";
|
||||||
import { usernameWithDomain, IActor } from "../../types/actor/actor.model";
|
import { usernameWithDomain, IActor } from "../../types/actor/actor.model";
|
||||||
|
@ -258,6 +259,10 @@ export default class AdminGroupProfile extends Vue {
|
||||||
key: this.$t("Domain") as string,
|
key: this.$t("Domain") as string,
|
||||||
value: (this.group.domain ? this.group.domain : this.$t("Local")) 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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,11 +126,11 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
|
import { formatBytes } from "@/utils/datetime";
|
||||||
import { GET_PERSON, SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
import { GET_PERSON, SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
||||||
import { IPerson } from "../../types/actor";
|
import { IPerson } from "../../types/actor";
|
||||||
import { usernameWithDomain } from "../../types/actor/actor.model";
|
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { IEvent } from "../../types/event.model";
|
|
||||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||||
|
|
||||||
const EVENTS_PER_PAGE = 10;
|
const EVENTS_PER_PAGE = 10;
|
||||||
|
@ -171,9 +171,9 @@ export default class AdminProfile extends Vue {
|
||||||
|
|
||||||
participationsPage = 1;
|
participationsPage = 1;
|
||||||
|
|
||||||
get metadata(): Array<object> {
|
get metadata(): Array<Record<string, unknown>> {
|
||||||
if (!this.person) return [];
|
if (!this.person) return [];
|
||||||
const res: object[] = [
|
const res: Record<string, unknown>[] = [
|
||||||
{
|
{
|
||||||
key: this.$t("Status") as string,
|
key: this.$t("Status") as string,
|
||||||
value: this.person.suspended ? this.$t("Suspended") : this.$t("Active"),
|
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,
|
key: this.$t("Domain") as string,
|
||||||
value: this.person.domain ? this.person.domain : this.$t("Local"),
|
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) {
|
if (!this.person.domain && this.person.user) {
|
||||||
res.push({
|
res.push({
|
||||||
|
@ -193,7 +197,7 @@ export default class AdminProfile extends Vue {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async suspendProfile() {
|
async suspendProfile(): Promise<void> {
|
||||||
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||||
mutation: SUSPEND_PROFILE,
|
mutation: SUSPEND_PROFILE,
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -229,7 +233,7 @@ export default class AdminProfile extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async unsuspendProfile() {
|
async unsuspendProfile(): Promise<void> {
|
||||||
const profileID = this.id;
|
const profileID = this.id;
|
||||||
this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
||||||
mutation: UNSUSPEND_PROFILE,
|
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;
|
this.organizedEventsPage = page;
|
||||||
await this.$apollo.queries.person.fetchMore({
|
await this.$apollo.queries.person.fetchMore({
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -274,7 +278,7 @@ export default class AdminProfile extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onParticipationsPageChange(page: number) {
|
async onParticipationsPageChange(page: number): Promise<void> {
|
||||||
this.participationsPage = page;
|
this.participationsPage = page;
|
||||||
await this.$apollo.queries.person.fetchMore({
|
await this.$apollo.queries.person.fetchMore({
|
||||||
variables: {
|
variables: {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
||||||
<tbody>
|
<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>{{ key }}</td>
|
||||||
<td v-if="elements && elements.length > 0">
|
<td v-if="elements && elements.length > 0">
|
||||||
<ul v-for="{ value, link: elementLink, active } in elements" :key="value">
|
<ul v-for="{ value, link: elementLink, active } in elements" :key="value">
|
||||||
|
@ -46,6 +46,9 @@
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-else-if="type == 'code'">
|
||||||
|
<code>{{ value }}</code>
|
||||||
|
</td>
|
||||||
<td v-else>{{ value }}</td>
|
<td v-else>{{ value }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -60,6 +63,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
import { Route } from "vue-router";
|
import { Route } from "vue-router";
|
||||||
|
import { formatBytes } from "@/utils/datetime";
|
||||||
import { GET_USER, SUSPEND_USER } from "../../graphql/user";
|
import { GET_USER, SUSPEND_USER } from "../../graphql/user";
|
||||||
import { usernameWithDomain } from "../../types/actor/actor.model";
|
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
@ -139,11 +143,16 @@ export default class AdminUserProfile extends Vue {
|
||||||
{
|
{
|
||||||
key: this.$i18n.t("Last IP adress"),
|
key: this.$i18n.t("Last IP adress"),
|
||||||
value: this.user.currentSignInIp || this.$t("Unknown"),
|
value: this.user.currentSignInIp || this.$t("Unknown"),
|
||||||
|
type: "code",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: this.$i18n.t("Participations"),
|
key: this.$i18n.t("Participations"),
|
||||||
value: this.user.participations.total,
|
value: this.user.participations.total,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Uploaded media size"),
|
||||||
|
value: formatBytes(this.user.mediaSize),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,11 @@
|
||||||
|
|
||||||
<form ref="form">
|
<form ref="form">
|
||||||
<subtitle>{{ $t("General information") }}</subtitle>
|
<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-field :label="$t('Title')" :type="checkTitleLength[0]" :message="checkTitleLength[1]">
|
||||||
<b-input size="is-large" aria-required="true" required v-model="event.title" />
|
<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",
|
__typename: "Person",
|
||||||
id: organizerActor.id,
|
id: organizerActor.id,
|
||||||
participations: {
|
participations: {
|
||||||
|
__typename: "PaginatedParticipantList",
|
||||||
total: 1,
|
total: 1,
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
|
@ -763,11 +768,13 @@ export default class EditEvent extends Vue {
|
||||||
res.endsOn = null;
|
res.endsOn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.pictureFile) {
|
||||||
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
||||||
res = { ...res, ...pictureObj };
|
res = { ...res, ...pictureObj };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.event.picture) {
|
if (this.event.picture && this.pictureFile) {
|
||||||
const oldPictureFile = (await buildFileFromIPicture(this.event.picture)) as File;
|
const oldPictureFile = (await buildFileFromIPicture(this.event.picture)) as File;
|
||||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||||
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
|
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<section class="container section" v-if="isCurrentActorAGroupAdmin">
|
<section class="container section" v-if="group && isCurrentActorAGroupAdmin">
|
||||||
<form @submit.prevent="updateGroup">
|
<form @submit.prevent="updateGroup">
|
||||||
<b-field :label="$t('Group name')">
|
<b-field :label="$t('Group name')">
|
||||||
<b-input v-model="group.name" />
|
<b-input v-model="group.name" />
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
<picture-upload
|
<picture-upload
|
||||||
:textFallback="$t('Avatar')"
|
:textFallback="$t('Avatar')"
|
||||||
v-model="avatarFile"
|
v-model="avatarFile"
|
||||||
:defaultImageSrc="group.avatar ? group.avatar.url : null"
|
:defaultImage="group.avatar"
|
||||||
/>
|
/>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<picture-upload
|
<picture-upload
|
||||||
:textFallback="$t('Banner')"
|
:textFallback="$t('Banner')"
|
||||||
v-model="bannerFile"
|
v-model="bannerFile"
|
||||||
:defaultImageSrc="group.banner ? group.banner.url : null"
|
:defaultImage="group.banner"
|
||||||
/>
|
/>
|
||||||
</b-field>
|
</b-field>
|
||||||
<p class="label">{{ $t("Group visibility") }}</p>
|
<p class="label">{{ $t("Group visibility") }}</p>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<picture-upload
|
<picture-upload
|
||||||
v-model="pictureFile"
|
v-model="pictureFile"
|
||||||
:textFallback="$t('Headline picture')"
|
:textFallback="$t('Headline picture')"
|
||||||
:defaultImageSrc="post.picture ? post.picture.url : null"
|
:defaultImage="post.picture"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<b-field
|
<b-field
|
||||||
|
|
|
@ -6,6 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.{Media, Users}
|
alias Mobilizon.{Media, Users}
|
||||||
alias Mobilizon.Media.Picture
|
alias Mobilizon.Media.Picture
|
||||||
|
alias Mobilizon.Users.User
|
||||||
import Mobilizon.Web.Gettext
|
import Mobilizon.Web.Gettext
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -37,8 +38,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||||
size: file.size
|
size: file.size
|
||||||
}}
|
}}
|
||||||
|
|
||||||
_error ->
|
nil ->
|
||||||
{:error, dgettext("errors", "Picture with ID %{id} was not found", id: picture_id)}
|
{:error, :not_found}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||||
def upload_picture(
|
def upload_picture(
|
||||||
_parent,
|
_parent,
|
||||||
%{file: %Plug.Upload{} = file} = args,
|
%{file: %Plug.Upload{} = file} = args,
|
||||||
%{context: %{current_user: user}}
|
%{context: %{current_user: %User{} = user}}
|
||||||
) do
|
) do
|
||||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||||
{:ok, %{name: _name, url: url, content_type: content_type, size: size}} <-
|
{:ok, %{name: _name, url: url, content_type: content_type, size: size}} <-
|
||||||
|
@ -75,7 +76,74 @@ defmodule Mobilizon.GraphQL.Resolvers.Picture do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload_picture(_parent, _args, _resolution) do
|
def upload_picture(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||||
{:error, dgettext("errors", "You need to login to upload a picture")}
|
|
||||||
|
@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
|
||||||
end
|
end
|
||||||
|
|
|
@ -525,6 +525,28 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
end
|
end
|
||||||
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()) ::
|
@spec update_user_login_information(User.t(), map()) ::
|
||||||
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
defp update_user_login_information(
|
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(:followersCount, :integer, description: "Number of followers for this actor")
|
||||||
field(:followingCount, :integer, description: "Number of actors following 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
|
resolve_type(fn
|
||||||
%Actor{type: :Person}, _ ->
|
%Actor{type: :Person}, _ ->
|
||||||
:person
|
:person
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
|
||||||
Schema representation for Group.
|
Schema representation for Group.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Mobilizon.GraphQL.Resolvers.Picture
|
||||||
use Absinthe.Schema.Notation
|
use Absinthe.Schema.Notation
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
|
@ -34,5 +35,10 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
|
||||||
field(:followers, list_of(:follower), description: "List of followers")
|
field(:followers, list_of(:follower), description: "List of followers")
|
||||||
field(:followersCount, :integer, description: "Number of followers for this actor")
|
field(:followersCount, :integer, description: "Number of followers for this actor")
|
||||||
field(:followingCount, :integer, description: "Number of actors following 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
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||||
|
|
||||||
alias Mobilizon.Addresses
|
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
|
alias Mobilizon.GraphQL.Schema
|
||||||
|
|
||||||
import_types(Schema.Actors.MemberType)
|
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(:followersCount, :integer, description: "Number of followers for this actor")
|
||||||
field(:followingCount, :integer, description: "Number of actors following 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
|
# This one should have a privacy setting
|
||||||
field :organized_events, :paginated_event_list do
|
field :organized_events, :paginated_event_list do
|
||||||
arg(:after_datetime, :datetime,
|
arg(:after_datetime, :datetime,
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||||
|
|
||||||
alias Mobilizon.Events
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.GraphQL.Resolvers.Person
|
alias Mobilizon.GraphQL.Resolvers.{Person, Picture}
|
||||||
alias Mobilizon.GraphQL.Schema
|
alias Mobilizon.GraphQL.Schema
|
||||||
|
|
||||||
import_types(Schema.Events.FeedTokenType)
|
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(:followersCount, :integer, description: "Number of followers for this actor")
|
||||||
field(:followingCount, :integer, description: "Number of actors following 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),
|
field(:feed_tokens, list_of(:feed_token),
|
||||||
resolve: dataloader(Events),
|
resolve: dataloader(Events),
|
||||||
description: "A list of the feed tokens for this person"
|
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")
|
field(:size, :integer, description: "The picture's size")
|
||||||
end
|
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"
|
@desc "An attached picture or a link to a picture"
|
||||||
input_object :picture_input do
|
input_object :picture_input do
|
||||||
# Either a full picture object
|
# Either a full picture object
|
||||||
|
@ -35,7 +43,7 @@ defmodule Mobilizon.GraphQL.Schema.PictureType do
|
||||||
object :picture_queries do
|
object :picture_queries do
|
||||||
@desc "Get a picture"
|
@desc "Get a picture"
|
||||||
field :picture, :picture do
|
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)
|
resolve(&Picture.picture/3)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -48,5 +56,13 @@ defmodule Mobilizon.GraphQL.Schema.PictureType do
|
||||||
arg(:file, non_null(:upload), description: "The picture file")
|
arg(:file, non_null(:upload), description: "The picture file")
|
||||||
resolve(&Picture.upload_picture/3)
|
resolve(&Picture.upload_picture/3)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,7 +26,7 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
|
||||||
)
|
)
|
||||||
|
|
||||||
field(:picture, :picture,
|
field(:picture, :picture,
|
||||||
description: "The event's picture",
|
description: "The posts's picture",
|
||||||
resolve: &Picture.picture/3
|
resolve: &Picture.picture/3
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||||
|
|
||||||
alias Mobilizon.Events
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.GraphQL.Resolvers.User
|
alias Mobilizon.GraphQL.Resolvers.{Picture, User}
|
||||||
alias Mobilizon.GraphQL.Schema
|
alias Mobilizon.GraphQL.Schema
|
||||||
|
|
||||||
import_types(Schema.SortType)
|
import_types(Schema.SortType)
|
||||||
|
@ -110,6 +110,21 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
field(:current_sign_in_ip, :string,
|
field(:current_sign_in_ip, :string,
|
||||||
description: "The IP adress the user's currently signed-in with"
|
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
|
end
|
||||||
|
|
||||||
@desc "The list of roles an user can have"
|
@desc "The list of roles an user can have"
|
||||||
|
|
|
@ -7,8 +7,10 @@ defmodule Mobilizon.Media do
|
||||||
|
|
||||||
alias Ecto.Multi
|
alias Ecto.Multi
|
||||||
|
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Media.{File, Picture}
|
alias Mobilizon.Media.{File, Picture}
|
||||||
alias Mobilizon.Storage.Repo
|
alias Mobilizon.Storage.{Page, Repo}
|
||||||
|
alias Mobilizon.Users.User
|
||||||
|
|
||||||
alias Mobilizon.Web.Upload
|
alias Mobilizon.Web.Upload
|
||||||
|
|
||||||
|
@ -35,6 +37,52 @@ defmodule Mobilizon.Media do
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
end
|
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 """
|
@doc """
|
||||||
Creates a picture.
|
Creates a picture.
|
||||||
"""
|
"""
|
||||||
|
@ -84,4 +132,19 @@ defmodule Mobilizon.Media do
|
||||||
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
|
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -609,22 +609,22 @@ msgstr "Vous n'avez pas la permission de supprimer ce jeton"
|
||||||
#, elixir-format
|
#, elixir-format
|
||||||
#: lib/graphql/resolvers/admin.ex:52
|
#: lib/graphql/resolvers/admin.ex:52
|
||||||
msgid "You need to be logged-in and a moderator to list action logs"
|
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
|
#, elixir-format
|
||||||
#: lib/graphql/resolvers/report.ex:26
|
#: lib/graphql/resolvers/report.ex:26
|
||||||
msgid "You need to be logged-in and a moderator to list reports"
|
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
|
#, elixir-format
|
||||||
#: lib/graphql/resolvers/report.ex:101
|
#: lib/graphql/resolvers/report.ex:101
|
||||||
msgid "You need to be logged-in and a moderator to update a report"
|
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
|
#, elixir-format
|
||||||
#: lib/graphql/resolvers/report.ex:41
|
#: lib/graphql/resolvers/report.ex:41
|
||||||
msgid "You need to be logged-in and a moderator to view a report"
|
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
|
#, elixir-format
|
||||||
#: lib/graphql/resolvers/admin.ex:236
|
#: lib/graphql/resolvers/admin.ex:236
|
||||||
|
@ -689,7 +689,7 @@ msgstr "Vous devez être connecté·e pour supprimer un groupe"
|
||||||
#, elixir-format
|
#, elixir-format
|
||||||
#: lib/graphql/resolvers/participant.ex:105
|
#: lib/graphql/resolvers/participant.ex:105
|
||||||
msgid "You need to be logged-in to join an event"
|
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
|
#, elixir-format
|
||||||
#: lib/graphql/resolvers/participant.ex:204
|
#: 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
|
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
|
setup %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
actor = insert(:actor, user: user)
|
actor = insert(:actor, user: user)
|
||||||
|
@ -17,13 +20,10 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||||
{:ok, conn: conn, user: user, actor: actor}
|
{:ok, conn: conn, user: user, actor: actor}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Resolver: Get picture" do
|
@picture_query """
|
||||||
test "picture/3 returns the information on a picture", context do
|
query Picture($id: ID!) {
|
||||||
%Picture{id: id} = picture = insert(:picture)
|
picture(id: $id) {
|
||||||
|
id
|
||||||
query = """
|
|
||||||
{
|
|
||||||
picture(id: "#{id}") {
|
|
||||||
name,
|
name,
|
||||||
alt,
|
alt,
|
||||||
url,
|
url,
|
||||||
|
@ -33,37 +33,46 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
@upload_picture_mutation """
|
||||||
context.conn
|
mutation UploadPicture($name: String!, $alt: String, $file: Upload!) {
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "picture"))
|
uploadPicture(
|
||||||
|
name: $name
|
||||||
assert json_response(res, 200)["data"]["picture"]["name"] == picture.file.name
|
alt: $alt
|
||||||
|
file: $file
|
||||||
assert json_response(res, 200)["data"]["picture"]["content_type"] ==
|
) {
|
||||||
picture.file.content_type
|
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["picture"]["size"] == 13_120
|
|
||||||
|
|
||||||
assert json_response(res, 200)["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
|
url
|
||||||
|
name
|
||||||
|
content_type
|
||||||
|
size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
describe "Resolver: Get picture" do
|
||||||
context.conn
|
test "picture/3 returns the information on a picture", %{conn: conn} do
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "picture"))
|
%Picture{id: id} = picture = insert(:picture)
|
||||||
|
|
||||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
res =
|
||||||
"Picture with ID 3 was not found"
|
conn
|
||||||
|
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: id})
|
||||||
|
|
||||||
|
assert res["data"]["picture"]["name"] == picture.file.name
|
||||||
|
|
||||||
|
assert res["data"]["picture"]["content_type"] ==
|
||||||
|
picture.file.content_type
|
||||||
|
|
||||||
|
assert res["data"]["picture"]["size"] == 13_120
|
||||||
|
|
||||||
|
assert res["data"]["picture"]["url"] =~ Endpoint.url()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "picture/3 returns nothing on a non-existent picture", %{conn: conn} do
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> AbsintheHelpers.graphql_query(query: @picture_query, variables: %{id: 3})
|
||||||
|
|
||||||
|
assert hd(res["errors"])["message"] == "Resource not found"
|
||||||
|
assert hd(res["errors"])["status_code"] == 404
|
||||||
end
|
end
|
||||||
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
|
test "upload_picture/3 uploads a new picture", %{conn: conn, user: user} do
|
||||||
picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
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 = %{
|
map = %{
|
||||||
"query" => mutation,
|
"query" => @upload_picture_mutation,
|
||||||
|
"variables" => picture,
|
||||||
picture.file => %Plug.Upload{
|
picture.file => %Plug.Upload{
|
||||||
path: "test/fixtures/picture.png",
|
path: "test/fixtures/picture.png",
|
||||||
filename: picture.file
|
filename: picture.file
|
||||||
|
@ -101,30 +97,20 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||||
"/api",
|
"/api",
|
||||||
map
|
map
|
||||||
)
|
)
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["uploadPicture"]["name"] == picture.name
|
assert res["data"]["uploadPicture"]["name"] == picture.name
|
||||||
assert json_response(res, 200)["data"]["uploadPicture"]["content_type"] == "image/png"
|
assert res["data"]["uploadPicture"]["content_type"] == "image/png"
|
||||||
assert json_response(res, 200)["data"]["uploadPicture"]["size"] == 10_097
|
assert res["data"]["uploadPicture"]["size"] == 10_097
|
||||||
assert json_response(res, 200)["data"]["uploadPicture"]["url"]
|
assert res["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} do
|
||||||
picture = %{name: "my pic", alt: "represents something", file: "picture.png"}
|
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 = %{
|
map = %{
|
||||||
"query" => mutation,
|
"query" => @upload_picture_mutation,
|
||||||
|
"variables" => picture,
|
||||||
picture.file => %Plug.Upload{
|
picture.file => %Plug.Upload{
|
||||||
path: "test/fixtures/picture.png",
|
path: "test/fixtures/picture.png",
|
||||||
filename: picture.file
|
filename: picture.file
|
||||||
|
@ -138,9 +124,368 @@ defmodule Mobilizon.GraphQL.Resolvers.PictureTest do
|
||||||
"/api",
|
"/api",
|
||||||
map
|
map
|
||||||
)
|
)
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
assert hd(res["errors"])["message"] == "You need to be logged in"
|
||||||
"You need to login to upload a picture"
|
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue