Merged in feature/apply-visibility (pull request #80)

Feature/apply visibility

Approved-by: Christian Cueni
This commit is contained in:
Ramon Wenger 2021-03-15 10:30:41 +00:00
commit 77db03eea7
53 changed files with 3850 additions and 181 deletions

View File

@ -221,3 +221,12 @@ To generate a new schema, use the management command
```
python manage.py export_schema_for_cypress
```
## GraphQL
### Generate GraphQL SDL Document
```
python manage.py export_schema_graphql
```

View File

@ -5,5 +5,6 @@
"reporterOptions": {
"mochaFile": "cypress/test-reports/cypress-results-[hash].xml",
"toConsole": true
}
},
"$schema": "https://on.cypress.io/cypress.schema.json"
}

View File

@ -0,0 +1,5 @@
module.exports = {
"extends": [
"plugin:cypress/recommended"
]
}

View File

@ -0,0 +1,74 @@
// import * as schema from '../fixtures/schema.json';
import {getModules, getMe} from '../support/helpers';
const mocks = {
UUID: () => 'Whatever',
GenericStreamFieldType: () => [],
};
const operations = {
MeQuery() {
return getMe({
schoolClasses: ['FLID2018a', 'Andere Klasse'],
teacher: true,
});
},
ModulesQuery: getModules,
UpdateSettings: {
updateSettings: {
success: true,
},
},
MySchoolClassQuery: {
me: {},
},
UpdateLastModule: {
updateLastModule: {
success: true,
},
},
SyncModuleVisibility: {
syncModuleVisibility: {
success: true,
},
},
};
describe('Apply module visibility', () => {
beforeEach(() => {
cy.server();
cy.task('getSchema').then(schema => {
cy.mockGraphql({
schema,
// endpoint: '/api/graphql'
mocks,
operations,
});
});
});
it('needs to be implemented', () => {
// Cypress.config({
// baseUrl: 'http://localhost:8080',
// });
cy.viewport('macbook-15');
// login as teacher
// cy.fakeLogin('nico.zickgraf', 'test');
cy.apolloLogin('nico.zickgraf', 'test');
// cy.wait('@gqlBetaLogin');
// go to module
cy.visit('/module/lohn-und-budget');
cy.selectClass('Andere Klasse');
// click on settings
cy.getByDataCy('module-settings-button').click();
// click on select button
cy.getByDataCy('select-school-class-button').click();
// select schoolclass
cy.getByDataCy('school-class-visibility-dropdown').select('FLID2018a');
// save changes
cy.getByDataCy('save-visibility-button').click();
cy.getByDataCy('module-title').should('exist');
});
});

View File

@ -1,6 +1,6 @@
import { GraphQLError } from 'graphql';
const schema = require('../fixtures/schema.json');
const schema = require('../../fixtures/schema.json');
describe('Email Verifcation', () => {
beforeEach(() => {
@ -19,7 +19,7 @@ describe('Email Verifcation', () => {
},
}
});
cy.apolloLogin('rahel.cueni', 'test')
cy.apolloLogin('rahel.cueni', 'test');
cy.visit('/license-activation');
cy.redeemCoupon('12345asfd');

View File

@ -1,4 +1,4 @@
const schema = require('../fixtures/schema_public.json');
const schema = require('../../fixtures/schema_public.json');
const isEmailAvailableUrl = '**/rest/deutsch/V1/customers/isEmailAvailable';
const checkPasswordUrl = '**/rest/deutsch/V1/integration/customer/token';
@ -20,7 +20,7 @@ describe('Login', () => {
message: 'success',
success: true
}
}
};
},
}
});
@ -69,5 +69,5 @@ describe('Login', () => {
cy.checkEmailAvailable('feuzaebi.ch');
cy.get('[data-cy="email-local-errors"]').contains('Bitte geben Sie eine gülitge E-Mail an');
})
});
});

View File

@ -11,7 +11,18 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
import {readFileSync} from 'fs';
import {resolve} from 'path';
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
on('task', {
getSchema() {
return readFileSync(
resolve(__dirname, '../../../schema.graphql'),
'utf8'
);
}
});
};

View File

