From 9f2b21574b76e7b1504e877a7defb8b73378fe49 Mon Sep 17 00:00:00 2001 From: Pavle Portic Date: Tue, 26 Mar 2019 03:51:12 +0100 Subject: [PATCH] Add option to unlock and relock perks in the tree --- backend/perks/urls.py | 3 +- backend/perks/views.py | 28 +++- backend/perktree/settings.py | 2 - backend/perktree/urls.py | 6 +- frontend/src/apis/auth.api.js | 11 +- frontend/src/apis/perk.api.js | 1 + frontend/src/apis/user.api.js | 23 +++ frontend/src/components/app.vue | 33 ++-- .../src/components/auth/signup.component.vue | 152 +++++++++--------- frontend/src/components/perks.component.vue | 114 +++++++++++-- frontend/src/components/trees.component.vue | 4 +- .../src/components/upload-perks.component.vue | 4 +- frontend/src/components/user.component.vue | 28 ++++ frontend/src/controllers/auth.controller.js | 6 +- ...perks.controller.js => perk.controller.js} | 6 +- frontend/src/controllers/user.controller.js | 23 +++ frontend/src/main.js | 14 +- frontend/src/router.js | 7 +- frontend/src/store.js | 12 +- 19 files changed, 327 insertions(+), 150 deletions(-) create mode 100644 frontend/src/apis/user.api.js create mode 100644 frontend/src/components/user.component.vue rename frontend/src/controllers/{perks.controller.js => perk.controller.js} (81%) create mode 100644 frontend/src/controllers/user.controller.js diff --git a/backend/perks/urls.py b/backend/perks/urls.py index 48d7902..6026c8e 100644 --- a/backend/perks/urls.py +++ b/backend/perks/urls.py @@ -4,6 +4,7 @@ from perks import views urlpatterns = [ path('trees', views.TreeView.as_view()), path('trees/', views.PerkView.as_view()), - path('users/', views.UserView.as_view()), + path('user', views.UserView.as_view()), + # path('unlock/', views.UserView.as_view()), ] diff --git a/backend/perks/views.py b/backend/perks/views.py index 86b21ac..f67a355 100644 --- a/backend/perks/views.py +++ b/backend/perks/views.py @@ -9,7 +9,7 @@ from os import environ from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework import authentication # , permissions +from rest_framework import permissions from .parser import PerkParser from .models import Perk, Tree, User @@ -23,7 +23,6 @@ if not PERKS_DIR: class TreeView(APIView): - authentication_classes = (authentication.TokenAuthentication,) # permission_classes = (permissions.IsAuthenticated,) def get(self, request, format=None): @@ -47,7 +46,6 @@ class TreeView(APIView): class PerkView(APIView): - authentication_classes = (authentication.TokenAuthentication,) # permission_classes = (permissions.IsAuthenticated,) def get(self, request, tree_id, format=None): @@ -85,11 +83,29 @@ class PerkView(APIView): class UserView(APIView): - authentication_classes = (authentication.TokenAuthentication,) + permission_classes = (permissions.IsAuthenticated,) - def get(self, request, user_id): - user = User.objects.get(id=user_id) + def get(self, request): + user = User.objects.get(base_user__id=request.user.id) serialized_user = UserSerializer(user).data return Response(serialized_user) + def patch(self, request): + user = User.objects.get(base_user__id=request.user.id) + if 'perks' in request.data: + current_perks = [perk.id for perk in user.perks.all()] + new_perks = request.data['perks'] + if len(current_perks) < len(new_perks): + for perk_id in new_perks: + perk = Perk.objects.get(id=perk_id) + user.perks.add(perk) + elif len(current_perks) > len(new_perks): + removed_perks = list(set(current_perks) ^ set(new_perks)) + for perk_id in removed_perks: + perk = Perk.objects.get(id=perk_id) + user.perks.remove(perk) + + serialized_user = UserSerializer(user).data + return Response(serialized_user) + diff --git a/backend/perktree/settings.py b/backend/perktree/settings.py index 00dcaa0..77e4b9c 100644 --- a/backend/perktree/settings.py +++ b/backend/perktree/settings.py @@ -129,8 +129,6 @@ USE_TZ = True STATIC_URL = '/django-static/' REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 10, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ) diff --git a/backend/perktree/urls.py b/backend/perktree/urls.py index 8cddc81..3910201 100644 --- a/backend/perktree/urls.py +++ b/backend/perktree/urls.py @@ -25,9 +25,9 @@ from rest_framework_simplejwt.views import ( urlpatterns = [ path('admin/', admin.site.urls), path('api/', include([ - path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), - path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path('token', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('token/refresh', TokenRefreshView.as_view(), name='token_refresh'), + path('token/verify', TokenVerifyView.as_view(), name='token_verify'), path('', include('perks.urls')) ])) ] diff --git a/frontend/src/apis/auth.api.js b/frontend/src/apis/auth.api.js index 7ab3a1a..c476f0a 100644 --- a/frontend/src/apis/auth.api.js +++ b/frontend/src/apis/auth.api.js @@ -8,10 +8,9 @@ import Axios from 'axios'; const ENDPOINTS = { - LOGIN: '/token/', - VERIFY: '/token/verify/', - REFRESH: '/token/refresh/', - USER: '/user/', + LOGIN: '/token', + VERIFY: '/token/verify', + REFRESH: '/token/refresh', }; const AUTH_HEADER = 'Authorization'; @@ -24,10 +23,6 @@ export default class AuthApi { return Axios.post(ENDPOINTS.LOGIN, data); } - static getUser() { - return Axios.get(ENDPOINTS.USER); - } - static signup(data) { return Axios.post(ENDPOINTS.USER, data); } diff --git a/frontend/src/apis/perk.api.js b/frontend/src/apis/perk.api.js index 3b037d1..beb858b 100644 --- a/frontend/src/apis/perk.api.js +++ b/frontend/src/apis/perk.api.js @@ -9,6 +9,7 @@ import Axios from 'axios'; const ENDPOINTS = { TREES: '/trees', + PERKS: '/perks', }; export default class AuthApi { diff --git a/frontend/src/apis/user.api.js b/frontend/src/apis/user.api.js new file mode 100644 index 0000000..fcd7117 --- /dev/null +++ b/frontend/src/apis/user.api.js @@ -0,0 +1,23 @@ +/* + * user.api.js + * Copyright (C) 2019 pavle + * + * Distributed under terms of the BSD-3-Clause license. + */ + +import Axios from 'axios'; + +const ENDPOINTS = { + USER: '/user', +}; + +export default class AuthApi { + static getUser() { + return Axios.get(ENDPOINTS.USER); + } + + static updatePerks(data) { + return Axios.patch(ENDPOINTS.USER, data); + } +} + diff --git a/frontend/src/components/app.vue b/frontend/src/components/app.vue index 933d648..b6217c2 100644 --- a/frontend/src/components/app.vue +++ b/frontend/src/components/app.vue @@ -14,7 +14,7 @@ > @@ -38,28 +38,25 @@ export default { name: 'app', data() { return { - // toolbarItems: { - // 'loggedIn': [ - // { icon: 'fas fa-code-branch ', text: 'Perk trees', path: '/trees' }, - // { icon: 'account_circle', text: 'Admin panel', path: '/admin' }, - // { icon: 'exit_to_app', text: 'Logout', path: '/logout' }, - // ], - // 'loggedOut': [{ icon: 'lock_open', text: 'Login', path: '/login' }], - // }, - toolbarItems: [ - { icon: 'fas fa-upload', text: 'Upload perks', to: { name: 'upload-perks' } }, - { icon: 'fas fa-code-branch', text: 'Perk trees', to: { name: 'trees' } }, - ], + toolbarItems: { + true: [ + { icon: 'fas fa-upload', text: 'Upload perks', to: { name: 'upload-perks' } }, + { icon: 'fas fa-code-branch', text: 'Perk trees', to: { name: 'trees' } }, + { icon: 'fas fa-sign-out-alt', text: 'Logout', to: { name: 'logout' } }, + ], + false: [{ icon: 'fas fa-sign-in-alt', text: 'Login', to: { name: 'login' } }], + }, }; }, computed: { - ...mapGetters(['token']), - authStatus() { - return this.token ? 'loggedIn' : 'loggedOut'; - }, + ...mapGetters(['authStatus']), }, mounted() { - AuthController.refreshToken(); + AuthController.refreshToken().catch(() => { + if (!this.$route.meta.guest) { + this.$router.push({ name: 'index' }); + } + }); }, }; diff --git a/frontend/src/components/auth/signup.component.vue b/frontend/src/components/auth/signup.component.vue index 39b31a1..e7e2fa0 100644 --- a/frontend/src/components/auth/signup.component.vue +++ b/frontend/src/components/auth/signup.component.vue @@ -1,90 +1,90 @@ + diff --git a/frontend/src/components/perks.component.vue b/frontend/src/components/perks.component.vue index 6121548..edae0e3 100644 --- a/frontend/src/components/perks.component.vue +++ b/frontend/src/components/perks.component.vue @@ -15,16 +15,32 @@ {{ perk }} - {{ effect }} + > + {{ selectedPerk.name }} + + {{ selectedPerk.level }} + + {{ selectedPerk.effect }} Close + Unlock + Lock @@ -36,7 +52,8 @@ import * as d3 from 'd3'; import Sankey from 'd3.chart.sankey'; import * as _ from 'lodash'; -import PerksController from '../controllers/perks.controller'; +import PerkController from '../controllers/perk.controller'; +import UserController from '../controllers/user.controller'; export default { name: 'perks', @@ -45,8 +62,20 @@ export default { data () { return { dialog: false, - perk: '', - effect: '', + selectedPerk: { + id: null, + name: '', + effect: '', + level: null, + }, + user: { + perks: [], + base_user: {}, + }, + graphData: { + nodes: [], + links: [], + }, colorScheme: [ '#458588', '#d79921', @@ -56,6 +85,30 @@ export default { ], }; }, + computed: { + canUnlock() { + const isLocked = _.indexOf(this.user.perks, this.selectedPerk.id); + if (isLocked !== -1) { + return false; + } + + const requirements = _.filter(this.graphData.links, { target: this.selectedPerk }); + const requirement_ids = _.map(requirements, 'source.id'); + + return requirement_ids.every((req) => this.user.perks.includes(req)); + }, + canLock() { + const dependencies = _.filter(this.graphData.links, { source: this.selectedPerk }); + const dependency_ids = _.map(dependencies, 'target.id'); + const isLocked = _.indexOf(this.user.perks, this.selectedPerk.id); + let hasUnlockedDependencies = false; + if (dependencies.length !== 0) { + hasUnlockedDependencies = dependency_ids.every((req) => this.user.perks.includes(req)); + } + + return !hasUnlockedDependencies && isLocked !== -1; + }, + }, methods: { renderGraph(graphData) { const svg = d3.select('#perktree').append('svg'); @@ -73,21 +126,58 @@ export default { return this.colorScheme[node.type]; }) .on('node:click', (node) => { - const clicked_node = _.find(nodes, (n) => { + const clickedNode = _.find(nodes, (n) => { return n.name === node.name; }); - if (clicked_node.effect) { - this.perk = clicked_node.name; - this.effect = clicked_node.effect; + if (clickedNode.effect) { + this.selectedPerk = clickedNode; this.dialog = true; } }) .draw(graphData); }, + unlock() { + if (this.canUnlock) { + const newPerks = _.clone(this.user.perks); + newPerks.push(this.selectedPerk.id); + UserController.updatePerks(newPerks).then((response) => { + this.user = response.data; + this.markUnlockedPerks(); + this.dialog = false; + }); + } + }, + lock() { + if (this.canLock) { + const newPerks = _.difference(this.user.perks, [this.selectedPerk.id]); + UserController.updatePerks(newPerks).then((response) => { + this.user = response.data; + this.markUnlockedPerks(); + this.dialog = false; + }); + } + }, + markUnlockedPerks() { + for (let i = 0; i < this.graphData.nodes.length; i++) { + const perkId = this.graphData.nodes[i].id; + const el = this.$el.querySelector(`[data-node-id="${perkId}"] rect`); + + if (_.indexOf(this.user.perks, perkId) !== -1) { + el.setAttribute('style', `stroke: #d65d0e !important; stroke-width: 3px; fill: ${el.style.fill};`); + } else { + el.setAttribute('style', `fill: ${el.style.fill};`); + } + } + }, }, mounted() { - PerksController.getPerks(this.$route.params.tree).then((response) => { - this.renderGraph(response.data); + PerkController.getPerks(this.$route.params.tree).then((response) => { + this.graphData = response.data; + this.renderGraph(this.graphData); + UserController.getUser().then((response) => { + this.user = response.data; + this.markUnlockedPerks(); + }); }); }, }; diff --git a/frontend/src/components/trees.component.vue b/frontend/src/components/trees.component.vue index 01d379b..25e0d42 100644 --- a/frontend/src/components/trees.component.vue +++ b/frontend/src/components/trees.component.vue @@ -20,7 +20,7 @@ + diff --git a/frontend/src/controllers/auth.controller.js b/frontend/src/controllers/auth.controller.js index 4f8a029..f81dba8 100644 --- a/frontend/src/controllers/auth.controller.js +++ b/frontend/src/controllers/auth.controller.js @@ -1,5 +1,5 @@ /* - * perks.controller.js + * auth.controller.js * Copyright (C) 2019 pavle * * Distributed under terms of the BSD-3-Clause license. @@ -40,7 +40,7 @@ export default class AuthController { static setupToken() { const access = this.getLocalStorageToken().access; AuthApi.setAuthHeader(access); - store.commit('setToken', access); + store.commit('login'); } static login(data) { @@ -56,7 +56,7 @@ export default class AuthController { static logout() { this.clearLocalStorageToken(); - store.commit('clearToken'); + store.commit('logout'); AuthApi.setAuthHeader(''); } diff --git a/frontend/src/controllers/perks.controller.js b/frontend/src/controllers/perk.controller.js similarity index 81% rename from frontend/src/controllers/perks.controller.js rename to frontend/src/controllers/perk.controller.js index 5ebdf49..f1f4028 100644 --- a/frontend/src/controllers/perks.controller.js +++ b/frontend/src/controllers/perk.controller.js @@ -1,14 +1,13 @@ /* - * perks.controller.js + * perk.controller.js * Copyright (C) 2019 pavle * * Distributed under terms of the BSD-3-Clause license. */ import PerkApi from '../apis/perk.api'; -// import router from '../router'; -export default class PerksController { +export default class PerkController { static getTrees() { return PerkApi.getTrees(); } @@ -23,3 +22,4 @@ export default class PerksController { return PerkApi.uploadPerks(data); } } + diff --git a/frontend/src/controllers/user.controller.js b/frontend/src/controllers/user.controller.js new file mode 100644 index 0000000..2b05990 --- /dev/null +++ b/frontend/src/controllers/user.controller.js @@ -0,0 +1,23 @@ +/* + * user.controller.js + * Copyright (C) 2019 pavle + * + * Distributed under terms of the BSD-3-Clause license. + */ + +import UserApi from '../apis/user.api'; + +export default class AuthController { + static getUser() { + return UserApi.getUser(); + } + + static updatePerks(perks) { + const data = { + perks, + }; + + return UserApi.updatePerks(data); + } +} + diff --git a/frontend/src/main.js b/frontend/src/main.js index 0918ec4..a0209eb 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -23,14 +23,14 @@ const configureHttp = () => { }, (error) => { if (error.response && error.response.status === 401) { - // AuthController.refreshToken().catch(() => { - // router.push({ - // name: 'logout', - // }); - // }); - router.push({ - name: 'logout', + AuthController.refreshToken().catch(() => { + router.push({ + name: 'logout', + }); }); + // router.push({ + // name: 'logout', + // }); } return Promise.reject(error); diff --git a/frontend/src/router.js b/frontend/src/router.js index 66fba42..2f3c00e 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -68,6 +68,11 @@ const router = new Router({ guest: true, }, }, + { + path: '/user', + name: 'user', + component: () => import(/* webpackChunkName: "user" */ './components/user.component'), + }, ], }); @@ -78,7 +83,7 @@ router.isCurrentRoute = (routeName) => { router.beforeEach((to, from, next) => { if (to.name === 'logout') { AuthController.logout(); - return next({ name: 'login' }); + return next({ name: 'index' }); } if (!to.meta.guest && !AuthController.getAuthStatus()) { diff --git a/frontend/src/store.js b/frontend/src/store.js index f5cf465..a5c87ac 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -11,19 +11,19 @@ import Vuex from 'vuex'; Vue.use(Vuex); const state = { - token: null, + authStatus: null, }; const getters = { - token: (state) => state.token, + authStatus: (state) => state.authStatus, }; const mutations = { - setToken(token) { - state.token = token; + login() { + state.authStatus = true; }, - clearToken() { - state.token = ''; + logout() { + state.authStatus = false; }, };