Merged in feature/highlight-proof-of-concept (pull request #38)

Feature/highlight proof of concept

Approved-by: Christian Cueni <christian.cueni@iterativ.ch>
This commit is contained in:
Ramon Wenger 2019-11-06 07:51:06 +00:00 committed by Christian Cueni
commit 9462665826
38 changed files with 984 additions and 180 deletions

View File

@ -35,7 +35,8 @@ django-libsass = "*"
bleach = "*"
newrelic = "*"
sentry-sdk = "==0.7.2"
"django-sendgrid-v5" = "*"
"django-sendgrid-v5" = "==0.8.0"
python-http-client = "==3.2.1"
coverage = "*"
graphql-relay = "*"
ipython = "*"

179
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "85020e50720b4f71b864719a7c4fae8a0cb4e80352d3bc7eb1e4726bf2405f6d"
"sha256": "725dd26226fe58559f67b30a09dec72086dc4681a69f2f2672f5bd87c9ac74b8"
},
"pipfile-spec": 6,
"requires": {
@ -23,6 +23,14 @@
],
"version": "==7.0.0"
},
"appnope": {
"hashes": [
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.0"
},
"backcall": {
"hashes": [
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
@ -48,18 +56,18 @@
},
"boto3": {
"hashes": [
"sha256:778e66711d7de9352b6330692ae44c0ba012559dabb4c39d7416b0135fb335a6",
"sha256:c8371f1f9c52f64cef1f14a773629fc8434ecca6195b2ef2a429a7bbbf8ecf23"
"sha256:08d949fede71c14db8b9b638edaa13831d79daed84e2da27750629fd606bdb57",
"sha256:4d7c2cc266917cd0ff7e5e039158de80991e21696a2e8bf85201de2d06d7ceea"
],
"index": "pypi",
"version": "==1.9.228"
"version": "==1.10.9"
},
"botocore": {
"hashes": [
"sha256:7c391c46cf1f8d6c04758f84b51337a64136077e4a160eced87551fdcc051669",
"sha256:c9148df92ba21a90ea32f2c7f185c31b1c8b8e48417d0ba8cad02b9b3336c09a"
"sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1",
"sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3"
],
"version": "==1.12.228"
"version": "==1.13.9"
},
"certifi": {
"hashes": [
@ -122,10 +130,10 @@
},
"decorator": {
"hashes": [
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
"sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
"sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
],
"version": "==4.4.0"
"version": "==4.4.1"
},
"dj-database-url": {
"hashes": [
@ -242,9 +250,9 @@
},
"draftjs-exporter": {
"hashes": [
"sha256:503f222c81de9a0619158d8f88b638f9069af8de233dc020faa782c7a3b22100"
"sha256:5839cbc29d7bce2fb99837a404ca40c3a07313f2a20e2700de7ad6aa9a9a18fb"
],
"version": "==2.1.6"
"version": "==2.1.7"
},
"factory-boy": {
"hashes": [
@ -256,16 +264,16 @@
},
"faker": {
"hashes": [
"sha256:1d3f700e8dfcefd6e657118d71405d53e86974448aba78884f119bbd84c0cddf",
"sha256:d5366e120191c5610fceeebfe1c298dc46da0277096f639c6dd7e2eaee0fa547"
"sha256:5902379d8df308a204fc11c4f621590ee83975805a6c7b2228203b9defa45250",
"sha256:5e8c755c619f332d5ec28b7586389665f136bcf528e165eb925e87c06a63eda7"
],
"version": "==2.0.1"
"version": "==2.0.3"
},
"future": {
"hashes": [
"sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
],
"version": "==0.17.1"
"version": "==0.18.2"
},
"graphene": {
"hashes": [
@ -322,11 +330,11 @@
},
"ipython": {
"hashes": [
"sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b",
"sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
],
"index": "pypi",
"version": "==7.8.0"
"version": "==7.9.0"
},
"ipython-genutils": {
"hashes": [
@ -351,30 +359,31 @@
},
"libsass": {
"hashes": [
"sha256:2457723fe04f4e690105f758aa125e809afc840812965095fa3f4edccd6275ef",
"sha256:2974772e7984b27a51a6d91ebc140183ddd574a9663bd02154ddfb75f13a3eed",
"sha256:2d067ce4f393fee2ce52bb810a364deac5454dfdb7945d31d1f4265f21f03ab8",
"sha256:57d0b99c4e3512233a44141f1bf852570d359724a606dfc4550eccd0f570460d",
"sha256:5b604e4f5befdecc76240c2ba243fd7e23c642ffc2dd86cbfd094a44ead6b08d",
"sha256:5dd647ffa1319a2a18572f41fee3bb561d7f77d8d4784074a00b2eb22c61a859",
"sha256:78f3f14e47612be4fa4b161278f2a3e880a19b6a3367f749e9ae240434b7e7f5",
"sha256:8d423e4b4c0e219488104b4ec4267688dbd816f3ae806beb4201918eff059b2d",
"sha256:a20473b0427d82e37fa68f0b3a8d219f0bb5ca6d3f7d93b0f5342219285e7064",
"sha256:c1f76c2a0993914f3c3088e9b6c7031f22e879c5d27a060cdc8c5aa1318eb9b6",
"sha256:c99fbc950f1955e8b6370aafdb9d84d324e4984a2e00a2b47f04dbcc3706a9d1",
"sha256:cb50f385117535f7671ac7ff3144c1ef0b8e088778c58d269ce6f31b87bfad72",
"sha256:f0f033a8154be60e1a2e1f79ee849ea69a1d62e5d476a78f69e4c7d8fd7c20e1",
"sha256:f2572b73b2e13e74b28388ae86c4fabb853ddbfc12279b4444243bd614710ce8",
"sha256:f8790db67e00c5bc7be1bdd81ed477563a4b191e839193ecc0c2c5ec679ec481"
"sha256:003a65b4facb4c5dbace53fb0f70f61c5aae056a04b4d112a198c3c9674b31f2",
"sha256:0fd8b4337b3b101c6e6afda9112cc0dc4bacb9133b59d75d65968c7317aa3272",
"sha256:338e9ae066bf1fde874e335324d5355c52d2081d978b4f74fc59536564b35b08",
"sha256:4dcfd561fb100250b89496e1362b96f2cc804f689a59731eb0f94f9a9e144f4a",
"sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f",
"sha256:6a51393d75f6e3c812785b0fa0b7d67c54258c28011921f204643b55f7355ec0",
"sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32",
"sha256:81a013a4c2a614927fd1ef7a386eddabbba695cbb02defe8f31cf495106e974c",
"sha256:845a9573b25c141164972d498855f4ad29367c09e6d76fad12955ad0e1c83013",
"sha256:8b5b6d1a7c4ea1d954e0982b04474cc076286493f6af2d0a13c2e950fbe0be95",
"sha256:9b59afa0d755089c4165516400a39a289b796b5612eeef5736ab7a1ebf96a67c",
"sha256:a7e685466448c9b1bf98243339793978f654a1151eb5c975f09b83c7a226f4c1",
"sha256:c93df526eeef90b1ea4799c1d33b6cd5aea3e9f4633738fb95c1287c13e6b404",
"sha256:e318f06f06847ff49b1f8d086ac9ebce1e63404f7ea329adab92f4f16ba0e00e",
"sha256:fc5f8336750f76f1bfae82f7e9e89ae71438d26fc4597e3ab4c05ca8fcd41d8a",
"sha256:fcb7ab4dc81889e5fc99cafbc2017bc76996f9992fc6b175f7a80edac61d71df"
],
"version": "==0.19.2"
"version": "==0.19.4"
},
"newrelic": {
"hashes": [
"sha256:9d6d3bf2e125410d485fd91279e1b0f50e8f0f5887284b345aed4ec81db33c0a"
"sha256:da9adab674d9fe7aa8fbabb6691b33fb7be94a32b6a80548ec7018be9df8ef65"
],
"index": "pypi",
"version": "==5.0.2.126"
"version": "==5.2.1.129"
},
"parso": {
"hashes": [
@ -438,11 +447,11 @@
},
"prompt-toolkit": {
"hashes": [
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
],
"version": "==2.0.9"
"version": "==2.0.10"
},
"psycopg2": {
"hashes": [
@ -517,14 +526,15 @@
"sha256:c2776054245db376ea26c859b80e9280b1a470b96ed998d60d35951f89bbbe79",
"sha256:e455ae0dfd5819ac483f7fecf08ab8693048d9dc47a0a6fe0d4aebf86d9d1d17"
],
"index": "pypi",
"version": "==3.2.1"
},
"pytz": {
"hashes": [
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.2"
"version": "==2019.3"
},
"raven": {
"hashes": [
@ -611,17 +621,17 @@
},
"text-unidecode": {
"hashes": [
"sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d",
"sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"
"sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
"sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
],
"version": "==1.2"
"version": "==1.3"
},
"traitlets": {
"hashes": [
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
"sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
],
"version": "==4.3.2"
"version": "==4.3.3"
},
"typing": {
"hashes": [
@ -640,11 +650,11 @@
},
"urllib3": {
"hashes": [
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"markers": "python_version >= '3.4'",
"version": "==1.25.3"
"version": "==1.25.6"
},
"wagtail": {
"hashes": [
@ -689,13 +699,21 @@
}
},
"develop": {
"appnope": {
"hashes": [
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.0"
},
"awscli": {
"hashes": [
"sha256:5c36a818130c8393b778e5b98b9bf38985522b62a684782512a2d57340697582",
"sha256:a022dfb7e6cc9ee8540c382b9bdc4637070dbbe2b09f3a74ad4f30719414c0b7"
"sha256:4a75cb44dc3dab14bc9bdb0d2731c37a5026bf0afa4acb620fec72c74e62915a",
"sha256:90b0b3e91a900e4569bc47f29769522337c46ff50e35f9e4a41830fdb425f000"
],
"index": "pypi",
"version": "==1.16.238"
"version": "==1.16.273"
},
"backcall": {
"hashes": [
@ -706,17 +724,18 @@
},
"botocore": {
"hashes": [
"sha256:7c391c46cf1f8d6c04758f84b51337a64136077e4a160eced87551fdcc051669",
"sha256:c9148df92ba21a90ea32f2c7f185c31b1c8b8e48417d0ba8cad02b9b3336c09a"
"sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1",
"sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3"
],
"version": "==1.12.228"
"version": "==1.13.9"
},
"colorama": {
"hashes": [
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"version": "==0.3.9"
"markers": "python_version != '2.6' and python_version != '3.3'",
"version": "==0.4.1"
},
"coverage": {
"hashes": [
@ -758,10 +777,10 @@
},
"decorator": {
"hashes": [
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
"sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
"sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
],
"version": "==4.4.0"
"version": "==4.4.1"
},
"docutils": {
"hashes": [
@ -780,11 +799,11 @@
},
"ipython": {
"hashes": [
"sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b",
"sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
],
"index": "pypi",
"version": "==7.8.0"
"version": "==7.9.0"
},
"ipython-genutils": {
"hashes": [
@ -831,11 +850,11 @@
},
"prompt-toolkit": {
"hashes": [
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
],
"version": "==2.0.9"
"version": "==2.0.10"
},
"ptyprocess": {
"hashes": [
@ -882,7 +901,7 @@
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
],
"markers": "python_version != '2.6'",
"markers": "python_version != '2.6' and python_version != '3.3'",
"version": "==5.1.2"
},
"rsa": {
@ -908,18 +927,18 @@
},
"traitlets": {
"hashes": [
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
"sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
],
"version": "==4.3.2"
"version": "==4.3.3"
},
"urllib3": {
"hashes": [
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"markers": "python_version >= '3.4'",
"version": "==1.25.3"
"version": "==1.25.6"
},
"wcwidth": {
"hashes": [

1
client/.gitignore vendored
View File

@ -4,6 +4,7 @@ dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
cypress/videos
# Editor directories and files
.idea

View File

@ -23,6 +23,8 @@
import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard';
import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard';
import NewObjectiveWizard from '@/components/objective-groups/NewObjectiveWizard';
import NewNoteWizard from '@/components/notes/NewNoteWizard';
import EditNoteWizard from '@/components/notes/EditNoteWizard';
import FullscreenImage from '@/components/FullscreenImage';
import FullscreenInfographic from '@/components/FullscreenInfographic';
import FullscreenVideo from '@/components/FullscreenVideo';
@ -50,6 +52,8 @@
NewProjectEntryWizard,
EditProjectEntryWizard,
NewObjectiveWizard,
NewNoteWizard,
EditNoteWizard,
FullscreenImage,
FullscreenInfographic,
FullscreenVideo

View File

@ -18,11 +18,15 @@
<h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3>
<h4 class="content-block__title" v-if="!contentBlock.indent">{{contentBlock.title}}</h4>
<component v-for="component in contentBlocksWithContentLists.contents"
:key="component.id"
:is="component.type"
v-bind="component">
</component>
<content-component v-for="component in contentBlocksWithContentLists.contents"
:key="component.id"
:component="component"
:root="root"
:parent="contentBlock"
:bookmarks="contentBlock.bookmarks"
:notes="contentBlock.notes"
>
</content-component>
</div>
@ -33,22 +37,6 @@
</template>
<script>
import TextBlock from '@/components/content-blocks/TextBlock';
import InstrumentWidget from '@/components/content-blocks/InstrumentWidget';
import Task from '@/components/content-blocks/Task';
import ImageBlock from '@/components/content-blocks/ImageBlock';
import ImageUrlBlock from '@/components/content-blocks/ImageUrlBlock';
import VideoBlock from '@/components/content-blocks/VideoBlock';
import LinkBlock from '@/components/content-blocks/LinkBlock';
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
import ThinglinkBlock from '@/components/content-blocks/ThinglinkBlock';
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
import ContentListBlock from '@/components/content-blocks/ContentListBlock';
import Assignment from '@/components/content-blocks/assignment/Assignment';
import Survey from '@/components/content-blocks/SurveyBlock';
import Solution from '@/components/content-blocks/Solution';
import AddContentButton from '@/components/AddContentButton';
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import UserWidget from '@/components/UserWidget';
@ -56,7 +44,7 @@
import EyeIcon from '@/components/icons/EyeIcon';
import PenIcon from '@/components/icons/PenIcon';
import TrashIcon from '@/components/icons/TrashIcon';
import ModuleRoomSlug from '@/components/content-blocks/ModuleRoomSlug'
import ContentComponent from '@/components/content-blocks/ContentComponent';
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
@ -77,24 +65,7 @@
name: 'content-block',
components: {
'text_block': TextBlock,
'basic_knowledge': InstrumentWidget, // for legacy
'instrument': InstrumentWidget,
'image_block': ImageBlock,
'image_url_block': ImageUrlBlock,
'video_block': VideoBlock,
'link_block': LinkBlock,
'document_block': DocumentBlock,
'infogram_block': InfogramBlock,
'genially_block': GeniallyBlock,
'thinglink_block': ThinglinkBlock,
'subtitle': SubtitleBlock,
'content_list': ContentListBlock,
'module_room_slug': ModuleRoomSlug,
Survey,
Solution,
Assignment,
Task,
ContentComponent,
AddContentButton,
VisibilityAction,
EyeIcon,
@ -181,6 +152,10 @@
},
hidden() {
return isHidden(this.contentBlock, this.schoolClass);
},
root() {
// we need the root content block id, not the generated content block if inside a content list block
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
}
},

View File

@ -1,6 +1,7 @@
<template>
<div class="modal__backdrop">
<div class="modal" :class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen}">
<div class="modal"
:class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}">
<div class="modal__header">
<slot name="header"></slot>
</div>
@ -32,6 +33,10 @@
fullscreen: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
}
},
@ -68,49 +73,6 @@
-ms-grid-rows: auto 1fr 65px;
position: relative;
&--hide-header {
grid-template-rows: 1fr 65px;
grid-template-areas: "body" "footer";
}
&--hide-header &__header {
display: none;
}
&--hide-header &__body {
padding: $default-padding;
}
&--fullscreen {
width: 95vw;
height: auto;
grid-template-rows: 1fr;
-ms-grid-rows: 1fr;
grid-template-areas: "body";
overflow: hidden;
}
&--fullscreen &__footer {
display: none;
}
&--fullscreen &__body {
padding: 0;
scrollbar-width: none;
margin-right: -5px;
height: auto;
max-height: 95vh;
&::-webkit-scrollbar {
display: none;
}
}
&--fullscreen &__close-button {
display: flex;
}
&__backdrop {
display: flex;
justify-content: center;
@ -165,5 +127,59 @@
border-top: 1px solid $color-silver-light;
padding: 16px $modal-lateral-padding;
}
$parent: &;
&--hide-header {
grid-template-rows: 1fr 65px;
grid-template-areas: "body" "footer";
#{$parent}__header {
display: none;
}
#{$parent}__body {
padding: $default-padding;
}
}
&--fullscreen {
width: 95vw;
height: auto;
grid-template-rows: 1fr;
-ms-grid-rows: 1fr;
grid-template-areas: "body";
overflow: hidden;
#{$parent}__footer {
display: none;
}
#{$parent}__body {
padding: 0;
scrollbar-width: none;
margin-right: -5px;
height: auto;
max-height: 95vh;
&::-webkit-scrollbar {
display: none;
}
}
#{$parent}__close-button {
display: flex;
}
}
&--small {
height: auto;
#{$parent}__body {
min-height: 0;
}
}
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<div class="content-component" :class="{'content-component--bookmarked': bookmarked}">
<bookmark-actions
v-if="showBookmarkActions()"
@add-note="addNote(component.id)"
@edit-note="editNote"
@bookmark="bookmarkContent(component.id, !bookmarked)"
:bookmarked="bookmarked"
:note="note"></bookmark-actions>
<component
:is="component.type"
v-bind="component"
:parent="parent"
>
</component>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
import TextBlock from '@/components/content-blocks/TextBlock';
import InstrumentWidget from '@/components/content-blocks/InstrumentWidget';
import ImageBlock from '@/components/content-blocks/ImageBlock';
import ImageUrlBlock from '@/components/content-blocks/ImageUrlBlock';
import VideoBlock from '@/components/content-blocks/VideoBlock';
import LinkBlock from '@/components/content-blocks/LinkBlock';
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
import ContentListBlock from '@/components/content-blocks/ContentListBlock';
import ModuleRoomSlug from '@/components/content-blocks/ModuleRoomSlug';
import Assignment from '@/components/content-blocks/assignment/Assignment';
import Survey from '@/components/content-blocks/SurveyBlock';
import Solution from '@/components/content-blocks/Solution';
import BookmarkActions from '@/components/notes/BookmarkActions';
import UPDATE_CONTENT_BOOKMARK from '@/graphql/gql/mutations/updateContentBookmark.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/contentBlockQuery.gql';
export default {
props: ['component', 'parent', 'bookmarks', 'notes', 'root'],
components: {
'text_block': TextBlock,
'basic_knowledge': InstrumentWidget, // for legacy
'instrument': InstrumentWidget,
'image_block': ImageBlock,
'image_url_block': ImageUrlBlock,
'video_block': VideoBlock,
'link_block': LinkBlock,
'document_block': DocumentBlock,
'infogram_block': InfogramBlock,
'genially_block': GeniallyBlock,
'subtitle': SubtitleBlock,
'content_list': ContentListBlock,
'module_room_slug': ModuleRoomSlug,
Survey,
Solution,
Assignment,
BookmarkActions
},
computed: {
...mapGetters(['editModule']),
bookmarked() {
return this.bookmarks && !!this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
},
note() {
const bookmark = this.bookmarks && this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
return bookmark && bookmark.note;
}
},
methods: {
addNote(id) {
this.$store.dispatch('addNote', {
content: id,
contentBlock: this.root
});
},
editNote() {
this.$store.dispatch('editNote', this.note);
},
bookmarkContent(uuid, bookmarked) {
this.$apollo.mutate({
mutation: UPDATE_CONTENT_BOOKMARK,
variables: {
input: {
uuid,
contentBlock: this.root,
bookmarked
}
},
update: (store, response) => {
const query = CONTENT_BLOCK_QUERY;
const variables = {id: this.root};
const data = store.readQuery({
query,
variables
});
const bookmarks = data.contentBlock.bookmarks;
if (bookmarked) {
bookmarks.push({
note: null,
uuid: uuid,
__typename: 'ContentBlockBookmarkNode'
});
} else {
let index = bookmarks.findIndex(element => {
return element.uuid === uuid;
});
if (index > -1) {
bookmarks.splice(index, 1);
}
}
data.contentBlock.bookmarks = bookmarks;
store.writeQuery({
data,
query,
variables
});
},
optimisticResponse: {
__typename: 'Mutation',
updateContentBookmark: {
__typename: 'UpdateContentBookmarkPayload',
success: true
}
}
});
},
showBookmarkActions() {
return this.component.type !== 'content_list' && this.component.type !== 'basic_knowledge' && !this.editModule;
}
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/_variables.scss";
.content-component {
position: relative;
&--bookmarked {
}
}
</style>

View File

@ -6,7 +6,10 @@
:key="contentBlock.id"
v-for="(contentBlock, index) in contentBlocks">
<p class="content-list__numbering">{{alphaIndex(index)}})</p>
<content-block :contentBlock="contentBlock"></content-block>
<content-block
:contentBlock="contentBlock"
:parent="parent"
></content-block>
</li>
</ol>
</div>
@ -38,7 +41,10 @@
return this.contents.map(contentBlock => {
return Object.assign({}, contentBlock, {
contents: [...contentBlock.value],
indent: true
indent: true,
bookmarks: this.parent.bookmarks,
notes: this.parent.notes,
root: this.parent.id
})
});
}
@ -57,7 +63,7 @@
&__item {
list-style: none;
position: relative;
padding: 0 2*15px;
padding: 0 0 0 2*15px;
}
&__numbering {

View File

@ -0,0 +1,16 @@
<template>
<svg class="add-note-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path
d="M81.5,88.39746H18.7627a2.49981,2.49981,0,0,1-2.5-2.5V35.35352L1.72559,20.71191A2.50054,2.50054,0,0,1,3.5,16.4502h78a2.49981,2.49981,0,0,1,2.5,2.5V85.89746A2.49981,2.49981,0,0,1,81.5,88.39746Zm-60.2373-5H79V21.4502H9.50488L20.53711,32.56152a2.5013,2.5013,0,0,1,.72559,1.76172Z"/>
<path d="M64.9209,55.08447H36.18457a2.5,2.5,0,0,1,0-5H64.9209a2.5,2.5,0,0,1,0,5Z"/>
<path d="M48.26318,66.95313V38.21582a2.5,2.5,0,0,1,5,0v28.7373a2.5,2.5,0,0,1-5,0Z"/>
</svg>
</template>
<style scoped lang="scss">
.add-note-icon {
width: 29px;
height: 25px;
margin-left: 2px;
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="bookmark-icon">
<g :class="{'bookmark-icon--bookmarked': bookmarked}">
<path class="bookmark-icon__background"
d="M51,67.32872a5.06849,5.06849,0,0,1,2.98875.97385L84,90V11H18V90L48.01333,68.30149A5.064,5.064,0,0,1,51,67.32872Z"
/>
<path id="bookmark-icon__outline"
d="M84.5,93.07715a2.49662,2.49662,0,0,1-1.43359-.45215L51,70.17871,18.93359,92.625A2.49964,2.49964,0,0,1,15,90.57715V11.42285a2.49981,2.49981,0,0,1,2.5-2.5h67a2.49981,2.49981,0,0,1,2.5,2.5v79.1543a2.49947,2.49947,0,0,1-2.5,2.5ZM51,65.15527a4.8673,4.8673,0,0,1,2.80762.88574L82,85.77539V13.92285H20V85.77539L48.19434,66.04A4.863,4.863,0,0,1,51,65.15527Z"/>
</g>
</svg>
</template>
<script>
export default {
props: ['bookmarked']
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.bookmark-icon {
width: 24px;
height: 28px;
&__background {
fill: white;
}
$parent: &;
&--bookmarked {
#{$parent}__background {
fill: $color-accent-1;
}
}
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<svg class="note-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path
d="M81.5,88.39746H18.7627a2.49981,2.49981,0,0,1-2.5-2.5V35.35352L1.72559,20.71191A2.50054,2.50054,0,0,1,3.5,16.4502h78a2.49981,2.49981,0,0,1,2.5,2.5V85.89746A2.49981,2.49981,0,0,1,81.5,88.39746Zm-60.2373-5H79V21.4502H9.50488L20.53711,32.56152a2.5013,2.5013,0,0,1,.72559,1.76172Z"/>
<path d="M61.9209,40.92676H39.18457a2.5,2.5,0,0,1,0-5H61.9209a2.5,2.5,0,0,1,0,5Z"/>
<path d="M62.13184,55.24219H39.39453a2.5,2.5,0,0,1,0-5h22.7373a2.5,2.5,0,0,1,0,5Z"/>
<path d="M62.13184,69.55859H39.39453a2.5,2.5,0,0,1,0-5h22.7373a2.5,2.5,0,0,1,0,5Z"/>
</svg>
</template>
<style scoped lang="scss">
.note-icon {
width: 29px;
height: 25px;
margin-left: 2px;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="bookmark-actions">
<a class="bookmark-actions__action" @click="$emit('bookmark')"
:class="{'bookmark-actions__action--bookmarked': bookmarked}">
<bookmark-icon :bookmarked="bookmarked"></bookmark-icon>
</a>
<a class="bookmark-actions__action" v-if="bookmarked && !note" @click="$emit('add-note')">
<add-note-icon></add-note-icon>
</a>
<a class="bookmark-actions__action bookmark-actions__action--noted" @click="$emit('edit-note')" v-if="note">
<note-icon></note-icon>
</a>
</div>
</template>
<script>
import BookmarkIcon from '@/components/icons/BookmarkIcon';
import AddNoteIcon from '@/components/icons/AddNoteIcon';
import NoteIcon from '@/components/icons/NoteIcon';
export default {
props: ['bookmarked', 'note'],
components: {
BookmarkIcon,
AddNoteIcon,
NoteIcon
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.bookmark-actions {
height: 100%;
min-height: 60px;
padding: 0 2*$large-spacing;
position: absolute;
right: -5*$large-spacing;
display: none;
@include desktop {
display: flex;
}
flex-direction: column;
align-content: center;
&__action {
opacity: 0;
transition: opacity 0.3s;
cursor: pointer;
width: 26px;
display: flex;
justify-content: center;
&--bookmarked, &--noted {
opacity: 1;
}
}
$parent: &;
&:hover {
#{$parent}__action {
opacity: 1;
}
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<note-form @save="editNote" @hide="hide" :note="currentNote"></note-form>
</template>
<script>
import NoteForm from '@/components/notes/NoteForm';
import UPDATE_NOTE_MUTATION from '@/graphql/gql/mutations/updateNote.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql';
import {mapGetters} from 'vuex';
export default {
components: {
NoteForm
},
computed: {
...mapGetters(['currentNote'])
},
methods: {
editNote(note) {
this.$apollo.mutate({
mutation: UPDATE_NOTE_MUTATION,
variables: {
input: {
note
}
},
refetchQueries: [{
query: MODULE_DETAILS_QUERY,
variables: {
slug: this.$route.params.slug
}
}]
}).then(() => {
this.$store.dispatch('hideModal');
});
},
hide() {
this.$store.dispatch('hideModal');
}
}
}
</script>

View File

@ -0,0 +1,93 @@
<template>
<note-form @save="addNote" @hide="hide" :note="note"></note-form>
</template>
<script>
import NoteForm from '@/components/notes/NoteForm';
import ADD_NOTE_MUTATION from '@/graphql/gql/mutations/addNote.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/contentBlockQuery.gql';
import {mapGetters} from 'vuex';
export default {
components: {
NoteForm
},
data() {
return {
note: {}
}
},
computed: {
...mapGetters(['currentContent', 'currentContentBlock'])
},
methods: {
addNote(note) {
const content = this.currentContent;
const contentBlock = this.currentContentBlock;
const text = note.text;
this.$apollo.mutate({
mutation: ADD_NOTE_MUTATION,
variables: {
input: {
note: {
content,
contentBlock,
text
}
}
},
update: (store, {data: {addNote: {note}}}) => {
const query = CONTENT_BLOCK_QUERY;
const variables = {id: contentBlock};
const data = store.readQuery({
query,
variables
});
const bookmarks = data.contentBlock.bookmarks;
let index = bookmarks.findIndex(element => {
return element.uuid === content;
});
if (index > -1) {
let el = bookmarks[index];
el.note = note;
bookmarks.splice(index, 1, el);
}
data.contentBlock.bookmarks = bookmarks;
store.writeQuery({
data,
query,
variables
});
},
optimisticResponse: {
__typename: 'Mutation',
addNote: {
__typename: 'AddNotePayload',
note: {
__typename: 'NoteNode',
id: -1,
text: text
}
}
}
}).then(() => {
this.$store.dispatch('hideModal');
});
},
hide() {
this.$store.dispatch('hideModal');
}
}
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<modal :hide-header="true" :small="true">
<modal-input v-on:input="localNote.text = $event"
placeholder="Notiz erfassen"
:value="localNote.text"
></modal-input>
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button"
@click="$emit('save', localNote)">Speichern</a>
<a class="button" @click="$emit('hide')">Abbrechen</a>
</div>
</modal>
</template>
<script>
import Modal from '@/components/Modal';
import ModalInput from '@/components/ModalInput';
export default {
props: ['note'],
components: {
Modal,
ModalInput
},
data() {
return {
localNote: Object.assign({},
{
...this.note
}
)
}
}
}
</script>

View File

@ -6,6 +6,13 @@ fragment ContentBlockParts on ContentBlockNode {
contents
userCreated
mine
bookmarks {
uuid
note {
id
text
}
}
hiddenFor {
edges {
node {

View File

@ -0,0 +1,8 @@
mutation AddNote($input: AddNoteInput!) {
addNote(input: $input) {
note {
id
text
}
}
}

View File

@ -0,0 +1,5 @@
mutation UpdateContentBookmark($input: UpdateContentBookmarkInput!) {
updateContentBookmark(input: $input) {
success
}
}

View File

@ -0,0 +1,8 @@
mutation UpdateNote($input: UpdateNoteInput!) {
updateNote(input: $input) {
note {
id
text
}
}
}

View File

@ -12,6 +12,7 @@ export default new Vuex.Store({
showMobileNavigation: false,
contentBlockPosition: {},
scrollPosition: 0,
currentContent: '',
currentContentBlock: '',
currentRoomEntry: '',
parentRoom: null,
@ -19,6 +20,7 @@ export default new Vuex.Store({
objectiveGroupType: '',
currentObjectiveGroup: '',
parentProject: null,
currentNote: null,
currentProjectEntry: null,
imageUrl: '',
infographic: {
@ -33,18 +35,17 @@ export default new Vuex.Store({
},
getters: {
showModal: state => {
return state.showModal
},
showMobileNavigation: state => {
return state.showMobileNavigation
},
showModal: state => state.showModal,
showMobileNavigation: state => state.showMobileNavigation,
scrollToAssignmentId: state => state.scrollToAssignmentId,
scrollToAssignmentReady: state => state.scrollToAssignmentReady,
scrollingToAssignment: state => state.scrollingToAssignment,
currentProjectEntry: state => state.currentProjectEntry,
editModule: state => state.editModule,
currentObjectiveGroup: state => state.currentObjectiveGroup
currentObjectiveGroup: state => state.currentObjectiveGroup,
currentContent: state => state.currentContent,
currentContentBlock: state => state.currentContentBlock,
currentNote: state => state.currentNote,
},
actions: {
@ -58,6 +59,7 @@ export default new Vuex.Store({
},
resetModalState({commit}) {
commit('setCurrentRoomEntry', '');
commit('setCurrentContent', '');
commit('setCurrentContentBlock', '');
commit('setContentBlockPosition', {});
commit('setParentRoom', null);
@ -73,6 +75,7 @@ export default new Vuex.Store({
type: ''
});
commit('setVimeoId', null);
commit('setCurrentNote', null);
},
resetContentBlockPosition({commit}) {
commit('setContentBlockPosition', {});
@ -122,6 +125,15 @@ export default new Vuex.Store({
commit('setCurrentProjectEntry', payload);
dispatch('showModal', 'edit-project-entry-wizard');
},
addNote({commit, dispatch}, payload) {
commit('setCurrentContentBlock', payload.contentBlock);
commit('setCurrentContent', payload.content);
dispatch('showModal', 'new-note-wizard');
},
editNote({commit, dispatch}, payload) {
commit('setCurrentNote', payload);
dispatch('showModal', 'edit-note-wizard');
},
showFullscreenImage({commit, dispatch}, payload) {
commit('setImageUrl', payload);
dispatch('showModal', 'fullscreen-image');
@ -146,7 +158,7 @@ export default new Vuex.Store({
scrollingToAssignment({commit, state, dispatch}, payload) {
if (payload && !state.scrollingToAssignment) {
commit('setScrollingToAssignment', true);
};
}
if (!payload && state.scrollingToAssignment) {
commit('setScrollingToAssignment', false);
@ -174,12 +186,18 @@ export default new Vuex.Store({
setContentBlockPosition(state, payload) {
state.contentBlockPosition = payload;
},
setCurrentContent(state, payload) {
state.currentContent = payload;
},
setCurrentContentBlock(state, payload) {
state.currentContentBlock = payload;
},
setParentRoom(state, payload) {
state.parentRoom = payload;
},
setCurrentNote(state, payload) {
state.currentNote = payload;
},
setCurrentRoomEntry(state, payload) {
state.currentRoomEntry = payload;
},

View File

@ -11,12 +11,13 @@ from basicknowledge.queries import BasicKnowledgeQuery
from books.schema.mutations.main import BookMutations
from books.schema.queries import BookQuery
from core.schema.mutations.main import CoreMutations
from notes.mutations import NoteMutations
from objectives.mutations import ObjectiveMutations
from objectives.schema import ObjectivesQuery
from portfolio.mutations import PortfolioMutations
from portfolio.schema import PortfolioQuery
from surveys.schema import SurveysQuery
from surveys.mutations import SurveysMutations
from surveys.mutations import SurveyMutations
from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery, ModuleRoomsQuery
from users.schema import AllUsersQuery, UsersQuery
@ -33,7 +34,7 @@ class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQ
class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations,
ProfileMutations, SurveysMutations, graphene.ObjectType):
ProfileMutations, SurveyMutations, NoteMutations, graphene.ObjectType):
if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='__debug')

View File

@ -0,0 +1,21 @@
# Generated by Django 2.0.6 on 2019-10-10 09:52
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('notes', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('books', '0014_auto_20190912_1228'),
]
operations = [
migrations.AddField(
model_name='contentblock',
name='bookmarks',
field=models.ManyToManyField(related_name='bookmarked_content_blocks', through='notes.ContentBlockBookmark', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -11,8 +11,9 @@ from books.blocks import TextBlock, BasicKnowledgeBlock, LinkBlock, VideoBlock,
ThinglinkBlock
from books.utils import get_type_and_value
from core.wagtail_utils import StrictHierarchyPage
from notes.models import ContentBlockBookmark
from surveys.models import Survey
from users.models import SchoolClass
from users.models import SchoolClass, User
logger = logging.getLogger(__name__)
@ -34,10 +35,14 @@ class ContentBlock(StrictHierarchyPage):
(BASE_SOCIETY, 'Instrument Gesellschaft'),
)
# blocks without owner are visible by default, need to be hidden for each class
hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_content_blocks')
# blocks with owner are hidden by default, need to be shown for each class
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_content_blocks')
user_created = models.BooleanField(default=False)
bookmarks = models.ManyToManyField(User, through=ContentBlockBookmark, related_name='bookmarked_content_blocks')
content_blocks = [
('text_block', TextBlock()),
('basic_knowledge', BasicKnowledgeBlock()),

View File

@ -10,6 +10,7 @@ from books.models import ContentBlock, Chapter, SchoolClass
from books.schema.inputs import ContentBlockInput
from books.schema.queries import ContentBlockNode
from core.utils import set_hidden_for, set_visible_for
from notes.models import ContentBlockBookmark
from .utils import handle_content_block, set_user_defined_block_type

View File

@ -5,6 +5,8 @@ from graphene_django.filter import DjangoFilterConnectionField
from api.utils import get_object
from books.utils import are_solutions_enabled_for
from notes.models import ContentBlockBookmark
from notes.schema import ContentBlockBookmarkNode
from rooms.models import ModuleRoomSlug
from ..models import Book, Topic, Module, Chapter, ContentBlock
@ -24,6 +26,7 @@ def process_module_room_slug_block(content):
class ContentBlockNode(DjangoObjectType):
mine = graphene.Boolean()
bookmarks = graphene.List(ContentBlockBookmarkNode)
class Meta:
model = ContentBlock
@ -54,6 +57,12 @@ class ContentBlockNode(DjangoObjectType):
self.contents.stream_data = updated_stream_data
return self.contents
def resolve_bookmarks(self, info, **kwargs):
return ContentBlockBookmark.objects.filter(
user=info.context.user,
content_block=self
)
class ChapterNode(DjangoObjectType):
content_blocks = DjangoFilterConnectionField(ContentBlockNode)

View File

@ -54,6 +54,7 @@ INSTALLED_APPS = [
'portfolio',
'statistics',
'surveys',
'notes',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',

0
server/notes/__init__.py Normal file
View File

3
server/notes/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
server/notes/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class NotesConfig(AppConfig):
name = 'notes'

13
server/notes/inputs.py Normal file
View File

@ -0,0 +1,13 @@
import graphene
from graphene import InputObjectType
class AddNoteArgument(InputObjectType):
content = graphene.UUID(required=True)
content_block = graphene.ID(required=True)
text = graphene.String(required=True)
class UpdateNoteArgument(InputObjectType):
id = graphene.ID(required=True)
text = graphene.String(required=True)

View File

@ -0,0 +1,46 @@
# Generated by Django 2.0.6 on 2019-10-10 09:52
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('books', '0014_auto_20190912_1228'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ContentBlockBookmark',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(unique=True)),
('content_block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='books.ContentBlock')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Note',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
],
),
migrations.AddField(
model_name='contentblockbookmark',
name='note',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.Note'),
),
migrations.AddField(
model_name='contentblockbookmark',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

22
server/notes/models.py Normal file
View File

@ -0,0 +1,22 @@
from django.db import models
# Create your models here.
from core.wagtail_utils import StrictHierarchyPage
from users.models import User
class Note(models.Model):
text = models.TextField()
class Bookmark(models.Model):
uuid = models.UUIDField(unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
note = models.OneToOneField(Note, null=True, on_delete=models.SET_NULL)
class Meta:
abstract = True
class ContentBlockBookmark(Bookmark):
content_block = models.ForeignKey('books.ContentBlock', on_delete=models.CASCADE)

102
server/notes/mutations.py Normal file
View File

@ -0,0 +1,102 @@
from builtins import PermissionError
import graphene
import json
from graphene import relay
from api.utils import get_object
from books.models import ContentBlock
from notes.inputs import AddNoteArgument, UpdateNoteArgument
from notes.models import ContentBlockBookmark, Note
from notes.schema import NoteNode
class UpdateContentBookmark(relay.ClientIDMutation):
class Input:
uuid = graphene.UUID(required=True)
content_block = graphene.ID(required=True)
bookmarked = graphene.Boolean(required=True)
success = graphene.Boolean()
errors = graphene.String()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
uuid = kwargs.get('uuid')
user = info.context.user
content_block_id = kwargs.get('content_block')
bookmarked = kwargs.get('bookmarked')
content_block = get_object(ContentBlock, content_block_id)
if bookmarked:
ContentBlockBookmark.objects.create(
content_block=content_block,
uuid=uuid,
user=user
)
else:
ContentBlockBookmark.objects.get(
content_block=content_block,
uuid=uuid,
user=user
).delete()
return cls(success=True)
class AddNote(relay.ClientIDMutation):
class Input:
note = graphene.Argument(AddNoteArgument)
note = graphene.Field(NoteNode)
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user
note = kwargs.get('note')
content_uuid = note.get('content')
content_block_id = note.get('content_block')
content_block = get_object(ContentBlock, content_block_id)
text = note.get('text')
bookmark = ContentBlockBookmark.objects.get(
content_block=content_block,
uuid=content_uuid,
user=user
)
bookmark.note = Note.objects.create(text=text)
bookmark.save()
return cls(note=bookmark.note)
class UpdateNote(relay.ClientIDMutation):
class Input:
note = graphene.Argument(UpdateNoteArgument)
note = graphene.Field(NoteNode)
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user
note = kwargs.get('note')
id = note.get('id')
text = note.get('text')
note = get_object(Note, id)
if note.contentblockbookmark.user != user:
raise PermissionError
note.text = text
note.save()
return cls(note=note)
class NoteMutations:
add_note = AddNote.Field()
update_note = UpdateNote.Field()
update_content_bookmark = UpdateContentBookmark.Field()

25
server/notes/schema.py Normal file
View File

@ -0,0 +1,25 @@
import graphene
from graphene import relay
from graphene_django import DjangoObjectType
from notes.models import Note, ContentBlockBookmark
class NoteNode(DjangoObjectType):
pk = graphene.Int()
class Meta:
model = Note
interfaces = (relay.Node,)
def resolve_pk(self, *args, **kwargs):
return self.id
class ContentBlockBookmarkNode(DjangoObjectType):
# note = graphene.
uuid = graphene.UUID()
note = graphene.Field(NoteNode)
class Meta:
model = ContentBlockBookmark

3
server/notes/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
server/notes/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -33,5 +33,5 @@ class UpdateAnswer(relay.ClientIDMutation):
return cls(answer=answer)
class SurveysMutations:
class SurveyMutations:
update_answer = UpdateAnswer.Field()