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
|
### 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
|
## Backup to S3
|
||||||
|
|
||||||
From https://pawelurbanek.com/heroku-postgresql-s3-backup
|
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 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 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 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 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 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,
|
"\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.
|
* 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.
|
* 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>
|
<template>
|
||||||
<img
|
<wagtail-image
|
||||||
:src="value.path"
|
:src="value.src"
|
||||||
alt=""
|
:srcset="value.srcset"
|
||||||
|
:alt="value.alt"
|
||||||
|
:original-height="value.height"
|
||||||
|
:original-width="value.width"
|
||||||
class="image-block"
|
class="image-block"
|
||||||
@click="openFullscreen"
|
@click="openFullscreen"
|
||||||
/>
|
></wagtail-image>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import WagtailImage from '@/components/ui/WagtailImage.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['value'],
|
props: ['value'],
|
||||||
|
components: { WagtailImage },
|
||||||
methods: {
|
methods: {
|
||||||
openFullscreen() {
|
openFullscreen() {
|
||||||
this.$store.dispatch('showFullscreenImage', this.value.path);
|
this.$store.dispatch('showFullscreenImage', this.value.src);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<img
|
<img
|
||||||
:src="value.url"
|
:src="value.url"
|
||||||
alt=""
|
:srcset="srcset"
|
||||||
class="image-block"
|
class="image-block"
|
||||||
|
alt=""
|
||||||
@click="openFullscreen"
|
@click="openFullscreen"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -10,6 +11,19 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['value'],
|
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: {
|
methods: {
|
||||||
openFullscreen() {
|
openFullscreen() {
|
||||||
this.$store.dispatch('showFullscreenImage', this.value.url);
|
this.$store.dispatch('showFullscreenImage', this.value.url);
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,16 @@
|
||||||
{{ module.title }}
|
{{ module.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="module__hero">
|
<div class="module__hero">
|
||||||
<img
|
<wagtail-image
|
||||||
:src="module.heroImage"
|
|
||||||
alt=""
|
|
||||||
class="module__hero-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
|
<h5
|
||||||
class="module__hero-source"
|
class="module__hero-source"
|
||||||
v-if="module.heroSource"
|
v-if="module.heroSource"
|
||||||
|
|
@ -87,6 +92,7 @@ import { graphql } from '@/__generated__';
|
||||||
import highlightSidebar from '@/helpers/highlight-sidebar';
|
import highlightSidebar from '@/helpers/highlight-sidebar';
|
||||||
import { doUpdateHighlight } from '@/graphql/mutations';
|
import { doUpdateHighlight } from '@/graphql/mutations';
|
||||||
import Mark from 'mark.js';
|
import Mark from 'mark.js';
|
||||||
|
import WagtailImage from '@/components/ui/WagtailImage.vue';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
module: ModuleNode;
|
module: ModuleNode;
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@
|
||||||
:to="moduleLink"
|
:to="moduleLink"
|
||||||
:class="['module-teaser', { 'module-teaser--small': !teaser }]"
|
:class="['module-teaser', { 'module-teaser--small': !teaser }]"
|
||||||
>
|
>
|
||||||
<div
|
<wagtail-image
|
||||||
:style="{ backgroundImage: 'url(' + heroImage + ')' }"
|
|
||||||
class="module-teaser__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__body">
|
||||||
<div class="module-teaser__content">
|
<div class="module-teaser__content">
|
||||||
<h3 class="module-teaser__content-meta-title">{{ metaTitle }}</h3>
|
<h3 class="module-teaser__content-meta-title">{{ metaTitle }}</h3>
|
||||||
|
|
@ -33,6 +36,7 @@
|
||||||
import Pill from '@/components/ui/Pill.vue';
|
import Pill from '@/components/ui/Pill.vue';
|
||||||
import { ModuleCategoryNode, ModuleLevelNode } from '@/__generated__/graphql';
|
import { ModuleCategoryNode, ModuleLevelNode } from '@/__generated__/graphql';
|
||||||
import { computed } from '@vue/reactivity';
|
import { computed } from '@vue/reactivity';
|
||||||
|
import WagtailImage from '@/components/ui/WagtailImage.vue';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
metaTitle: string;
|
metaTitle: string;
|
||||||
|
|
@ -102,10 +106,7 @@ const moduleLink = computed(() => {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
background-position: center;
|
//prevent image from shrinking
|
||||||
background-size: 100% auto;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
// prevent image from shrinking
|
|
||||||
flex-shrink: 0;
|
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 "./highlightParts.gql"
|
||||||
|
#import "./wagtailImageParts.gql"
|
||||||
|
|
||||||
fragment ModuleLegacyParts on ModuleNode {
|
fragment ModuleLegacyParts on ModuleNode {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
|
@ -6,8 +8,10 @@ fragment ModuleLegacyParts on ModuleNode {
|
||||||
teaser
|
teaser
|
||||||
intro
|
intro
|
||||||
slug
|
slug
|
||||||
heroImage
|
|
||||||
heroSource
|
heroSource
|
||||||
|
heroImage {
|
||||||
|
...WagtailImageParts
|
||||||
|
}
|
||||||
solutionsEnabled
|
solutionsEnabled
|
||||||
highlights {
|
highlights {
|
||||||
...HighlightLegacyParts
|
...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
|
id
|
||||||
title
|
title
|
||||||
metaTitle
|
metaTitle
|
||||||
heroImage
|
heroImage {
|
||||||
|
id
|
||||||
|
src
|
||||||
|
srcset
|
||||||
|
alt
|
||||||
|
}
|
||||||
created
|
created
|
||||||
changes {
|
changes {
|
||||||
newContentBlocks
|
newContentBlocks
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,19 @@ graphql(`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
graphql(`
|
||||||
|
fragment WagtailImageParts on WagtailImageNode {
|
||||||
|
id
|
||||||
|
src
|
||||||
|
alt
|
||||||
|
width
|
||||||
|
height
|
||||||
|
title
|
||||||
|
srcset
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
graphql(`
|
graphql(`
|
||||||
fragment ModuleParts on ModuleNode {
|
fragment ModuleParts on ModuleNode {
|
||||||
id
|
id
|
||||||
|
|
@ -98,7 +111,7 @@ graphql(`
|
||||||
teaser
|
teaser
|
||||||
intro
|
intro
|
||||||
slug
|
slug
|
||||||
heroImage
|
heroImage { ...WagtailImageParts }
|
||||||
heroSource
|
heroSource
|
||||||
solutionsEnabled
|
solutionsEnabled
|
||||||
highlights {
|
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
|
# mysite/api/graphene_wagtail.py
|
||||||
# Taken from https://github.com/patrick91/wagtail-ql/blob/master/backend/graphene_utils/converter.py and slightly adjusted
|
# Taken from https://github.com/patrick91/wagtail-ql/blob/master/backend/graphene_utils/converter.py and slightly adjusted
|
||||||
import logging
|
import logging
|
||||||
|
from wagtail.images.views.serve import generate_image_url
|
||||||
from graphene.types import Scalar
|
from graphene.types import Scalar
|
||||||
from graphene_django.converter import convert_django_field
|
from graphene_django.converter import convert_django_field
|
||||||
from graphql_relay import to_global_id
|
from graphql_relay import to_global_id
|
||||||
from wagtail.fields import StreamField
|
from wagtail.fields import StreamField
|
||||||
from wagtail.documents.models import Document
|
|
||||||
from wagtail.images.models import Image
|
from wagtail.images.models import Image
|
||||||
|
|
||||||
from assignments.models import Assignment
|
from assignments.models import Assignment
|
||||||
|
|
@ -14,6 +13,7 @@ from basicknowledge.models import BasicKnowledge
|
||||||
from books.models import CustomDocument
|
from books.models import CustomDocument
|
||||||
from surveys.models import Survey
|
from surveys.models import Survey
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,17 +42,35 @@ def get_document_json(document_id):
|
||||||
return None
|
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):
|
def augment_fields(raw_data):
|
||||||
for data in raw_data:
|
for data in raw_data:
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
_type = data['type']
|
_type = data['type']
|
||||||
if _type == 'image_block':
|
if _type == 'image_block':
|
||||||
_value = data['value']
|
_value = data['value']
|
||||||
value = {
|
value = get_wagtail_image_dict(_value)
|
||||||
# 'value': _value,
|
|
||||||
# 'id': d['id'],
|
|
||||||
'path': Image.objects.get(id=_value).file.url
|
|
||||||
}
|
|
||||||
data['value'] = value
|
data['value'] = value
|
||||||
if _type == 'assignment':
|
if _type == 'assignment':
|
||||||
_value = data['value']
|
_value = data['value']
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
from wagtail.images.models import Image
|
||||||
|
from api.graphene_wagtail import generate_image_url
|
||||||
|
|
||||||
class ModuleInterface(relay.Node):
|
class ModuleInterface(relay.Node):
|
||||||
pk = graphene.Int()
|
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')
|
topic = graphene.Field('books.schema.nodes.TopicNode')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_pk(parent, info, **kwargs):
|
def resolve_pk(parent, info, **kwargs):
|
||||||
return parent.id
|
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 .content import *
|
||||||
from .snapshot import *
|
from .snapshot import *
|
||||||
from .topic import *
|
from .topic import *
|
||||||
|
from .wagtail_image import *
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from django.db.models import Q
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
from notes.models import Highlight, ModuleBookmark
|
from notes.models import Highlight, ModuleBookmark
|
||||||
from objectives.schema import ObjectiveGroupNode
|
from objectives.schema import ObjectiveGroupNode
|
||||||
from surveys.models import Answer
|
from surveys.models import Answer
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from graphene import relay, ObjectType
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from books.models.snapshot import Snapshot
|
from books.models.snapshot import Snapshot
|
||||||
|
from books.schema.nodes.wagtail_image import WagtailImageNode
|
||||||
from ..interfaces import ChapterInterface
|
from ..interfaces import ChapterInterface
|
||||||
from ..interfaces.contentblock import ContentBlockInterface
|
from ..interfaces.contentblock import ContentBlockInterface
|
||||||
from ...models import ContentBlock
|
from ...models import ContentBlock
|
||||||
|
|
@ -116,7 +117,7 @@ class SnapshotNode(DjangoObjectType):
|
||||||
title = graphene.String()
|
title = graphene.String()
|
||||||
chapters = graphene.List(SnapshotChapterNode)
|
chapters = graphene.List(SnapshotChapterNode)
|
||||||
meta_title = graphene.String()
|
meta_title = graphene.String()
|
||||||
hero_image = graphene.String()
|
hero_image = graphene.Field(WagtailImageNode)
|
||||||
changes = graphene.Field(SnapshotChangesNode)
|
changes = graphene.Field(SnapshotChangesNode)
|
||||||
mine = graphene.Boolean()
|
mine = graphene.Boolean()
|
||||||
shared = graphene.Boolean(required=True)
|
shared = graphene.Boolean(required=True)
|
||||||
|
|
@ -149,9 +150,7 @@ class SnapshotNode(DjangoObjectType):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_hero_image(parent, info, **kwargs):
|
def resolve_hero_image(parent, info, **kwargs):
|
||||||
if parent.module.hero_image:
|
return parent.module.hero_image
|
||||||
return parent.module.hero_image.file.url
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_changes(parent, info, **kwargs):
|
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) {
|
module(slug: $slug, id: $id) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
heroImage {
|
||||||
|
id
|
||||||
|
src
|
||||||
|
height
|
||||||
|
width
|
||||||
|
}
|
||||||
objectiveGroups {
|
objectiveGroups {
|
||||||
objectives {
|
objectives {
|
||||||
id
|
id
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,6 @@ AWS_S3_CUSTOM_DOMAIN = "{}.s3-{}.amazonaws.com".format(
|
||||||
)
|
)
|
||||||
if USE_AWS:
|
if USE_AWS:
|
||||||
STORAGE_BACKEND = "storages.backends.s3boto3.S3Boto3Storage"
|
STORAGE_BACKEND = "storages.backends.s3boto3.S3Boto3Storage"
|
||||||
# use with cloudfront
|
|
||||||
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
|
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
|
||||||
else:
|
else:
|
||||||
STORAGE_BACKEND = "django.core.files.storage.FileSystemStorage"
|
STORAGE_BACKEND = "django.core.files.storage.FileSystemStorage"
|
||||||
|
|
@ -388,6 +387,7 @@ WAGTAILSEARCH_BACKENDS = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WAGTAILDOCS_DOCUMENT_MODEL = "books.CustomDocument"
|
WAGTAILDOCS_DOCUMENT_MODEL = "books.CustomDocument"
|
||||||
|
WAGTAILADMIN_BASE_URL = "/cms/"
|
||||||
|
|
||||||
|
|
||||||
GRAPHQL_QUERIES_DIR = os.path.join(
|
GRAPHQL_QUERIES_DIR = os.path.join(
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import path, re_path, include
|
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
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 django.views.generic import RedirectView
|
||||||
from wagtail.admin import urls as wagtailadmin_urls
|
|
||||||
from wagtail import urls as wagtail_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.documents import urls as wagtaildocs_urls
|
||||||
|
from wagtail.images.views.serve import ServeView
|
||||||
from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls
|
from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls
|
||||||
|
|
||||||
from core import views
|
from core import views
|
||||||
|
|
@ -17,12 +18,17 @@ urlpatterns = [
|
||||||
re_path(r"^guru/", admin.site.urls),
|
re_path(r"^guru/", admin.site.urls),
|
||||||
re_path(r"^statistics/", include("statistics.urls", namespace="statistics")),
|
re_path(r"^statistics/", include("statistics.urls", namespace="statistics")),
|
||||||
# wagtail
|
# 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/autocomplete/", include(autocomplete_admin_urls)),
|
||||||
re_path(r"^cms/pages/(\d+)/$", override_wagtailadmin_explore_default_ordering),
|
re_path(r"^cms/pages/(\d+)/$", override_wagtailadmin_explore_default_ordering),
|
||||||
re_path(r"^cms/", include(wagtailadmin_urls)),
|
re_path(r"^cms/", include(wagtailadmin_urls)),
|
||||||
re_path(r"^documents/", include(wagtaildocs_urls)),
|
re_path(r"^documents/", include(wagtaildocs_urls)),
|
||||||
|
|
||||||
# graphql backend
|
# graphql backend
|
||||||
re_path(r"^api/", include("api.urls", namespace="api")),
|
re_path(r"^api/", include("api.urls", namespace="api")),
|
||||||
|
|
||||||
# favicon
|
# favicon
|
||||||
re_path(
|
re_path(
|
||||||
r"^favicon\.ico$",
|
r"^favicon\.ico$",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import requests
|
import requests
|
||||||
from core.logger import get_logger
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.http.request import HttpRequest
|
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.shortcuts import render
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from graphene_django.views import GraphQLView
|
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 sentry_sdk.api import start_transaction
|
||||||
from wagtail.admin.views.pages import listing
|
from wagtail.admin.views.pages import listing
|
||||||
|
|
||||||
|
from core.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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 HttpResponseRedirect(request.path_info + "?ordering=ord")
|
||||||
|
|
||||||
return listing.IndexView.as_view()(request, parent_page_id)
|
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 DEBUG=True
|
||||||
export ENABLE_SILKY=False
|
export ENABLE_SILKY=False
|
||||||
export SECRET_KEY=FOOBAR
|
export SECRET_KEY=FOOBAR
|
||||||
export USE_AWS=False
|
export USE_AWS=True
|
||||||
export WAGTAILADMIN_BASE_URL=/
|
export WAGTAILADMIN_BASE_URL=/
|
||||||
export ALLOW_BETA_LOGIN=True
|
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 THEME=my-kv
|
||||||
#export APP_FLAVOR=my-kv
|
#export APP_FLAVOR=my-kv
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ type ModuleNode implements ModuleInterface {
|
||||||
metaTitle: String!
|
metaTitle: String!
|
||||||
level: ModuleLevelNode
|
level: ModuleLevelNode
|
||||||
category: ModuleCategoryNode
|
category: ModuleCategoryNode
|
||||||
heroImage: String!
|
heroImage: WagtailImageNode!
|
||||||
|
|
||||||
"""e.g. 'Reuters', 'Wikipedia'"""
|
"""e.g. 'Reuters', 'Wikipedia'"""
|
||||||
heroSource: String!
|
heroSource: String!
|
||||||
|
|
@ -140,10 +140,22 @@ interface ModuleInterface {
|
||||||
"""The ID of the object"""
|
"""The ID of the object"""
|
||||||
id: ID!
|
id: ID!
|
||||||
pk: Int
|
pk: Int
|
||||||
heroImage: String!
|
heroImage: WagtailImageNode!
|
||||||
topic: TopicNode
|
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 {
|
type TopicNode implements Node {
|
||||||
"""Der Seitentitel, der öffentlich angezeigt werden soll"""
|
"""Der Seitentitel, der öffentlich angezeigt werden soll"""
|
||||||
title: String!
|
title: String!
|
||||||
|
|
@ -564,7 +576,7 @@ type SnapshotNode implements Node {
|
||||||
hiddenObjectives(offset: Int, before: String, after: String, first: Int, last: Int, text: String): ObjectiveNodeConnection!
|
hiddenObjectives(offset: Int, before: String, after: String, first: Int, last: Int, text: String): ObjectiveNodeConnection!
|
||||||
title: String
|
title: String
|
||||||
metaTitle: String
|
metaTitle: String
|
||||||
heroImage: String
|
heroImage: WagtailImageNode
|
||||||
changes: SnapshotChangesNode
|
changes: SnapshotChangesNode
|
||||||
mine: Boolean
|
mine: Boolean
|
||||||
}
|
}
|
||||||
|
|
@ -740,12 +752,13 @@ type ProjectNode implements Node {
|
||||||
slug: String!
|
slug: String!
|
||||||
objectives: String!
|
objectives: String!
|
||||||
appearance: String!
|
appearance: String!
|
||||||
student: PublicUserNode
|
student: PublicUserNode!
|
||||||
final: Boolean!
|
final: Boolean!
|
||||||
schoolClass: SchoolClassNode
|
schoolClass: SchoolClassNode
|
||||||
entries: [ProjectEntryNode]
|
entries: [ProjectEntryNode]
|
||||||
pk: Int
|
pk: Int
|
||||||
entriesCount: Int
|
entriesCount: Int
|
||||||
|
owner: PublicUserNode
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectEntryNode implements Node {
|
type ProjectEntryNode implements Node {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ class RecentModuleFilter(FilterSet):
|
||||||
order_by = OrderingFilter(fields=(("recent_modules__visited", "visited"),))
|
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):
|
class SchoolClassNode(DjangoObjectType):
|
||||||
pk = graphene.Int()
|
pk = graphene.Int()
|
||||||
members = graphene.List("users.schema.ClassMemberNode")
|
members = graphene.List("users.schema.ClassMemberNode")
|
||||||
|
|
@ -109,6 +115,9 @@ class PrivateUserNode(DjangoObjectType):
|
||||||
def resolve_pk(self, info, **kwargs):
|
def resolve_pk(self, info, **kwargs):
|
||||||
return self.id
|
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):
|
def resolve_permissions(self, info):
|
||||||
return self.get_all_permissions()
|
return self.get_all_permissions()
|
||||||
|
|
||||||
|
|
@ -164,6 +173,10 @@ class PublicUserNode(DjangoObjectType):
|
||||||
only_fields = ["full_name", "first_name", "last_name", "avatar_url"]
|
only_fields = ["full_name", "first_name", "last_name", "avatar_url"]
|
||||||
interfaces = (relay.Node,)
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
|
def resolve_avatar_url(self, info, **kwargs):
|
||||||
|
return get_resized_avatar_url(self.avatar_url)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_is_me(parent: User, info, **kwargs):
|
def resolve_is_me(parent: User, info, **kwargs):
|
||||||
return info.context.user.pk == parent.pk
|
return info.context.user.pk == parent.pk
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue