Merged in feature/topic-404 (pull request #106)

Feature/topic 404

Approved-by: Christian Cueni
This commit is contained in:
Ramon Wenger 2022-05-23 18:21:37 +00:00
commit 7da52b03a1
9 changed files with 124 additions and 26 deletions

View File

@ -9,6 +9,7 @@ import {typeDefs} from '@/graphql/typedefs';
import {resolvers} from '@/graphql/resolvers'; import {resolvers} from '@/graphql/resolvers';
import cache from './cache'; import cache from './cache';
import {router} from '@/router';
export default function (uri, networkErrorCallback) { export default function (uri, networkErrorCallback) {
const httpLink = createHttpLink({ const httpLink = createHttpLink({
@ -64,19 +65,37 @@ export default function (uri, networkErrorCallback) {
} }
}); });
const notFoundLink = new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
const {data} = response;
if (data) {
if (data.topic && data.topic.__typename === 'NotFound') {
// redirect to general 404 page.
// todo: specific topic not found page, with navigation?
router.push({
name: 'not-found'
});
}
}
return response;
});
});
let composedLink; let composedLink;
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
composedLink = ApolloLink.from([ composedLink = ApolloLink.from([
createOmitTypenameLink, createOmitTypenameLink,
errorLink, errorLink,
httpLink notFoundLink,
httpLink,
]); ]);
} else { } else {
composedLink = ApolloLink.from([ composedLink = ApolloLink.from([
consoleLink, consoleLink,
createOmitTypenameLink, createOmitTypenameLink,
errorLink, errorLink,
httpLink notFoundLink,
httpLink,
]); ]);
} }

View File

@ -0,0 +1,3 @@
fragment NotFoundParts on NotFound {
reason
}

View File

@ -1,6 +1,8 @@
#import "../fragments/topicParts.gql" #import "../fragments/topicParts.gql"
#import "../fragments/notFoundParts.gql"
query Topic($slug: String!){ query Topic($slug: String!){
topic(slug: $slug) { topic(slug: $slug) {
...TopicParts ...TopicParts
...NotFoundParts
} }
} }

View File

@ -63,7 +63,7 @@
BookTopicNavigation, BookTopicNavigation,
ModuleTeaser, ModuleTeaser,
PlayIcon, PlayIcon,
BulbIcon BulbIcon,
}, },
apollo: { apollo: {
@ -71,7 +71,7 @@
return { return {
query: TOPIC_QUERY, query: TOPIC_QUERY,
variables: { variables: {
slug: this.$route.params.topicSlug slug: this.$route.params.topicSlug,
}, },
update(data) { update(data) {
return this.$getRidOfEdges(data).topic || {}; return this.$getRidOfEdges(data).topic || {};
@ -81,26 +81,26 @@
this.saveMe = false; this.saveMe = false;
this.updateLastVisitedTopic(this.topic.id); this.updateLastVisitedTopic(this.topic.id);
} }
} },
}; };
} },
}, },
data() { data() {
return { return {
topic: { topic: {
modules: { modules: {
edges: [] edges: [],
} },
}, },
saveMe: false saveMe: false,
}; };
}, },
computed: { computed: {
modules() { modules() {
return this.topic.modules; return this.topic.modules;
} },
}, },
mounted() { mounted() {
@ -116,12 +116,15 @@
this.$store.dispatch('showFullscreenVideo', this.topic.vimeoId); this.$store.dispatch('showFullscreenVideo', this.topic.vimeoId);
}, },
updateLastVisitedTopic(topicId) { updateLastVisitedTopic(topicId) {
if (!topicId) {
return;
}
this.$apollo.mutate({ this.$apollo.mutate({
mutation: UPDATE_LAST_TOPIC_MUTATION, mutation: UPDATE_LAST_TOPIC_MUTATION,
variables: { variables: {
input: { input: {
id: topicId id: topicId,
} },
}, },
update(store, {data: {updateLastTopic: {topic}}}) { update(store, {data: {updateLastTopic: {topic}}}) {
if (topic) { if (topic) {
@ -131,15 +134,15 @@
const data = { const data = {
me: { me: {
...me, ...me,
lastTopic: topic lastTopic: topic,
} },
}; };
store.writeQuery({query, data}); store.writeQuery({query, data});
} }
} }
} },
}); });
} },
}, },
}; };
</script> </script>

View File

@ -33,11 +33,18 @@ const submission = () => import('@/pages/studentSubmission');
const postLoginRedirectUrlKey = 'postLoginRedirectionUrl'; const postLoginRedirectUrlKey = 'postLoginRedirectionUrl';
const notFoundRoute = {
component: p404,
meta: {
layout: 'blank',
},
};
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: start component: start,
}, },
...moduleRoutes, ...moduleRoutes,
...authRoutes, ...authRoutes,
@ -85,15 +92,17 @@ const routes = [
default: default:
return '/unknown-auth-error'; return '/unknown-auth-error';
} }
} },
}, },
{path: '/styleguide', component: styleGuidePage}, {path: '/styleguide', component: styleGuidePage},
{
path: '/not-found',
name: 'not-found',
...notFoundRoute
},
{ {
path: '*', path: '*',
component: p404, ...notFoundRoute
meta: {
layout: 'blank',
},
}, },
]; ];

