By Emmanuelle Delescolle
.
├── docs
│ └── img
├── front
│ ├── public
│ ├── src
│ └── tests
└── radioify
├── management
├── migrations
├── static
└── tests
Directory structure
.
├── docs
│ └── img
├── front
│ ├── public
│ ├── src
│ └── tests
└── radioify
├── management
├── migrations
├── static
└── tests
Starting your Vue project
npm install -g @vue/cli
vue create front
module.exports = {
devServer: {
proxy: {
'/api/*': {
target: 'http://localhost:8000/',
},
'/ws/*': {
target: 'http://localhost:8000/',
ws: true,
},
},
},
};
front/vue.config.js
From Vue to Django
...
urlpatterns += [
re_path(r'^(?P<url>.*)$', VueView.as_view()),
]
urls.py
From Django to Vue
...
class VueView(RedirectView):
url = "http://localhost:8080/%(url)s"
radioify/views.py
create
inspect
add
build
serve
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Home',
component: Home,
}, {
path: '/artists/:id',
name: 'Artists',
component: ArtistDetail,
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
export default router;
front/src/router/index.js
Similar to urls.py
<template src="./App.html" />
<script src="./App.js" />
<style src="./style/custom.scss" lang="scss"/>
front/src/App.vue
<div id="app" class="d-flex h-100">
<transition name="fade" mode="out-in">
<router-view
id="main"
:key="$route.name + ($route.params.id || '') + ($route.params.query || '')"
/>
</transition>
</div>
front/src/App.vue
import YTResult from '@/models/YTResult';
import router from '@/router';
export default {
name: 'YTButton',
components: {
},
props: {
itemId: String,
itemTitle: String,
},
data() {
return {
isBusy: false,
};
},
front/src/components/YTButton/component.js
methods: {
download() {
this.isBusy = true;
YTResult.download(this.itemId).then((res) => {
const { data } = res.response;
router.push({ name: 'Songs', params: { id: data.song_id } });
}).catch((err) => {
this.$bvToast.toast(`while downloading ${this.itemTitle}`, {
title: 'Something went wrong',
variant: 'danger',
});
}).finally(() => {
this.isBusy = false;
});
},
},
computed: {
icon() {
return this.isBusy ? 'spinner' : 'cloud-download-alt';
},
iconClass() {
return this.isBusy ? '' : 'text-info';
},
},
};
<button :disabled="isBusy" @click="download">
<font-awesome-icon :icon="icon" :spin="isBusy" :pulse="isBusy" :class="iconClass"/>
</button>
front/src/components/YTButton/template.html
class ArtistViewSet(ModelViewSet):
queryset = Artist.objects.all()
serializer_class ArtistSerializer
radioify/viewsets.py
class ArtistSerializer(ModelSerializer):
class Meta:
model = Artist
fields = ('id', '__str__', 'name', 'songs')
radioify/serializers.py
@register
class ArtistEndpoint(Endpoint):
model = Artist
extra_fields = ('songs', )
search_fields = ('name', )
filter_fields = ('songs__id', )
radioify/endpoints.py
const Axios = axios.create({
baseURL: config.api_base_url,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
secure: false,
withCredentials: false,
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFTOKEN',
});
Vue.prototype.$http = Axios;
front/src/store/index.js
VuexORM.use(VuexORMAxios, {
axios: Axios,
});
VuexORM.use(VuexORMisDirtyPlugin);
const database = new VuexORM.Database();
database.register(Artist);
front/src/store/index.js
Vue.use(Vuex);
const store = new Vuex.Store({
plugins: [VuexORM.install(database)],
});
export default store;
front/src/store/index.js
import Song from '@/models/Song';
import BaseModel from './Base';
export default class Artist extends BaseModel {
static entity = 'radioify/artists';
static fields() {
return {
id: this.attr(null),
name: this.string(''),
image: this.string(''),
bio: this.attr(),
info: this.attr(),
songs: this.hasMany(Song, 'artist'),
};
}
}
front/src/models/Artist.js
import { Model } from '@vuex-orm/core';
export default class BaseModel extends Model {
static fetchAll() {
return this.api().get(`${this.entity}/`, { dataKey: 'results' });
}
static fetch(id) {
return this.api().get(`${this.entity}/${id}/`);
}
static filter(params) {
return this.api().get(`${this.entity}/`, { params, dataKey: 'results' });
}
}
front/src/models/Base.js
front/src/components/ArtistDetail/template.html
<ul class="song-list">
<li v-for="song in artist.songs" :index="song.id">
<a title="Direct play" @click="playNow(song.file)">
<font-awesome-icon icon="play" class="text-success" />
</a>
</li>
</ul>
front/src/components/ArtistDetail/component.js
computed: {
artist() {
return Artist.query().with(['songs']).where('id', parseInt(this.$route.params.id, 10)).first();
},
},
routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import radioify.routing
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter(
radioify.routing.websocket_urlpatterns
)
)
})
radioify/routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/queue/(?P<username>\w+)/$', consumers.QueueConsumer),
]
front/src/main.js
Vue.use(VueNativeSock, `${protocol}://${window.location.host}${config.websocket_base_url}/queue/`,
connectManually: true,
format: 'json',
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 30000,
store,
});
front/src/store/index.js
const store = new Vuex.Store({
state: {
user,
socket: {
isConnected: false,
message: '',
reconnectError: false,
},
},
mutations: {
SOCKET_ONOPEN(state, event) {
Vue.prototype.$socket = event.currentTarget;
state.socket.isConnected = true;
},
},
});
radioify/views.py
class LoginView(APIView):
permission_classes = (AllowAny, )
def post(self, request, format=None):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
login(request, serializer.validated_data['user'])
user_serializer = UserSerializer(serializer.validated_data['user'])
return Response(user_serializer.data, status=200)
return Response(None, status=401)
class LogoutView(APIView):
permission_classes = (IsAuthenticated, )
def post(self, request, format=None):
logout(request)
return Response(status=204)
front/src/store/index.js
const store = new Vuex.Store({
mutations: {
auth_success(state, data) {
state.user = data;
state.user.isAuthenticated = true;
},
},
actions: {
login({ commit }, data) {
return new Promise((resolve, reject) => {
Axios({
url: '/auth/login/',
data,
method: 'POST',
}).then((resp) => {
commit('auth_success', resp.data);
resolve(resp);
}).catch((err) => {
commit('auth_error');
reject(err);
});
});
},
logout({ commit }) {
return new Promise((resolve, reject) => {
Axios({
url: '/auth/logout/',
data: {},
method: 'POST',
}).then(() => {
commit('auth_loggedout');
}).catch((err) => {
commit('auth_error');
reject(err);
});
});
},
},
});
front/src/router/index.js
const routes = [
{
path: '/podcasts/details/:id/',
name: 'Episodes',
component: Episodes,
meta: {
requiresAuth: true,
},
},
];
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth) && !store.state.user.isAuthenticated) {
next('/login/');
return;
}
next();
});