Merge branch 'develop'

This commit is contained in:
Ramon Wenger 2021-11-10 15:59:42 +01:00
commit 8bcb76709c
43 changed files with 993 additions and 197 deletions

View File

@ -128,21 +128,8 @@ Change DATABASE URL (e.g after a rollback)
### Backup
Create a backup
See [Docs](./docs/heroku-backup.md)
`heroku pg:backups:capture --app <appname>`
The following command will provide a URL to where the backup can be downloaded (expires after 60 minutes)
`heroku pg:backups:url b001 --app <appname>`
To restore a backup, use
`heroku pg:backups:restore b001 DATABASE_URL --app <appname>`
To see the backup schedule
`heroku pg:backus:schedules --app <appname>`
## AWS

View File

@ -2,7 +2,7 @@
const path = require('path');
const config = require('../config');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const {VueLoaderPlugin} = require('vue-loader');
const {isDev, styleRule, assetsPath} = require('./utils');
@ -42,7 +42,7 @@ module.exports = {
alias: {
'@': resolve('src'),
styles: resolve('src/styles'),
gql: resolve('src/graphql/gql')
gql: resolve('src/graphql/gql'),
},
},
module: {
@ -64,9 +64,9 @@ module.exports = {
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/]
appendTsSuffixTo: [/\.vue$/],
},
exclude: /node_modules/
exclude: /node_modules/,
},
{
test: /\.js$/,
@ -79,7 +79,7 @@ module.exports = {
{
test: /\.(gql|graphql)$/,
loader: 'graphql-tag/loader',
exclude: /node_modules/
exclude: /node_modules/,
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,

View File

@ -20,11 +20,13 @@ const classMemberIdIterator = idGenerator('ClassMemberNode');
const chapterIdIterator = idGenerator('ChapterNode');
const moduleIdIterator = idGenerator('ModuleNode');
const contentBlockIdIterator = idGenerator('ContentBlockNode');
const instrumentIdGenerator = idGenerator('InstrumentNode');
const getClassMemberId = () => classMemberIdIterator.next().value;
const getChapterId = () => chapterIdIterator.next().value;
const getModuleId = () => moduleIdIterator.next().value;
const getContentBlockId = () => contentBlockIdIterator.next().value;
const getInstrumentId = () => instrumentIdGenerator.next().value;
export default {
UUID: () => '123-456-789',
@ -108,5 +110,11 @@ export default {
RoomEntryNode: () => ({
title: 'A Room Entry',
contents: [],
}),
InstrumentNode: () => ({
contents: [],
title: 'instrument-title',
slug: 'instrument-slug',
id: getInstrumentId(),
})
};

View File

@ -0,0 +1,88 @@
describe('Instruments Page', () => {
beforeEach(() => {
cy.setup();
});
it('opens the instruments page', () => {
const analyse = {
name: 'Analyse',
category: 'LANGUAGE_COMMUNICATION',
type: 'analyse',
};
const argumentation = {
name: 'Argumentation',
category: 'LANGUAGE_COMMUNICATION',
type: 'argumentation',
};
const ethik = {
name: 'Ethik',
category: 'SOCIETY',
type: 'ethik',
};
cy.mockGraphqlOps({
operations: {
MeQuery: {},
InstrumentsQuery: {
instruments: [
{
type: analyse,
title: 'Instrument: Analyse',
slug: 'analyse',
},
{
type: argumentation,
title: 'Instrument: Argumentation',
slug: 'argumentation',
},
{
type: ethik,
title: 'Instrument: Ethik',
slug: 'ethik',
}
],
},
InstrumentTypesQuery: {
instrumentTypes: [
analyse,
argumentation,
{
name: 'Beschreibung',
category: 'LANGUAGE_COMMUNICATION',
type: 'beschreibung',
},
ethik,
{
name: 'Identität und Sozialisation',
category: 'SOCIETY',
type: 'identitt-und-sozialisation',
},
],
},
},
});
cy.visit('instruments/');
cy.getByDataCy('instrument').should('have.length', 3);
cy.getByDataCy('filter-language-communication').click();
cy.getByDataCy('instrument').should('have.length', 2);
cy.getByDataCy('filter-society').click();
cy.getByDataCy('instrument').should('have.length', 1);
cy.getByDataCy('filter-interdisciplinary').click();
cy.getByDataCy('instrument').should('have.length', 0);
cy.getByDataCy('filter-analyse').click();
cy.getByDataCy('instrument').should('have.length', 1);
cy.getByDataCy('filter-ethik').click();
cy.getByDataCy('instrument').should('have.length', 1);
cy.getByDataCy('filter-all-instruments').click();
cy.getByDataCy('instrument').should('have.length', 3);
});
});

View File

@ -6,21 +6,34 @@ describe('Sidebar', () => {
});
it('should open sidebar and stay open', () => {
const {me} = getMinimalMe({});
const operations = {
MeQuery: getMinimalMe({}),
MeQuery: {
me: {
...me,
schoolClasses: {
edges: [
...me.schoolClasses.edges,
{node: {}},
],
},
},
},
ProjectsQuery: {
projects: []
}
projects: [],
},
};
cy.mockGraphqlOps({
operations
operations,
});
cy.visit('/portfolio');
cy.getByDataCy('sidebar').should('not.exist');
cy.getByDataCy('user-widget-avatar').click();
cy.getByDataCy('sidebar').should('exist');
cy.getByDataCy('class-selection').click();
cy.getByDataCy('class-selection-entry').should('have.length', 2);
cy.getByDataCy('close-profile-sidebar-link').click();
cy.getByDataCy('sidebar').should('not.exist');
});

View File

@ -43746,6 +43746,13 @@
"integrity": "sha512-q8GgAIPU7xHCsUhB1PUgR//8GoI0bUdMRUKd69jw2UcKy7pGuu0NbJsOGqdSpdpvhO4LY/dgqohPEkE1TrBwKQ==",
"requires": {
"vue": "^2.1.10"
},
"dependencies": {
"vue": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
}
}
},
"svgo": {
@ -45176,6 +45183,13 @@
"babel-preset-env": "^1.6.0",
"rollup-plugin-babel": "^3.0.2",
"vue": "^2.4.4"
},
"dependencies": {
"vue": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
}
}
},
"vuejs-logger": {

View File

@ -0,0 +1,152 @@
<template>
<a
:class="typeClass"
class="filter-entry">
<span class="filter-entry__text">{{ text }}</span>
<span class="filter-entry__icon-wrapper">
<chevron-right class="filter-entry__icon"/>
</span>
</a>
</template>
<script>
import ChevronRight from '@/components/icons/ChevronRight';
import {INTERDISCIPLINARY, LANGUAGE_COMMUNICATION, SOCIETY} from '@/consts/instrument.consts';
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFiler.gql';
export default {
props: {
text: {
type: String,
required: true,
},
type: {
type: String,
default: '',
},
isCategory: {
type: Boolean,
default: false,
},
category: {
type: String,
default: '',
},
},
components: {
ChevronRight,
},
apollo: {
instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY
}
},
data() {
return {
instrumentFilter: {
currentFilter: '',
},
};
},
computed: {
isActive() {
if (!this.instrumentFilter.currentFilter) {
return this.type === '';
}
// eslint-disable-next-line
const [_, identifier] = this.instrumentFilter.currentFilter.split(':');
console.log(identifier, this.type);
return this.type === identifier;
},
typeClass() {
return {
'filter-entry--language-communication': this.category === LANGUAGE_COMMUNICATION,
'filter-entry--society': this.category === SOCIETY,
'filter-entry--interdisciplinary': this.category === INTERDISCIPLINARY,
'filter-entry--active': this.isActive,
'filter-entry--category': this.isCategory,
};
},
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.filter-entry {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
&__text {
@include sub-heading;
line-height: 1.5;
color: inherit;
}
&__icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 20px;
width: 20px;
border-radius: 10px;
}
&__icon {
width: 10px;
height: 10px;
}
$root: &;
@mixin filter-block($color) {
&#{$root}--category {
color: $color;
}
&#{$root}--active {
#{$root}__icon-wrapper {
background-color: $color;
}
}
#{$root}__icon {
fill: $color;
}
}
&--language-communication {
@include filter-block($color-accent-2-dark);
}
&--society {
@include filter-block($color-accent-1-dark);
}
&--interdisciplinary {
@include filter-block($color-accent-4-dark);
}
&--active {
#{$root}__text {
font-weight: 600;
}
#{$root}__icon-wrapper {
background-color: black;
}
#{$root}__icon {
fill: white;
}
}
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div class="filter-group">
<filter-entry
:text="title"
v-bind="$attrs"
:type="category"
:category="category"
:is-category="true"
@click.native="setCategoryFilter(category)"/>
<div class="filter-group__children">
<filter-entry
:key="type.id"
:data-cy="`filter-${type.type}`"
:category="type.category"
:text="type.name"
:type="type.type"
v-for="type in types"
@click.native="setFilter(`type:${type.type}`)"
/>
</div>
</div>
</template>
<script>
import ChevronRight from '@/components/icons/ChevronRight';
import FilterEntry from '@/components/instruments/FilterEntry';
import SET_FILTER_MUTATION from 'gql/local/mutations/setInstrumentFilter.gql';
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFiler.gql';
export default {
props: {
title: {
type: String,
default: '',
},
types: {
type: Array,
default: () => [],
},
category: {
type: String,
default: '',
},
},
components: {
FilterEntry,
ChevronRight,
},
apollo: {
instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY
}
},
data() {
return {
instrumentFilter: {
currentFilter: ''
}
};
},
inheritAttrs: false,
methods: {
setCategoryFilter(category) {
if (category) {
this.setFilter(`category:${category}`);
} else {
this.setFilter(``);
}
},
setFilter(filter) {
this.$apollo.mutate({
mutation: SET_FILTER_MUTATION,
variables: {
filter
}
});
}
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.filter-group {
border-bottom: 1px solid $color-silver;
padding: $medium-spacing 0;
display: flex;
flex-direction: column;
&__children {
padding-left: $medium-spacing;
}
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<div
:class="typeClass"
class="instrument-entry">
<h4 class="instrument-entry__category">{{ categoryName }}</h4>
<h3 class="instrument-entry__title">{{ instrument.title }}</h3>
</div>
</template>
<script>
import {INTERDISCIPLINARY, LANGUAGE_COMMUNICATION, SOCIETY} from '@/consts/instrument.consts';
import instrumentType from '@/helpers/instrumentType';
export default {
props: {
instrument: {
type: Object,
required: true,
default: undefined,
},
},
computed: {
typeClass() {
return {
'instrument-entry__language-communication': this.instrument.type.category === LANGUAGE_COMMUNICATION,
'instrument-entry__society': this.instrument.type.category === SOCIETY,
'instrument-entry__interdisciplinary': this.instrument.type.category === INTERDISCIPLINARY,
};
},
categoryName() {
return instrumentType(this.instrument);
},
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.instrument-entry {
padding: $medium-spacing;
margin-bottom: $medium-spacing;
border-radius: 8px;
&__title {
@include heading-3;
margin-bottom: 0;
}
&__category {
@include sub-heading;
margin-bottom: $small-spacing;
}
$root: &;
&__language-communication {
background-color: $color-accent-2-light;
#{$root}__category {
color: $color-accent-2-dark;
}
}
&__society {
background-color: $color-accent-1-light;
#{$root}__category {
color: $color-accent-1-dark;
}
}
&__interdisciplinary {
background-color: $color-accent-4-light;
#{$root}__category {
color: $color-accent-4-dark;
}
}
}
</style>

View File

@ -1,97 +1,90 @@
<template>
<div class="instrument-filter">
<checkbox
:checked="type.enabled"
:class="`instrument-filter__checkbox--${type.cls}`"
:label="type.label"
:key="i"
class="instrument-filter__checkbox"
v-for="(type, i) in types"
@input="change($event, i)"
<filter-group
title="Alle Instrumente"
data-cy="filter-all-instruments"
/>
<filter-group
:types="languageCommunicationTypes"
:category="LANGUAGE_COMMUNICATION"
title="Sprache und Kommunikation"
data-cy="filter-language-communication"
class="instrument-filter__group--language"
/>
<filter-group
:types="societyTypes"
:category="SOCIETY"
title="Gesellschaft"
data-cy="filter-society"
/>
<filter-group
:category="INTERDISCIPLINARY"
title="Überfachliche Instrumente"
data-cy="filter-interdisciplinary"
/>
</div>
</template>
<script>
import {INTERDISCIPLINARY, LANGUAGE_COMMUNICATION, SOCIETY} from '@/consts/instrument.consts';
import Checkbox from '@/components/ui/Checkbox';
import FilterGroup from '@/components/instruments/FilterGroup';
import INSTRUMENT_TYPES_QUERY from 'gql/queries/instrumentTypesQuery';
export default {
components: {
Checkbox
FilterGroup,
Checkbox,
},
data() {
return {
filter: [],
types: [
{
label: 'Sprache und Kommunikation',
enabled: true,
prop: 'LANGUAGE_COMMUNICATION',
cls: 'language'
},
{
label: 'Gesellschaft',
enabled: true,
prop: 'SOCIETY',
cls: 'society'
},
{
label: 'Überfachliches Instrument',
enabled: true,
prop: 'INTERDISCIPLINARY',
cls: 'interdisciplinary'
},
]
filter: '',
LANGUAGE_COMMUNICATION,
SOCIETY,
INTERDISCIPLINARY,
instrumentTypes: [],
};
},
methods: {
change(enabled, index) {
let type = this.types[index];
this.types = [
...this.types.slice(0, index),
{
...type,
enabled
computed: {
languageCommunicationTypes() {
return this.instrumentTypes.filter(t => t.category === 'LANGUAGE_COMMUNICATION');
},
...this.types.slice(index + 1),
];
this.$emit('filter',
this.types
.filter(t => t.enabled)
.map(t => t.prop)
);
societyTypes() {
return this.instrumentTypes.filter(t => t.category === 'SOCIETY');
},
interdisciplinaryTypes() {
return this.instrumentTypes.filter(t => t.category === 'INTERDISCIPLINARY');
},
},
methods: {
setFilter(filter) {
this.filter = filter;
this.$emit('filter', filter);
}
},
apollo: {
instrumentTypes: {
query: INSTRUMENT_TYPES_QUERY,
},
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "~styles/helpers";
.instrument-filter {
display: flex;
align-content: center;
&__checkbox {
&--language {
/deep/ input:checked + .checkbox {
background-color: $color-accent-1-dark;
}
}
&--society {
/deep/ input:checked + .checkbox {
background-color: $color-accent-2-dark;
}
}
&--interdisciplinary {
/deep/ input:checked + .checkbox {
background-color: $color-accent-4-dark;
}
}
}
flex-direction: column;
}
</style>

View File

@ -32,6 +32,7 @@
import ActivityEntry from '@/components/profile/ActivityEntry';
import SCROLL_TO_MUTATION from '@/graphql/gql/local/mutations/scrollTo.gql';
import instrumentType from '@/helpers/instrumentType';
export default {
props: ['instrument', 'filter'],
@ -52,11 +53,7 @@
return this.applyFilter('bookmarks') ? this.instrument.bookmarks : [];
},
type() {
if (this.instrument.type === 'LANGUAGE_COMMUNICATION') {
return 'Sprache & Kommunikation';
} else {
return 'Gesellschaft';
}
return instrumentType(this.instrument);
}
},
methods: {

View File

@ -0,0 +1,3 @@
export const LANGUAGE_COMMUNICATION = 'LANGUAGE_COMMUNICATION';
export const SOCIETY = 'SOCIETY';
export const INTERDISCIPLINARY = 'INTERDISCIPLINARY';

View File

@ -24,6 +24,10 @@ const writeLocalCache = cache => {
__typename: 'HelloEmail',
email: '',
},
instrumentFilter: {
__typename: 'InstrumentFilter',
currentFilter: 'abc'
}
},
});
};

View File

@ -0,0 +1,5 @@
query InstrumentFilter {
instrumentFilter @client {
currentFilter
}
}

View File

@ -0,0 +1,3 @@
mutation($filter: String!) {
setInstrumentFilter(filter: $filter) @client
}

View File

@ -0,0 +1,7 @@
query InstrumentTypesQuery {
instrumentTypes {
name
type
category
}
}

View File

@ -1,12 +0,0 @@
query InstrumentQuery($type: String!){
instruments(type: $type) {
edges {
node {
id
title
contents
slug
}
}
}
}

View File

@ -1,13 +1,14 @@
query InstrumentQuery {
query InstrumentsQuery {
instruments {
edges {
node {
id
title
contents
slug
type {
id
type
}
category
name
}
}
}

View File

@ -1,6 +1,7 @@
import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql';
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
import SIDEBAR from '@/graphql/gql/local/sidebar.gql';
import INSTRUMENT_FILTER from '@/graphql/gql/local/instrumentFiler.gql';
export const resolvers = {
Mutation: {
@ -16,6 +17,12 @@ export const resolvers = {
cache.writeQuery({query: HELLO_EMAIL, data});
return data.helloEmail;
},
setInstrumentFilter: (_, {filter}, {cache}) => {
const data = cache.readQuery({query: INSTRUMENT_FILTER});
data.instrumentFilter.currentFilter = filter;
cache.writeQuery({query: INSTRUMENT_FILTER, data});
return data.instrumentFilter;
},
toggleSidebar: (_, {sidebar: {profile, navigation}}, {cache}) => {
const data = cache.readQuery({query: SIDEBAR});
if (typeof profile !== 'undefined') {
@ -26,6 +33,6 @@ export const resolvers = {
}
cache.writeQuery({query: SIDEBAR, data});
return data.sidebar;
}
}
},
},
};

View File

@ -19,6 +19,10 @@ export const typeDefs = gql`
profile: Boolean
}
type InstrumentFilter {
currentFilter: String!
}
type Mutation {
scrollTo(scrollTo: String!): ScrollPosition
helloEmail(email: String!): HelloEmail

View File

@ -0,0 +1,13 @@
import {LANGUAGE_COMMUNICATION, SOCIETY} from '@/consts/instrument.consts';
const instrumentType = ({type: {category}}) => {
if (category === LANGUAGE_COMMUNICATION) {
return 'Sprache & Kommunikation';
} else if (category === SOCIETY) {
return 'Gesellschaft';
} else {
return 'Überfachliches Instrument';
}
};
export default instrumentType;

View File

@ -1,10 +1,5 @@
<template>
<div class="instrument-overview">
<div class="instrument-overview__heading">
<h1 class="instrument-overview__title">
Instrumente
</h1>
</div>
<instrument-filter
class="instrument-overview__filter"
@filter="updateFilter"/>
@ -12,10 +7,10 @@
<router-link
:to="{name: 'instrument', params: {slug: instrument.slug}}"
:key="instrument.id"
class="instrument-overview__list-item"
data-cy="instrument"
tag="div"
v-for="instrument in filteredInstruments">
{{ instrument.title }}
<instrument-entry :instrument="instrument"/>
</router-link>
</div>
</div>
@ -23,11 +18,15 @@
<script>
import InstrumentFilter from '@/components/instruments/InstrumentFilter';
import InstrumentEntry from '@/components/instruments/InstrumentEntry';
import INSTRUMENTS_QUERY from '@/graphql/gql/queries/instrumentsQuery.gql';
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFiler.gql';
export default {
components: {
InstrumentFilter
InstrumentFilter,
InstrumentEntry,
},
apollo: {
@ -35,27 +34,43 @@
query: INSTRUMENTS_QUERY,
update(data) {
return this.$getRidOfEdges(data).instruments;
},
},
instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY,
update({instrumentFilter}) {
const {currentFilter} = instrumentFilter;
if (currentFilter && currentFilter.indexOf(':') > -1) {
const [filterType, identifier] = currentFilter.split(':');
this.filter = i => i.type[filterType] === identifier;
} else {
this.filter = i => i; // identity
}
}
return instrumentFilter;
},
},
},
data() {
return {
instruments: [],
filter: []
filter: i => i, // identity
instrumentFilter: {
currentFilter: '',
},
};
},
computed: {
filteredInstruments() {
return this.instruments.filter(instrument => this.filter.includes(instrument.type) || !this.filter.length);
}
return this.instruments.filter(i => this.filter(i));
},
},
methods: {
updateFilter(filter) {
this.filter = filter;
}
},
},
};
</script>
@ -65,24 +80,14 @@
.instrument-overview {
display: grid;
grid-template-rows: auto auto 1fr;
@include centered(800px);
&__heading {
padding: 2*$large-spacing 0;
width: 100%;
display: flex;
justify-content: flex-start;
}
&__title {
max-width: $screen-width;
line-height: 1.2;
margin-bottom: 0;
}
//grid-template-rows: auto auto 1fr;
grid-template-columns: 300px auto;
grid-column-gap: $small-spacing;
//@include centered(800px);
padding: 0 $small-spacing;
&__filter {
justify-self: start;
}
&__list {
@ -91,6 +96,7 @@
width: 100%;
display: flex;
flex-direction: column;
justify-self: center;
}
&__list-item {

View File

@ -28,6 +28,12 @@
font-size: toRem(18px);
}
@mixin sub-heading {
font-family: $sans-serif-font-family;
font-weight: 400;
font-size: toRem(16px);
}
@mixin modal-heading {
@include heading-2;
margin-bottom: 0;

View File

@ -1,6 +1,26 @@
### list backups
### List backups
```
heroku login
heroku pg:backups --app skillbox-prod
```
### Create a backup
`heroku pg:backups:capture --app <appname>`
The following command will provide a URL to where the backup can be downloaded (expires after 60 minutes)
`heroku pg:backups:url b001 --app <appname>`
To restore a backup, use
`heroku pg:backups:restore b001 DATABASE_URL --app <appname>`
To see the backup schedule
`heroku pg:backus:schedules --app <appname>`
To download a backup use
`heroku pg:backups:download --app <appname>`

View File

@ -7,7 +7,7 @@ from graphene_django.debug import DjangoDebug
from api import graphene_wagtail # Keep this import exactly here, it's necessary for StreamField conversion
from assignments.schema.mutations import AssignmentMutations
from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery
from basicknowledge.queries import BasicKnowledgeQuery
from basicknowledge.queries import InstrumentQuery
from books.schema.mutations import BookMutations
from books.schema.queries import BookQuery
from news.schema import AllNewsTeasersQuery
@ -27,7 +27,7 @@ from users.mutations import ProfileMutations
class CustomQuery(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,
StudentSubmissionQuery, BasicKnowledgeQuery, PortfolioQuery, SurveysQuery, AllNewsTeasersQuery, graphene.ObjectType):
StudentSubmissionQuery, InstrumentQuery, PortfolioQuery, SurveysQuery, AllNewsTeasersQuery, graphene.ObjectType):
node = relay.Node.Field()
if settings.DEBUG:

View File

@ -7,6 +7,7 @@ from django.http import HttpResponse
from django.urls import reverse
from django.utils.html import format_html
from assignments.helpers import write_assignments_to_csv, write_submissions_to_csv
from assignments.models import Assignment, StudentSubmission, SubmissionFeedback
@ -38,10 +39,7 @@ class AssignmentAdmin(admin.ModelAdmin):
response['Content-Disposition'] = 'attachment;filename=assignment-export.csv'
writer = csv.writer(response)
field_names = ['ID', 'Titel', 'Auftragstext', 'Modul']
writer.writerow(field_names)
for assignment in queryset.all():
writer.writerow([assignment.id, assignment.title, assignment.assignment, assignment.module])
write_assignments_to_csv(writer, queryset)
return response
@ -52,11 +50,7 @@ class AssignmentAdmin(admin.ModelAdmin):
response['Content-Disposition'] = 'attachment;filename=assignment-submission-export.csv'
writer = csv.writer(response)
field_names = ['Assignment-ID', 'Text', 'Mit Lehrer geteilt',]
writer.writerow(field_names)
for assignment in queryset.all():
for submission in assignment.submissions.filter(final=True):
writer.writerow([submission.assignment.id, submission.text, submission.final])
write_submissions_to_csv(writer, queryset)
return response

View File

@ -0,0 +1,13 @@
def write_assignments_to_csv(writer, queryset):
field_names = ['ID', 'Titel', 'Auftragstext', 'Modul']
writer.writerow(field_names)
for assignment in queryset.all():
writer.writerow([assignment.id, assignment.title, assignment.assignment, assignment.module])
def write_submissions_to_csv(writer, queryset):
field_names = ['Assignment-ID', 'Text', 'Mit Lehrer geteilt', ]
writer.writerow(field_names)
for submission in queryset.all():
writer.writerow([submission.assignment.id, submission.text, submission.final])

View File

@ -0,0 +1,27 @@
import csv
from django.core.management.base import BaseCommand
from assignments.helpers import write_assignments_to_csv, write_submissions_to_csv
from assignments.models import Assignment, StudentSubmission
class Command(BaseCommand):
help = """
Export assignments with submissions
"""
def handle(self, *args, **options):
ids = [171, 112, 113, 114, 272, 246, 250, 348, 598]
assignments = Assignment.objects.filter(id__in=ids)
with open('./export-assignments.csv', 'w') as f:
writer = csv.writer(f)
write_assignments_to_csv(writer, assignments)
submissions = StudentSubmission.objects.filter(assignment__id__in=ids)
with open('./export-submissions.csv', 'w') as f:
writer = csv.writer(f)
write_submissions_to_csv(writer, submissions)

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.24 on 2021-10-20 12:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('basicknowledge', '0007_basicknowledge_intro'),
]
operations = [
migrations.CreateModel(
name='InstrumentType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('category', models.CharField(choices=[('language_communication', 'Sprache & Kommunikation'), ('society', 'Gesellschaft'), ('interdisciplinary', 'Überfachliches Instrument')], max_length=100)),
],
),
migrations.AddField(
model_name='basicknowledge',
name='new_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='basicknowledge.InstrumentType'),
),
]

View File

@ -0,0 +1,56 @@
# Generated by Django 2.2.24 on 2021-10-20 12:13
from django.db import migrations
from basicknowledge.models import INTERDISCIPLINARY, LANGUAGE_COMMUNICATION, SOCIETY
def create_types(apps, schema_editor):
BasicKnowledge = apps.get_model('basicknowledge', 'BasicKnowledge')
InstrumentType = apps.get_model('basicknowledge', 'InstrumentType')
language_type=InstrumentType.objects.create(
name='Sprache & Kommunikation',
category=LANGUAGE_COMMUNICATION
)
society_type=InstrumentType.objects.create(
name='Gesellschaft',
category=SOCIETY
)
interdisciplinary_type=InstrumentType.objects.create(
name='Überfachliches Instrument',
category=INTERDISCIPLINARY
)
instruments = []
for instrument in BasicKnowledge.objects.filter(type=LANGUAGE_COMMUNICATION):
instrument.new_type=language_type
instruments.append(instrument)
for instrument in BasicKnowledge.objects.filter(type=SOCIETY):
instrument.new_type=society_type
instruments.append(instrument)
for instrument in BasicKnowledge.objects.filter(type=INTERDISCIPLINARY):
instrument.new_type=interdisciplinary_type
instruments.append(instrument)
BasicKnowledge.objects.bulk_update(instruments, ['new_type'])
def delete_types(apps, schema_editor):
BasicKnowledge = apps.get_model('basicknowledge', 'BasicKnowledge')
InstrumentType = apps.get_model('basicknowledge', 'InstrumentType')
instruments = []
for instrument in BasicKnowledge.objects.all():
instrument.new_type = None
instruments.append(instrument)
BasicKnowledge.objects.bulk_update(instruments, ['new_type'])
InstrumentType.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('basicknowledge', '0008_auto_20211020_1202'),
]
operations = [
migrations.RunPython(create_types, delete_types)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-10-30 20:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('basicknowledge', '0009_auto_20211020_1213'),
]
operations = [
migrations.RenameField(
model_name='basicknowledge',
old_name='type',
new_name='old_type',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-10-31 11:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('basicknowledge', '0010_auto_20211030_2004'),
]
operations = [
migrations.AlterField(
model_name='instrumenttype',
name='name',
field=models.CharField(max_length=255, unique=True),
),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.24 on 2021-11-01 10:08
from django.db import migrations
from basicknowledge.models import LANGUAGE_COMMUNICATION, SOCIETY
from core.logger import get_logger
logger = get_logger(__name__)
def create_new_types(apps, schema_editor):
InstrumentType = apps.get_model('basicknowledge', 'InstrumentType')
language_communication_types = [
'Analyse', 'Argumentation', 'Beschreibung', 'Grafiken', 'Interview',
'Kommunikation', 'Korrespondenz', 'Orthografie', 'Präsentation',
'Struktur', 'Umfrage', 'Zusammenfassung', 'Blog'
]
society_types = [
'Ethik', 'Identität und Sozialisation', 'Kultur', 'Ökologie',
'Politik', 'Recht', 'Technologie', 'Wirtschaft'
]
for type_name in language_communication_types:
obj, created = InstrumentType.objects.get_or_create(name=type_name, category=LANGUAGE_COMMUNICATION)
for type_name in society_types:
obj, created = InstrumentType.objects.get_or_create(name=type_name, category=SOCIETY)
class Migration(migrations.Migration):
dependencies = [
('basicknowledge', '0011_auto_20211031_1144'),
]
operations = [
migrations.RunPython(create_new_types, migrations.RunPython.noop)
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.24 on 2021-11-10 11:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('basicknowledge', '0012_auto_20211101_1008'),
]
operations = [
migrations.AlterField(
model_name='basicknowledge',
name='new_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='instruments', to='basicknowledge.InstrumentType'),
),
]

View File

@ -1,13 +1,39 @@
from django.db import models
from django.utils.text import slugify
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.core.fields import StreamField, RichTextField
from wagtail.core.fields import RichTextField, StreamField
from wagtail.images.blocks import ImageChooserBlock
from books.blocks import LinkBlock, VideoBlock, DocumentBlock, SectionTitleBlock, InfogramBlock, \
GeniallyBlock, InstrumentTextBlock, SubtitleBlock, ThinglinkBlock
from books.blocks import DocumentBlock, GeniallyBlock, InfogramBlock, InstrumentTextBlock, LinkBlock, SectionTitleBlock, \
SubtitleBlock, ThinglinkBlock, VideoBlock
from core.constants import DEFAULT_RICH_TEXT_FEATURES
from core.wagtail_utils import StrictHierarchyPage
LANGUAGE_COMMUNICATION = 'language_communication'
SOCIETY = 'society'
INTERDISCIPLINARY = 'interdisciplinary'
class InstrumentType(models.Model):
CATEGORY_CHOICES = (
(LANGUAGE_COMMUNICATION, 'Sprache & Kommunikation'),
(SOCIETY, 'Gesellschaft'),
(INTERDISCIPLINARY, 'Überfachliches Instrument'),
)
name = models.CharField(max_length=255, unique=True)
category = models.CharField(
max_length=100,
choices=CATEGORY_CHOICES
)
@property
def type(self):
return slugify(self.name.lower())
def __str__(self):
return self.type
class BasicKnowledge(StrictHierarchyPage):
parent_page_types = ['books.book']
@ -27,24 +53,16 @@ class BasicKnowledge(StrictHierarchyPage):
('subtitle', SubtitleBlock()),
], null=True, blank=True)
LANGUAGE_COMMUNICATION = 'language_communication'
SOCIETY = 'society'
INTERDISCIPLINARY = 'interdisciplinary'
new_type = models.ForeignKey(InstrumentType, null=True, on_delete=models.PROTECT, related_name='instruments')
TYPE_CHOICES = (
(LANGUAGE_COMMUNICATION, 'Sprache & Kommunikation'),
(SOCIETY, 'Gesellschaft'),
(INTERDISCIPLINARY, 'Überfachliches Instrument'),
)
type = models.CharField(
old_type = models.CharField(
max_length=100,
choices=TYPE_CHOICES
choices=InstrumentType.CATEGORY_CHOICES
)
content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('type'),
FieldPanel('new_type'),
FieldPanel('intro'),
StreamFieldPanel('contents')
]

View File

@ -1,34 +1,54 @@
import graphene
from graphene import relay
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from api.utils import get_object
from notes.models import InstrumentBookmark
from notes.schema import InstrumentBookmarkNode
from .models import BasicKnowledge
from .models import BasicKnowledge, InstrumentType
class InstrumentTypeNode(DjangoObjectType):
type = graphene.String(required=True)
class Meta:
model = InstrumentType
only_fields = [
'name', 'category', 'type', 'id'
]
@staticmethod
def resolve_type(root: InstrumentType, info, **kwargs):
return root.type
class InstrumentNode(DjangoObjectType):
bookmarks = graphene.List(InstrumentBookmarkNode)
type = graphene.Field(InstrumentTypeNode)
class Meta:
model = BasicKnowledge
filter_fields = ['slug', 'type']
filter_fields = ['slug']
interfaces = (relay.Node,)
only_fields = [
'slug', 'title', 'intro', 'type', 'contents',
'slug', 'title', 'intro', 'contents',
]
@staticmethod
def resolve_type(root: BasicKnowledge, info, **kwargs):
return root.new_type
def resolve_bookmarks(self, info, **kwargs):
return InstrumentBookmark.objects.filter(
user=info.context.user,
instrument=self
)
class BasicKnowledgeQuery(object):
class InstrumentQuery(object):
instrument = graphene.Field(InstrumentNode, slug=graphene.String(), id=graphene.ID())
instruments = DjangoFilterConnectionField(InstrumentNode)
instruments = graphene.List(InstrumentNode)
instrument_types = graphene.List(InstrumentTypeNode)
def resolve_instrument(self, info, **kwargs):
slug = kwargs.get('slug')
@ -42,3 +62,6 @@ class BasicKnowledgeQuery(object):
def resolve_instruments(self, info, **kwargs):
return BasicKnowledge.objects.all().live()
def resolve_instrument_types(self, info, **kwargs):
return InstrumentType.objects.filter(instruments__isnull=False)

View File

View File

@ -0,0 +1,27 @@
from books.factories import InstrumentFactory, InstrumentTypeFactory
from core.tests.base_test import SkillboxTestCase
INSTRUMENT_TYPES_QUERY = """
query InstrumentTypesQuery {
instrumentTypes {
name
type
category
}
}
"""
class InstrumentTypesQueryTestCase(SkillboxTestCase):
def setUp(self) -> None:
self.createDefault()
self.type = InstrumentTypeFactory(name='Type O Negative')
second_type = InstrumentTypeFactory(name='Typecast')
InstrumentTypeFactory(name='Guitar')
self.instrument = InstrumentFactory(new_type=self.type)
InstrumentFactory(new_type=second_type)
def test_instrument_types_empty_not_returned(self):
result = self.get_client().get_result(INSTRUMENT_TYPES_QUERY)
self.assertIsNone(result.errors)
self.assertEqual(len(result.data['instrumentTypes']), 2)

View File

@ -9,10 +9,10 @@ from wagtail.core.models import Page, Site
from wagtail.core.rich_text import RichText
from assignments.models import Assignment
from basicknowledge.models import BasicKnowledge
from books.blocks import BasicKnowledgeBlock, ImageUrlBlock, LinkBlock, AssignmentBlock, VideoBlock
from books.models import Book, Topic, Module, Chapter, ContentBlock, TextBlock
from core.factories import BasePageFactory, fake, DummyImageFactory, fake_paragraph, fake_title
from basicknowledge.models import BasicKnowledge, INTERDISCIPLINARY, InstrumentType, LANGUAGE_COMMUNICATION, SOCIETY
from books.blocks import AssignmentBlock, BasicKnowledgeBlock, ImageUrlBlock, LinkBlock, VideoBlock
from books.models import Book, Chapter, ContentBlock, Module, TextBlock, Topic
from core.factories import BasePageFactory, DummyImageFactory, fake, fake_paragraph, fake_title
class BookFactory(BasePageFactory):
@ -70,9 +70,18 @@ class TextBlockFactory(wagtail_factories.StructBlockFactory):
model = TextBlock
class InstrumentTypeFactory(factory.DjangoModelFactory):
class Meta:
model = InstrumentType
category = factory.Iterator([LANGUAGE_COMMUNICATION, SOCIETY, INTERDISCIPLINARY])
name = factory.LazyAttribute(lambda x: fake.text(max_nb_chars=20))
class InstrumentFactory(BasePageFactory):
title = factory.LazyAttribute(fake_title)
type = factory.Iterator([BasicKnowledge.LANGUAGE_COMMUNICATION, BasicKnowledge.SOCIETY, BasicKnowledge.INTERDISCIPLINARY])
old_type = factory.Iterator([LANGUAGE_COMMUNICATION, SOCIETY, INTERDISCIPLINARY])
new_type = factory.SubFactory(InstrumentTypeFactory)
class Meta:
model = BasicKnowledge
@ -83,7 +92,6 @@ class InstrumentFactory(BasePageFactory):
return super()._create(model_class, *args, **kwargs)
class BasicKnowledgeBlockFactory(wagtail_factories.StructBlockFactory):
description = factory.LazyAttribute(fake_paragraph)
basic_knowledge = factory.SubFactory(InstrumentFactory)

View File

@ -23,7 +23,7 @@ class SkillboxTestCase(TestCase):
self.school_class = SchoolClass.objects.get(name='skillbox')
def get_client(self, user=None) -> Client:
def get_client(self, user=None) -> GQLClient:
request = RequestFactory().get('/')
if user is None:
user = self.teacher

View File

@ -206,12 +206,6 @@ type AssignmentNodeEdge {
cursor: String!
}
enum BasicKnowledgeType {
LANGUAGE_COMMUNICATION
SOCIETY
INTERDISCIPLINARY
}
type ChapterBookmarkNode implements Node {
user: PrivateUserNode!
note: NoteNode
@ -461,7 +455,8 @@ type CustomQuery {
project(id: ID, slug: String): ProjectNode
projects: [ProjectNode]
instrument(slug: String, id: ID): InstrumentNode
instruments(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, type: String): InstrumentNodeConnection
instruments: [InstrumentNode]
instrumentTypes: [InstrumentTypeNode]
studentSubmission(id: ID!): StudentSubmissionNode
assignment(id: ID!): AssignmentNode
assignments(offset: Int, before: String, after: String, first: Int, last: Int): AssignmentNodeConnection
@ -484,7 +479,7 @@ type CustomQuery {
me: PrivateUserNode
allUsers(offset: Int, before: String, after: String, first: Int, last: Int, username: String, email: String): PrivateUserNodeConnection
myActivity(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, slug_Icontains: String, slug_In: [String], title: String, title_Icontains: String, title_In: [String]): ModuleNodeConnection
myInstrumentActivity(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, type: String): InstrumentNodeConnection
myInstrumentActivity(offset: Int, before: String, after: String, first: Int, last: Int, slug: String): InstrumentNodeConnection
_debug: DjangoDebug
}
@ -609,9 +604,9 @@ type InstrumentNode implements Node {
slug: String!
intro: String!
contents: GenericStreamFieldType
type: BasicKnowledgeType!
id: ID!
bookmarks: [InstrumentBookmarkNode]
type: InstrumentTypeNode
}
type InstrumentNodeConnection {
@ -624,6 +619,19 @@ type InstrumentNodeEdge {
cursor: String!
}
enum InstrumentTypeCategory {
LANGUAGE_COMMUNICATION
SOCIETY
INTERDISCIPLINARY
}
type InstrumentTypeNode {
id: ID!
name: String!
category: InstrumentTypeCategory!
type: String!
}
scalar JSONString
input JoinClassInput {