Add option to unlock and relock perks in the tree

This commit is contained in:
Pavle Portic 2019-03-26 03:51:12 +01:00
parent a38afc2d6a
commit 9f2b21574b
Signed by: TheEdgeOfRage
GPG Key ID: 6758ACE46AA2A849
19 changed files with 327 additions and 150 deletions

View File

@ -4,6 +4,7 @@ from perks import views
urlpatterns = [
path('trees', views.TreeView.as_view()),
path('trees/<int:tree_id>', views.PerkView.as_view()),
path('users/<int:user_id>', views.UserView.as_view()),
path('user', views.UserView.as_view()),
# path('unlock/<int:perk_id>', views.UserView.as_view()),
]

View File

@ -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)

View File

@ -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',
)

View File

@ -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'))
]))
]

View File

@ -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);
}

View File

@ -9,6 +9,7 @@ import Axios from 'axios';
const ENDPOINTS = {
TREES: '/trees',
PERKS: '/perks',
};
export default class AuthApi {

View File

@ -0,0 +1,23 @@
/*
* user.api.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* 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);
}
}

View File

@ -14,7 +14,7 @@
></v-text-field>
<v-btn
flat
v-for="item in toolbarItems"
v-for="item in toolbarItems[authStatus]"
:key="item.text"
:to="item.to"
>
@ -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' });
}
});
},
};
</script>

View File

@ -1,90 +1,90 @@
<template>
<v-layout>
<v-flex md12 lg6 offset-lg3 >
<v-card class="signup-form-card">
<h3>Sign up</h3>
<form @submit="submit">
<v-text-field
v-model="username"
name="username"
label="Username"
type="text"
v-validate="'required|max:255'"
data-vv-name="username"
required>
</v-text-field>
<v-text-field
v-model="password"
name="password"
label="Password"
type="password"
v-validate="'required|min:8|max:20'"
data-vv-name="password"
required>
</v-text-field>
<v-text-field
v-model="passwordConfirm"
name="passwordConfirm"
label="Confirm password"
type="password"
v-validate="'required|min:8|max:20'"
data-vv-name="passwordConfirm"
required>
</v-text-field>
<p v-for="(error, index) in signupErrors"
:key="index"
class="signup-errors">
{{ error }}
</p>
<v-layout>
<v-flex md12 lg6 offset-lg3 >
<v-card class="signup-form-card">
<h3>Sign up</h3>
<form @submit="submit">
<v-text-field
v-model="username"
label="Username"
type="text"
required>
</v-text-field>
<v-text-field
v-model="email"
label="Email"
type="email"
required>
</v-text-field>
<v-text-field
v-model="password"
label="Password"
type="password"
required>
</v-text-field>
<v-text-field
v-model="passwordConfirm"
label="Confirm password"
type="password"
required>
</v-text-field>
<p v-for="(error, index) in signupErrors"
:key="index"
class="signup-errors">
{{ error }}
</p>
<v-btn type="submit">Signup</v-btn>
</form>
</v-card>
</v-flex>
</v-layout>
<v-btn type="submit">Signup</v-btn>
</form>
</v-card>
</v-flex>
</v-layout>
</template>
<script>
import AuthController from '../../controllers/auth.controller';
import AuthController from '../../controllers/auth.controller';
export default {
name: 'signup',
data() {
return {
username: '',
password: '',
passwordConfirm: '',
signupErrors: [],
};
},
methods: {
submit(event) {
event.preventDefault();
if (this.password !== this.passwordConfirm) {
this.signupErrors.push('Лозинке нису исте');
return;
}
export default {
name: 'signup',
data() {
return {
username: '',
email: '',
password: '',
passwordConfirm: '',
signupErrors: [],
};
},
methods: {
submit(event) {
event.preventDefault();
if (this.password !== this.passwordConfirm) {
this.signupErrors.push('Passwords do not match');
return;
}
const data = {
username: this.username,
password: this.password,
};
AuthController.register(data).then(() => {
this.$router.push({ name: 'index' });
}).catch((error) => {
if (error.response.status === 401) {
this.signupErrors.push(error.response.data);
}
});
},
},
};
const data = {
username: this.username,
email: this.email,
password: this.password,
};
AuthController.register(data).then(() => {
this.$router.push({ name: 'index' });
}).catch((error) => {
if (error.response) {
this.signupErrors.push(error.response.data);
}
});
},
},
};
</script>
<style lang="stylus">
.signup-form-card
padding 2rem
padding 2rem
.login-errors
color red
color red
</style>

View File

@ -15,16 +15,32 @@
<v-card-title
class="headline grey darken-2"
primary-title
>{{ perk }}</v-card-title>
<v-card-text>{{ effect }}</v-card-text>
>
{{ selectedPerk.name }}
<v-spacer></v-spacer>
{{ selectedPerk.level }}
</v-card-title>
<v-card-text>{{ selectedPerk.effect }}</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
color="error"
flat
@click="dialog = false"
>Close</v-btn>
<v-btn
v-if="canUnlock"
color="primary"
flat
@click="unlock()"
>Unlock</v-btn>
<v-btn
v-else-if="canLock"
color="warning"
flat
@click="lock()"
>Lock</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@ -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();
});
});
},
};

View File

@ -20,7 +20,7 @@
<script>
// import * as _ from 'lodash';
import PerksController from '../controllers/perks.controller';
import PerkController from '../controllers/perk.controller';
export default {
name: 'trees',
@ -37,7 +37,7 @@ export default {
},
},
mounted() {
PerksController.getTrees().then((response) => {
PerkController.getTrees().then((response) => {
this.trees = response.data;
});
},

View File

@ -49,7 +49,7 @@
<script>
import UploadButton from 'vuetify-upload-button';
import PerksController from '../controllers/perks.controller';
import PerkController from '../controllers/perk.controller';
export default {
name: 'admin-panel',
@ -71,7 +71,7 @@ export default {
methods: {
upload(file) {
this.loading = true;
PerksController.uploadPerks(file).then(() => {
PerkController.uploadPerks(file).then(() => {
this.loading = false;
this.$router.push({ name: 'trees' });
}).catch((error) => {

View File

@ -0,0 +1,28 @@
<template>
<v-container>
<h1>Dashboard</h1>
</v-container>
</template>
<script>
import AuthController from '../controllers/auth.controller';
export default {
name: 'Index',
components: {
},
data () {
return {
};
},
methods: {
},
mounted() {
AuthController.getUser().then((response) => {
console.log(response.data);
});
},
};
</script>

View File

@ -1,5 +1,5 @@
/*
* perks.controller.js
* auth.controller.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* 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('');
}

View File

@ -1,14 +1,13 @@
/*
* perks.controller.js
* perk.controller.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* 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);
}
}

View File

@ -0,0 +1,23 @@
/*
* user.controller.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* 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);
}
}

View File

@ -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);

View File

@ -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()) {

View File

@ -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;
},
};