@ -161,7 +161,7 @@ Cypress.Commands.add('redeemCoupon', coupon => {
cy.get('[data-cy="coupon-input"]').type(coupon);
}
cy.get('[data-cy="coupon-button"]').click();
})
});
Cypress.Commands.add('assertStartPage', (onboarding) => {
if (onboarding) {
@ -174,3 +174,18 @@ Cypress.Commands.add('assertStartPage', (onboarding) => {
Cypress.Commands.add('skipOnboarding', (onboarding) => {
cy.get('[data-cy=onboarding-skip-link]').click();
});
Cypress.Commands.add('getByDataCy', (selector) => {
return cy.get(`[data-cy=${selector}]`);
});
Cypress.Commands.add('selectClass', (schoolClass) => {
cy.getByDataCy('user-widget-avatar').click();
cy.getByDataCy('class-selection').click();
cy.getByDataCy('class-selection-entry').contains(schoolClass).click();
});
Cypress.Commands.add('fakeLogin', () => {
cy.log('Logging in (fake)');
cy.setCookie('loginStatus', 'true');
});

View File

@ -0,0 +1,353 @@
const getSchoolClassNode = (id, schoolClassName) => ({
'id': btoa(`SchoolClassNode:${id}`),
'name': schoolClassName,
'__typename': 'SchoolClassNode',
});
export const getMe = ({schoolClasses, teacher}) => {
let schoolClassNodes;
if (schoolClasses) {
schoolClassNodes = schoolClasses.map((schoolClass, i) => getSchoolClassNode(i, schoolClass));
} else {
schoolClassNodes = [getSchoolClassNode(1, 'FLID2018a')];
}
return {
'me': {
'id': 'VXNlck5vZGU6NQ==',
'pk': 5,
'username': 'rahel.cueni',
'email': 'rahel.cueni@skillbox.example',
'expiryDate': '3596153600',
'firstName': 'Rahel',
'lastName': 'Cueni',
'avatarUrl': '',
'isTeacher': false,
'lastModule': {
'id': 'TW9kdWxlTm9kZToxNw==',
'slug': 'lohn-und-budget',
'__typename': 'ModuleNode',
},
'selectedClass': {
'id': 'U2Nob29sQ2xhc3NOb2RlOjI=',
'__typename': 'SchoolClassNode',
},
'lastTopic': {
'id': 'VG9waWNOb2RlOjU=',
'slug': 'geld-und-kauf',
'__typename': 'TopicNode',
},
'schoolClasses': {
'edges': schoolClassNodes.map(scn => ({
node: scn,
'__typename': 'SchoolClassNodeEdge',
})),
'__typename': 'SchoolClassNodeConnection',
},
'__typename': 'UserNode',
'onboardingVisited': true,
'permissions': teacher ? ['users.can_manage_school_class_content'] : [],
},
};
};
export const getAssignments = () => {
return {
'assignments': {
'edges': [
{
'node': {
'id': 'QXNzaWdubWVudE5vZGU6MQ==',
'title': 'Ein Auftragstitel',
'assignment': 'Ein Auftrag',
'solution': null,
'submission': {
'id': 'U3R1ZGVudFN1Ym1pc3Npb25Ob2RlOjE=',
'text': 'Hir ist ein Feler gewesen',
'final': false,
'document': '',
'submissionFeedback': {
'id': 'U3VibWlzc2lvbkZlZWRiYWNrTm9kZTox',
'text': '\ud83d\ude42\ud83d\ude10\ud83e\udd2c\ud83d\udc4d\ud83e\udd22\ud83e\udd22\ud83e\udd22\ud83e\udd22\ud83d\ude2e\ud83e\udd17',
'teacher': {
'firstName': 'Nico',
'lastName': 'Zickgraf',
'__typename': 'UserNode',
},
'__typename': 'SubmissionFeedbackNode',
},
'__typename': 'StudentSubmissionNode',
},
'__typename': 'AssignmentNode',
},
'__typename': 'AssignmentNodeEdge',
},
],
'__typename': 'AssignmentNodeConnection',
},
};
};
export const getModules = () => {
return {
'lohn-und-budget': {
'id': 'TW9kdWxlTm9kZToyOA==',
'title': 'Lohn und Budget',
'metaTitle': 'Modul 1',
'teaser': 'Die Berufsbildung ist ein neuer Lebensabschnit',
'intro': '\n <p>Sie stehen am Anfang eines neuen Lebensabschnitts. In Ihrer Rolle als Berufslernende oder Berufslernender haben Sie Verantwortung übernommen.</p>\n <p>Wie erging es Ihnen am ersten Arbeits- und Schultag?</p>\n ',
'slug': 'lohn-und-budget',
'heroImage': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==',
'solutionsEnabled': false,
'bookmark': {
'note': null,
'__typename': 'ModuleBookmarkNode',
},
'__typename': 'ModuleNode',
'assignments': {
'edges': [
{
'node': {
'id': 'QXNzaWdubWVudE5vZGU6MQ==',
'title': 'Ein Auftragstitel',
'assignment': 'Ein Auftrag',
'solution': null,
'submission': {
'id': 'U3R1ZGVudFN1Ym1pc3Npb25Ob2RlOjE=',
'text': 'Hir ist ein Feler gewesen',
'final': false,
'document': '',
'submissionFeedback': {
'id': 'U3VibWlzc2lvbkZlZWRiYWNrTm9kZTox',
'text': '🙂😐🤬👍🤢🤢🤢🤢😮🤗',
'teacher': {
'firstName': 'Nico',
'lastName': 'Zickgraf',
'__typename': 'UserNode',
},
'__typename': 'SubmissionFeedbackNode',
},
'__typename': 'StudentSubmissionNode',
},
'__typename': 'AssignmentNode',
},
'__typename': 'AssignmentNodeEdge',
},
],
'__typename': 'AssignmentNodeConnection',
},
'objectiveGroups': {
'edges': [],
'__typename': 'ObjectiveGroupNodeConnection',
},
'chapters': {
'edges': [
{
'node': {
'id': 'Q2hhcHRlck5vZGU6MTg=',
'title': '1.1 Lehrbeginn',
'description': 'Wie sieht Ihr Konsumverhalten aus?',
'bookmark': null,
'contentBlocks': {
'edges': [
{
'node': {
'id': 'Q29udGVudEJsb2NrTm9kZToxOQ==',
'slug': 'assignment',
'title': 'Assignment',
'type': 'NORMAL',
'contents': [
{
'type': 'assignment',
'value': {
'title': 'Ein Auftragstitel',
'assignment': 'Ein Auftrag',
'id': 'QXNzaWdubWVudE5vZGU6MQ==',
},
'id': 'df8212ee-3e82-49fa-977e-c4b60789163e',
},
],
'userCreated': false,
'mine': false,
'bookmarks': [],
'hiddenFor': {
'edges': [],
'__typename': 'SchoolClassNodeConnection',
},
'visibleFor': {
'edges': [],
'__typename': 'SchoolClassNodeConnection',
},
'__typename': 'ContentBlockNode',
},
'__typename': 'ContentBlockNodeEdge',
},
],
'__typename': 'ContentBlockNodeConnection',
},
'__typename': 'ChapterNode',
},
'__typename': 'ChapterNodeEdge',
},
],
'__typename': 'ChapterNodeConnection',
},
},
'geld': {
'id': 'TW9kdWxlTm9kZTo0Mg==',
'title': 'Geld',
'metaTitle': 'Modul 2',
'teaser': ' Geld braucht jeder von uns im t\u00e4glichen Leben.',
'intro': '\n <p>Jeder B\u00fcrger nutzt es. Nahezu jeden Tag. Kaum ein Tag vergeht, an dem wir nicht mit M\u00fcnzen oder Geldscheinen bezahlen, bargeldlose \u00dcberweisungen t\u00e4tigen oder andere Zahlungsmethoden verwenden. Doch was genau befindet sich da eigentlich in unserem Geldbeutel? Was ist das, was auf unseren Konten liegt und die Bezeichnung Geld tr\u00e4gt?</p>\n ',
'slug': 'geld',
'heroImage': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==',
'solutionsEnabled': false,
'bookmark': null,
'__typename': 'ModuleNode',
'assignments': {
'edges': [],
'__typename': 'AssignmentNodeConnection',
},
'objectiveGroups': {
'edges': [],
'__typename': 'ObjectiveGroupNodeConnection',
},
'chapters': {
'edges': [
{
'node': {
'id': 'Q2hhcHRlck5vZGU6MzI=',
'title': '2.1 Eine Welt ohne Geld?',
'description': '',
'bookmark': null,
'contentBlocks': {
'edges': [
{
'node': {
'id': 'Q29udGVudEJsb2NrTm9kZToxOQ==',
'slug': 'assignment',
'title': 'Assignment',
'type': 'NORMAL',
'contents': [
{
'type': 'assignment',
'value': {
'title': 'Ein Auftragstitel',
'assignment': 'Ein Auftrag',
'id': 'QXNzaWdubWVudE5vZGU6MQ==',
},
'id': 'df8212ee-3e82-49fa-977e-c4b60789163e',
},
],
'userCreated': false,
'mine': false,
'bookmarks': [],
'hiddenFor': {
'edges': [],
'__typename': 'SchoolClassNodeConnection',
},
'visibleFor': {
'edges': [],
'__typename': 'SchoolClassNodeConnection',
},
'__typename': 'ContentBlockNode',
},
'__typename': 'ContentBlockNodeEdge',
},
],
'__typename': 'ContentBlockNodeConnection',
},
'__typename': 'ChapterNode',
},
'__typename': 'ChapterNodeEdge',
},
],
'__typename': 'ChapterNodeConnection',
},
},
'lerntipps': {
'id': 'TW9kdWxlTm9kZTo3MA==',
'title': 'Lerntipps',
'metaTitle': 'Modul 4',
'teaser': 'Lerntipps',
'intro': '\n <p>Sie stehen am Anfang eines neuen Lebensabschnitts. In Ihrer Rolle als Berufslernende oder Berufslernender haben Sie Verantwortung übernommen.</p>\n <p>Wie erging es Ihnen am ersten Arbeits- und Schultag?</p>\n ',
'slug': 'lerntipps',
'heroImage': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==',
'solutionsEnabled': false,
'bookmark': {
'note': null,
'__typename': 'ModuleBookmarkNode',
},
'__typename': 'ModuleNode',
'assignments': {
'edges': [],
'__typename': 'AssignmentNodeConnection',
},
'objectiveGroups': {
'edges': [],
'__typename': 'ObjectiveGroupNodeConnection',
},
'chapters': {
'edges': [
{
'node': {
'id': 'Q2hhcHRlck5vZGU6MTg=',
'title': '1.1 Lehrbeginn',
'description': 'Wie sieht Ihr Konsumverhalten aus?',
'bookmark': null,
'contentBlocks': {
'edges': [],
'__typename': 'ContentBlockNodeConnection',
},
'__typename': 'ChapterNode',
},
'__typename': 'ChapterNodeEdge',
},
],
'__typename': 'ChapterNodeConnection',
},
},
'random': {
'id': 'TW9kdWxlTm9kZTo1NA==',
'title': 'Random',
'metaTitle': 'Modul 5',
'teaser': 'Random',
'intro': '\n <p>Sie stehen am Anfang eines neuen Lebensabschnitts. In Ihrer Rolle als Berufslernende oder Berufslernender haben Sie Verantwortung übernommen.</p>\n <p>Wie erging es Ihnen am ersten Arbeits- und Schultag?</p>\n ',
'slug': 'random',
'heroImage': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==',
'solutionsEnabled': false,
'bookmark': {
'note': null,
'__typename': 'ModuleBookmarkNode',
},
'__typename': 'ModuleNode',
'assignments': {
'edges': [],
'__typename': 'AssignmentNodeConnection',
},
'objectiveGroups': {
'edges': [],
'__typename': 'ObjectiveGroupNodeConnection',
},
'chapters': {
'edges': [
{
'node': {
'id': 'Q2hhcHRlck5vZGU6MTg=',
'title': '1.1 Lehrbeginn',
'description': 'Wie sieht Ihr Konsumverhalten aus?',
'bookmark': null,
'contentBlocks': {
'edges': [],
'__typename': 'ContentBlockNodeConnection',
},
'__typename': 'ChapterNode',
},
'__typename': 'ChapterNodeEdge',
},
],
'__typename': 'ChapterNodeConnection',
},
},
};
};

26
client/cypress/support/index.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
// Intellisense for custom Commands: https://github.com/cypress-io/cypress-example-todomvc#cypress-intellisense
declare namespace Cypress {
interface Chainable<Subject> {
/**
* Login via API call to the GraphQL endpoint, without calling the frontend. Faster than the other login.
* @param username
* @param password
* @example
* cy.apolloLogin('nico.zickgraf', 'test')
*/
apolloLogin(username: string, password: string): Chainable<any>
/**
* Selects an element based on the `data-cy=xxx` attribute
* @param selector - The value of the data-cy attribute to select
* @example
* cy.getByDataCy('my-new-button')
*/
getByDataCy(selector: string): Chainable<any>
selectClass(schoolClass: string): void
login(username:string, password:string, visitLogin?: boolean): void
fakeLogin(username:string, password:string): void
}
}

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"resolveJsonModule": true,
"types": [
"cypress"
]
},
"include": [
"**/*.*"
]
}

1312
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -44,11 +44,6 @@
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
@ -112,11 +107,19 @@
"babel-jest": "^24.8.0",
"canvas": "^2.5.0",
"cypress": "^6.2.1",
"eslint-plugin-cypress": "^2.11.2",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"graphql-config": "^3.2.0",
"jest": "^24.8.0",
"jest-serializer-vue": "^2.0.2",
"jest-transform-graphql": "^2.1.0",
"jest-transform-stub": "^2.0.0",
"jest-watch-typeahead": "^0.3.1",
"typescript": "^4.2.3",
"vue-jest": "^3.0.4"
}
}

View File

@ -9,7 +9,7 @@
</template>
<script>
import BaseInput from '@/components/inputs/BaseInput';
import BaseInput from '@/components/ui/BaseInput';
export default {
props: {

View File

@ -91,7 +91,7 @@
import AssignmentForm from '@/components/content-forms/AssignmentForm';
import TextForm from '@/components/content-forms/TextForm';
import TrashIcon from '@/components/icons/TrashIcon';
import Checkbox from '@/components/Checkbox';
import Checkbox from '@/components/ui/Checkbox';
import {meQuery} from '@/graphql/queries';

View File

@ -13,7 +13,7 @@
</template>
<script>
import Checkbox from '@/components/Checkbox';
import Checkbox from '@/components/ui/Checkbox';
export default {

View File

@ -45,13 +45,15 @@
class="module-navigation__toggle-menu"
v-if="canManageContent"
>
<a
class="module-navigation__actions"
data-cy="module-snapshots-button">Snapshots</a>
<router-link
:to="{name: 'module-settings'}"
class="module-navigation__actions"
data-cy="module-settings-button">Einstellungen</router-link>
<toggle-editing v-if="onModulePage"/>
<toggle-solutions-for-module
:slug="module.slug"
:enabled="module.solutionsEnabled"
class="module-navigation__solution-toggle"
data-cy="toggle-enable-solutions"
v-if="onModulePage && module.id"/>
</div>
</nav>
@ -159,8 +161,7 @@
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "~styles/helpers";
@mixin module-navigation-typography {
font-family: $sans-serif-font-family;
@ -220,5 +221,10 @@
display: flex;
align-items: center;
}
&__actions {
@include regular-text;
margin-right: $medium-spacing;
}
}
</style>

View File

@ -9,15 +9,6 @@
export default {
mixins: [me],
computed: {
currentClassName() {
let currentClass = this.me.schoolClasses.find(schoolClass => {
return schoolClass.id === this.me.selectedClass.id;
});
return currentClass ? currentClass.name : this.me.schoolClasses.length ? this.me.schoolClasses[0].name : '';
}
}
};
</script>

View File

@ -1,30 +1,29 @@
<template>
<checkbox
<toggle
:checked="checked"
class="toggle-editing"
label="Modul anpassen"
@input="toggle"/>
</template>
<script>
import Checkbox from '@/components/Checkbox';
import {mapState, mapActions} from 'vuex';
import Toggle from '@/components/ui/Toggle';
export default {
components: {
Checkbox,
Toggle
},
computed: {
...mapState({
checked: 'editModule',
})
}),
},
methods: {
...mapActions({
toggle: 'editModule'
})
}
toggle: 'editModule',
}),
},
};
</script>

