diff --git a/client/src/components/book-navigation/ContentNavigation.vue b/client/src/components/book-navigation/ContentNavigation.vue index ed4c327b..7dbe4ae2 100644 --- a/client/src/components/book-navigation/ContentNavigation.vue +++ b/client/src/components/book-navigation/ContentNavigation.vue @@ -6,7 +6,7 @@
Themen @@ -73,6 +73,7 @@ import BookTopicNavigation from '@/components/book-navigation/BookTopicNavigation'; import sidebarMixin from '@/mixins/sidebar'; + import meMixin from '@/mixins/me'; export default { props: { @@ -81,13 +82,27 @@ } }, - mixins: [sidebarMixin], + mixins: [sidebarMixin, meMixin], components: { BookTopicNavigation, Logo }, + computed: { + topicRoute() { + if (this.me.lastTopic && this.me.lastTopic.slug) { + return { + name: 'topic', + params: { + topicSlug: this.me.lastTopic.slug + } + } + } + return '/book/topic/berufliche-grundbildung' + } + }, + methods: { isActive(linkName) { return linkName === 'book' && this.$route.path.indexOf('module') > -1; diff --git a/client/src/graphql/gql/fragments/topicParts.gql b/client/src/graphql/gql/fragments/topicParts.gql new file mode 100644 index 00000000..3edc4716 --- /dev/null +++ b/client/src/graphql/gql/fragments/topicParts.gql @@ -0,0 +1,17 @@ +#import "./moduleParts.gql" +fragment TopicParts on TopicNode { + id + title + teaser + slug + description + vimeoId + instructions + modules { + edges { + node { + ...ModuleParts + } + } + } +} diff --git a/client/src/graphql/gql/fragments/userParts.gql b/client/src/graphql/gql/fragments/userParts.gql index 4724b6dd..6a57ae18 100644 --- a/client/src/graphql/gql/fragments/userParts.gql +++ b/client/src/graphql/gql/fragments/userParts.gql @@ -12,6 +12,10 @@ fragment UserParts on UserNode { id slug } + lastTopic { + id + slug + } selectedClass { id } diff --git a/client/src/graphql/gql/mutations/updateLastTopic.gql b/client/src/graphql/gql/mutations/updateLastTopic.gql new file mode 100644 index 00000000..8bb52075 --- /dev/null +++ b/client/src/graphql/gql/mutations/updateLastTopic.gql @@ -0,0 +1,8 @@ +#import "../fragments/topicParts.gql" +mutation UpdateLastTopic($input: UpdateLastTopicInput!) { + updateLastTopic(input: $input) { + topic { + ...TopicParts + } + } +} diff --git a/client/src/graphql/gql/topicQuery.gql b/client/src/graphql/gql/topicQuery.gql index a477166a..64226fe0 100644 --- a/client/src/graphql/gql/topicQuery.gql +++ b/client/src/graphql/gql/topicQuery.gql @@ -1,18 +1,6 @@ -#import "./fragments/moduleParts.gql" +#import "./fragments/topicParts.gql" query Topic($slug: String!){ topic(slug: $slug) { - id - title - teaser - description - vimeoId - instructions - modules { - edges { - node { - ...ModuleParts - } - } - } + ...TopicParts } } diff --git a/client/src/pages/start.vue b/client/src/pages/start.vue index 328d1ce7..47098720 100644 --- a/client/src/pages/start.vue +++ b/client/src/pages/start.vue @@ -63,10 +63,13 @@ import PortfolioIllustration from '@/components/illustrations/PortfolioIllustration'; import RoomsIllustration from '@/components/illustrations/RoomsIllustration'; - import {meQuery} from '@/graphql/queries'; import MobileHeader from '@/components/MobileHeader'; + import meMixin from '@/mixins/me'; + export default { + + mixins: [meMixin], components: { MobileHeader, HeaderBar, @@ -77,12 +80,6 @@ RoomsIllustration }, - data() { - return { - me: {} - } - }, - computed: { moduleRoute() { if (this.me.lastModule && this.me.lastModule.slug) { @@ -98,9 +95,6 @@ } }, - apollo: { - me: meQuery - }, } diff --git a/client/src/pages/topic.vue b/client/src/pages/topic.vue index 20130572..12fcc509 100644 --- a/client/src/pages/topic.vue +++ b/client/src/pages/topic.vue @@ -45,6 +45,9 @@ import me from '@/mixins/me'; import BookTopicNavigation from '@/components/book-navigation/BookTopicNavigation'; + import UPDATE_LAST_TOPIC_MUTATION from '@/graphql/gql/mutations/updateLastTopic.gql'; + import ME_QUERY from '@/graphql/gql/meQuery.gql'; + export default { mixins: [me], @@ -64,6 +67,12 @@ }, update(data) { return this.$getRidOfEdges(data).topic || {}; + }, + result(r, key) { + if (this.saveMe) { + this.saveMe = false; + this.updateLastVisitedTopic(this.topic.id); + } } } } @@ -75,7 +84,8 @@ modules: { edges: [] } - } + }, + saveMe: false } }, @@ -85,9 +95,37 @@ } }, + mounted() { + if (!this.topic.id) { // component was loaded before topic, apollo not ready yet + this.saveMe = true; // needs saving, apollo will do this + } else { + this.updateLastVisitedTopic(this.topic.id); + } + }, + methods: { openVideo() { this.$store.dispatch('showFullscreenVideo', this.topic.vimeoId); + }, + updateLastVisitedTopic(topicId) { + this.$apollo.mutate({ + mutation: UPDATE_LAST_TOPIC_MUTATION, + variables: { + input: { + id: topicId + } + }, + update(store, {data: {updateLastTopic: {topic}}}) { + if (topic) { + let query = ME_QUERY; + const data = store.readQuery({query}); + if (data) { + data.me.lastTopic = topic; + store.writeQuery({query, data}) + } + } + } + }); } }, } diff --git a/server/books/schema/mutations/main.py b/server/books/schema/mutations/main.py index 5d2bb6f4..075f8cf8 100644 --- a/server/books/schema/mutations/main.py +++ b/server/books/schema/mutations/main.py @@ -1,5 +1,5 @@ from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock -from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule +from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, UpdateLastTopic class BookMutations(object): @@ -8,3 +8,4 @@ class BookMutations(object): delete_content_block = DeleteContentBlock.Field() update_solution_visibility = UpdateSolutionVisibility.Field() update_last_module = UpdateLastModule.Field() + update_last_topic = UpdateLastTopic.Field() diff --git a/server/books/schema/mutations/module.py b/server/books/schema/mutations/module.py index 1165f2d6..98b8f765 100644 --- a/server/books/schema/mutations/module.py +++ b/server/books/schema/mutations/module.py @@ -2,8 +2,8 @@ import graphene from graphene import relay from api.utils import get_errors, get_object -from books.models import Module -from books.schema.queries import ModuleNode +from books.models import Module, Topic +from books.schema.queries import ModuleNode, TopicNode class UpdateSolutionVisibility(relay.ClientIDMutation): @@ -71,3 +71,26 @@ class UpdateLastModule(relay.ClientIDMutation): except Exception as e: errors = ['Error: {}'.format(e)] return cls(errors=errors) + + +class UpdateLastTopic(relay.ClientIDMutation): + class Input: + # todo: use slug here too + id = graphene.ID() + + topic = graphene.Field(TopicNode) + + @classmethod + def mutate_and_get_payload(cls, root, info, **args): + user = info.context.user + id = args.get('id') + + topic = get_object(Topic, id) + if not topic: + raise Topic.DoesNotExist + + user.last_topic = topic + user.save() + + return cls(topic=topic) + diff --git a/server/users/migrations/0018_user_last_topic.py b/server/users/migrations/0018_user_last_topic.py new file mode 100644 index 00000000..f2da46b8 --- /dev/null +++ b/server/users/migrations/0018_user_last_topic.py @@ -0,0 +1,20 @@ +# Generated by Django 2.1.15 on 2020-06-15 14:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('books', '0021_auto_20200525_1447'), + ('users', '0017_auto_20200430_1251'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='last_topic', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='books.Topic'), + ), + ] diff --git a/server/users/models.py b/server/users/models.py index 99fd867b..a551808f 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -17,6 +17,7 @@ DEFAULT_SCHOOL_ID = 1 class User(AbstractUser): last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True) + last_topic = models.ForeignKey('books.Topic', related_name='+', on_delete=models.SET_NULL, null=True) avatar_url = models.CharField(max_length=254, blank=True, default='') email = models.EmailField(_('email address'), unique=True) hep_id = models.PositiveIntegerField(null=True, blank=False) diff --git a/server/users/schema.py b/server/users/schema.py index b23434d9..d3bab44c 100644 --- a/server/users/schema.py +++ b/server/users/schema.py @@ -50,7 +50,8 @@ class UserNode(DjangoObjectType): class Meta: model = User filter_fields = ['username', 'email'] - only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', 'avatar_url', + only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', + 'last_topic', 'avatar_url', 'selected_class', 'expiry_date'] interfaces = (relay.Node,) @@ -64,7 +65,7 @@ class UserNode(DjangoObjectType): return self.selected_class() def resolve_expiry_date(self, info): - if not self.hep_id: # concerns users that already have an (old) account + if not self.hep_id: # concerns users that already have an (old) account return format(datetime(2020, 7, 31), 'U') # just set some expiry date else: return self.license_expiry_date