Merge branch 'develop' into feature/VBV-621-teilnehmer-profil
This commit is contained in:
commit
b6ac2ac4b3
|
|
@ -6,6 +6,7 @@ import { useUserStore } from "@/stores/user";
|
|||
import { useRoute } from "vue-router";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { profileNextRoute, useEntities } from "@/services/onboarding";
|
||||
import AvatarImage from "@/components/ui/AvatarImage.vue";
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -36,17 +37,23 @@ const validOrganisation = computed(() => {
|
|||
return selectedOrganisation.value.id !== 0;
|
||||
});
|
||||
|
||||
/* TODO: We do this later (not in the first release)
|
||||
const {
|
||||
upload: avatarUpload,
|
||||
loading: avatarLoading,
|
||||
error: avatarError,
|
||||
fileInfo: avatarFileInfo,
|
||||
} = useFileUpload();
|
||||
const avatarError = ref(false);
|
||||
const avatarLoading = ref(false);
|
||||
|
||||
watch(avatarFileInfo, (info) => {
|
||||
console.log("fileInfo changed", info);
|
||||
})*/
|
||||
async function avatarUpload(e: Event) {
|
||||
const { files } = e.target as HTMLInputElement;
|
||||
if (!files?.length) return;
|
||||
avatarLoading.value = true;
|
||||
avatarError.value = false;
|
||||
|
||||
try {
|
||||
await user.setUserAvatar(files[0]);
|
||||
} catch (e) {
|
||||
avatarError.value = true;
|
||||
} finally {
|
||||
avatarLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedOrganisation, async (organisation) => {
|
||||
await user.setUserOrganisation(organisation.id);
|
||||
|
|
@ -74,7 +81,6 @@ const nextRoute = computed(() => {
|
|||
|
||||
<ItDropdownSelect v-model="selectedOrganisation" :items="organisations" />
|
||||
|
||||
<!--- TODO: We do this later (not in the first release)
|
||||
<div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24">
|
||||
<div>
|
||||
<h3 class="mb-3">{{ $t("a.Profilbild") }}</h3>
|
||||
|
|
@ -103,7 +109,6 @@ const nextRoute = computed(() => {
|
|||
</div>
|
||||
<AvatarImage :loading="avatarLoading" :image-url="user.avatar_url" />
|
||||
</div>
|
||||
-->
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
|
|
|
|||
|
|
@ -24,27 +24,30 @@ export async function uploadFile(fileData: FileData, file: File) {
|
|||
if (fileData.fields) {
|
||||
return s3Upload(fileData, file);
|
||||
} else {
|
||||
return directUpload(fileData, file);
|
||||
return directUpload(fileData.url, file);
|
||||
}
|
||||
}
|
||||
|
||||
function directUpload(fileData: FileData, file: File) {
|
||||
export function directUpload(url: string, file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const headers = {
|
||||
const headers: HeadersInit = {
|
||||
Accept: "application/json",
|
||||
} as HeadersInit;
|
||||
};
|
||||
|
||||
const csrfToken = getCookieValue("csrftoken");
|
||||
if (csrfToken) {
|
||||
headers["X-CSRFToken"] = csrfToken;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: formData,
|
||||
};
|
||||
// @ts-ignore
|
||||
options.headers["X-CSRFToken"] = getCookieValue("csrftoken");
|
||||
|
||||
return handleUpload(fileData.url, options);
|
||||
return handleUpload(url, options);
|
||||
}
|
||||
|
||||
function s3Upload(fileData: FileData, file: File) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import log from "loglevel";
|
|||
|
||||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||
import { setI18nLanguage } from "@/i18nextWrapper";
|
||||
import { directUpload } from "@/services/files";
|
||||
import dayjs from "dayjs";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
|
|
@ -150,5 +151,9 @@ export const useUserStore = defineStore({
|
|||
this.$state.organisation = organisation;
|
||||
await itPost("/api/core/me/", { organisation }, { method: "PUT" });
|
||||
},
|
||||
async setUserAvatar(file: File) {
|
||||
const r = await directUpload("/api/core/avatar/", file);
|
||||
this.$state.avatar_url = r.url;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from django_ratelimit.exceptions import Ratelimited
|
|||
from graphene_django.views import GraphQLView
|
||||
|
||||
from vbv_lernwelt.api.directory import list_entities
|
||||
from vbv_lernwelt.api.user import get_cockpit_type, get_profile, me_user_view
|
||||
from vbv_lernwelt.api.user import get_cockpit_type, get_profile, me_user_view, post_avatar
|
||||
from vbv_lernwelt.assignment.views import request_assignment_completion_status
|
||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||
from vbv_lernwelt.core.schema import schema
|
||||
|
|
@ -54,6 +54,7 @@ from vbv_lernwelt.importer.views import (
|
|||
coursesessions_trainers_import,
|
||||
t2l_sync,
|
||||
)
|
||||
from vbv_lernwelt.media_files.views import user_image
|
||||
from vbv_lernwelt.notify.views import email_notification_settings
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
|
|
@ -99,6 +100,7 @@ urlpatterns = [
|
|||
# user management
|
||||
path("sso/", include("vbv_lernwelt.sso.urls")),
|
||||
re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
|
||||
re_path(r'api/core/avatar/$', post_avatar, name='post_avatar'),
|
||||
re_path(r'api/core/entities/$', list_entities, name='list_entities'),
|
||||
path(r'api/core/profile/<signed_int:course_session_id>/<uuid:user_id>', get_profile, name='get_profile_view'),
|
||||
|
||||
|
|
@ -141,6 +143,8 @@ urlpatterns = [
|
|||
name="request_assignment_completion_status"),
|
||||
|
||||
# documents
|
||||
path("api/core/userimage/<int:image_id>", user_image, name="user_image"),
|
||||
|
||||
# TODO: remfactor to files app
|
||||
path(r'api/core/document/start/', document_upload_start,
|
||||
name='file_upload_start'),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.course.models import Course, CourseSessionUser
|
|||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.iam.permissions import can_view_profile
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.media_files.models import UserImage
|
||||
|
||||
|
||||
@api_view(["GET", "PUT"])
|
||||
|
|
@ -72,3 +73,17 @@ def get_profile(request, course_session_id: int, user_id: str):
|
|||
return Response(status=403)
|
||||
|
||||
return Response(UserSerializer(course_session_user.user).data)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def post_avatar(request):
|
||||
if "file" not in request.FILES:
|
||||
return Response(status=400)
|
||||
|
||||
request.user.avatar = UserImage.objects.create(
|
||||
file=request.FILES["file"],
|
||||
)
|
||||
request.user.save()
|
||||
|
||||
return Response({"url": request.user.avatar_url})
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class UserAdmin(auth_admin.UserAdmin):
|
|||
},
|
||||
),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
(_("Profile"), {"fields": ("organisation", "language")}),
|
||||
(_("Profile"), {"fields": ("organisation", "language", "avatar")}),
|
||||
(_("Additional data"), {"fields": ("additional_json_data",)}),
|
||||
)
|
||||
list_display = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.files import File
|
||||
from environs import Env
|
||||
|
||||
from vbv_lernwelt.media_files.models import UserImage
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
|
||||
|
|
@ -24,52 +28,54 @@ default_users = [
|
|||
"email": "student",
|
||||
"first_name": "Student",
|
||||
"last_name": "Meier",
|
||||
"avatar_url": "/static/avatars/avatar_iterativ.png",
|
||||
"avatar_image": "avatar_iterativ.png",
|
||||
},
|
||||
{
|
||||
"email": "daniel.egger@iterativ.ch",
|
||||
"first_name": "Daniel",
|
||||
"last_name": "Egger",
|
||||
"avatar_url": "/static/avatars/avatar_iterativ.png",
|
||||
"avatar_image": "avatar_iterativ.png",
|
||||
},
|
||||
{
|
||||
"email": "axel.manderbach@lernetz.ch",
|
||||
"first_name": "Axel",
|
||||
"last_name": "Manderbach",
|
||||
"avatar_url": "/static/avatars/avatar_axel.jpg",
|
||||
"avatar_image": "avatar_axel.jpg",
|
||||
},
|
||||
{
|
||||
"email": "christoph.bosshard@vbv-afa.ch",
|
||||
"first_name": "Christoph",
|
||||
"last_name": "Bosshard",
|
||||
"avatar_url": "/static/avatars/avatar_christoph.png",
|
||||
"avatar_image": "avatar_christoph.png",
|
||||
"password": "myvbv1234",
|
||||
},
|
||||
{
|
||||
"email": "alexandra.vangelista@lernetz.ch",
|
||||
"first_name": "Alexandra",
|
||||
"last_name": "Vangelista",
|
||||
"avatar_url": "/static/avatars/avatar_alexandra.png",
|
||||
"avatar_image": "avatar_alexandra.png",
|
||||
"password": "myvbv1234",
|
||||
},
|
||||
{
|
||||
"email": "chantal.rosenberg@vbv-afa.ch",
|
||||
"first_name": "Chantal",
|
||||
"last_name": "Rosenberg",
|
||||
"avatar_url": "/static/avatars/avatar_chantal.png",
|
||||
"avatar_image": "avatar_chantal.png",
|
||||
"password": "myvbv1234",
|
||||
},
|
||||
{
|
||||
"email": "bianca.muster@eiger-versicherungen.ch",
|
||||
"first_name": "Bianca",
|
||||
"last_name": "Muster",
|
||||
"avatar_url": "/static/avatars/avatar_bianca.png",
|
||||
"avatar_image": "avatar_bianca.png",
|
||||
"password": "myvbv1234",
|
||||
},
|
||||
]
|
||||
|
||||
AVATAR_DIR = settings.APPS_DIR / "static" / "avatars"
|
||||
|
||||
def create_default_users(default_password="test"):
|
||||
|
||||
def create_default_users(default_password="test", set_avatar=False):
|
||||
admin_group, created = Group.objects.get_or_create(name="admin_group")
|
||||
_content_creator_grop, _created = Group.objects.get_or_create(
|
||||
name="content_creator_grop"
|
||||
|
|
@ -81,9 +87,9 @@ def create_default_users(default_password="test"):
|
|||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
avatar_url,
|
||||
language,
|
||||
password,
|
||||
avatar_image: str = None,
|
||||
):
|
||||
user, _ = User.objects.get_or_create(
|
||||
id=_id,
|
||||
|
|
@ -92,25 +98,33 @@ def create_default_users(default_password="test"):
|
|||
language=language,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
avatar_url=avatar_url,
|
||||
password=make_password(password),
|
||||
)
|
||||
|
||||
if avatar_image and set_avatar:
|
||||
with open(AVATAR_DIR / avatar_image, "rb") as f:
|
||||
image, _ = UserImage.objects.get_or_create(
|
||||
file=File(f),
|
||||
)
|
||||
user.avatar = image
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
def _create_student_user(
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
avatar_url="",
|
||||
password=default_password,
|
||||
language="de",
|
||||
avatar_image=None,
|
||||
id=None,
|
||||
):
|
||||
student_user = _create_user(
|
||||
email=email,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
avatar_url=avatar_url,
|
||||
avatar_image=avatar_image,
|
||||
language=language,
|
||||
password=password,
|
||||
_id=id,
|
||||
|
|
@ -120,13 +134,18 @@ def create_default_users(default_password="test"):
|
|||
student_user.save()
|
||||
|
||||
def _create_admin_user(
|
||||
email, first_name, last_name, avatar_url="", id=None, password=default_password
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
avatar_image=None,
|
||||
id=None,
|
||||
password=default_password,
|
||||
):
|
||||
admin_user = _create_user(
|
||||
email=email,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
avatar_url=avatar_url,
|
||||
avatar_image=avatar_image,
|
||||
password=password,
|
||||
language="de",
|
||||
_id=id,
|
||||
|
|
@ -145,7 +164,7 @@ def create_default_users(default_password="test"):
|
|||
email=email,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
avatar_url="",
|
||||
avatar_image=None,
|
||||
language="de",
|
||||
password=password,
|
||||
)
|
||||
|
|
@ -158,7 +177,7 @@ def create_default_users(default_password="test"):
|
|||
email="info@iterativ.ch",
|
||||
first_name="Info",
|
||||
last_name="Iterativ",
|
||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
||||
avatar_image="avatar_iterativ.png",
|
||||
password=env("IT_DEFAULT_ADMIN_PASSWORD", default_password),
|
||||
)
|
||||
|
||||
|
|
@ -166,7 +185,7 @@ def create_default_users(default_password="test"):
|
|||
email="admin",
|
||||
first_name="Peter",
|
||||
last_name="Adminson",
|
||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
||||
avatar_image="avatar_iterativ.png",
|
||||
id=ADMIN_USER_ID,
|
||||
password=env("IT_DEFAULT_ADMIN_PASSWORD", default_password),
|
||||
)
|
||||
|
|
@ -183,7 +202,7 @@ def create_default_users(default_password="test"):
|
|||
email="expert-vv.expert2@eiger-versicherungen.ch",
|
||||
first_name="Christa",
|
||||
last_name="von Allmen",
|
||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
||||
avatar_image="uk1.patrizia.huggel.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
email="expert-vv.expert3@eiger-versicherungen.ch",
|
||||
|
|
@ -199,53 +218,53 @@ def create_default_users(default_password="test"):
|
|||
email="patrizia.huggel@eiger-versicherungen.ch",
|
||||
first_name="Patrizia",
|
||||
last_name="Huggel",
|
||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
||||
avatar_image="uk1.patrizia.huggel.jpg",
|
||||
password="myvbv1234",
|
||||
)
|
||||
_create_student_user(
|
||||
email="andreas.feuz@eiger-versicherungen.ch",
|
||||
first_name="Andreas",
|
||||
last_name="Feuz",
|
||||
avatar_url="/static/avatars/uk1.daniel.tanaka.jpg",
|
||||
avatar_image="uk1.daniel.tanaka.jpg",
|
||||
password="myvbv1234",
|
||||
)
|
||||
_create_student_user(
|
||||
email="daniel.tanaka@eiger-versicherung.ch",
|
||||
first_name="Daniel",
|
||||
last_name="Tanaka",
|
||||
avatar_url="/static/avatars/uk1.daniel.tanaka.jpg",
|
||||
avatar_image="uk1.daniel.tanaka.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
email="maria.spini@eiger-versicherung.ch",
|
||||
first_name="Maria",
|
||||
last_name="Spini",
|
||||
avatar_url="/static/avatars/uk1.maria.spini.jpg",
|
||||
avatar_image="uk1.maria.spini.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
email="christian.koller@eiger-versicherung.ch",
|
||||
first_name="Christian",
|
||||
last_name="Koller",
|
||||
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
||||
avatar_image="uk1.christian.koller.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
email="michael.meier@example.com",
|
||||
first_name="Michael",
|
||||
last_name="Meier",
|
||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
||||
avatar_image="uk1.michael.meier.jpg",
|
||||
password="myvbv1234",
|
||||
)
|
||||
_create_student_user(
|
||||
email="lina.egger@example.com",
|
||||
first_name="Lina",
|
||||
last_name="Egger",
|
||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
||||
avatar_image="uk1.lina.egger.jpg",
|
||||
password="myvbv1234",
|
||||
)
|
||||
_create_student_user(
|
||||
email="evelyn.schmid@example.com",
|
||||
first_name="Evelyn",
|
||||
last_name="Schmid",
|
||||
avatar_url="/static/avatars/uk1.evelyn.schmid.jpg",
|
||||
avatar_image="uk1.evelyn.schmid.jpg",
|
||||
)
|
||||
|
||||
_create_student_user(
|
||||
|
|
@ -272,7 +291,7 @@ def create_default_users(default_password="test"):
|
|||
email="luca.dupont@assurance.ch",
|
||||
first_name="Luca",
|
||||
last_name="Dupont",
|
||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
||||
avatar_image="uk1.michael.meier.jpg",
|
||||
password="myafa1234",
|
||||
language="fr",
|
||||
)
|
||||
|
|
@ -280,7 +299,7 @@ def create_default_users(default_password="test"):
|
|||
email="patrick.muster@eiger-versicherungen.ch",
|
||||
first_name="Patrick",
|
||||
last_name="Muster",
|
||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
||||
avatar_image="uk1.michael.meier.jpg",
|
||||
password="myvbv1234",
|
||||
language="de",
|
||||
)
|
||||
|
|
@ -288,7 +307,7 @@ def create_default_users(default_password="test"):
|
|||
email="geraldine.kolly@assurance.ch",
|
||||
first_name="Géraldine",
|
||||
last_name="Kolly",
|
||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
||||
avatar_image="uk1.patrizia.huggel.jpg",
|
||||
password="myafa1234",
|
||||
language="fr",
|
||||
)
|
||||
|
|
@ -299,35 +318,35 @@ def create_default_users(default_password="test"):
|
|||
email="test-trainer1@example.com",
|
||||
first_name="Test",
|
||||
last_name="Trainer1",
|
||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
||||
avatar_image="uk1.patrizia.huggel.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
id=TEST_TRAINER2_USER_ID,
|
||||
email="test-trainer2@example.com",
|
||||
first_name="Test",
|
||||
last_name="Trainer2",
|
||||
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
||||
avatar_image="uk1.christian.koller.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
id=TEST_STUDENT1_USER_ID,
|
||||
email="test-student1@example.com",
|
||||
first_name="Test",
|
||||
last_name="Student1",
|
||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
||||
avatar_image="uk1.michael.meier.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
id=TEST_STUDENT2_USER_ID,
|
||||
email="test-student2@example.com",
|
||||
first_name="Test",
|
||||
last_name="Student2",
|
||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
||||
avatar_image="uk1.lina.egger.jpg",
|
||||
)
|
||||
_create_student_user(
|
||||
id=TEST_STUDENT3_USER_ID,
|
||||
email="test-student3@example.com",
|
||||
first_name="Test",
|
||||
last_name="Student3",
|
||||
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
||||
avatar_image="uk1.christian.koller.jpg",
|
||||
)
|
||||
_create_staff_user(
|
||||
email="matthias.wirth@vbv-afa.ch",
|
||||
|
|
@ -341,7 +360,6 @@ def create_default_users(default_password="test"):
|
|||
last_name="Regionalleiter",
|
||||
password=default_password,
|
||||
language="de",
|
||||
avatar_url="",
|
||||
)
|
||||
_create_user(
|
||||
_id=TEST_MENTOR1_USER_ID,
|
||||
|
|
@ -350,7 +368,6 @@ def create_default_users(default_password="test"):
|
|||
last_name="Mentor",
|
||||
password=default_password,
|
||||
language="de",
|
||||
avatar_url="",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
|
||||
from graphene import String
|
||||
from graphene.types.generic import GenericScalar
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
|
|
@ -7,6 +8,8 @@ from vbv_lernwelt.core.models import User
|
|||
|
||||
|
||||
class UserObjectType(DjangoObjectType):
|
||||
avatar_url = String(source="avatar_url")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ from vbv_lernwelt.core.create_default_users import create_default_users
|
|||
@click.command()
|
||||
def command():
|
||||
print("Creating default users.")
|
||||
create_default_users()
|
||||
create_default_users(set_avatar=True)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
from vbv_lernwelt.core.model_utils import migrate_avatars
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("media_files", "0001_initial"),
|
||||
("core", "0003_organisations"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="avatar",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Avatar image for the user",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="media_files.userimage",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_avatars),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="avatar_url",
|
||||
),
|
||||
]
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
|
|
@ -144,3 +146,39 @@ def remove_organisations(apps=None, schema_editor=None):
|
|||
Organisation.objects.filter(
|
||||
organisation_id=org_id,
|
||||
).delete()
|
||||
|
||||
|
||||
def migrate_avatars(apps=None, schema_editor=None):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
if apps is None:
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.media_files.models import UserImage
|
||||
else:
|
||||
User = apps.get_model("core", "User")
|
||||
UserImage = apps.get_model("media_files", "UserImage")
|
||||
|
||||
# Models created by Django migration don't contain methods of the parent model.
|
||||
# We need to add them manually.
|
||||
from wagtail.images.models import AbstractImage
|
||||
|
||||
UserImage.get_upload_to = AbstractImage.get_upload_to
|
||||
|
||||
avatar_dir = settings.APPS_DIR / "static" / "avatars"
|
||||
|
||||
for user in User.objects.all().exclude(
|
||||
avatar_url="/static/avatars/myvbv-default-avatar.png"
|
||||
):
|
||||
if not user.avatar_url:
|
||||
continue
|
||||
|
||||
avatar_file = user.avatar_url.split("/")[-1]
|
||||
|
||||
try:
|
||||
with open(avatar_dir / avatar_file, "rb") as f:
|
||||
image = UserImage.objects.create(
|
||||
file=File(f),
|
||||
)
|
||||
user.avatar = image
|
||||
user.save()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import uuid
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models import JSONField
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class Organisation(models.Model):
|
||||
|
|
@ -34,9 +35,14 @@ class User(AbstractUser):
|
|||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
avatar_url = models.CharField(
|
||||
max_length=254, blank=True, default="/static/avatars/myvbv-default-avatar.png"
|
||||
avatar = models.ForeignKey(
|
||||
"media_files.UserImage",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text="Avatar image for the user",
|
||||
)
|
||||
|
||||
email = models.EmailField("email address", unique=True)
|
||||
sso_id = models.UUIDField(
|
||||
"SSO subscriber ID", unique=True, null=True, blank=True, default=None
|
||||
|
|
@ -48,6 +54,16 @@ class User(AbstractUser):
|
|||
Organisation, on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
if self.avatar:
|
||||
filter_spec = "fill-400x400"
|
||||
self.avatar.get_rendition(filter_spec)
|
||||
url = reverse("user_image", kwargs={"image_id": self.avatar.id})
|
||||
return f"{url}?filter={filter_spec}"
|
||||
else:
|
||||
return "/static/avatars/myvbv-default-avatar.png"
|
||||
|
||||
|
||||
class SecurityRequestResponseLog(models.Model):
|
||||
label = models.CharField(max_length=255, blank=True, default="")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
from unittest import skipIf
|
||||
|
||||
from django.core.files import File
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from vbv_lernwelt.course.creators.test_utils import create_user
|
||||
from vbv_lernwelt.media_files.models import UserImage
|
||||
|
||||
TEST_IMAGE = Path(__file__).parent / "test_images" / "user1_profile.jpg"
|
||||
|
||||
|
||||
@skipIf(
|
||||
os.environ.get("ENABLE_S3_STORAGE_UNIT_TESTS") is None,
|
||||
"Only enable tests by setting ENABLE_S3_STORAGE_UNIT_TESTS=1",
|
||||
)
|
||||
class UserImageViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_user("supervisor")
|
||||
|
||||
with open(TEST_IMAGE, "rb") as f:
|
||||
self.user_image, _ = UserImage.objects.get_or_create(
|
||||
file=File(f, name=TEST_IMAGE.name),
|
||||
)
|
||||
|
||||
def test_image(self):
|
||||
# GIVEN
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# WHEN
|
||||
response = self._get_image(self.user_image.id, "fill-300x150")
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "image/jpeg")
|
||||
|
||||
def test_invalid_id(self):
|
||||
# GIVEN
|
||||
image_id = 123456789
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# WHEN
|
||||
response = self._get_image(image_id)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_invalid_filter(self):
|
||||
# GIVEN
|
||||
self.client.force_login(self.user)
|
||||
filter_spec = "invalid-filter"
|
||||
|
||||
# WHEN
|
||||
response = self._get_image(self.user_image.id, filter_spec)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(
|
||||
response.content, f"Invalid filter spec: {filter_spec}".encode()
|
||||
)
|
||||
|
||||
def _get_image(self, image_id, filter_spec=None):
|
||||
url = reverse("user_image", kwargs={"image_id": image_id})
|
||||
if filter_spec:
|
||||
url += f"?filter={filter_spec}"
|
||||
|
||||
return self.client.get(url)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import imghdr
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
import structlog
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from wagtail.images.exceptions import InvalidFilterSpecError
|
||||
from wagtail.images.models import SourceImageIOError
|
||||
|
||||
from vbv_lernwelt.media_files.models import UserImage
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def user_image(request, image_id):
|
||||
image = get_object_or_404(UserImage, id=image_id)
|
||||
|
||||
filter_spec = request.GET.get("filter", "original")
|
||||
|
||||
try:
|
||||
rendition = image.get_rendition(filter_spec)
|
||||
except SourceImageIOError:
|
||||
return HttpResponse(
|
||||
"Source image file not found", content_type="text/plain", status=410
|
||||
)
|
||||
except InvalidFilterSpecError:
|
||||
return HttpResponse(
|
||||
"Invalid filter spec: " + filter_spec,
|
||||
content_type="text/plain",
|
||||
status=400,
|
||||
)
|
||||
|
||||
rendition.file.open("rb")
|
||||
image_format = imghdr.what(rendition.file)
|
||||
return StreamingHttpResponse(
|
||||
FileWrapper(rendition.file), content_type="image/" + image_format
|
||||
)
|
||||
|
|
@ -1,78 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Anzeigebild" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119.93 99">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
stroke-width: 2.29px;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke: #a66635;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2, .cls-3 {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.cls-2, .cls-3 {
|
||||
stroke-width: 2.52px;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
stroke: #7d4e2a;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #a66635;
|
||||
}
|
||||
|
||||
.cls-4, .cls-5 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #3c3c3b;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #afc8df;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #97b3cd;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: #7d95ac;
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: #edf2f6;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<ellipse class="cls-9" cx="59.96" cy="64.42" rx="59.96" ry="34.58"/>
|
||||
<g>
|
||||
<path class="cls-5" d="m74.08,46.16c-.44-.42-1.14-.4-1.55.05-1.23,1.31-2.55,2.59-3.91,3.86,2.73-4.53,4.4-9.49,4.4-13.87,0-6.3-3.53-10.37-8.99-10.37-2.5,0-5.32.86-8.16,2.5-8.5,4.91-15.47,15.76-16.02,24.67,0,.03-.02.06-.02.09-.02.11-1.95,11.31-10.65,17.37-1.32.03-2.54-.05-3.64-.28-.59-.12-1.17.26-1.3.86-.12.59.26,1.17.86,1.3,1.11.23,2.31.34,3.59.34,6.44,0,14.93-2.82,23.98-8.05,1.52-.88,3.03-1.81,4.52-2.79.19-.11.38-.23.57-.35.04-.03.09-.06.13-.08.24-.15.47-.3.7-.46,0,0,.02-.01.03-.02.53-.36,1.05-.74,1.56-1.14,5.08-3.62,9.87-7.75,13.94-12.06.42-.44.4-1.14-.04-1.55Zm-30.12,11.01c0-6.08,5.83-14.39,13.01-18.53,2.54-1.46,4.96-2.24,7-2.24,3.17,0,4.91,1.82,4.91,5.12,0,2.48-.98,5.33-2.62,8.12.11-.58.17-1.16.17-1.71,0-3.29-2.03-5.41-5.17-5.41-1.63,0-3.5.58-5.39,1.67-5.3,3.06-9.45,9.22-9.45,14.02,0,1.67.53,3.04,1.46,3.99-.16-.02-.32-.04-.48-.07-2.23-.5-3.44-2.22-3.44-4.96Zm11.31,3.2c-1.34.69-2.6,1.05-3.69,1.05-1.94,0-2.97-1.11-2.97-3.21,0-3.96,3.83-9.51,8.36-12.12,1.56-.9,3.04-1.37,4.29-1.37,1.94,0,2.97,1.11,2.97,3.21,0,3.48-2.96,8.19-6.76,11.05-.55.38-1.11.74-1.67,1.11-.18.1-.35.19-.52.29Zm1.69-30.15c2.5-1.44,4.94-2.21,7.06-2.21,4.26,0,6.8,3.05,6.8,8.17,0,.81-.06,1.64-.18,2.48-.93-2.81-3.31-4.47-6.67-4.47-2.43,0-5.23.88-8.1,2.53-6.84,3.95-12.56,11.52-13.84,17.9,0-.18-.02-.36-.02-.55,0-8.4,6.71-19.1,14.95-23.86Zm-16.43,28.37c1.09,3.08,3.37,5.13,6.46,5.69.43.09.88.15,1.35.17-5.47,2.84-10.69,4.74-15.16,5.54,3.85-3.63,6.09-8.04,7.34-11.41Z"/>
|
||||
<path class="cls-3" d="m27.8,71.52c17.06.45,36.59-15.19,46.56-25.16"/>
|
||||
<line class="cls-3" x1="46.7" y1="66.61" x2="51.7" y2="43.79"/>
|
||||
<line class="cls-3" x1="66.69" y1="53.27" x2="63.26" y2="39.41"/>
|
||||
<path class="cls-3" d="m67.18,26.01l-15.64,7.88c-.7.4-.92.56-1.32.91"/>
|
||||
<line class="cls-2" x1="67.66" y1="78.58" x2="72.58" y2="56.13"/>
|
||||
<line class="cls-2" x1="86.47" y1="66.17" x2="81.41" y2="45.68"/>
|
||||
<path class="cls-6" d="m47.64,47.78l.14-1.02c.2-1.45,1.2-2.99,2.36-3.66.04-.03.09-.05.13-.07l10.37-5.33s.09-.05.13-.07c.96-.55,1.83-1.73,2.18-2.97L71.84,2.47c.24-.85.84-1.65,1.49-2.03.06-.03.12-.07.18-.09l.5-.22c.38-.17.71-.16.96-.02l20.26,11.77c-.25-.14-.58-.15-.96.02l-.5.22c-.06.03-.12.06-.18.09-.65.38-1.25,1.18-1.49,2.03l-8.88,32.19c-.36,1.24-1.22,2.42-2.18,2.97-.04.03-.09.05-.13.07l-10.37,5.33s-.09.05-.13.07c-1.16.67-2.17,2.21-2.36,3.66l-.14,1.02c-.07.53.08.92.35,1.08l-20.26-11.77c-.28-.16-.43-.55-.35-1.08Z"/>
|
||||
<path class="cls-7" d="m93.59,12.21c-.65.38-1.25,1.18-1.49,2.03l-8.88,32.19c-.36,1.24-1.22,2.42-2.18,2.97-.04.03-.09.05-.13.07l-10.37,5.33s-.09.05-.13.07c-1.16.67-2.17,2.21-2.36,3.66l-.14,1.02c-.13.97.47,1.45,1.26.99l15.17-8.76c.77-.44,1.46-1.39,1.73-2.38l9.54-35.79c.36-1.34-.29-2.18-1.33-1.72l-.5.22c-.06.03-.12.06-.18.09Z"/>
|
||||
<path class="cls-8" d="m68.83,56.45l-20.26-11.77c-.41.64-.7,1.37-.79,2.07l-.14,1.02c-.07.53.08.92.35,1.08l20.26,11.77c-.28-.16-.43-.55-.35-1.08l.14-1.02c.1-.71.39-1.44.79-2.07Z"/>
|
||||
<path class="cls-8" d="m61.85,36.69c.49-.58.89-1.29,1.1-2.04L71.84,2.47c.1-.35.27-.68.47-.98l20.26,11.77c-.2.31-.37.64-.47.99l-8.88,32.19c-.21.74-.61,1.46-1.1,2.04l-20.26-11.77Z"/>
|
||||
<path class="cls-4" d="m94.96,58.5c-.44-.42-1.14-.4-1.55.05-1.23,1.31-2.55,2.59-3.91,3.86,2.73-4.53,4.4-9.49,4.4-13.87,0-6.3-3.53-10.37-8.99-10.37-2.5,0-5.32.86-8.16,2.5-8.5,4.91-15.47,15.76-16.02,24.67,0,.03-.02.06-.02.09-.02.11-1.95,11.31-10.65,17.37-1.32.03-2.54-.05-3.64-.28-.59-.12-1.17.26-1.3.86-.12.59.26,1.17.86,1.3,1.11.23,2.31.34,3.59.34,6.44,0,14.93-2.82,23.98-8.05,1.52-.88,3.03-1.81,4.52-2.79.19-.11.38-.23.57-.35.04-.03.09-.06.13-.08.24-.15.47-.3.7-.46,0,0,.02-.01.03-.02.53-.36,1.05-.74,1.56-1.14,5.08-3.62,9.87-7.75,13.94-12.06.42-.44.4-1.14-.04-1.55Zm-30.12,11.01c0-6.08,5.83-14.39,13.01-18.53,2.54-1.46,4.96-2.24,7-2.24,3.17,0,4.91,1.82,4.91,5.12,0,2.48-.98,5.33-2.62,8.12.11-.58.17-1.16.17-1.71,0-3.29-2.03-5.41-5.17-5.41-1.63,0-3.5.58-5.39,1.67-5.3,3.06-9.45,9.22-9.45,14.02,0,1.67.53,3.04,1.46,3.99-.16-.02-.32-.04-.48-.07-2.23-.5-3.44-2.22-3.44-4.96Zm11.31,3.2c-1.34.69-2.6,1.05-3.69,1.05-1.94,0-2.97-1.11-2.97-3.21,0-3.96,3.83-9.51,8.36-12.12,1.56-.9,3.04-1.37,4.29-1.37,1.94,0,2.97,1.11,2.97,3.21,0,3.48-2.96,8.19-6.76,11.05-.55.38-1.11.74-1.67,1.11-.18.1-.35.19-.52.29Zm1.69-30.15c2.5-1.44,4.94-2.21,7.06-2.21,4.26,0,6.8,3.05,6.8,8.17,0,.81-.06,1.64-.18,2.48-.93-2.81-3.31-4.47-6.67-4.47-2.43,0-5.23.88-8.1,2.53-6.84,3.95-12.56,11.52-13.84,17.9,0-.18-.02-.36-.02-.55,0-8.4,6.71-19.1,14.95-23.86Zm-16.43,28.37c1.09,3.08,3.37,5.13,6.46,5.69.43.09.88.15,1.35.17-5.47,2.84-10.69,4.74-15.16,5.54,3.85-3.63,6.09-8.04,7.34-11.41Z"/>
|
||||
<path class="cls-2" d="m48.68,83.86c17.06.45,36.59-15.19,46.56-25.16"/>
|
||||
<path class="cls-2" d="m87.32,38.72l-14.9,7.51c-.7.4-.92.56-1.32.91"/>
|
||||
<line class="cls-1" x1="58.22" y1="82.57" x2="37.41" y2="70.56"/>
|
||||
</g>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 120 120" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
|
||||
<g id="Artboard1" transform="matrix(1,0,0,1.21111,0,0)">
|
||||
<rect x="0" y="0" width="119.93" height="99" style="fill:none;"/>
|
||||
<g transform="matrix(1,0,0,1,11.64,0.93876)">
|
||||
<g transform="matrix(0.805954,0,0,0.670366,0,15.3769)">
|
||||
<ellipse cx="59.96" cy="64.42" rx="59.96" ry="34.58" style="fill:rgb(237,242,246);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.805954,0,0,0.670366,0,15.3769)">
|
||||
<path d="M74.08,46.16C73.64,45.74 72.94,45.76 72.53,46.21C71.3,47.52 69.98,48.8 68.62,50.07C71.35,45.54 73.02,40.58 73.02,36.2C73.02,29.9 69.49,25.83 64.03,25.83C61.53,25.83 58.71,26.69 55.87,28.33C47.37,33.24 40.4,44.09 39.85,53C39.85,53.03 39.83,53.06 39.83,53.09C39.81,53.2 37.88,64.4 29.18,70.46C27.86,70.49 26.64,70.41 25.54,70.18C24.95,70.06 24.37,70.44 24.24,71.04C24.12,71.63 24.5,72.21 25.1,72.34C26.21,72.57 27.41,72.68 28.69,72.68C35.13,72.68 43.62,69.86 52.67,64.63C54.19,63.75 55.7,62.82 57.19,61.84C57.38,61.73 57.57,61.61 57.76,61.49C57.8,61.46 57.85,61.43 57.89,61.41C58.13,61.26 58.36,61.11 58.59,60.95C58.59,60.95 58.61,60.94 58.62,60.93C59.15,60.57 59.67,60.19 60.18,59.79C65.26,56.17 70.05,52.04 74.12,47.73C74.54,47.29 74.52,46.59 74.08,46.18L74.08,46.16ZM43.96,57.17C43.96,51.09 49.79,42.78 56.97,38.64C59.51,37.18 61.93,36.4 63.97,36.4C67.14,36.4 68.88,38.22 68.88,41.52C68.88,44 67.9,46.85 66.26,49.64C66.37,49.06 66.43,48.48 66.43,47.93C66.43,44.64 64.4,42.52 61.26,42.52C59.63,42.52 57.76,43.1 55.87,44.19C50.57,47.25 46.42,53.41 46.42,58.21C46.42,59.88 46.95,61.25 47.88,62.2C47.72,62.18 47.56,62.16 47.4,62.13C45.17,61.63 43.96,59.91 43.96,57.17ZM55.27,60.37C53.93,61.06 52.67,61.42 51.58,61.42C49.64,61.42 48.61,60.31 48.61,58.21C48.61,54.25 52.44,48.7 56.97,46.09C58.53,45.19 60.01,44.72 61.26,44.72C63.2,44.72 64.23,45.83 64.23,47.93C64.23,51.41 61.27,56.12 57.47,58.98C56.92,59.36 56.36,59.72 55.8,60.09C55.62,60.19 55.45,60.28 55.28,60.38L55.27,60.37ZM56.96,30.22C59.46,28.78 61.9,28.01 64.02,28.01C68.28,28.01 70.82,31.06 70.82,36.18C70.82,36.99 70.76,37.82 70.64,38.66C69.71,35.85 67.33,34.19 63.97,34.19C61.54,34.19 58.74,35.07 55.87,36.72C49.03,40.67 43.31,48.24 42.03,54.62C42.03,54.44 42.01,54.26 42.01,54.07C42.01,45.67 48.72,34.97 56.96,30.21L56.96,30.22ZM40.53,58.59C41.62,61.67 43.9,63.72 46.99,64.28C47.42,64.37 47.87,64.43 48.34,64.45C42.87,67.29 37.65,69.19 33.18,69.99C37.03,66.36 39.27,61.95 40.52,58.58L40.53,58.59Z" style="fill:rgb(60,60,59);fill-rule:nonzero;"/>
|
||||
<path d="M27.8,71.52C44.86,71.97 64.39,56.33 74.36,46.36" style="fill-rule:nonzero;stroke:rgb(125,78,42);stroke-width:1px;"/>
|
||||
<path d="M46.7,66.61L51.7,43.79" style="fill:none;fill-rule:nonzero;stroke:rgb(125,78,42);stroke-width:1px;"/>
|
||||
<path d="M66.69,53.27L63.26,39.41" style="fill:none;fill-rule:nonzero;stroke:rgb(125,78,42);stroke-width:1px;"/>
|
||||
<path d="M67.18,26.01L51.54,33.89C50.84,34.29 50.62,34.45 50.22,34.8" style="fill-rule:nonzero;stroke:rgb(125,78,42);stroke-width:1px;"/>
|
||||
<path d="M67.66,78.58L72.58,56.13" style="fill:none;fill-rule:nonzero;"/>
|
||||
<path d="M86.47,66.17L81.41,45.68" style="fill:none;fill-rule:nonzero;"/>
|
||||
<path d="M47.64,47.78L47.78,46.76C47.98,45.31 48.98,43.77 50.14,43.1C50.18,43.07 50.23,43.05 50.27,43.03L60.64,37.7C60.64,37.7 60.73,37.65 60.77,37.63C61.73,37.08 62.6,35.9 62.95,34.66L71.84,2.47C72.08,1.62 72.68,0.82 73.33,0.44C73.39,0.41 73.45,0.37 73.51,0.35L74.01,0.13C74.39,-0.04 74.72,-0.03 74.97,0.11L95.23,11.88C94.98,11.74 94.65,11.73 94.27,11.9L93.77,12.12C93.71,12.15 93.65,12.18 93.59,12.21C92.94,12.59 92.34,13.39 92.1,14.24L83.22,46.43C82.86,47.67 82,48.85 81.04,49.4C81,49.43 80.95,49.45 80.91,49.47L70.54,54.8C70.54,54.8 70.45,54.85 70.41,54.87C69.25,55.54 68.24,57.08 68.05,58.53L67.91,59.55C67.84,60.08 67.99,60.47 68.26,60.63L48,48.86C47.72,48.7 47.57,48.31 47.65,47.78L47.64,47.78Z" style="fill:rgb(175,200,223);fill-rule:nonzero;"/>
|
||||
<path d="M93.59,12.21C92.94,12.59 92.34,13.39 92.1,14.24L83.22,46.43C82.86,47.67 82,48.85 81.04,49.4C81,49.43 80.95,49.45 80.91,49.47L70.54,54.8C70.54,54.8 70.45,54.85 70.41,54.87C69.25,55.54 68.24,57.08 68.05,58.53L67.91,59.55C67.78,60.52 68.38,61 69.17,60.54L84.34,51.78C85.11,51.34 85.8,50.39 86.07,49.4L95.61,13.61C95.97,12.27 95.32,11.43 94.28,11.89L93.78,12.11C93.72,12.14 93.66,12.17 93.6,12.2L93.59,12.21Z" style="fill:rgb(151,179,205);fill-rule:nonzero;"/>
|
||||
<path d="M68.83,56.45L48.57,44.68C48.16,45.32 47.87,46.05 47.78,46.75L47.64,47.77C47.57,48.3 47.72,48.69 47.99,48.85L68.25,60.62C67.97,60.46 67.82,60.07 67.9,59.54L68.04,58.52C68.14,57.81 68.43,57.08 68.83,56.45Z" style="fill:rgb(125,149,172);fill-rule:nonzero;"/>
|
||||
<path d="M61.85,36.69C62.34,36.11 62.74,35.4 62.95,34.65L71.84,2.47C71.94,2.12 72.11,1.79 72.31,1.49L92.57,13.26C92.37,13.57 92.2,13.9 92.1,14.25L83.22,46.44C83.01,47.18 82.61,47.9 82.12,48.48L61.86,36.71L61.85,36.69Z" style="fill:rgb(125,149,172);fill-rule:nonzero;"/>
|
||||
<path d="M94.96,58.5C94.52,58.08 93.82,58.1 93.41,58.55C92.18,59.86 90.86,61.14 89.5,62.41C92.23,57.88 93.9,52.92 93.9,48.54C93.9,42.24 90.37,38.17 84.91,38.17C82.41,38.17 79.59,39.03 76.75,40.67C68.25,45.58 61.28,56.43 60.73,65.34C60.73,65.37 60.71,65.4 60.71,65.43C60.69,65.54 58.76,76.74 50.06,82.8C48.74,82.83 47.52,82.75 46.42,82.52C45.83,82.4 45.25,82.78 45.12,83.38C45,83.97 45.38,84.55 45.98,84.68C47.09,84.91 48.29,85.02 49.57,85.02C56.01,85.02 64.5,82.2 73.55,76.97C75.07,76.09 76.58,75.16 78.07,74.18C78.26,74.07 78.45,73.95 78.64,73.83C78.68,73.8 78.73,73.77 78.77,73.75C79.01,73.6 79.24,73.45 79.47,73.29C79.47,73.29 79.49,73.28 79.5,73.27C80.03,72.91 80.55,72.53 81.06,72.13C86.14,68.51 90.93,64.38 95,60.07C95.42,59.63 95.4,58.93 94.96,58.52L94.96,58.5ZM64.84,69.51C64.84,63.43 70.67,55.12 77.85,50.98C80.39,49.52 82.81,48.74 84.85,48.74C88.02,48.74 89.76,50.56 89.76,53.86C89.76,56.34 88.78,59.19 87.14,61.98C87.25,61.4 87.31,60.82 87.31,60.27C87.31,56.98 85.28,54.86 82.14,54.86C80.51,54.86 78.64,55.44 76.75,56.53C71.45,59.59 67.3,65.75 67.3,70.55C67.3,72.22 67.83,73.59 68.76,74.54C68.6,74.52 68.44,74.5 68.28,74.47C66.05,73.97 64.84,72.25 64.84,69.51ZM76.15,72.71C74.81,73.4 73.55,73.76 72.46,73.76C70.52,73.76 69.49,72.65 69.49,70.55C69.49,66.59 73.32,61.04 77.85,58.43C79.41,57.53 80.89,57.06 82.14,57.06C84.08,57.06 85.11,58.17 85.11,60.27C85.11,63.75 82.15,68.46 78.35,71.32C77.8,71.7 77.24,72.06 76.68,72.43C76.5,72.53 76.33,72.62 76.16,72.72L76.15,72.71ZM77.84,42.56C80.34,41.12 82.78,40.35 84.9,40.35C89.16,40.35 91.7,43.4 91.7,48.52C91.7,49.33 91.64,50.16 91.52,51C90.59,48.19 88.21,46.53 84.85,46.53C82.42,46.53 79.62,47.41 76.75,49.06C69.91,53.01 64.19,60.58 62.91,66.96C62.91,66.78 62.89,66.6 62.89,66.41C62.89,58.01 69.6,47.31 77.84,42.55L77.84,42.56ZM61.41,70.93C62.5,74.01 64.78,76.06 67.87,76.62C68.3,76.71 68.75,76.77 69.22,76.79C63.75,79.63 58.53,81.53 54.06,82.33C57.91,78.7 60.15,74.29 61.4,70.92L61.41,70.93Z" style="fill:rgb(166,102,53);fill-rule:nonzero;"/>
|
||||
<path d="M48.68,83.86C65.74,84.31 85.27,68.67 95.24,58.7" style="fill-rule:nonzero;"/>
|
||||
<path d="M87.32,38.72L72.42,46.23C71.72,46.63 71.5,46.79 71.1,47.14" style="fill-rule:nonzero;"/>
|
||||
<path d="M58.22,82.57L37.41,70.56" style="fill:none;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 7.6 KiB |
Loading…
Reference in New Issue