Merge branch 'feature/MS-924-Wagtail-Image-Optimization' into develop
# Conflicts: # client/src/components/modules/Module.vue
This commit is contained in:
commit
69d3fa845b
16
README.md
16
README.md
|
|
@ -244,10 +244,24 @@ python manage.py export_schema_for_cypress
|
|||
|
||||
### Generate GraphQL SDL Document
|
||||
|
||||
|
||||
for linux:
|
||||
```bash
|
||||
cd server
|
||||
./graphql-schema.sh && npm run codegen --../prefix client
|
||||
```
|
||||
python manage.py export_schema_graphql
|
||||
|
||||
For macOS: (there is a problem with the sed command)
|
||||
|
||||
```bash
|
||||
cd server
|
||||
./macos-graphql-schema.sh && npm run codegen --../prefix client
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Backup to S3
|
||||
|
||||
From https://pawelurbanek.com/heroku-postgresql-s3-backup
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ const documents = {
|
|||
"\n fragment SchoolClassParts on SchoolClassNode {\n id\n name\n }\n": types.SchoolClassPartsFragmentDoc,
|
||||
"\n fragment UserParts on PrivateUserNode {\n id\n pk\n username\n email\n firstName\n lastName\n avatarUrl\n expiryDate\n readOnly\n lastModuleLevel {\n id\n name\n filterAttributeType\n }\n lastModule {\n id\n slug\n }\n lastTopic {\n id\n slug\n }\n selectedClass {\n id\n readOnly\n }\n recentModules(orderBy: \"-visited\") {\n edges {\n node {\n ...ModuleParts\n }\n }\n }\n schoolClasses {\n ...SchoolClassParts\n }\n }\n": types.UserPartsFragmentDoc,
|
||||
"\n fragment TeamParts on TeamNode {\n name\n code\n id\n members {\n firstName\n lastName\n id\n isMe\n }\n }\n": types.TeamPartsFragmentDoc,
|
||||
"\n fragment ModuleParts on ModuleNode {\n id\n title\n metaTitle\n teaser\n intro\n slug\n heroImage\n heroSource\n solutionsEnabled\n highlights {\n ...HighlightParts\n }\n language\n inEditMode @client\n level {\n id\n name\n }\n category {\n id\n name\n }\n topic {\n slug\n title\n }\n bookmark {\n note {\n id\n text\n }\n }\n }\n": types.ModulePartsFragmentDoc,
|
||||
"\nfragment WagtailImageParts on WagtailImageNode {\n id\n src\n alt\n width\n height\n title\n srcset\n }\n": types.WagtailImagePartsFragmentDoc,
|
||||
"\n fragment ModuleParts on ModuleNode {\n id\n title\n metaTitle\n teaser\n intro\n slug\n heroImage { ...WagtailImageParts }\n heroSource\n solutionsEnabled\n highlights {\n ...HighlightParts\n }\n language\n inEditMode @client\n level {\n id\n name\n }\n category {\n id\n name\n }\n topic {\n slug\n title\n }\n bookmark {\n note {\n id\n text\n }\n }\n }\n": types.ModulePartsFragmentDoc,
|
||||
"\n query MeQuery {\n me {\n ...UserParts\n team {\n ...TeamParts\n }\n isTeacher\n permissions\n onboardingVisited\n }\n }\n ": types.MeQueryDocument,
|
||||
"\n fragment InstrumentHighlightsWithIdOnlyFragment on InstrumentNode {\n highlights {\n id\n }\n }\n ": types.InstrumentHighlightsWithIdOnlyFragmentFragmentDoc,
|
||||
"\n fragment ChapterHighlightsWithIdOnlyFragment on ChapterNode {\n highlights {\n id\n }\n }\n ": types.ChapterHighlightsWithIdOnlyFragmentFragmentDoc,
|
||||
|
|
@ -194,7 +195,11 @@ export function graphql(source: "\n fragment TeamParts on TeamNode {\n name\
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ModuleParts on ModuleNode {\n id\n title\n metaTitle\n teaser\n intro\n slug\n heroImage\n heroSource\n solutionsEnabled\n highlights {\n ...HighlightParts\n }\n language\n inEditMode @client\n level {\n id\n name\n }\n category {\n id\n name\n }\n topic {\n slug\n title\n }\n bookmark {\n note {\n id\n text\n }\n }\n }\n"): (typeof documents)["\n fragment ModuleParts on ModuleNode {\n id\n title\n metaTitle\n teaser\n intro\n slug\n heroImage\n heroSource\n solutionsEnabled\n highlights {\n ...HighlightParts\n }\n language\n inEditMode @client\n level {\n id\n name\n }\n category {\n id\n name\n }\n topic {\n slug\n title\n }\n bookmark {\n note {\n id\n text\n }\n }\n }\n"];
|
||||
export function graphql(source: "\nfragment WagtailImageParts on WagtailImageNode {\n id\n src\n alt\n width\n height\n title\n srcset\n }\n"): (typeof documents)["\nfragment WagtailImageParts on WagtailImageNode {\n id\n src\n alt\n width\n height\n title\n srcset\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ModuleParts on ModuleNode {\n id\n title\n metaTitle\n teaser\n intro\n slug\n heroImage { ...WagtailImageParts }\n heroSource\n solutionsEnabled\n highlights {\n ...HighlightParts\n }\n language\n inEditMode @client\n level {\n id\n name\n }\n category {\n id\n name\n }\n topic {\n slug\n title\n }\n bookmark {\n note {\n id\n text\n }\n }\n }\n"): (typeof documents)["\n fragment ModuleParts on ModuleNode {\n id\n title\n metaTitle\n teaser\n intro\n slug\n heroImage { ...WagtailImageParts }\n heroSource\n solutionsEnabled\n highlights {\n ...HighlightParts\n }\n language\n inEditMode @client\n level {\n id\n name\n }\n category {\n id\n name\n }\n topic {\n slug\n title\n }\n bookmark {\n note {\n id\n text\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,18 +1,24 @@
|
|||
<template>
|
||||
<img
|
||||
:src="value.path"
|
||||
alt=""
|
||||
<wagtail-image
|
||||
:src="value.src"
|
||||
:srcset="value.srcset"
|
||||
:alt="value.alt"
|
||||
:original-height="value.height"
|
||||
:original-width="value.width"
|
||||
class="image-block"
|
||||
@click="openFullscreen"
|
||||
/>
|
||||
></wagtail-image>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WagtailImage from '@/components/ui/WagtailImage.vue';
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
components: { WagtailImage },
|
||||
methods: {
|
||||
openFullscreen() {
|
||||
this.$store.dispatch('showFullscreenImage', this.value.path);
|
||||
this.$store.dispatch('showFullscreenImage', this.value.src);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<img
|
||||
:src="value.url"
|
||||
alt=""
|
||||
:srcset="srcset"
|
||||
class="image-block"
|
||||
alt=""
|
||||
@click="openFullscreen"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -10,6 +11,19 @@
|
|||
<script>
|
||||
export default {
|
||||
props: ['value'],
|
||||
computed: {
|
||||
srcset() {
|
||||
if (this.value.url.includes('ucarecdn')) {
|
||||
return (
|
||||
this.value.url +
|
||||
'-/resize/300/ 300w,' +
|
||||
this.value.url +
|
||||
'-/resize/800/ 800w,'
|
||||
);
|
||||
}
|
||||
return this.value.url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openFullscreen() {
|
||||
this.$store.dispatch('showFullscreenImage', this.value.url);
|
||||
|
|
|
|||
|
|
@ -28,11 +28,16 @@
|
|||
{{ module.title }}
|
||||
</h1>
|
||||
<div class="module__hero">
|
||||
<img
|
||||
:src="module.heroImage"
|
||||
alt=""
|
||||
<wagtail-image
|
||||
class="module__hero-image"
|
||||
/>
|
||||
:src="module.heroImage.src"
|
||||
:srcset="module.heroImage.srcset"
|
||||
:original-width="module.heroImage.width"
|
||||
:original-height="module.heroImage.height"
|
||||
:alt="module.heroImage.alt"
|
||||
></wagtail-image>
|
||||
|
||||
|
||||
<h5
|
||||
class="module__hero-source"
|
||||
v-if="module.heroSource"
|
||||
|
|
@ -87,6 +92,7 @@ import { graphql } from '@/__generated__';
|
|||
import highlightSidebar from '@/helpers/highlight-sidebar';
|
||||
import { doUpdateHighlight } from '@/graphql/mutations';
|
||||
import Mark from 'mark.js';
|
||||
import WagtailImage from '@/components/ui/WagtailImage.vue';
|
||||
|
||||
export interface Props {
|
||||
module: ModuleNode;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@
|
|||
:to="moduleLink"
|
||||
:class="['module-teaser', { 'module-teaser--small': !teaser }]"
|
||||
>
|
||||
<div
|
||||
:style="{ backgroundImage: 'url(' + heroImage + ')' }"
|
||||
<wagtail-image
|
||||
class="module-teaser__image"
|
||||
/>
|
||||
:src="heroImage.src"
|
||||
:srcset="heroImage.srcset"
|
||||
:original-height="heroImage.height"
|
||||
:original-width="heroImage.width"
|
||||
></wagtail-image>
|
||||
<div class="module-teaser__body">
|
||||
<div class="module-teaser__content">
|
||||
<h3 class="module-teaser__content-meta-title">{{ metaTitle }}</h3>
|
||||
|
|
@ -33,6 +36,7 @@
|
|||
import Pill from '@/components/ui/Pill.vue';
|
||||
import { ModuleCategoryNode, ModuleLevelNode } from '@/__generated__/graphql';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import WagtailImage from '@/components/ui/WagtailImage.vue';
|
||||
|
||||
export interface Props {
|
||||
metaTitle: string;
|
||||
|
|
@ -102,10 +106,7 @@ const moduleLink = computed(() => {
|
|||
width: 100%;
|
||||
max-height: 150px;
|
||||
height: 150px;
|
||||
background-position: center;
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
// prevent image from shrinking
|
||||
//prevent image from shrinking
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div class="wagtail-image">
|
||||
<div
|
||||
:class="['wagtail-image__background', { loaded: loaded }]"
|
||||
:style="{ height: backgroundHeight }"
|
||||
ref="imgElement"
|
||||
>
|
||||
<img
|
||||
:src="props.src"
|
||||
:srcset="props.srcset"
|
||||
:alt="props.alt"
|
||||
class="wagtail-image__image"
|
||||
:sizes="computedSizes"
|
||||
loading="eager"
|
||||
v-show="loaded"
|
||||
@load="handleLoad"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
export interface Props {
|
||||
src: string;
|
||||
alt?: string;
|
||||
originalWidth: number;
|
||||
originalHeight: number;
|
||||
srcset?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
alt: '',
|
||||
});
|
||||
|
||||
const imgElement = ref(null);
|
||||
const width = ref(0);
|
||||
const height = ref(0);
|
||||
const loaded = ref(false);
|
||||
const backgroundHeight = ref();
|
||||
const scaledHeight = ref();
|
||||
|
||||
//
|
||||
const updateDimensions = () => {
|
||||
if (imgElement.value && imgElement.value.parentElement) {
|
||||
const { clientWidth, clientHeight } = imgElement.value.parentElement;
|
||||
width.value = clientWidth;
|
||||
height.value = clientHeight;
|
||||
}
|
||||
calculateBackgroundHeight();
|
||||
};
|
||||
|
||||
const calculateBackgroundHeight = () => {
|
||||
// calculate the hight of the background so, that you see a gray box of correct height before image is loaded
|
||||
if (width.value) {
|
||||
const scalingFactor = width.value / props.originalWidth;
|
||||
scaledHeight.value = Math.round(props.originalHeight * scalingFactor);
|
||||
|
||||
if (width.value) {
|
||||
backgroundHeight.value = `${scaledHeight.value}px`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
backgroundHeight.value = '100%';
|
||||
};
|
||||
|
||||
const handleLoad = () => {
|
||||
loaded.value = true; // Set loaded to true when the image loads
|
||||
};
|
||||
|
||||
const computedSizes = computed(() => {
|
||||
// the default set of image sizes is [160px, 320px, 800px, 1600px]
|
||||
let size = '100vw';
|
||||
|
||||
if (width.value <= 300) {
|
||||
size = '160px';
|
||||
}
|
||||
if (300 < width.value && width.value <= 600) {
|
||||
size = '320px';
|
||||
}
|
||||
if (600 < width.value && width.value <= 1200) {
|
||||
size = '800px';
|
||||
}
|
||||
return size;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
updateDimensions();
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateDimensions);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'styles/helpers';
|
||||
|
||||
.wagtail-image {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
&__background {
|
||||
max-height: 100%;
|
||||
background-color: $color-silver-light;
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover; // Ensures the image covers the allocated area without distorting aspect ratio
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.wagtail-image__background.loaded {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
#import "./highlightParts.gql"
|
||||
#import "./wagtailImageParts.gql"
|
||||
|
||||
fragment ModuleLegacyParts on ModuleNode {
|
||||
id
|
||||
title
|
||||
|
|
@ -6,8 +8,10 @@ fragment ModuleLegacyParts on ModuleNode {
|
|||
teaser
|
||||
intro
|
||||
slug
|
||||
heroImage
|
||||
heroSource
|
||||
heroImage {
|
||||
...WagtailImageParts
|
||||
}
|
||||
solutionsEnabled
|
||||
highlights {
|
||||
...HighlightLegacyParts
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
fragment WagtailImageParts on WagtailImageNode {
|
||||
id
|
||||
src
|
||||
alt
|
||||
width
|
||||
height
|
||||
title
|
||||
srcset
|
||||
}
|
||||
|
|
@ -5,7 +5,12 @@ query SnapshotDetail($id: ID!) {
|
|||
id
|
||||
title
|
||||
metaTitle
|
||||
heroImage
|
||||
heroImage {
|
||||
id
|
||||
src
|
||||
srcset
|
||||
alt
|
||||
}
|
||||
created
|
||||
changes {
|
||||
newContentBlocks
|
||||
|
|
|
|||
|
|
@ -90,6 +90,19 @@ graphql(`
|
|||
}
|
||||
}
|
||||
`);
|
||||
|
||||
graphql(`
|
||||
fragment WagtailImageParts on WagtailImageNode {
|
||||
id
|
||||
src
|
||||
alt
|
||||
width
|
||||
height
|
||||
title
|
||||
srcset
|
||||
}
|
||||
`);
|
||||
|
||||
graphql(`
|
||||
fragment ModuleParts on ModuleNode {
|
||||
id
|
||||
|
|
@ -98,7 +111,7 @@ graphql(`
|
|||
teaser
|
||||
intro
|
||||
slug
|
||||
heroImage
|
||||
heroImage { ...WagtailImageParts }
|
||||
heroSource
|
||||
solutionsEnabled
|
||||
highlights {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# Files handling
|
||||
|
||||
This document describes how files are handled in this appication.
|
||||
|
||||
# Types of files
|
||||
|
||||
static files: files that are not changed by the application, e.g. images, fonts, etc.¨
|
||||
|
||||
### content documents:
|
||||
|
||||
Files that belong to the content and are managed by the content editors in the CMS (pdf, excel, word, etc.)
|
||||
|
||||
### user documents:
|
||||
|
||||
Files that are uploaded by the users (pdf, etc.). Therefore not visible in the CMS.
|
||||
Images are handled seprately from documents since images require additional processing (resizing, cropping, etc.).
|
||||
Visible in the django admin.
|
||||
|
||||
### content images:
|
||||
|
||||
Images that belong to the content and are managed by the content editors in the CMS.
|
||||
|
||||
## User documents
|
||||
|
||||
User documents and images hare handled by uploadcare. In the database we oly have the url.
|
||||
|
||||
## Content images
|
||||
|
||||
Content Images are served directly from S3. The permissions are handled by django. Request to django are checked and redirected to the S3 bucket.
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
# mysite/api/graphene_wagtail.py
|
||||
# Taken from https://github.com/patrick91/wagtail-ql/blob/master/backend/graphene_utils/converter.py and slightly adjusted
|
||||
import logging
|
||||
|
||||
from wagtail.images.views.serve import generate_image_url
|
||||
from graphene.types import Scalar
|
||||
from graphene_django.converter import convert_django_field
|
||||
from graphql_relay import to_global_id
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail.documents.models import Document
|
||||
from wagtail.images.models import Image
|
||||
|
||||
from assignments.models import Assignment
|
||||
|
|
@ -14,6 +13,7 @@ from basicknowledge.models import BasicKnowledge
|
|||
from books.models import CustomDocument
|
||||
from surveys.models import Survey
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -42,17 +42,35 @@ def get_document_json(document_id):
|
|||
return None
|
||||
|
||||
|
||||
def get_wagtail_image_dict(image_id: int) -> dict | None:
|
||||
from books.schema.nodes import get_srcset, get_src
|
||||
|
||||
try:
|
||||
image = Image.objects.get(id=image_id)
|
||||
value = {
|
||||
'value': image_id,
|
||||
'id': image.id,
|
||||
'src': get_src(image),
|
||||
'alt': image.title,
|
||||
'title': image.title,
|
||||
'width': image.width,
|
||||
'height': image.height,
|
||||
'srcset': get_srcset(image),
|
||||
}
|
||||
return value
|
||||
except Image.DoesNotExist:
|
||||
logger.error('Image {} does not exist'.format(image_id))
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def augment_fields(raw_data):
|
||||
for data in raw_data:
|
||||
if isinstance(data, dict):
|
||||
_type = data['type']
|
||||
if _type == 'image_block':
|
||||
_value = data['value']
|
||||
value = {
|
||||
# 'value': _value,
|
||||
# 'id': d['id'],
|
||||
'path': Image.objects.get(id=_value).file.url
|
||||
}
|
||||
value = get_wagtail_image_dict(_value)
|
||||
data['value'] = value
|
||||
if _type == 'assignment':
|
||||
_value = data['value']
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
|
||||
from wagtail.images.models import Image
|
||||
from api.graphene_wagtail import generate_image_url
|
||||
|
||||
class ModuleInterface(relay.Node):
|
||||
pk = graphene.Int()
|
||||
hero_image = graphene.String(required=True)
|
||||
hero_image = graphene.Field('books.schema.nodes.WagtailImageNode', required=True)
|
||||
topic = graphene.Field('books.schema.nodes.TopicNode')
|
||||
|
||||
@staticmethod
|
||||
def resolve_pk(parent, info, **kwargs):
|
||||
return parent.id
|
||||
|
||||
@staticmethod
|
||||
def resolve_hero_image(parent, info, **kwargs):
|
||||
if parent.hero_image:
|
||||
return parent.hero_image.file.url
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ from .module import *
|
|||
from .content import *
|
||||
from .snapshot import *
|
||||
from .topic import *
|
||||
from .wagtail_image import *
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from django.db.models import Q
|
|||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
||||
from notes.models import Highlight, ModuleBookmark
|
||||
from objectives.schema import ObjectiveGroupNode
|
||||
from surveys.models import Answer
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from graphene import relay, ObjectType
|
|||
from graphene_django import DjangoObjectType
|
||||
|
||||
from books.models.snapshot import Snapshot
|
||||
from books.schema.nodes.wagtail_image import WagtailImageNode
|
||||
from ..interfaces import ChapterInterface
|
||||
from ..interfaces.contentblock import ContentBlockInterface
|
||||
from ...models import ContentBlock
|
||||
|
|
@ -116,7 +117,7 @@ class SnapshotNode(DjangoObjectType):
|
|||
title = graphene.String()
|
||||
chapters = graphene.List(SnapshotChapterNode)
|
||||
meta_title = graphene.String()
|
||||
hero_image = graphene.String()
|
||||
hero_image = graphene.Field(WagtailImageNode)
|
||||
changes = graphene.Field(SnapshotChangesNode)
|
||||
mine = graphene.Boolean()
|
||||
shared = graphene.Boolean(required=True)
|
||||
|
|
@ -149,9 +150,7 @@ class SnapshotNode(DjangoObjectType):
|
|||
|
||||
@staticmethod
|
||||
def resolve_hero_image(parent, info, **kwargs):
|
||||
if parent.module.hero_image:
|
||||
return parent.module.hero_image.file.url
|
||||
return ''
|
||||
return parent.module.hero_image
|
||||
|
||||
@staticmethod
|
||||
def resolve_changes(parent, info, **kwargs):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene import relay
|
||||
|
||||
from wagtail.images.models import Image
|
||||
|
||||
from wagtail.images.views.serve import generate_image_url
|
||||
|
||||
|
||||
def get_srcset(image: Image) -> str:
|
||||
return (
|
||||
f"{generate_image_url(image, 'width-160')} 160w, "
|
||||
f"{generate_image_url(image, 'width-320')} 320w, "
|
||||
f"{generate_image_url(image, 'width-800')} 800w, "
|
||||
f"{generate_image_url(image, 'width-1600')} 1600w"
|
||||
)
|
||||
|
||||
|
||||
def get_src(image: Image) -> str:
|
||||
return generate_image_url(image, f'width-{min(3840, image.width)}')
|
||||
|
||||
|
||||
class WagtailImageNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = [
|
||||
"title",
|
||||
"width",
|
||||
"height",
|
||||
]
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
src = graphene.String()
|
||||
alt = graphene.String()
|
||||
srcset = graphene.String()
|
||||
|
||||
def resolve_src(self, info):
|
||||
return get_src(self)
|
||||
|
||||
def resolve_alt(self, info):
|
||||
return self.title
|
||||
|
||||
def resolve_srcset(self, info):
|
||||
return get_srcset(self)
|
||||
|
|
@ -3,6 +3,12 @@ query ModulesQuery($slug: String, $id: ID) {
|
|||
module(slug: $slug, id: $id) {
|
||||
id
|
||||
title
|
||||
heroImage {
|
||||
id
|
||||
src
|
||||
height
|
||||
width
|
||||
}
|
||||
objectiveGroups {
|
||||
objectives {
|
||||
id
|
||||
|
|
|
|||
|
|
@ -266,7 +266,6 @@ AWS_S3_CUSTOM_DOMAIN = "{}.s3-{}.amazonaws.com".format(
|
|||
)
|
||||
if USE_AWS:
|
||||
STORAGE_BACKEND = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
# use with cloudfront
|
||||
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
|
||||
else:
|
||||
STORAGE_BACKEND = "django.core.files.storage.FileSystemStorage"
|
||||
|
|
@ -388,6 +387,7 @@ WAGTAILSEARCH_BACKENDS = {
|
|||
}
|
||||
}
|
||||
WAGTAILDOCS_DOCUMENT_MODEL = "books.CustomDocument"
|
||||
WAGTAILADMIN_BASE_URL = "/cms/"
|
||||
|
||||
|
||||
GRAPHQL_QUERIES_DIR = os.path.join(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from django.conf import settings
|
||||
from django.urls import path, re_path, include
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path, re_path, include
|
||||
from django.views.generic import RedirectView
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
from wagtail.images.views.serve import ServeView
|
||||
from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls
|
||||
|
||||
from core import views
|
||||
|
|
@ -17,12 +18,17 @@ urlpatterns = [
|
|||
re_path(r"^guru/", admin.site.urls),
|
||||
re_path(r"^statistics/", include("statistics.urls", namespace="statistics")),
|
||||
# wagtail
|
||||
re_path(r'^api/images/([^/]*)/(\d*)/([^/]*)/[^/]*$', login_required(ServeView.as_view(action='redirect')),
|
||||
name='wagtailimages_serve'),
|
||||
|
||||
re_path(r"^cms/autocomplete/", include(autocomplete_admin_urls)),
|
||||
re_path(r"^cms/pages/(\d+)/$", override_wagtailadmin_explore_default_ordering),
|
||||
re_path(r"^cms/", include(wagtailadmin_urls)),
|
||||
re_path(r"^documents/", include(wagtaildocs_urls)),
|
||||
|
||||
# graphql backend
|
||||
re_path(r"^api/", include("api.urls", namespace="api")),
|
||||
|
||||
# favicon
|
||||
re_path(
|
||||
r"^favicon\.ico$",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import requests
|
||||
from core.logger import get_logger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse, HttpResponseRedirect
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from graphene_django.views import GraphQLView
|
||||
|
|
@ -11,6 +11,8 @@ from graphql import get_operation_ast, parse
|
|||
from sentry_sdk.api import start_transaction
|
||||
from wagtail.admin.views.pages import listing
|
||||
|
||||
from core.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -68,3 +70,4 @@ def override_wagtailadmin_explore_default_ordering(request, parent_page_id):
|
|||
return HttpResponseRedirect(request.path_info + "?ordering=ord")
|
||||
|
||||
return listing.IndexView.as_view()(request, parent_page_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ export DATABASE_URL=postgres://skillbox:skillbox@localhost:5432/skillbox
|
|||
export DEBUG=True
|
||||
export ENABLE_SILKY=False
|
||||
export SECRET_KEY=FOOBAR
|
||||
export USE_AWS=False
|
||||
export USE_AWS=True
|
||||
export WAGTAILADMIN_BASE_URL=/
|
||||
export ALLOW_BETA_LOGIN=True
|
||||
export USE_404_FALLBACK_IMAGE=True
|
||||
|
||||
export AWS_STORAGE_BUCKET_NAME=skillbox-files-preprod
|
||||
export AWS_REGION=eu-central-1
|
||||
#export THEME=my-kv
|
||||
#export APP_FLAVOR=my-kv
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ type ModuleNode implements ModuleInterface {
|
|||
metaTitle: String!
|
||||
level: ModuleLevelNode
|
||||
category: ModuleCategoryNode
|
||||
heroImage: String!
|
||||
heroImage: WagtailImageNode!
|
||||
|
||||
"""e.g. 'Reuters', 'Wikipedia'"""
|
||||
heroSource: String!
|
||||
|
|
@ -140,10 +140,22 @@ interface ModuleInterface {
|
|||
"""The ID of the object"""
|
||||
id: ID!
|
||||
pk: Int
|
||||
heroImage: String!
|
||||
heroImage: WagtailImageNode!
|
||||
topic: TopicNode
|
||||
}
|
||||
|
||||
type WagtailImageNode implements Node {
|
||||
title: String!
|
||||
width: Int!
|
||||
height: Int!
|
||||
|
||||
"""The ID of the object"""
|
||||
id: ID!
|
||||
src: String
|
||||
alt: String
|
||||
srcset: String
|
||||
}
|
||||
|
||||
type TopicNode implements Node {
|
||||
"""Der Seitentitel, der öffentlich angezeigt werden soll"""
|
||||
title: String!
|
||||
|
|
@ -564,7 +576,7 @@ type SnapshotNode implements Node {
|
|||
hiddenObjectives(offset: Int, before: String, after: String, first: Int, last: Int, text: String): ObjectiveNodeConnection!
|
||||
title: String
|
||||
metaTitle: String
|
||||
heroImage: String
|
||||
heroImage: WagtailImageNode
|
||||
changes: SnapshotChangesNode
|
||||
mine: Boolean
|
||||
}
|
||||
|
|
@ -740,12 +752,13 @@ type ProjectNode implements Node {
|
|||
slug: String!
|
||||
objectives: String!
|
||||
appearance: String!
|
||||
student: PublicUserNode
|
||||
student: PublicUserNode!
|
||||
final: Boolean!
|
||||
schoolClass: SchoolClassNode
|
||||
entries: [ProjectEntryNode]
|
||||
pk: Int
|
||||
entriesCount: Int
|
||||
owner: PublicUserNode
|
||||
}
|
||||
|
||||
type ProjectEntryNode implements Node {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ class RecentModuleFilter(FilterSet):
|
|||
order_by = OrderingFilter(fields=(("recent_modules__visited", "visited"),))
|
||||
|
||||
|
||||
def get_resized_avatar_url(avatar_url, size=120):
|
||||
# resize the avatar to 120px if it is on ucarecdn.com
|
||||
if "ucarecdn.com" in avatar_url:
|
||||
return f"{avatar_url}-/resize/{size}/"
|
||||
return avatar_url
|
||||
|
||||
class SchoolClassNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
members = graphene.List("users.schema.ClassMemberNode")
|
||||
|
|
@ -109,6 +115,9 @@ class PrivateUserNode(DjangoObjectType):
|
|||
def resolve_pk(self, info, **kwargs):
|
||||
return self.id
|
||||
|
||||
def resolve_avatar_url(self, info, **kwargs):
|
||||
return get_resized_avatar_url(self.avatar_url, size=240)
|
||||
|
||||
def resolve_permissions(self, info):
|
||||
return self.get_all_permissions()
|
||||
|
||||
|
|
@ -164,6 +173,10 @@ class PublicUserNode(DjangoObjectType):
|
|||
only_fields = ["full_name", "first_name", "last_name", "avatar_url"]
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_avatar_url(self, info, **kwargs):
|
||||
return get_resized_avatar_url(self.avatar_url)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def resolve_is_me(parent: User, info, **kwargs):
|
||||
return info.context.user.pk == parent.pk
|
||||
|
|
|
|||
Loading…
Reference in New Issue