View File

@ -7,7 +7,7 @@
</template>
<script>
import Checkbox from '@/components/Checkbox';
import Checkbox from '@/components/ui/Checkbox';
import UPDATE_SOLUTION_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateSolutionVisibility.gql';
import MODULE_FRAGMENT from '@/graphql/gql/fragments/moduleParts.gql';

View File

@ -11,7 +11,7 @@
</template>
<script>
import BaseInput from '@/components/inputs/BaseInput';
import BaseInput from '@/components/ui/BaseInput';
export default {
props: {

View File

@ -0,0 +1,90 @@
<template>
<label
:for="id"
class="toggle">
<input
:id="id"
:checked="checked"
class="toggle__input"
type="checkbox"
@change.prevent="$emit('input', $event.target.checked)">
<span class="toggle__toggle-wrapper">
<span class="toggle__toggle"/>
</span>
<span class="toggle__label">{{ label }}</span>
</label>
</template>
<script>
export default {
props: {
label: {
type: String,
default: '',
},
checked: {
type: Boolean,
default: false,
},
},
data() {
return {
id: null,
};
},
mounted() {
this.id = this._uid;
},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.toggle {
@include default-box-shadow;
border-radius: $round-border-radius;
border: 1px solid $color-silver;
padding: $small-spacing $medium-spacing;
display: flex;
cursor: pointer;
&__toggle-wrapper {
background-color: $color-silver;
width: 42px;
height: 24px;
border-radius: $round-border-radius;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 6px;
margin-right: $small-spacing;
}
&__input {
display: none;
}
&__input:checked + &__toggle-wrapper {
background-color: $color-brand;
}
&__input:checked + &__toggle-wrapper &__toggle {
margin-left: 14px;
}
&__toggle {
width: 16px;
height: 16px;
border-radius: $round-border-radius;
background-color: $color-white;
margin-left: 0;
transition: margin-left 0.5s;
}
&__label {
@include regular-text;
}
}
</style>

View File

@ -1,4 +1,4 @@
import {InMemoryCache, defaultDataIdFromObject} from 'apollo-cache-inmemory/lib/index';
import {InMemoryCache, defaultDataIdFromObject} from 'apollo-cache-inmemory';
import {createHttpLink} from 'apollo-link-http';
import {onError} from 'apollo-link-error';
import {ApolloClient} from 'apollo-client';

View File

@ -0,0 +1,5 @@
mutation SyncModuleVisibility($input: SyncModuleVisibilityInput!) {
syncModuleVisibility(input: $input) {
success
}
}

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag';
export const typeDefs = gql`
type SidebarInput {
input SidebarInput {
navigation: Boolean
profile: Boolean
}

View File

@ -5,13 +5,13 @@ export default {
return {
me: {
selectedClass: {
id: ''
id: '',
},
permissions: [],
schoolClasses: [],
isTeacher: false
isTeacher: false,
},
showPopover: false
showPopover: false,
};
},
@ -21,8 +21,8 @@ export default {
return {
name: 'topic',
params: {
topicSlug: this.me.lastTopic.slug
}
topicSlug: this.me.lastTopic.slug,
},
};
}
return '/book/topic/berufliche-grundbildung';
@ -33,15 +33,23 @@ export default {
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
},
currentClassName() {
let currentClass = this.me.schoolClasses.find(schoolClass => {
return schoolClass.id === this.me.selectedClass.id;
});
return currentClass
? currentClass.name
: (this.me.schoolClasses.length ? this.me.schoolClasses[0].name : '');
},
},
apollo: {
me: {
query: ME_QUERY,
update(data) {
return this.$getRidOfEdges(data).me;
update({me}) {
return this.$getRidOfEdges(me);
},
fetchPolicy: 'cache-first'
fetchPolicy: 'cache-first',
},
},
};

View File

@ -1,6 +1,6 @@
<template>
<div class="module-page">
<module-navigation/>
<module-navigation v-if="showNavigation" />
<router-view/>
</div>
</template>
@ -11,6 +11,12 @@
export default {
components: {
ModuleNavigation
},
computed: {
showNavigation() {
return !this.$route.meta.hideNavigation;
}
}
};
</script>

View File

@ -0,0 +1,42 @@
<template>
<div class="module-settings">
<h1 class="module-settings__page-title">Einstellungen</h1>
<section class="module-settings__section">
<h2 class="module-settings__heading">Lösungen</h2>
<p class="module-settings__paragraph">Wollen Sie die Lösungen in diesem Modul für Lernende anzeigen?</p>
<checkbox label="Lösungen anzeigen"/>
</section>
<section class="module-settings__section">
<h2 class="module-settings__heading">Sichtbarkeit</h2>
<p class="module-settings__paragraph">Haben Sie die Sichtbarkeit für eine andere Klasse bereits angepasst? Dann
können Sie diese Anpassungen hier
übernehmen</p>
</section>
<section class="module-settings__section">
<router-link
:to="{name: 'visibility'}"
data-cy="select-school-class-button"
class="button button--primary"
>Klasse auswählen</router-link>
</section>
</div>
</template>
<script>
import Checkbox from '@/components/ui/Checkbox';
export default {
components: {
Checkbox
}
};
</script>
<style scoped lang="scss">
@import 'styles/_helpers';
.module-settings {
@include settings-page;
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div class="module-visibility">
<h1 class="module-visibility__page-title">Sichtbarkeit</h1>
<div class="module-visibility__section">
<p class="module-visibility__paragraph">
Wollen Sie die angepasste Sichtbarkeit (
<eye-icon class="module-visibility__inline-icon"/>
) von Inhalten einer anderen Klasse übernehmen?
</p>
</div>
<div class="module-visibility__form module-visibility__section">
Von
<select
:value="selectedClassId"
data-cy="school-class-visibility-dropdown"
class="skillbox-input skillbox-dropdown module-visibility__dropdown"
@change="select($event.target.value)">
<option
value=""
selected>-
</option>
<option
:key="schoolClass.id"
:value="schoolClass.id"
v-for="schoolClass in schoolClasses">{{ schoolClass.name }}
</option>
</select>
für {{ currentClassName }} übernehmen.
</div>
<div class="module-visibility__section">
<a
class="button button--primary"
data-cy="save-visibility-button"
@click="sync">Anpassungen übernehmen</a>
</div>
</div>
</template>
<script>
import EyeIcon from '@/components/icons/EyeIcon';
import me from '@/mixins/me';
import SYNC_VISIBILITY_MUTATION from '@/graphql/gql/mutations/syncModuleVisibility.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery';
import {MODULE_PAGE} from '@/router/module.names';
export default {
mixins: [me],
components: {
EyeIcon,
},
data() {
return {
selectedClassId: '',
};
},
computed: {
schoolClasses() {
return this.me.schoolClasses.filter(schoolClass => schoolClass.id !== this.me.selectedClass.id);
},
slug() {
return this.$route.params.slug;
}
},
methods: {
select(selectedClassId) {
this.selectedClassId = selectedClassId;
},
sync() {
if (this.selectedClassId) {
const slug = this.slug;
this.$apollo.mutate({
mutation: SYNC_VISIBILITY_MUTATION,
variables: {
input: {
module: slug,
templateSchoolClass: this.selectedClassId,
schoolClass: this.me.selectedClass.id,
},
},
refetchQueries: [
{
query: MODULE_DETAILS_QUERY,
variables: {
slug,
},
},
],
},
).then(() => {
this.$router.push({
name: MODULE_PAGE,
params: {
slug
}
});
});
}
},
},
};
</script>
<style scoped lang="scss">
@import '~styles/_helpers';
.module-visibility {
@include settings-page;
margin: 0 auto;
&__inline-icon {
width: 25px;
height: 25px;
vertical-align: middle;
}
&__dropdown {
width: 200px;
margin: 0 $medium-spacing;
}
&__form {
display: flex;
align-items: center;
@include regular-text;
font-weight: 600;
}
}
</style>

View File

@ -266,7 +266,7 @@
import {register} from '../hep-client/index';
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
import Checkbox from '@/components/Checkbox';
import Checkbox from '@/components/ui/Checkbox';
import LoadingButton from '@/components/LoadingButton';
import pageTitleMixin from '@/mixins/page-title';

View File

@ -1,8 +1,5 @@
import Vue from 'vue';
// import index from '@/pages/index'
import topic from '@/pages/topic';
import moduleBase from '@/pages/module-base';
import module from '@/pages/module';
import rooms from '@/pages/rooms';
import room from '@/pages/room';
import newRoom from '@/pages/newRoom';
@ -10,7 +7,6 @@ import editRoom from '@/pages/editRoom';
import article from '@/pages/article';
import instrument from '@/pages/instrument';
import instrumentOverview from '@/pages/instrumentOverview';
import submissions from '@/pages/submissions';
import p404 from '@/pages/p404';
import start from '@/pages/start';
import submission from '@/pages/studentSubmission';
@ -21,7 +17,7 @@ import activity from '@/pages/activity';
import Router from 'vue-router';
import surveyPage from '@/pages/survey';
import styleGuidePage from '@/pages/styleguide';
import moduleRoom from '@/pages/moduleRoom';
import moduleRoom from '@/pages/module/moduleRoom';
import login from '@/pages/login';
import betaLogin from '@/pages/beta-login';
import hello from '@/pages/hello';
@ -40,6 +36,8 @@ import onboardingStart from '@/pages/onboarding/start';
import onboardingStep1 from '@/pages/onboarding/step1';
import onboardingStep2 from '@/pages/onboarding/step2';
import onboardingStep3 from '@/pages/onboarding/step3';
import moduleRoutes from './module.routes';
import portfolioRoutes from './portfolio.routes';
import store from '@/store/index';
@ -52,7 +50,7 @@ const routes = [
{
path: '/',
name: 'home',
component: start
component: start,
},
{
path: '/login',
@ -60,8 +58,8 @@ const routes = [
component: login,
meta: {
layout: 'public',
public: true
}
public: true,
},
},
{
path: '/hello',
@ -69,8 +67,8 @@ const routes = [
component: hello,
meta: {
layout: 'public',
public: true
}
public: true,
},
},
{
path: '/beta-login',
@ -78,27 +76,10 @@ const routes = [
component: betaLogin,
meta: {
layout: 'public',
public: true
}
},
{
path: '/module/:slug',
component: moduleBase,
children: [
{
path: '',
name: 'module',
component: module,
meta: {filter: true}
},
{
path: 'submissions/:id',
name: 'submissions',
component: submissions,
meta: {filter: true}
}
]
public: true,
},
},
...moduleRoutes,
{path: '/rooms', name: 'rooms', component: rooms, meta: {filter: true}},
{path: '/new-room/', name: 'new-room', component: newRoom},
{path: '/edit-room/:id', name: 'edit-room', component: editRoom, props: true},
@ -108,13 +89,13 @@ const routes = [
name: 'moduleRoom',
component: moduleRoom,
props: true,
meta: {layout: 'fullScreen'}
meta: {layout: 'fullScreen'},
},
{path: '/article/:slug', name: 'article', component: article, meta: {layout: 'simple'}},
{
path: '/instruments/',
name: 'instrument-overview',
component: instrumentOverview
component: instrumentOverview,
},
{path: '/instrument/:slug', name: 'instrument', component: instrument, meta: {layout: 'simple'}},
{path: '/submission/:id', name: 'submission', component: submission, meta: {layout: 'simple'}},
@ -132,11 +113,11 @@ const routes = [
path: 'old-classes',
name: 'old-classes',
component: oldClasses,
meta: {isProfile: true}
meta: {isProfile: true},
},
{path: 'create-class', name: 'create-class', component: createClass, meta: {layout: 'simple'}},
{path: 'show-code', name: 'show-code', component: showCode, meta: {layout: 'simple'}},
]
],
},
{path: 'join-class', name: 'join-class', component: joinClass, meta: {layout: 'public'}},
{
@ -144,7 +125,7 @@ const routes = [
component: surveyPage,
name: 'survey',
props: true,
meta: {layout: 'simple'}
meta: {layout: 'simple'},
},
{
path: '/register',
@ -153,7 +134,7 @@ const routes = [
meta: {
public: true,
layout: 'public',
}
},
},
{
path: '/check-email',
@ -161,8 +142,8 @@ const routes = [
name: 'checkEmail',
meta: {
public: true,
layout: 'public'
}
layout: 'public',
},
},
{
path: '/verify-email',
@ -170,16 +151,16 @@ const routes = [
name: 'emailVerification',
meta: {
public: true,
layout: 'public'
}
layout: 'public',
},
},
{
path: '/license-activation',
component: licenseActivation,
name: 'licenseActivation',
meta: {
layout: 'public'
}
layout: 'public',
},
},
{
path: '/forgot-password',
@ -187,13 +168,13 @@ const routes = [
name: 'forgotPassword',
meta: {
layout: 'public',
public: true
}
public: true,
},
},
{
path: '/news',
component: news,
name: 'news'
name: 'news',
},
{
path: '/onboarding',
@ -205,7 +186,7 @@ const routes = [
name: 'onboarding-start',
meta: {
layout: 'blank',
next: ONBOARDING_STEP_1
next: ONBOARDING_STEP_1,
},
},
{
@ -215,7 +196,7 @@ const routes = [
meta: {
layout: 'blank',
next: ONBOARDING_STEP_2,
illustration: 'contents'
illustration: 'contents',
},
},
{
@ -225,7 +206,7 @@ const routes = [
meta: {
layout: 'blank',
next: ONBOARDING_STEP_3,
illustration: 'rooms'
illustration: 'rooms',
},
},
{
@ -235,19 +216,19 @@ const routes = [
meta: {
layout: 'blank',
next: 'home',
illustration: 'portfolio'
illustration: 'portfolio',
},
},
]
],
},
{path: '/styleguide', component: styleGuidePage},
{
path: '*',
component: p404,
meta: {
layout: 'blank'
}
}
layout: 'blank',
},
},
];
Vue.use(Router);
@ -260,7 +241,7 @@ const router = new Router({
return savedPosition;
}
return {x: 0, y: 0};
}
},
});
router.afterEach((to, from) => {

View File

@ -0,0 +1,4 @@
export const SUBMISSIONS_PAGE = 'submissions';
export const MODULE_PAGE = 'module';
export const MODULE_SETTINGS_PAGE = 'module-settings';
export const VISIBILITY_PAGE = 'visibility';

View File

@ -0,0 +1,41 @@
import moduleBase from '@/pages/module/module-base';
import module from '@/pages/module/module';
import submissions from '@/pages/submissions';
import moduleVisibility from '@/pages/module/moduleVisibility';
import {MODULE_PAGE, MODULE_SETTINGS_PAGE, SUBMISSIONS_PAGE, VISIBILITY_PAGE} from '@/router/module.names';
import settingsPage from '@/pages/module/moduleSettings';
export default [
{
path: '/module/:slug',
component: moduleBase,
children: [
{
path: '',
name: MODULE_PAGE,
component: module,
meta: {filter: true},
},
{
path: 'submissions/:id',
name: SUBMISSIONS_PAGE,
component: submissions,
meta: {filter: true},
},
{
path: 'settings',
name: MODULE_SETTINGS_PAGE,
component: settingsPage,
},
{
path: 'visibility',
name: VISIBILITY_PAGE,
component: moduleVisibility,
meta: {
layout: 'simple',
hideNavigation: true,
},
},
],
},
];

View File

@ -204,9 +204,6 @@ export default new Vuex.Store({
setScrollPosition(state, payload) {
state.scrollPosition = payload;
},
setNewContentBlock(state, payload) {
state.newContentBlock = payload;
},
setContentBlockPosition(state, payload) {
state.contentBlockPosition = payload;
},

View File

@ -211,3 +211,32 @@
line-height: $default-heading-line-height;
margin-top: -0.2rem; // to offset the 1.2 line height, it leaves a padding on top
}
@mixin settings-page {
padding: $section-spacing;
@include desktop {
width: 800px;
display: flex;
flex-direction: column;
justify-self: center;
}
&__page-title {
@include main-title;
margin-bottom: $section-spacing;
}
&__heading {
@include heading-1;
margin-bottom: $medium-spacing;
}
&__paragraph {
@include lead-paragraph;
}
&__section {
margin-bottom: $section-spacing;
}
}

View File

@ -55,6 +55,7 @@ $list-height: 52px;
$default-border-radius: 13px;
$input-border-radius: 3px;
$round-border-radius: 32px;
//modal stuff
$modal-lateral-padding: 34px;

1302
schema.graphql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,6 @@ from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery, ModuleRoomsQuery
from users.schema import AllUsersQuery, UsersQuery
from users.mutations import ProfileMutations
from registration.mutations_public import RegistrationMutations
class CustomQuery(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,
@ -36,7 +35,7 @@ class CustomQuery(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, Objec
class CustomMutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations,
ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, SpellCheckMutations,
ProfileMutations, SurveyMutations, NoteMutations, SpellCheckMutations,
CouponMutations, graphene.ObjectType):
if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='_debug')

View File

@ -10,8 +10,6 @@ from books.blocks import DEFAULT_RICH_TEXT_FEATURES
from core.wagtail_utils import StrictHierarchyPage
from users.models import SchoolClass
logger = logging.getLogger(__name__)
class Module(StrictHierarchyPage):
class Meta:
@ -60,6 +58,34 @@ class Module(StrictHierarchyPage):
def get_child_ids(self):
return self.get_children().values_list('id', flat=True)
def sync_from_school_class(self, school_class_template, school_class_to_sync):
# import here so we don't get a circular import error
from books.models import Chapter, ContentBlock
# get chapters of module
chapters = Chapter.get_by_parent(self)
content_block_query = ContentBlock.objects.none()
# get content blocks of chapters
for chapter in chapters:
content_block_query = content_block_query.union(ContentBlock.get_by_parent(chapter))
# clear all `hidden for` and `visible for` for class `school_class_to_sync`
for content_block in school_class_to_sync.hidden_content_blocks.intersection(content_block_query):
content_block.hidden_for.remove(school_class_to_sync)
for content_block in school_class_to_sync.visible_content_blocks.intersection(content_block_query):
content_block.visible_for.remove(school_class_to_sync)
# get all content blocks with `hidden for` for class `school_class_pattern`
for content_block in school_class_template.hidden_content_blocks.intersection(content_block_query):
# add `school_class_to_sync` to these blocks' `hidden for`
content_block.hidden_for.add(school_class_to_sync)
# get all content blocks with `visible for` for class `school_class_pattern`
for content_block in school_class_template.visible_content_blocks.intersection(content_block_query):
# add `school_class_to_sync` to these blocks' `visible for`
content_block.visible_for.add(school_class_to_sync)
class RecentModule(models.Model):
module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='recent_modules')

View File

@ -1,6 +1,7 @@
from books.schema.mutations.chapter import UpdateChapterVisibility
from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock
from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, UpdateLastTopic
from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility
from books.schema.mutations.topic import UpdateLastTopic
class BookMutations(object):
@ -11,3 +12,4 @@ class BookMutations(object):
update_last_module = UpdateLastModule.Field()
update_last_topic = UpdateLastTopic.Field()
update_chapter_visibility = UpdateChapterVisibility.Field()
sync_module_visibility = SyncModuleVisibility.Field()

View File

@ -3,9 +3,10 @@ from datetime import datetime
import graphene
from graphene import relay
from api.utils import get_errors, get_object
from books.models import Module, Topic, RecentModule
from books.schema.queries import ModuleNode, TopicNode
from api.utils import get_object
from books.models import Module, RecentModule
from books.schema.queries import ModuleNode
from users.models import SchoolClass
class UpdateSolutionVisibility(relay.ClientIDMutation):
@ -75,23 +76,30 @@ class UpdateLastModule(relay.ClientIDMutation):
return cls(last_module=last_module.module)
class UpdateLastTopic(relay.ClientIDMutation):
class SyncModuleVisibility(relay.ClientIDMutation):
class Input:
# todo: use slug here too
id = graphene.ID()
module = graphene.String(required=True)
template_school_class = graphene.ID(required=True)
school_class = graphene.ID(required=True)
topic = graphene.Field(TopicNode)
success = graphene.Boolean()
@classmethod
def mutate_and_get_payload(cls, root, info, **args):
user = info.context.user
id = args.get('id')
if not user.is_teacher():
raise Exception('Permission denied')
topic = get_object(Topic, id)
if not topic:
raise Topic.DoesNotExist
module_slug = args.get('module')
template_id = args.get('template_school_class')
school_class_id = args.get('school_class')
user.last_topic = topic
user.save()
module = Module.objects.get(slug=module_slug)
template = get_object(SchoolClass, template_id)
school_class = get_object(SchoolClass, school_class_id)
if not template.is_user_in_schoolclass(user) or not school_class.is_user_in_schoolclass(user):
raise Exception('Permission denied')
return cls(topic=topic)
module.sync_from_school_class(template, school_class)
return cls(success=True)

View File

@ -0,0 +1,28 @@
import graphene
from graphene import relay
from api.utils import get_object
from books.models import Topic
from books.schema.queries import TopicNode
class UpdateLastTopic(relay.ClientIDMutation):
class Input:
# todo: use slug here too
id = graphene.ID()
topic = graphene.Field(TopicNode)
@classmethod
def mutate_and_get_payload(cls, root, info, **args):
user = info.context.user
id = args.get('id')
topic = get_object(Topic, id)
if not topic:
raise Topic.DoesNotExist
user.last_topic = topic
user.save()
return cls(topic=topic)

View File

@ -199,7 +199,7 @@ class TopicNode(DjangoObjectType):
class Meta:
model = Topic
only_fields = [
'slug', 'title', 'meta_title', 'teaser', 'description', 'vimeo_id', 'order', 'instructions'
'slug', 'title', 'teaser', 'description', 'vimeo_id', 'order', 'instructions'
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],

View File

@ -0,0 +1,193 @@
import logging
from django.test import TestCase, RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from books.models import ContentBlock, Chapter
from books.factories import ModuleFactory
from users.factories import SchoolClassFactory
from users.models import User
from users.services import create_users
CONTENT_BLOCK_QUERY = """
query ContentBlockQuery($id: ID!) {
contentBlock(id: $id) {
hiddenFor {
edges {
node {
id
name
}
}
}
visibleFor {
edges {
node {
id
name
}
}
}
}
}
"""
SYNC_MUTATION = """
mutation SyncMutationVisibility($input: SyncModuleVisibilityInput!) {
syncModuleVisibility(input: $input) {
success
}
}
"""
class CopyVisibilityForClassesTestCase(TestCase):
"""
what do we want to happen?
we have 3 public content blocks [X, Y, Z]
we have 2 custom content block [M, N]
we have 2 school classes [A, B]
one public content block is hidden for class A | [X, Y]
one custom content block is visible for class A | [M]
class B also sees two of three public content blocks, but one is different from what A sees | [X, Z]
class B doesn't see the custom content block, but another one | [N]
so A sees | [X, Y, M]
B sees | [X, Z, N]
we want to copy the settings from class A to class B
now class B sees the same content blocks as class A | [X, Y, N]
"""
def setUp(self):
module = ModuleFactory(slug='some-module')
chapter = Chapter(title='Some Chapter')
module.add_child(instance=chapter)
create_users()
teacher = User.objects.get(username='teacher')
student1 = User.objects.get(username='student1')
student2 = User.objects.get(username='student2')
# school class to be used as the pattern or model
template_school_class = SchoolClassFactory(name='template-class', users=[teacher, student1])
# school class to be synced, e.g. adapted to be like the other
school_class_to_be_synced = SchoolClassFactory(name='class-to-be-synced', users=[teacher, student2])
default_content_block = ContentBlock(title='default block', slug='default')
hidden_content_block = ContentBlock(title='hidden block', slug='hidden')
other_hidden_content_block = ContentBlock(title='other hidden block', slug='other-hidden')
custom_content_block = ContentBlock(title='custom block', slug='custom', owner=teacher)
other_custom_content_block = ContentBlock(title='other custom block', slug='other-custom', owner=teacher)
chapter.specific.add_child(instance=default_content_block)
chapter.specific.add_child(instance=hidden_content_block)
chapter.specific.add_child(instance=custom_content_block)
chapter.specific.add_child(instance=other_custom_content_block)
chapter.specific.add_child(instance=other_hidden_content_block)
hidden_content_block.hidden_for.add(template_school_class)
custom_content_block.visible_for.add(template_school_class)
other_hidden_content_block.hidden_for.add(school_class_to_be_synced)
other_custom_content_block.visible_for.add(school_class_to_be_synced)
teacher_request = RequestFactory().get('/')
teacher_request.user = teacher
self.teacher_client = Client(schema=schema, context_value=teacher_request)
student1_request = RequestFactory().get('/')
student1_request.user = student1
self.student1_client = Client(schema=schema, context_value=student1_request)
student2_request = RequestFactory().get('/')
student2_request.user = student2
self.student2_client = Client(schema=schema, context_value=student2_request)
self.template_school_class = template_school_class
self.school_class_to_be_synced = school_class_to_be_synced
self.module = module
self.chapter = to_global_id('ChapterNode', chapter.pk)
self.default_content_block = to_global_id('ContentBlockNode', default_content_block.pk)
self.hidden_content_block = to_global_id('ContentBlockNode', hidden_content_block.pk)
self.custom_content_block = to_global_id('ContentBlockNode', custom_content_block.pk)
self.other_custom_content_block = to_global_id('ContentBlockNode', other_custom_content_block.pk)
self.other_hidden_content_block = to_global_id('ContentBlockNode', other_hidden_content_block.pk)
def _get_result(self, query, client, id):
result = client.execute(query, variables={
'id': id
})
return result
def _test_in_sync(self):
# the hidden block is hidden for both now
hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block)
hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
self.assertTrue('template-class' in map(lambda x: x['node']['name'], hidden_for))
self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for))
# the other hidden block is hidden for no one now
other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
self.other_hidden_content_block)
hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
self.assertEqual(len(hidden_for), 0)
# the default block is still hidden for no one
default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block)
hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
self.assertEqual(len(hidden_for), 0)
# the custom block is visible for both
custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block)
visible_for = custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
self.assertTrue('template-class' in map(lambda x: x['node']['name'], visible_for))
self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for))
# the other custom block is visible for no one
other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
self.other_custom_content_block)
visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
self.assertEqual(len(visible_for), 0)
def test_hidden_for_and_visible_for_set_correctly(self):
self.assertEqual(ContentBlock.objects.count(), 5)
hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block)
hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
self.assertTrue('template-class' in map(lambda x: x['node']['name'], hidden_for))
self.assertFalse('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for))
other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
self.other_hidden_content_block)
hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
self.assertFalse('template-class' in map(lambda x: x['node']['name'], hidden_for))
self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for))
default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block)
hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor').get('edges')
self.assertEqual(len(hidden_for), 0)
custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block)
visible_for = custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
self.assertTrue('template-class' in map(lambda x: x['node']['name'], visible_for))
self.assertFalse('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for))
other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
self.other_custom_content_block)
visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor').get('edges')
self.assertFalse('template-class' in map(lambda x: x['node']['name'], visible_for))
self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for))
def test_syncs_correctly(self):
self.module.sync_from_school_class(self.template_school_class, self.school_class_to_be_synced)
self._test_in_sync()
def test_mutation(self):
self.teacher_client.execute(SYNC_MUTATION, variables={
'input': {
'module': self.module.slug,
'templateSchoolClass': to_global_id('SchoolClassNode', self.template_school_class.pk),
'schoolClass': to_global_id('SchoolClassNode', self.school_class_to_be_synced.pk)
}
})
self._test_in_sync()

View File

@ -335,6 +335,11 @@ LOGGING = {
'level': 'WARNING',
'propagate': True,
},
'wagtail': {
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
},
}
}
@ -348,25 +353,6 @@ if not DEBUG and os.environ.get('SENTRY_DSN'):
send_default_pii=True
)
# LOGGING['handlers'] = {
# 'sentry': {
# 'level': 'ERROR', # ERROR, WARNING, INFO
# 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
# 'tags': {'custom-tag': 'x'},
# },
# 'console': {
# 'level': 'INFO',
# 'class': 'logging.StreamHandler',
# 'stream': sys.stdout,
# 'formatter': 'simple_format'
# }
# }
#
# for k, v in LOGGING['loggers'].items():
# LOGGING['loggers'][k]['handlers'] = ['sentry', 'console']
# else:
# RAVEN_CONFIG = {}
RAVEN_DSN_JS = os.environ.get('RAVEN_DSN_JS', '')
GOOGLE_TAG_MANAGER_CONTAINER_ID = os.environ.get('GOOGLE_TAG_MANAGER_CONTAINER_ID')

View File

@ -1,25 +0,0 @@
{
"schema": {
"request": {
"url": "http://localhost:8000/graphql",
"method": "POST",
"postIntrospectionQuery": true,
"options": {
"headers": {
"user-agent": "JS GraphQL"
}
}
}
},
"endpoints": [
{
"name": "Default (http://localhost:8000/graphql",
"url": "http://localhost:8000/graphql",
"options": {
"headers": {
"user-agent": "JS GraphQL"
}
}
}
]
}

View File

@ -13,7 +13,7 @@ from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from api.schema_public import schema
from core.hep_client import HepClient
from core.tests.mock_hep_data_factory import ME_DATA, VALID_TEACHERS_ORDERS
from users.models import License