View File

@ -6,6 +6,14 @@ from graphene_django.filter import DjangoFilterConnectionField
from books.models import Topic, Module from books.models import Topic, Module
from books.schema.nodes import ModuleNode from books.schema.nodes import ModuleNode
class NotFoundFailure:
reason = 'Not Found'
class NotFound(graphene.ObjectType):
reason = graphene.String()
class TopicNode(DjangoObjectType): class TopicNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
@ -27,3 +35,14 @@ class TopicNode(DjangoObjectType):
def resolve_modules(self, *args, **kwargs): def resolve_modules(self, *args, **kwargs):
return Module.get_by_parent(self) return Module.get_by_parent(self)
class TopicOr404Node(graphene.Union):
class Meta:
types = (TopicNode, NotFound)
@classmethod
def resolve_type(cls, instance, info):
if type(instance).__name__ == "Topic":
return TopicNode
return NotFound

View File

@ -5,7 +5,8 @@ from graphene_django.filter import DjangoFilterConnectionField
from api.utils import get_object from api.utils import get_object
from core.logger import get_logger from core.logger import get_logger
from .connections import TopicConnection, ModuleConnection from .connections import TopicConnection, ModuleConnection
from .nodes import ContentBlockNode, ChapterNode, ModuleNode, TopicNode, SnapshotNode from .nodes import ContentBlockNode, ChapterNode, ModuleNode, NotFoundFailure, SnapshotNode, \
TopicOr404Node
from ..models import Book, Topic, Module, Chapter, Snapshot from ..models import Book, Topic, Module, Chapter, Snapshot
logger = get_logger(__name__) logger = get_logger(__name__)
@ -13,7 +14,7 @@ logger = get_logger(__name__)
class BookQuery(object): class BookQuery(object):
node = relay.Node.Field() node = relay.Node.Field()
topic = graphene.Field(TopicNode, slug=graphene.String()) topic = graphene.Field(TopicOr404Node, slug=graphene.String())
module = graphene.Field(ModuleNode, slug=graphene.String(), id=graphene.ID()) module = graphene.Field(ModuleNode, slug=graphene.String(), id=graphene.ID())
chapter = relay.Node.Field(ChapterNode) chapter = relay.Node.Field(ChapterNode)
content_block = relay.Node.Field(ContentBlockNode) content_block = relay.Node.Field(ContentBlockNode)
@ -63,5 +64,8 @@ class BookQuery(object):
if id is not None: if id is not None:
return get_object(Topic, id) return get_object(Topic, id)
if slug is not None: if slug is not None:
return Topic.objects.get(slug=slug) try:
return Topic.objects.get(slug=slug)
except Topic.DoesNotExist:
return NotFoundFailure
return None return None

View File

@ -0,0 +1,33 @@
from graphql.error import GraphQLLocatedError
from core.tests.base_test import SkillboxTestCase
TOPIC_QUERY = """
query TopicQuery($slug: String!) {
topic(slug: $slug) {
__typename
...on TopicNode {
title
}
...on NotFound {
reason
}
}
}
"""
class ContentBlockTestCase(SkillboxTestCase):
def setUp(self) -> None:
self.createDefault()
self.client = self.get_client()
def test_topic(self):
slug = "non-existing"
result = self.client.execute(TOPIC_QUERY, variables={
"slug": slug
})
self.assertIsNone(result.get('errors'))
topic = result.get('data').get('topic')
self.assertEqual(topic.get('__typename'), 'NotFound')
self.assertEqual(topic.get('reason'), 'Not Found')

View File

@ -702,6 +702,10 @@ interface Node {
id: ID! id: ID!
} }
type NotFound {
reason: String
}
type NoteNode implements Node { type NoteNode implements Node {
id: ID! id: ID!
text: String! text: String!
@ -852,7 +856,7 @@ type Query {
assignment(id: ID!): AssignmentNode assignment(id: ID!): AssignmentNode
assignments: [AssignmentNode] assignments: [AssignmentNode]
node(id: ID!): Node node(id: ID!): Node
topic(slug: String): TopicNode topic(slug: String): TopicOr404Node
module(slug: String, id: ID): ModuleNode module(slug: String, id: ID): ModuleNode
chapter(id: ID!): ChapterNode chapter(id: ID!): ChapterNode
contentBlock(id: ID!): ContentBlockNode contentBlock(id: ID!): ContentBlockNode
@ -1127,6 +1131,8 @@ type TopicNode implements Node {
modules(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, slug_Icontains: String, slug_In: [String], title: String, title_Icontains: String, title_In: [String]): ModuleNodeConnection modules(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, slug_Icontains: String, slug_In: [String], title: String, title_Icontains: String, title_In: [String]): ModuleNodeConnection
} }
union TopicOr404Node = TopicNode | NotFound
scalar UUID scalar UUID
input UpdateAnswerArgument { input UpdateAnswerArgument {