resolve conflicts

This commit is contained in:
Christian Cueni 2019-04-10 09:37:42 +02:00
commit aca36b8a66
55 changed files with 1144 additions and 265 deletions

2
.gitignore vendored
View File

@ -40,3 +40,5 @@ server/media/
# pyenv
.python-version
.coverage

View File

@ -36,3 +36,4 @@ bleach = "*"
newrelic = "*"
sentry-sdk = "==0.7.2"
"django-sendgrid-v5" = "*"
coverage = "*"

92
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "c0e186a3fb465dc3cb6985204c534ea52652af00103c61601d0f859bacdee533"
"sha256": "97ff5ca56ac835d40353e34e32ec8333ccb23822bcf971644b7641429d9774e1"
},
"pipfile-spec": 6,
"requires": {
@ -41,19 +41,18 @@
},
"boto3": {
"hashes": [
"sha256:39e9b6516a72864f5a6b69f38edac087cc4ba623095f4528b916620e63de32b3",
"sha256:a3eb22bb975a200a69084ec2bb69819e483ad531bf05e7b73861fc1b333aad42"
"sha256:3927beac97e5467f869d63d60920b83c2d39964f69fbf944bc1db724116bfe1a",
"sha256:be88cae6f16bb9fe3b850b6c8259b297f60b46855175cadae57594c9a403c582"
],
"index": "pypi",
"version": "==1.9.120"
"version": "==1.9.124"
},
"botocore": {
"hashes": [
"sha256:2bf8768887bfb008406eec725eecf6f174307dac00f5fad244cdd5d39c6c2147",
"sha256:c0f9c57e7a8c65f17a62a0926c3e73686f6ae1c08abf3b5a63cc3a5bcc4c437b"
"sha256:bb756a8da2c6e3ccf42dccb0ac71c1df2e07844db339183da06f4e0285b251d0",
"sha256:fc7560a2676df2f0bab4ef0638277b86e5a00944c2ce1c3bb124b3066e6d3d2a"
],
"markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==1.12.120"
"version": "==1.12.124"
},
"certifi": {
"hashes": [
@ -74,9 +73,45 @@
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*'",
"version": "==7.0"
},
"coverage": {
"hashes": [
"sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9",
"sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74",
"sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390",
"sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8",
"sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe",
"sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf",
"sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e",
"sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741",
"sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09",
"sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd",
"sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034",
"sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420",
"sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c",
"sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab",
"sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba",
"sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e",
"sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609",
"sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2",
"sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49",
"sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b",
"sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d",
"sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce",
"sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9",
"sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4",
"sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773",
"sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723",
"sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c",
"sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f",
"sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1",
"sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260",
"sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a"
],
"index": "pypi",
"version": "==4.5.3"
},
"dj-database-url": {
"hashes": [
"sha256:7f4c78d2a090df8dfaf56d5d3ff7bbee17360436e4879558317e2314424864cd"
@ -209,14 +244,12 @@
"sha256:00b7011757c4907546f17d0e47df098b542ea2b04c966ee0e80a493aae2c13c8",
"sha256:745ac8b9c9526e338696e07b7f2e206e5e317e5744e22fdd7c2894bf19af41f1"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*'",
"version": "==1.0.4"
},
"future": {
"hashes": [
"sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
],
"markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version >= '2.6'",
"version": "==0.17.1"
},
"graphene": {
@ -239,7 +272,6 @@
"sha256:889e869be5574d02af77baf1f30b5db9ca2959f1c9f5be7b2863ead5a3ec6181",
"sha256:9462e22e32c7f03b667373ec0a84d95fba10e8ce2ead08f29fbddc63b671b0c1"
],
"markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==2.1"
},
"graphql-relay": {
@ -261,7 +293,6 @@
"sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3",
"sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"
],
"markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==1.0.1"
},
"idna": {
@ -269,7 +300,6 @@
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*'",
"version": "==2.8"
},
"jmespath": {
@ -298,15 +328,14 @@
"sha256:e8941881063691d50f9cc8b8d6d8fd7bec86a8c461b2a4fc87188a5fc44d6ba4",
"sha256:f4b29b0c70d753c754a58aaad7c31ad3309ca4a26f9aa64e695157251f6832ad"
],
"markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==0.18.0"
},
"newrelic": {
"hashes": [
"sha256:b0f2ef6c817d9b5389cb3ef0f06037abbd1c1ed1a4ae04f293dadeb0f78ea924"
"sha256:1d08248dee0d33116a145de723a2ae86e57a10674145ce4c8af3c316423bd140"
],
"index": "pypi",
"version": "==4.14.0.115"
"version": "==4.16.0.116"
},
"pillow": {
"hashes": [
@ -344,7 +373,6 @@
"sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af",
"sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c"
],
"markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==2.2.1"
},
"psycopg2": {
@ -432,7 +460,6 @@
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version < '4' and python_version >= '2.7' and python_version != '3.3.*'",
"version": "==2.21.0"
},
"rjsmin": {
@ -453,7 +480,6 @@
"sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e",
"sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021"
],
"markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==0.2.0"
},
"sendgrid": {
@ -476,7 +502,6 @@
"sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c",
"sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8"
],
"markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'",
"version": "==3.4.0.3"
},
"six": {
@ -484,7 +509,6 @@
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==1.12.0"
},
"text-unidecode": {
@ -553,21 +577,13 @@
}
},
"develop": {
"appnope": {
"hashes": [
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.0"
},
"awscli": {
"hashes": [
"sha256:9cbc48eff6f8c9ee2072be2caf569dd5f18734d61e020556b792ba509bbb03f0",
"sha256:e0b9afc24c591ecb04d02a34d9e8d3508a4bd25c713d10a358cdad09abbbcf8e"
"sha256:87258e4719978f51dae8c62e15cd0486a778ddcb530645f3bc035239b800f184",
"sha256:fbd9dc00ecd7060f36e5768122c9293672b82748fa224cb13e22e6322532d8db"
],
"index": "pypi",
"version": "==1.16.130"
"version": "==1.16.134"
},
"backcall": {
"hashes": [
@ -578,11 +594,10 @@
},
"botocore": {
"hashes": [
"sha256:2bf8768887bfb008406eec725eecf6f174307dac00f5fad244cdd5d39c6c2147",
"sha256:c0f9c57e7a8c65f17a62a0926c3e73686f6ae1c08abf3b5a63cc3a5bcc4c437b"
"sha256:bb756a8da2c6e3ccf42dccb0ac71c1df2e07844db339183da06f4e0285b251d0",
"sha256:fc7560a2676df2f0bab4ef0638277b86e5a00944c2ce1c3bb124b3066e6d3d2a"
],
"markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==1.12.120"
"version": "==1.12.124"
},
"colorama": {
"hashes": [
@ -633,7 +648,6 @@
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
],
"markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==4.4.0"
},
"docutils": {
@ -766,7 +780,6 @@
"sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e",
"sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021"
],
"markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==0.2.0"
},
"six": {
@ -774,7 +787,6 @@
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'",
"version": "==1.12.0"
},
"traitlets": {

View File

@ -36,7 +36,7 @@ aliases:
- echo "This pipeline rules!"
- *setup-tests
- npm install --prefix client
# - npm run "install:cypress" --prefix client
# - npm run "install:cypress" --prefix client
- psql -U $DATABASE_USER -h $DATABASE_HOST -c "create database $DATABASE_NAME"
- python server/manage.py dummy_data
- python server/manage.py runserver &
@ -55,6 +55,7 @@ pipelines:
branches:
master:
- step: *unittest-python
- step: *cypress-test
develop:
- step: *unittest-python
@ -67,4 +68,5 @@ pipelines:
custom:
prod:
- step: *unittest-python
- step: *cypress-test
- step: *deploy-prod

View File

@ -9,7 +9,7 @@ describe('The Login Page', () => {
cy.get('#id_password').type(`${password}{enter}`);
cy.getCookie('sessionid').should('exist');
cy.get('.start-page__title').should('contain', 'skillbox')
cy.get('.start-page__header').should('exist')
});
// it('logs in programmatically without using the UI', () => {
// cy.visit('/accounts/login/'); // have to get a csrf token by getting the base page first

View File

@ -1,5 +1,6 @@
describe('New project', () => {
it('creates a new project and displays it', () => {
cy.viewport('macbook-15');
cy.login('rahel.cueni', 'test');
cy.visit('/portfolio');

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<title>skillbox</title>
<link href='https://fonts.googleapis.com/css?family=Material+Icons' rel="stylesheet" type="text/css">

View File

@ -3042,6 +3042,12 @@
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=",
"dev": true
},
"supports-color": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
@ -7191,10 +7197,9 @@
}
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=",
"dev": true
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"move-concurrently": {
"version": "1.0.1",

View File

@ -51,6 +51,7 @@
"graphql-tag": "^2.9.2",
"html-webpack-plugin": "^2.30.1",
"lodash": "^4.17.10",
"moment": "^2.24.0",
"node-notifier": "^5.1.2",
"node-sass": "^4.9.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",

View File

@ -1,7 +1,8 @@
<template>
<div :class="{'no-scroll': showModal}">
<div :class="{'no-scroll': showModal || showMobileNavigation}" class="app">
<component :is="showModal" v-if="showModal"></component>
<component :is="layout"></component>
<mobile-navigation v-if="showMobileNavigation"></mobile-navigation>
</div>
</template>
@ -10,6 +11,7 @@
import SimpleLayout from '@/layouts/SimpleLayout';
import BlankLayout from '@/layouts/BlankLayout';
import Modal from '@/components/Modal';
import MobileNavigation from '@/components/MobileNavigation';
import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard';
import EditContentBlockWizard from '@/components/content-block-form/EditContentBlockWizard';
import NewRoomEntryWizard from '@/components/rooms/room-entries/NewRoomEntryWizard';
@ -21,6 +23,8 @@
import FullscreenInfographic from '@/components/FullscreenInfographic';
import FullscreenVideo from '@/components/FullscreenVideo';
import {mapGetters} from 'vuex';
export default {
name: 'App',
@ -29,6 +33,7 @@
SimpleLayout,
BlankLayout,
Modal,
MobileNavigation,
NewContentBlockWizard,
EditContentBlockWizard,
NewRoomEntryWizard,
@ -45,9 +50,7 @@
layout() {
return (this.$route.meta.layout || 'default') + '-layout';
},
showModal() {
return this.$store.state.showModal
}
...mapGetters(['showModal', 'showMobileNavigation'])
},
mounted() {
@ -58,6 +61,18 @@
<style lang="scss">
@import "styles/main.scss";
body {
overflow: hidden;
height: 100vh;
}
.app {
overflow-y: auto;
height: 100vh;
/*for IE10+*/
display: flex;
}
.no-scroll {
overflow-y: hidden;
}

View File

@ -37,15 +37,20 @@
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.add-objective-group-button {
display: grid;
display: none;
grid-template-columns: 45px auto;
align-items: center;
margin-top: -20px;
margin-bottom: 35px;
cursor: pointer;
@include desktop {
display: grid;
}
&__icon {
width: 25px;
height: 25px;

View File

@ -1,5 +1,6 @@
<template>
<component :is="component" v-bind="properties" class="add-widget" @click="$emit('click')" :class="{ 'add-widget--reverse': reverse }">
<component :is="component" v-bind="properties" class="add-widget" @click="$emit('click')"
:class="{ 'add-widget--reverse': reverse }">
<add-icon class="add-widget__add"></add-icon>
</component>
</template>
@ -47,12 +48,16 @@
@import "@/styles/_mixins.scss";
.add-widget {
display: flex;
display: none;
align-items: center;
justify-content: center;
@include widget-shadow;
cursor: pointer;
@include desktop {
display: flex;
}
&__add {
width: 80px;
fill: $color-grey;

View File

@ -69,7 +69,11 @@
width: 46px;
height: 46px;
border-radius: 23px;
display: grid;
display: flex;
justify-content: center;
@supports (display: grid) {
display: grid
}
justify-items: center;
align-items: center;

View File

@ -0,0 +1,126 @@
<template>
<header class="header-bar">
<top-navigation></top-navigation>
<router-link to="/" class="header-bar__logo"><logo></logo></router-link>
<div class="user-header">
<user-widget v-bind="me"></user-widget>
<logout-widget></logout-widget>
</div>
<book-navigation v-if="showSubnavigation">
</book-navigation>
</header>
</template>
<script>
import TopNavigation from '@/components/TopNavigation.vue';
import BookNavigation from '@/components/book-navigation/BookNavigation';
import UserWidget from '@/components/UserWidget.vue';
import LogoutWidget from '@/components/LogoutWidget.vue';
import Logo from '@/components/icons/Logo';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
export default {
components: {
TopNavigation,
UserWidget,
LogoutWidget,
BookNavigation,
Logo
},
computed: {
showSubnavigation() {
return this.$route.meta.subnavigation;
}
},
data() {
return {
me: {}
}
},
apollo: {
me: {
query: ME_QUERY,
},
},
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.header-bar {
display: -ms-grid;
@supports (display: grid) {
display: none;
@include desktop {
display: grid;
}
}
align-items: center;
justify-content: space-around;
background-color: $color-white;
grid-auto-rows: 50px;
width: 100%;
@include desktop {
grid-template-columns: 1fr 1fr 1fr;
}
/*
* For IE10+
*/
-ms-grid-columns: 1fr 1fr 1fr;
-ms-grid-rows: 50px 50px;
/*
* For IE10+
*/
& > :nth-child(1) {
-ms-grid-column: 1;
-ms-grid-row-align: center;
}
/*
* For IE10+
*/
& > :nth-child(3) {
-ms-grid-column: 3;
-ms-grid-row-align: center;
-ms-grid-column-align: end;
justify-self: end;
}
& > :nth-child(4) {
-ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
&__logo {
color: #17A887;
font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex;
justify-self: center;
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
}
.user-header {
display: flex;
}
</style>

View File

@ -32,7 +32,7 @@
&__logout {
font-family: $sans-serif-font-family;
line-height: 16px;
margin: 0 15px 0 20px;
margin: 0 15px 0 $large-spacing;
background: none;
color: inherit;
border: none;

View File

@ -0,0 +1,54 @@
<template>
<div class="mobile-header">
<router-link to="/">
<logo></logo>
</router-link>
<a @click="showMobileNavigation">
<hamburger class="mobile-header__hamburger"></hamburger>
</a>
</div>
</template>
<script>
import Logo from '@/components/icons/Logo';
import Hamburger from '@/components/icons/Hamburger';
export default {
components: {
Logo,
Hamburger
},
methods: {
showMobileNavigation() {
this.$store.dispatch('showMobileNavigation', true);
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.mobile-header {
justify-content: space-between;
align-items: center;
display: flex;
@include desktop {
display: none;
}
padding: 0 $medium-spacing;
&__hamburger {
width: 30px;
height: 30px;
fill: $color-grey;
}
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<div class="mobile-navigation">
<top-navigation class="mobile-navigation__main" :mobile="true"></top-navigation>
<div class="mobile-navigation__close-button" @click="hideMobileNavigation">
<cross class="mobile-navigation__close-icon"></cross>
</div>
<div class="mobile-navigation__subnavigation"></div>
<div class="mobile-navigation__secondary">
<user-widget class="mobile-navigation__user-widget" v-bind="me"></user-widget>
<logout-widget class="mobile-navigation__logout-widget"></logout-widget>
</div>
</div>
</template>
<script>
import Cross from '@/components/icons/Cross';
import UserWidget from '@/components/UserWidget';
import LogoutWidget from '@/components/LogoutWidget';
import TopNavigation from '@/components/TopNavigation';
import {meQuery} from '@/graphql/queries';
export default {
components: {
TopNavigation,
Cross,
UserWidget,
LogoutWidget
},
methods: {
hideMobileNavigation() {
this.$store.dispatch('showMobileNavigation', false);
}
},
apollo: {
me: meQuery
},
data() {
return {
me: {}
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.mobile-navigation {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
background-color: white;
z-index: 20;
display: grid;
grid-template-columns: 1fr 50px;
grid-template-rows: 50px 100px auto 100px;
grid-template-areas: "m m" "m m" "s s";
&--with-subnavigation {
grid-template-areas: "m m" "m m" "sub sub" "s s";
}
height: 100vh;
overflow-y: auto;
@include desktop {
display: none;
}
&__main {
background-color: $color-brand;
padding: $medium-spacing;
grid-area: m;
}
&__main-link {
}
&__close-button {
grid-row: 1;
grid-column: 2;
align-self: center;
justify-self: center;
}
&__close-icon {
width: 30px;
height: 30px;
opacity: 0.5;
fill: $color-white;
}
&__secondary {
grid-area: s;
padding: $medium-spacing;
display: flex;
flex-direction: column;
}
&__user-widget {
margin-bottom: $small-spacing;
}
&__logout-widget {
margin-left: -$large-spacing;
}
}
</style>

View File

@ -59,9 +59,13 @@
border-radius: 12px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
border: 1px solid $color-lightgrey;
display: -ms-grid;
@supports (display: grid) {
display: grid;
}
grid-template-rows: auto 1fr 65px;
grid-template-areas: "header" "body" "footer";
-ms-grid-rows: auto 1fr 65px;
position: relative;
&--hide-header {
@ -81,6 +85,7 @@
width: 95vw;
height: auto;
grid-template-rows: 1fr;
-ms-grid-rows: 1fr;
grid-template-areas: "body";
overflow: hidden;
}
@ -107,7 +112,11 @@
}
&__backdrop {
display: flex;
justify-content: center;
@supports (display: grid) {
display: grid;
}
position: fixed;
top: 0;
left: 0;
@ -119,12 +128,14 @@
&__header {
grid-area: header;
-ms-grid-row: 1;
padding: 10px $modal-lateral-padding;
border-bottom: 1px solid $color-lightgrey;
}
&__body {
grid-area: body;
-ms-grid-row: 2;
padding: 10px $modal-lateral-padding;
overflow: auto;
box-sizing: border-box;
@ -150,6 +161,7 @@
&__footer {
grid-area: footer;
-ms-grid-row: 3;
border-top: 1px solid $color-lightgrey;
padding: 16px $modal-lateral-padding;
}

View File

@ -18,10 +18,21 @@
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
@import "@/styles/_mixins.scss";
.news-teaser {
display: flex;
border-bottom: 1px solid $color-lightgrey;
padding-bottom: $large-spacing;
text-align: center;
@include desktop {
border-bottom: 0;
border-left: 1px solid $color-lightgrey;
padding-bottom: 0;
text-align: left;
}
padding-left: $medium-spacing;
flex-direction: column;
}

View File

@ -1,5 +1,5 @@
<template>
<nav class="top-navigation">
<nav class="top-navigation" :class="{'top-navigation--mobile': mobile}">
<router-link to="/book/topic/geld-und-kauf" active-class="top-navigation__link--active"
:class="{'top-navigation__link--active': isActive('book')}"
class="top-navigation__link">Inhalte
@ -13,6 +13,12 @@
<script>
export default {
props: {
mobile: {
default: false
}
},
methods: {
isActive(linkName) {
return linkName === 'book' && this.$route.path.indexOf('module') > -1;
@ -23,6 +29,7 @@
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.top-navigation {
display: flex;
@ -38,5 +45,22 @@
color: $color-brand;
}
}
$parent: &;
&--mobile {
flex-direction: column;
#{$parent}__link {
color: rgba($color-white, 0.6);
@include heading-4;
line-height: 2em;
padding: 0;
&--active {
color: $color-white;
}
}
}
}
</style>

View File

@ -38,6 +38,7 @@
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.widget-footer {
background-color: $color-grey--lighter;
@ -47,6 +48,11 @@
position: relative;
border-bottom-left-radius: $default-border-radius;
border-bottom-right-radius: $default-border-radius;
visibility: hidden;
@include desktop {
visibility: visible;
}
/*
* For IE10+

View File

@ -66,8 +66,12 @@
@import "@/styles/_functions.scss";
.content-block-element-chooser-widget {
display: -ms-grid;
@supports (display: grid) {
display: grid;
}
grid-template-columns: repeat(6, 1fr);
-ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
grid-column-gap: 0px;
font-family: $sans-serif-font-family;
text-align: center;
@ -78,6 +82,31 @@
position: relative;
margin-bottom: 20px;
/*IE10+*/
& > :nth-child(1) {
-ms-grid-column: 1;
}
& > :nth-child(2) {
-ms-grid-column: 2;
}
& > :nth-child(3) {
-ms-grid-column: 3;
}
& > :nth-child(4) {
-ms-grid-column: 4;
}
& > :nth-child(5) {
-ms-grid-column: 5;
}
& > :nth-child(6) {
-ms-grid-column: 6;
}
&::before {
content: "";
position: absolute;

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M15,50a2.48,2.48,0,0,0,2.49,2.5h65a2.5,2.5,0,0,0,0-5h-65A2.5,2.5,0,0,0,15,50Z"/>
<path d="M15,20.5A2.48,2.48,0,0,0,17.49,23h65a2.5,2.5,0,0,0,0-5h-65A2.5,2.5,0,0,0,15,20.5Z"/>
<path d="M15,79.5A2.48,2.48,0,0,0,17.49,82h65a2.5,2.5,0,0,0,0-5h-65A2.5,2.5,0,0,0,15,79.5Z"/>
</svg>
</template>

View File

@ -0,0 +1,37 @@
<template>
<svg class="logo" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1350 250">
<path
d="M304.4,242.15a60,60,0,0,1-19.59-3.1,64.2,64.2,0,0,1-17.6-9.63l-2.94-2.22,21.17-34,3.58,3.21a21.91,21.91,0,0,0,6,4,15.21,15.21,0,0,0,5.81,1.09c4,0,6.51-1.44,8.08-4.68l1.15-2.19L263.73,85.72H313.8L334.27,143l17.38-57.3h48.8L353,208.39c-4.53,11.34-10.91,19.87-19,25.41h0C326,239.34,316,242.15,304.4,242.15Zm-29.33-17a53.63,53.63,0,0,0,12.38,6.3,51.94,51.94,0,0,0,17,2.66c10,0,18.42-2.33,25.12-6.94h0c6.71-4.62,12.1-11.92,16-21.71L388.67,93.79h-31l-22.74,75-26.79-75H275.94L319,195l-2.88,5.47c-2.87,5.92-8.18,9.11-15.29,9.11a23.28,23.28,0,0,1-8.88-1.69,24.83,24.83,0,0,1-4.58-2.53Z"
style="fill:#36c0a1"/>
<path
d="M458.66,113a12.63,12.63,0,0,0-6.43,1.39,4.55,4.55,0,0,0-2.36,4.18q0,3.22,4.4,5.25a93.59,93.59,0,0,0,14,4.61,178.08,178.08,0,0,1,21.33,7.29,40.28,40.28,0,0,1,14.79,11q6.32,7.39,6.33,19,0,17.8-14,28.19t-37.19,10.4A102.76,102.76,0,0,1,430,200.15a84.64,84.64,0,0,1-25.4-12.33l13.29-27.22a97.33,97.33,0,0,0,21.76,10.72A64.21,64.21,0,0,0,460.16,175a14.94,14.94,0,0,0,7.07-1.39,4.33,4.33,0,0,0,2.57-4q0-3.22-4.18-5.25a84.51,84.51,0,0,0-13.83-4.61A157.5,157.5,0,0,1,431,152.67a40,40,0,0,1-14.58-10.93q-6.22-7.29-6.22-18.86,0-18,13.72-28.51t36-10.5q26.79,0,51.23,14.15l-14.36,27.22Q473,113,458.66,113Z"
style="fill:#36c0a1"/>
<path d="M604.69,202.4l-21.22-40.51-8.79,9.22v31.3h-43.3V43.35h43.3v77.38l32.15-34.94h48.87l-42.66,45,42.87,71.6Z"
style="fill:#36c0a1"/>
<path
d="M712.25,36.49q6.22,6.22,6.22,16.08t-6.22,16.08q-6.22,6.22-16.08,6.22T680,68.64q-6.33-6.21-6.32-16.08T680,36.49q6.32-6.21,16.18-6.22T712.25,36.49Zm-37.51,49.3H718V202.4h-43.3Z"
style="fill:#36c0a1"/>
<path d="M748.47,43.35h43.3V202.4h-43.3Z" style="fill:#36c0a1"/>
<path d="M823.5,43.35h43.3V202.4H823.5Z" style="fill:#36c0a1"/>
<path
d="M1002.06,91.79A50.33,50.33,0,0,1,1021,113q6.75,13.72,6.75,31.73,0,17.8-6.54,31.19a48.35,48.35,0,0,1-18.54,20.69q-12,7.29-27.87,7.29A44,44,0,0,1,956.19,200a40.21,40.21,0,0,1-14.36-11.15v13.5h-43.3V43.35h43.3V99.29a38.85,38.85,0,0,1,13.93-11.15,41.53,41.53,0,0,1,18-3.86Q989.85,84.29,1002.06,91.79Zm-23.8,70.63q5.79-7.18,5.79-18.76t-5.79-18.76a18.82,18.82,0,0,0-15.43-7.18,18.59,18.59,0,0,0-15.22,7.18q-5.79,7.19-5.79,18.76t5.79,18.76a18.58,18.58,0,0,0,15.22,7.18A18.8,18.8,0,0,0,978.27,162.42Z"
style="fill:#36c0a1"/>
<path
d="M1142.8,91.69a54.24,54.24,0,0,1,22.62,20.9q8,13.5,8,31.51,0,17.8-8,31.4a54,54,0,0,1-22.62,21q-14.58,7.4-34.08,7.4t-34.19-7.4a53.86,53.86,0,0,1-22.73-21q-8-13.61-8-31.4,0-18,8-31.51a54.08,54.08,0,0,1,22.73-20.9q14.68-7.4,34.19-7.4T1142.8,91.69Zm-49.52,34.08q-5.79,7.18-5.79,18.76,0,11.79,5.79,18.86a18.92,18.92,0,0,0,15.43,7.07,18.7,18.7,0,0,0,15.22-7.07q5.79-7.07,5.79-18.86,0-11.58-5.79-18.76a18.6,18.6,0,0,0-15.22-7.18A18.81,18.81,0,0,0,1093.28,125.77Z"
style="fill:#36c0a1"/>
<path
d="M1176.45,85.79h49.73L1242.26,116l18-30.23h47.16L1271,142.6l39,59.81h-49.73l-18-33-20.58,33h-47.59l39-59.59Z"
style="fill:#36c0a1"/>
<path
d="M245,105.8A38.35,38.35,0,0,0,229.9,89.74h0a46.56,46.56,0,0,0-46.21,1.09A41.77,41.77,0,0,0,171.45,103a38.76,38.76,0,0,0-11.67-12,42.9,42.9,0,0,0-24.06-6.82,44.09,44.09,0,0,0-21.4,5.16,41.05,41.05,0,0,0-8.13,5.83v-9.4H58V201.83h48.19V144.37c0-5.32,1.23-9.42,3.77-12.55a11.7,11.7,0,0,1,9.27-4.46,9.48,9.48,0,0,1,7.75,3.35c2.09,2.45,3.11,5.75,3.11,10.09v61h48.19V144.37c0-5.26,1.24-9.49,3.69-12.59a11.44,11.44,0,0,1,9.15-4.43,9.48,9.48,0,0,1,7.75,3.35c2.09,2.45,3.11,5.75,3.11,10.09v61h48.19V129.28A51.17,51.17,0,0,0,245,105.8Zm-2.87,88h-32v-53c0-6.25-1.7-11.41-5-15.33a17.51,17.51,0,0,0-14-6.18h0a19.38,19.38,0,0,0-15.37,7.49c-3.61,4.55-5.44,10.48-5.44,17.6v49.39h-32v-53c0-6.25-1.7-11.41-5-15.33a17.53,17.53,0,0,0-14-6.18h0a19.66,19.66,0,0,0-15.44,7.45c-3.69,4.56-5.57,10.49-5.57,17.63v49.39h-32v-100h32v18.16h5.19l2.25-3.54a34.63,34.63,0,0,1,12.63-12,36.13,36.13,0,0,1,17.53-4.17,35,35,0,0,1,19.62,5.49,31.51,31.51,0,0,1,12.17,15.38l.46,1.18h6.52l.45-1a36.75,36.75,0,0,1,50.91-16.55,30,30,0,0,1,11.93,12.74,43.2,43.2,0,0,1,4.33,19.81Z"
style="fill:#36c0a1"/>
</svg>
</template>
<style scoped lang="scss">
.logo {
width: 250px;
height: 48px;
}
</style>

View File

@ -30,11 +30,19 @@
box-sizing: border-box;
background-color: $color-white;
border-radius: $default-border-radius;
display: flex;
flex-direction: column;
@supports (display: grid) {
display: grid;
}
grid-template-rows: 1fr 55px;
&__page {
display: flex;
justify-content: center;
@supports (display: grid) {
display: grid;
}
justify-items: center;
align-items: center;
height: 100%;

View File

@ -6,7 +6,7 @@
<entry-count-widget :entry-count="entriesCount"></entry-count-widget>
<owner-widget :name="owner"></owner-widget>
</router-link>
<widget-footer v-if="isOwner">
<widget-footer v-if="isOwner" class="project-widget__footer">
<template slot-scope="scope">
<li class="popover-links__link"><a @click="$emit('delete', id)">Projekt löschen</a></li>
<li class="popover-links__link"><a @click="$emit('edit', id)">Projekt bearbeiten</a></li>
@ -70,14 +70,35 @@
box-sizing: border-box;
display: -ms-grid;
margin-bottom: $large-spacing;
@supports (display: grid) {
display: grid;
margin-bottom: 0;
}
grid-template-rows: 150px 1fr;
-ms-grid-rows: 150px 48px;
-ms-grid-columns: 1fr;
&__content {
padding: 23px;
cursor: pointer;
-ms-grid-row: 1;
/*
* For IE10+
*/
display: -ms-grid;
-ms-grid-rows: 50px 30px 30px;
& > :nth-child(1) {
-ms-grid-row: 1;
}
& > :nth-child(2) {
-ms-grid-row: 2;
}
& > :nth-child(3) {
-ms-grid-row: 3;
}
}
&__title {
@ -85,6 +106,10 @@
font-weight: 600;
}
&__footer {
-ms-grid-row: 2;
}
@include skillbox-colors;
}
</style>

View File

@ -90,6 +90,7 @@
@supports (display: grid) {
display: grid;
}
height: 260px;
grid-template-rows: 210px 1fr;
/*overflow: hidden;*/
@include widget-shadow;
@ -113,6 +114,21 @@
padding: 22px;
color: $color-darkgrey-1;
cursor: pointer;
/*
* For IE10+
*/
display: -ms-grid;
-ms-grid-rows: 80px 30px 30px;
& > :nth-child(1) {
-ms-grid-row: 1;
}
& > :nth-child(2) {
-ms-grid-row: 2;
}
& > :nth-child(3) {
-ms-grid-row: 3;
}
}
&__title {

View File

@ -24,6 +24,8 @@
position: absolute;
right: 0;
bottom: -110px;
display: flex;
flex-direction: column;
background-color: $color-white;
padding: 20px;
z-index: 10;

View File

@ -6,6 +6,10 @@
<style lang="scss">
.blank-layout {
/*
For IE11
*/
display: flex;
}
</style>

View File

@ -1,64 +1,38 @@
<template>
<div class="container skillbox" :class="specialContainerClass">
<header class="header skillbox__header">
<top-navigation></top-navigation>
<router-link to="/" class="skillbox__header-logo">skillbox</router-link>
<div class="user-header">
<user-widget v-bind="me"></user-widget>
<logout-widget></logout-widget>
</div>
<book-navigation v-if="showSubnavigation">
</book-navigation>
</header>
<header-bar class="header skillbox__header">
</header-bar>
<filter-bar v-if="showFilter"></filter-bar>
<mobile-header class="header skillbox__header skillbox__header--mobile"></mobile-header>
<router-view></router-view>
<filter-bar v-if="showFilter" class="skillbox__filter-bar"></filter-bar>
<router-view class="skillbox__content"></router-view>
<footer class="skillbox__footer">Footer</footer>
</div>
</template>
<script>
import TopNavigation from '@/components/TopNavigation.vue';
import BookNavigation from '@/components/book-navigation/BookNavigation';
import UserWidget from '@/components/UserWidget.vue';
import FilterBar from '@/components/FilterBar.vue';
import LogoutWidget from '@/components/LogoutWidget.vue';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
import FilterBar from '@/components/FilterBar';
import HeaderBar from '@/components/HeaderBar';
import MobileHeader from '@/components/MobileHeader';
export default {
components: {
TopNavigation,
UserWidget,
FilterBar,
LogoutWidget,
BookNavigation
HeaderBar,
MobileHeader
},
computed: {
showFilter() {
return this.$route.meta.filter;
},
showSubnavigation() {
return this.$route.meta.subnavigation;
},
specialContainerClass() {
let cls = this.$store.state.specialContainerClass;
return [cls ? `skillbox--${cls}` : '', {'skillbox--show-filter': this.showFilter}]
}
},
data() {
return {
me: {}
}
},
apollo: {
me: {
query: ME_QUERY,
},
},
}
</script>
@ -69,7 +43,9 @@
.skillbox {
margin: 0 auto;
width: 100%;
@supports (display: grid) {
display: grid;
}
grid-template-rows: auto 1fr;
min-height: 100vh;
grid-auto-rows: 1fr;
@ -79,81 +55,41 @@
&--show-filter {
grid-template-rows: auto auto 1fr;
-ms-grid-rows: 50px 50px 30px 1fr; // 1 extra row for gap
grid-template-areas: "h" "." "c";
}
/*
* For IE10+
*/
&--show-filter &__content {
-ms-grid-row: 4;
-ms-grid-column: 1;
}
&--show-filter &__filter-bar {
-ms-grid-row: 2;
-ms-grid-column: 1;
}
/*
* For IE10+
*/
display: -ms-grid;
-ms-grid-rows: auto 32px 1fr; // 1 extra row for gap
-ms-grid-rows: 50px 30px 1fr; // 1 extra row for gap
-ms-grid-columns: 1fr;
@include skillbox-colors;
&__header {
grid-area: h;
display: -ms-grid;
@supports (display: grid) {
display: grid;
}
align-items: center;
justify-content: space-around;
background-color: $color-white;
grid-auto-rows: 50px;
width: 100%;
@include desktop {
grid-template-columns: 1fr 1fr 1fr;
-ms-grid-row: 1;
}
/*
* For IE10+
*/
-ms-grid-columns: 1fr 1fr 1fr;
-ms-grid-rows: 60px 60px;
/*
* For IE10+
*/
& > :nth-child(1) {
&__content {
-ms-grid-row: 3;
-ms-grid-column: 1;
-ms-grid-row-align: center;
}
/*
* For IE10+
*/
& > :nth-child(3) {
-ms-grid-column: 3;
-ms-grid-row-align: center;
-ms-grid-column-align: end;
justify-self: end;
}
& > :nth-child(4) {
-ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
}
&__header-logo {
color: #17A887;
font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex;
justify-self: center;
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
&__footer {
@ -165,12 +101,12 @@
* For IE10+
*/
& > :nth-child(2) {
-ms-grid-row: 3;
}
& > :nth-child(3) {
-ms-grid-row: 4;
-ms-grid-column: 1;
}
}
.user-header {
display: flex;
}
</style>

View File

@ -13,14 +13,19 @@
.layout {
&--simple {
display: -ms-grid;
@supports (display: grid) {
display: grid;
}
padding: 20px;
width: 100%;
@include desktop {
grid-template-columns: 1fr 640px 1fr;
-ms-grid-columns: 1fr 640px 1fr;
& > :nth-child(2) {
grid-column: 2;
-ms-grid-column: 2;
}
}
@ -31,8 +36,12 @@
justify-self: end;
cursor: pointer;
display:flex;
justify-content:end;
@include desktop {
grid-column: 3;
-ms-grid-column: 3;
}
&__icon {

View File

@ -1,7 +1,7 @@
<template>
<div class="portfolio__page">
<div class="portfolio">
<add-project></add-project>
<add-project class="portfolio__add-project"></add-project>
<project-widget
v-for="project in projects"
@ -107,23 +107,37 @@
@import "@/styles/_variables.scss";
.portfolio {
display: -ms-grid;
display: flex;
flex-direction: column;
@supports (display: grid) {
display: grid;
}
grid-row-gap: 30px;
grid-auto-rows: 200px;
width: 640px;
max-width: 640px;
width: auto;
justify-self: center;
box-sizing: border-box;
padding: $large-spacing $medium-spacing;
&__page {
display: flex;
@supports (display: grid) {
display: grid;
}
align-content: center;
justify-content: center;
padding-top: $large-spacing;
}
/*IE10*/
&__add-project {
margin-bottom: $large-spacing;
@supports (display: grid) {
margin-bottom: 0;
}
}
}
</style>

View File

@ -107,12 +107,19 @@
&__content {
background-color: $color-grey--lighter;
display: flex;
flex-direction: column;
max-width: 840px;
align-content: center;
margin: 0 auto;
@supports (display: grid) {
display: grid;
grid-template-columns: 840px;
width: auto;
}
grid-template-columns: minmax(max-content, 840px);
grid-row-gap: 30px;
justify-content: center;
padding-top: 30px;
padding-bottom: 50px;
padding: $large-spacing $medium-spacing;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="rooms-page">
<room-widget v-for="room in filteredRooms" v-bind="room" :key="room.name"></room-widget>
<add-room v-if="canAddRoom" data-cy="add-room"></add-room>
<add-room class="rooms-page__add-room" v-if="canAddRoom" data-cy="add-room"></add-room>
</div>
</template>
@ -63,7 +63,10 @@
@import "@/styles/_mixins.scss";
.rooms-page {
display: -ms-grid;
display: flex;
flex-wrap: wrap;
align-content: start;
@supports (display: grid) {
display: grid;
}
@ -71,6 +74,7 @@
padding: 50px 15px;
@include desktop {
grid-template-columns: repeat(3, 1fr);
-ms-grid-columns: 1fr 30px 1fr 30px 1fr;
padding: 50px 45px;
}
grid-column-gap: 30px;
@ -81,26 +85,22 @@
justify-self: center;
box-sizing: border-box;
/*
* For IE10+
*/
-ms-grid-columns: 1fr 1fr 1fr;
-ms-grid-rows: 260px;
&__add-room {
visibility: hidden;
/*
* SHAME SHAME SHAME
* this is very hacky, but we have a dynamic amount of elements. better to be safe than sorry
* SHAME SHAME SHAME
*/
@for $i from 1 to 101 {
& > :nth-child(#{$i}) {
@if ($i%3) == 0 {
-ms-grid-column: 3;
} @else {
-ms-grid-column: ($i%3);
@include desktop {
visibility: visible;
}
}
-ms-grid-row: floor(($i - 1)/3)+1;
& > div {
flex: 0 0 30%;
margin-bottom: $large-spacing;
margin-right: 1%;
@supports (display: grid) {
margin-bottom: inherit;
margin-right: inherit;
}
}

View File

@ -1,6 +1,9 @@
<template>
<div class="start-page">
<h1 class="start-page__title h1"><span class="start-page__my">my</span>skillbox</h1>
<header-bar class="start-page__header"></header-bar>
<mobile-header class="start-page__header start-page__header--mobile"></mobile-header>
<div class="start-page__sections start-sections">
<section-block
@ -43,20 +46,22 @@
</div>
</div>
</template>
<script>
import SectionBlock from '@/components/SectionBlock.vue';
import NewsTeaser from '@/components/NewsTeaser.vue';
import HeaderBar from '@/components/HeaderBar';
import ContentsIllustration from '@/components/illustrations/ContentsIllustration';
import PortfolioIllustration from '@/components/illustrations/PortfolioIllustration';
import RoomsIllustration from '@/components/illustrations/RoomsIllustration';
import {meQuery} from '@/graphql/queries';
import MobileHeader from '@/components/MobileHeader';
export default {
components: {
MobileHeader,
HeaderBar,
SectionBlock,
NewsTeaser,
ContentsIllustration,
@ -97,11 +102,21 @@
@import "@/styles/_mixins.scss";
.start-page {
display: flex;
flex-direction: column;
justify-content: space-between;
@supports (display: grid) {
display: grid;
justify-content: stretch;
}
grid-template-rows: auto 1fr auto;
min-height: 100vh;
width: 100vw;
box-sizing: border-box;
padding-top: 2*$large-spacing;
&__header {
}
&__title {
color: $color-brand;
@ -132,6 +147,8 @@
padding-right: 120px;
}
display: -ms-grid;
-ms-grid-column-align: center;
-ms-grid-columns: 1fr 1fr 1fr;
margin-bottom: 90px;
@supports (display: grid) {
display: grid;
@ -143,6 +160,24 @@
grid-row-gap: 15px;
grid-column-gap: 50px;
justify-items: start;
align-items: center;
/*
* For IE10+
*/
& > :nth-child(1) {
-ms-grid-row: 1;
-ms-grid-column: 1;
}
& > :nth-child(2) {
-ms-grid-row: 1;
-ms-grid-column: 2;
}
& > :nth-child(3) {
-ms-grid-row: 1;
-ms-grid-column: 3;
}
}
.news {
@ -150,8 +185,39 @@
color: $color-white;
padding-top: $large-spacing;
padding-bottom: $large-spacing;
display: flex;
justify-content: space-around;
-ms-grid-columns: 1fr 1fr 1fr;
@supports (display: grid) {
display: grid;
grid-template-columns: 1fr;
}
grid-row-gap: $large-spacing;
@include desktop {
grid-template-columns: repeat(5, 1fr);
}
/*
* For IE10+
*/
& > :nth-child(1) {
-ms-grid-row: 1;
-ms-grid-column: 1;
}
& > :nth-child(2) {
-ms-grid-row: 1;
-ms-grid-column: 2;
}
& > :nth-child(3) {
-ms-grid-row: 1;
-ms-grid-column: 3;
}
& > :nth-child(4) {
-ms-grid-row: 1;
-ms-grid-column: 4;
}
@include desktop {
/*padding-left: 120px;*/
@ -166,29 +232,21 @@
padding-bottom: 24px;
text-align: center;
color: inherit;
margin-bottom: 0;
}
&__more {
color: $color-white;
font-family: $sans-serif-font-family;
border-left: 1px solid $color-lightgrey;
line-height: $default-line-height;
padding-left: $medium-spacing;
font-weight: $font-weight-bold;
}
&__teasers {
@supports (display: grid) {
display: grid;
}
text-align: center;
@include desktop {
grid-template-columns: 1fr 1fr 1fr;
}
grid-row-gap: 15px;
grid-column-gap: 50px;
text-align: left;
border-left: 1px solid $color-lightgrey;
}
}
}
</style>

View File

@ -1,5 +1,9 @@
<template>
<div class="submissions-page">
<div>
<a class="button button--primary submissions-page__back" @click="back">Zurück zur Aufgabe</a>
</div>
<assignment-with-submissions v-if="!$apollo.queries.assignment.loading"
:assignment="assignment"></assignment-with-submissions>
</div>
@ -32,6 +36,12 @@
},
},
methods: {
back() {
this.$router.go(-1);
}
},
data() {
return {
assignment: {
@ -46,6 +56,11 @@
@import "@/styles/_mixins.scss";
.submissions-page {
display: grid;
grid-row-gap: $large-spacing;
grid-template-rows: auto 1fr;
@include desktop {
width: 800px;
}

View File

@ -21,6 +21,8 @@ import project from '@/pages/project'
import editProject from '@/pages/editProject'
import newProject from '@/pages/newProject'
import store from '@/store/index';
const routes = [
{path: '/', component: start, meta: {layout: 'blank'}},
{
@ -48,7 +50,12 @@ const routes = [
{path: '/edit-room/:id', name: 'edit-room', component: editRoom, props: true},
{path: '/room/:slug', name: 'room', component: room, props: true},
{path: '/article/:slug', name: 'article', component: article, meta: {layout: 'simple'}},
{path: '/basic-knowledge/', name: 'basic-knowledge-overview', component: basicknowledgeOverview, meta: {subnavigation: true}},
{
path: '/basic-knowledge/',
name: 'basic-knowledge-overview',
component: basicknowledgeOverview,
meta: {subnavigation: true}
},
{path: '/basic-knowledge/:slug', name: 'basic-knowledge', component: basicknowledge, meta: {layout: 'simple'}},
{path: '/submission/:id', name: 'submission', component: submission, meta: {layout: 'simple'}},
{path: '/portfolio', name: 'portfolio', component: portfolio},
@ -70,7 +77,8 @@ const routes = [
];
Vue.use(Router);
export default new Router({
const router = new Router({
routes,
mode: 'history',
scrollBehavior(to, from, savedPosition) {
@ -79,4 +87,8 @@ export default new Router({
}
return {x: 0, y: 0}
}
})
});
router.afterEach((to, from) => {
store.dispatch('showMobileNavigation', false);
});
export default router;

View File

@ -9,6 +9,7 @@ export default new Vuex.Store({
state: {
specialContainerClass: '',
showModal: '',
showMobileNavigation: false,
contentBlockPosition: {},
scrollPosition: 0,
filterForSchoolClass: '',
@ -27,7 +28,14 @@ export default new Vuex.Store({
vimeoId: null
},
getters: {},
getters: {
showModal: state => {
return state.showModal
},
showMobileNavigation: state => {
return state.showMobileNavigation
}
},
actions: {
setSpecialContainerClass({commit}, payload) {
@ -108,6 +116,9 @@ export default new Vuex.Store({
commit('setVimeoId', payload);
dispatch('showModal', 'fullscreen-video');
},
showMobileNavigation({commit}, payload) {
commit('setShowMobileNavigation', payload);
}
},
mutations: {
@ -158,6 +169,9 @@ export default new Vuex.Store({
},
setVimeoId(state, payload) {
state.vimeoId = payload;
},
setShowMobileNavigation(state, payload) {
state.showMobileNavigation = payload;
}
}
})

View File

@ -2,15 +2,24 @@
&__header {
grid-template-rows: 50px minmax(50px, max-content) auto;
-ms-grid-rows: 50px 50px auto;
grid-row-gap: 30px;
display: -ms-grid;
display: -ms-flex;
@supports (display: grid) {
display: grid;
}
}
&__meta {
border-bottom: 1px solid $color-lightgrey;
align-self: end;
padding: 20px 0;
width: 100%;
margin-bottom: $large-spacing;
@supports (display: grid) {
margin-bottom: 0;
}
}
&__title {
@ -24,8 +33,11 @@
}
&__content {
display: -ms-grid;
display: flex;
flex-direction: column;
@supports (display: grid) {
display: grid;
}
grid-row-gap: 40px;
padding-bottom: 40px;

3
server/.coveragerc Normal file
View File

@ -0,0 +1,3 @@
[report]
omit =
*/site-packages/*

23
server/api/test_utils.py Normal file
View File

@ -0,0 +1,23 @@
from django.test import RequestFactory, TestCase
from graphene.test import Client
from api.schema import schema
from users.models import User
from users.services import create_users
def create_client(user):
request = RequestFactory().get('/')
request.user = user
return Client(schema=schema, context_value=request)
class DefaultUserTestCase(TestCase):
def setUp(self):
create_users()
self.teacher = User.objects.get(username='teacher')
self.teacher2 = User.objects.get(username='teacher2')
self.student1 = User.objects.get(username='student1')
self.student2 = User.objects.get(username='student2')
self.student_second_class = User.objects.get(username='student_second_class')

View File

@ -1,25 +1,13 @@
from django.test import RequestFactory, TestCase
from graphene.test import Client
from graphql_relay import to_global_id
from api.utils import get_graphql_mutation
from api.test_utils import create_client, DefaultUserTestCase
from assignments.models import Assignment, StudentSubmission
from books.factories import ModuleFactory
from ..factories import AssignmentFactory
from users.models import User
from users.services import create_users
from api.schema import schema
class AssignmentPermissionsTestCase(TestCase):
class AssignmentPermissionsTestCase(DefaultUserTestCase):
def setUp(self):
create_users()
self.teacher = User.objects.get(username='teacher')
self.teacher2 = User.objects.get(username='teacher2')
self.student1 = User.objects.get(username='student1')
self.student2 = User.objects.get(username='student2')
self.student_second_class = User.objects.get(username='student_second_class')
super(AssignmentPermissionsTestCase, self).setUp()
self.assignment = AssignmentFactory(
owner=self.teacher
)
@ -27,10 +15,6 @@ class AssignmentPermissionsTestCase(TestCase):
self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk)
self.module_id = to_global_id('ModuleNode', self.assignment.module.pk)
def _create_client(self, user):
request = RequestFactory().get('/')
request.user = user
return Client(schema=schema, context_value=request)
def _submit_submission(self, user=None):
mutation = '''
@ -53,9 +37,9 @@ class AssignmentPermissionsTestCase(TestCase):
'''
if user is None:
client = self._create_client(self.student1)
client = create_client(self.student1)
else:
client = self._create_client(user)
client = create_client(user)
return client.execute(mutation, variables={
'input': {
@ -82,7 +66,7 @@ class AssignmentPermissionsTestCase(TestCase):
self.assertEqual(StudentSubmission.objects.count(), 1)
def _test_visibility(self, user, count):
client = self._create_client(user)
client = create_client(user)
query = '''
query AssignmentWithSubmissions($id: ID!) {
assignment(id: $id) {

View File

@ -350,6 +350,6 @@ GRAPHQL_MUTATIONS_DIR = os.path.join(GRAPHQL_QUERIES_DIR, 'mutations')
EMAIL_BACKEND = 'sendgrid_backend.SendgridBackend'
SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"]
SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY")
SENDGRID_SANDBOX_MODE_IN_DEBUG = False
DEFAULT_FROM_EMAIL='noreply@myskillbox.ch'

View File

@ -1,6 +1,7 @@
import graphene
from graphene import relay, InputObjectType
from graphql_relay import from_global_id
from rest_framework.exceptions import PermissionDenied
from api.utils import get_object
from books.models import Module
@ -67,13 +68,18 @@ class AddObjectiveGroup(relay.ClientIDMutation):
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
owner = info.context.user
if not owner.has_perm('users.can_manage_school_class_content'):
raise PermissionDenied('Missing permissions')
objective_group_data = kwargs.get('objective_group')
title = objective_group_data.get('title')
if title != 'society':
title = 'language_communication'
module_id = objective_group_data.get('module')
module = get_object(Module, module_id)
owner = info.context.user
new_objective_group = ObjectiveGroup.objects.create(title=title, module=module, owner=owner)
objectives = objective_group_data.get('objectives')
for objective in objectives:
@ -89,9 +95,15 @@ class UpdateObjectiveGroup(relay.ClientIDMutation):
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user
if not user.has_perm('users.can_manage_school_class_content'):
raise PermissionDenied('Missing permissions')
objective_group_data = kwargs.get('objective_group')
id = objective_group_data.get('id')
objective_group = get_object(ObjectiveGroup, id)
objectives = objective_group_data.get('objectives')
existing_objective_ids = list(objective_group.objectives.values_list('id', flat=True))
for objective in objectives:

View File

@ -26,7 +26,6 @@ class ObjectiveGroupNode(DjangoObjectType):
return self.owner is not None and self.owner.pk == info.context.user.pk
class ObjectiveNode(DjangoObjectType):
pk = graphene.Int()

View File

@ -1 +0,0 @@
# Create your tests here.

View File

View File

@ -4,12 +4,11 @@ from graphql_relay import to_global_id
from api.schema import schema
from portfolio.factories import ProjectFactory
from portfolio.models import Project
from rooms.models import Room
from users.factories import SchoolClassFactory
from users.models import User, SchoolClass
from users.models import User
from users.services import create_users
from api.test_utils import create_client, DefaultUserTestCase
from portfolio.models import Project
class ProjectQuery(TestCase):
def setUp(self):
@ -60,3 +59,30 @@ class ProjectQuery(TestCase):
self.assertEqual(result.get('errors')[0]['message'], 'Permission denied: Incorrect project')
class ProjectMutationsTestCase(DefaultUserTestCase):
def test_add_project(self):
client = create_client(self.student1)
mutation = """
mutation AddProjectMutation($input: AddProjectInput!){
addProject(input: $input){
project {
id
}
errors
}
}
"""
result = client.execute(mutation, variables={
'input': {
"project": {
"title": "Rick Astley",
"description": "She wants to dance with me",
"objectives": "Dance with me",
"appearance": "green"
}
}
})
self.assertIsNone(result.get('errors'))
self.assertEqual(Project.objects.count(), 1)

View File

@ -1,10 +1,10 @@
from django.contrib.auth import get_user_model
from django.db import models
from django_extensions.db.models import TitleDescriptionModel, TitleSlugDescriptionModel
from django_extensions.db.models import TitleSlugDescriptionModel
from wagtail.core.fields import StreamField
from books.blocks import ImageUrlBlock, LinkBlock, VideoBlock
from books.models import ContentBlock, TextBlock
from books.models import TextBlock
from users.models import SchoolClass
@ -37,3 +37,6 @@ class RoomEntry(TitleSlugDescriptionModel):
def __str__(self):
return 'RoomEntry {}-{}-{}'.format(self.id, self.title, self.author)
def can_user_see_entry(self, user):
return user.is_superuser or self.room.school_class.is_user_in_schoolclass(user)

View File

@ -84,14 +84,24 @@ class MutateRoomEntry(relay.ClientIDMutation):
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
room_entry_data = kwargs.get('room_entry')
if room_entry_data.get('room') is not None:
room_entry_data['room'] = get_object(Room, room_entry_data.get('room')).id
room_entry_data['author'] = info.context.user.pk
if room_entry_data.get('id') is not None:
# update path
instance = get_object(RoomEntry, room_entry_data.get('id'))
if not instance.room.school_class.is_user_in_schoolclass(info.context.user):
raise Exception('You are in the wrong class')
if instance.author.pk != info.context.user.pk:
raise Exception('You are not the author')
serializer = RoomEntrySerializer(instance, data=room_entry_data, partial=True)
else:
# add path
room_entry_data['author'] = info.context.user.pk
serializer = RoomEntrySerializer(data=room_entry_data)
if serializer.is_valid():

View File

@ -56,14 +56,29 @@ class RoomsQuery(object):
return Room.objects.filter(school_class__in=user.school_classes.all())
def resolve_room(self, info, **kwargs):
return get_by_id_or_slug(Room, **kwargs)
room = get_by_id_or_slug(Room, **kwargs)
if room.school_class.is_user_in_schoolclass(info.context.user):
return room
else:
return None
def resolve_room_entry(self, info, **kwargs):
slug = kwargs.get('slug')
id = kwargs.get('id')
room_entry = None
if id is not None:
return get_object(RoomEntry, id)
room_entry = get_object(RoomEntry, id)
if slug is not None:
return RoomEntry.objects.get(slug=slug)
room_entry = RoomEntry.objects.get(slug=slug)
if room_entry and room_entry.can_user_see_entry(info.context.user):
return room_entry
else:
return None
def resolve_all_room_entries(self, info, **kwargs):
if not info.context.user.is_superuser:
return RoomEntry.objects.none()
else:
return RoomEntry.objects.all()

View File

@ -7,14 +7,14 @@ from core.factories import UserFactory
from rooms.factories import RoomEntryFactory, RoomFactory
from rooms.models import RoomEntry
from users.factories import SchoolClassFactory
from users.services import create_users
class RoomEntryMutationsTestCase(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche')
self.yet_another_user = UserFactory(username='hansueli')
s = SchoolClassFactory(users=[self.user, self.another_user])
s2 = SchoolClassFactory(users=[self.yet_another_user])
self.room_entry = RoomEntryFactory(author=self.user, room=RoomFactory(school_class=s))
request = RequestFactory().get('/')
@ -64,3 +64,73 @@ class RoomEntryMutationsTestCase(TestCase):
self.assertIsNotNone(result.get('errors'))
self.assertEqual(RoomEntry.objects.count(), 1)
def test_update_room_entry_not_owner_but_same_class(self):
self.assertEqual(RoomEntry.objects.count(), 1)
mutation = '''
mutation UpdateRoomEntry($input: UpdateRoomEntryInput!){
updateRoomEntry(input: $input) {
roomEntry {
title
author {
firstName
}
}
errors
}
}
'''
request = RequestFactory().get('/')
request.user = self.another_user
client = Client(schema=schema, context_value=request)
new_title = 'new title, Alte!'
result = client.execute(mutation, variables={
'input': {
'roomEntry': {
'id': to_global_id('RoomEntryNode', self.room_entry.pk),
'title': new_title
}
}
})
entry = RoomEntry.objects.get(pk=self.room_entry.pk)
self.assertIsNotNone(result.get('errors'))
self.assertEqual(entry.title, self.room_entry.title)
def test_update_room_entry_not_owner_from_other_class(self):
self.assertEqual(RoomEntry.objects.count(), 1)
mutation = '''
mutation UpdateRoomEntry($input: UpdateRoomEntryInput!){
updateRoomEntry(input: $input) {
roomEntry {
title
author {
firstName
}
}
errors
}
}
'''
request = RequestFactory().get('/')
request.user = self.yet_another_user
client = Client(schema=schema, context_value=request)
new_title = 'new title, Alte!'
result = client.execute(mutation, variables={
'input': {
'roomEntry': {
'id': to_global_id('RoomEntryNode', self.room_entry.pk),
'title': new_title
}
}
})
entry = RoomEntry.objects.get(pk=self.room_entry.pk)
self.assertIsNotNone(result.get('errors'))
self.assertEqual(entry.title, self.room_entry.title)

View File

@ -0,0 +1,117 @@
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from core.factories import UserFactory
from rooms.factories import RoomFactory, RoomEntryFactory
from users.factories import SchoolClassFactory
class RoomQueryPermission(TestCase):
@staticmethod
def get_first_contents(result):
return result.get('data').get('rooms').get('edges')[0]
def setUp(self):
self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche')
sc1 = SchoolClassFactory(users=[self.user])
sc2 = SchoolClassFactory(users=[self.another_user])
self.room1 = RoomFactory(school_class=sc1)
self.room2 = RoomFactory(school_class=sc2)
request = RequestFactory().get('/')
request.user = self.user
self.client = Client(schema=schema, context_value=request)
def test_student_should_only_see_rooms_of_class(self):
query = '''
query {
rooms {
edges {
node {
title
}
}
}
}
'''
result = self.client.execute(query)
self.assertIsNone(result.get('errors'))
self.assertEqual(len(result.get('data').get('rooms').get('edges')), 1)
self.assertEqual(result.get('data').get('rooms').get('edges')[0].get('node').get('title'), self.room1.title)
def test_student_should_not_be_able_to_query_rooms_of_other_classes(self):
query = '''
query RoomQuery($slug: String) {
room(slug: $slug) {
title
}
}
'''
result = self.client.execute(query, variables={
'slug': self.room2.slug
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('room'), None)
class RoomEntryQueryPermissions(TestCase):
@staticmethod
def get_first_contents(result):
return result.get('data').get('rooms').get('edges')[0]
def setUp(self):
self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche')
sc1 = SchoolClassFactory(users=[self.user])
sc2 = SchoolClassFactory(users=[self.another_user])
room1 = RoomFactory(school_class=sc1)
room2 = RoomFactory(school_class=sc2)
self.roomEntry1 = RoomEntryFactory(room=room1, author=self.user)
self.roomEntry2 = RoomEntryFactory(room=room2, author=self.another_user)
request = RequestFactory().get('/')
request.user = self.user
self.client = Client(schema=schema, context_value=request)
def test_user_should_see_room_entries_from_own_class(self):
query = '''
query RoomEntryQuery($slug: String) {
roomEntry(slug: $slug) {
title
}
}
'''
result = self.client.execute(query, variables={
'slug': self.roomEntry1.slug
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('roomEntry').get('title'), self.roomEntry1.title)
def test_user_should_not_see_room_entries_from_orther_class(self):
query = '''
query RoomEntryQuery($slug: String) {
roomEntry(slug: $slug) {
title
}
}
'''
result = self.client.execute(query, variables={
'slug': self.roomEntry2.slug
})
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('roomEntry'), None)

View File

@ -51,6 +51,9 @@ class SchoolClass(models.Model):
def __str__(self):
return 'SchoolClass {}-{}-{}'.format(self.id, self.name, self.year)
def is_user_in_schoolclass(self, user):
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0
class Role(models.Model):
key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True)

View File

@ -45,3 +45,9 @@ class UsersQuery(object):
def resolve_me(self, info, **kwargs):
return info.context.user
def resolve_all_users(self, info, **kwargs):
if not info.context.user.is_superuser:
return User.objects.none()
else:
return User.objects.all()