author table fetched, fetching album table
album table fetched, integrating author and album table
tables integrated
chained actions: token -> user -> album
#django/api/urls
from django.urls import path
from music.api import apiview
from rest_framework.authtoken import views
urlpatterns = [
path('album_list/', apiview.AlbumList.as_view(), name='AlbumList'),
path('user_list/', apiview.UserList.as_view(), name='UserList'),
path('api-token-auth/', views.obtain_auth_token, name='AuthToken'),
]
----------------------------
#django/api/serializers
from rest_framework import serializers
from music.models import Album
from django.contrib.auth.models import User
class MusicSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ('id', 'artist', 'album_title', 'genre', 'album_logo', 'date_posted', 'author')
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username')
--------------------------------------
#django/models
from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User
class Album(models.Model):
artist = models.CharField(max_length=50)
album_title = models.CharField(max_length=50)
genre = models.CharField(max_length=50)
album_logo = models.FileField()
date_posted = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
#form submitted without action redirect to detail
def get_absolute_url(self):
return reverse('music:detail', kwargs={'pk': self.pk})
#query album.objects.get(pk=1)
def __str__(self):
return self.album_title + ' - ' + self.artist
---------------------------------------
#django/api/apiview
from music.models import Album
from music.api.serializers import MusicSerializer, UserSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.http import Http404
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
class AlbumList(APIView):
def get(self, request, format=None):
username = request.GET.get('author')
data_length = request.GET.get('data_length')
#filter by author
if username==None:
albums = Album.objects.all()
else:
author_id = get_object_or_404(User, username=username).pk
albums = Album.objects.filter(author=author_id).order_by('-date_posted')
#filter by data length
if data_length!=None:
try:
int(data_length)
except ValueError:
return Response('data length is invvalid', status=status.HTTP_406_NOT_ACCEPTABLE)
else:
albums = albums[:int(data_length)]
serializer = MusicSerializer(albums, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = MusicSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AlbumDetail(APIView):
def get_object(self, pk):
try:
return Album.objects.get(pk=pk)
except Album.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
album = self.get_object(pk)
serializer = MusicSerializer(album)
return Response(serializer.data)
def put(self, request, pk, format=None):
album = self.get_object(pk)
#only owner can edit
if album.author != request.user:
return Response({"detail": "You do not have permission to perform this action."},
status= status.HTTP_403_FORBIDDEN)
serializer = MusicSerializer(album, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
album = self.get_object(pk)
# only owner can delete
if album.author != request.user:
return Response({"detail": "You do not have permission to perform this action."},
status=status.HTTP_403_FORBIDDEN)
album.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class UserList(APIView):
def get(self, request, format=None):
users = User.objects.all()
serializer = UserSerializer(users, many=True)
return Response(serializer.data)
//react/pages/album.js
import React, { Component } from 'react';
import '../App.css';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';
import ReactTable from "react-table";
import "react-table/react-table.css";
import { Table } from 'reactstrap';
class Albums extends Component {
constructor(props) {
super(props);
this.state = {
AlbumAuthorData: [],
AlbumAuthorIntegrated: false,
};
}
componentDidMount() {
if (this.props.loggedin) {
this.props.dispatch(getUsers(this.props.token));
}
//check every 2 second to see if users are fetched. If so, start fetching Albums
const waitGetUsers = setInterval(() => {
if (this.props.gotUsers) {
this.props.dispatch(getAlbums(this.props.token));
clearInterval(waitGetUsers);
}
}, 2000)
//check every 5 second to see if albums are fetched. If so, start integrating Albums and Authors
const waitGetAlbums = setInterval(() => {
if (this.props.gotAlbums) {
let newAlbums = []
this.props.albums.map((album, index) => {
const newAlbum = {
'id': album.id,
'album_title': album.album_title,
'artist': album.artist,
'genre': album.genre,
'date_posted': album.date_posted,
'author': this.props.users.filter(author => author.id == album.author)[0].username
}
newAlbums.push(newAlbum);
});
this.setState({ AlbumAuthorData: newAlbums, AlbumAuthorIntegrated: true });
clearInterval(waitGetAlbums);
}
}, 5000)
}
componentDidUpdate() {
}
render() {
if (!this.props.loggedin) {
return <Redirect to='/login' />
}
const userTableContent = [];
if (this.props.gotUsers) {
this.props.users.forEach(user => {
userTableContent.push(
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.username}</td>
</tr>
)
});
}
return (
<div style={{ marginTop: '60px' }}>
{this.props.error}
{
this.props.gotUsers ?
<div style={{ width: '50%' }}>
<h6 style={{ textAlign: 'center' }}>Author Table</h6>
<Table bordered size='sm'>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{userTableContent}
</tbody>
</Table>
</div>
: <span>fetching authors...</span>
}
{
this.props.gotAlbums ?
<ReactTable
data={this.props.albums}
columns={[
{
Header: "Album Table",
columns: [
{
Header: "ID",
accessor: "id"
},
{
Header: "Title",
accessor: "album_title"
},
{
Header: "Artist",
accessor: "artist"
},
{
Header: "Genre",
accessor: "genre"
},
{
Header: "Author",
accessor: "author"
},
{
Header: "Posted",
accessor: "date_posted"
},
]
},
]}
defaultPageSize={20}
style={{
height: "400px", // This will force the table body to overflow and scroll, since there is not enough room
marginTop: '20px'
}}
className="-striped -highlight"
/>
: <p>fetching albums...</p>
}
{
this.state.AlbumAuthorIntegrated ?
<ReactTable
data={this.state.AlbumAuthorData}
columns={[
{
Header: "Album with Author Table",
columns: [
{
Header: "ID",
accessor: "id"
},
{
Header: "Title",
accessor: "album_title"
},
{
Header: "Artist",
accessor: "artist"
},
{
Header: "Genre",
accessor: "genre"
},
{
Header: "Author",
accessor: "author"
},
{
Header: "Posted",
accessor: "date_posted"
},
]
},
]}
defaultPageSize={20}
style={{
height: "400px", // This will force the table body to overflow and scroll, since there is not enough room
marginTop: '20px'
}}
className="-striped -highlight"
/>
: <p>integrating authors and albums...</p>
}
</div>
);
}
}
export default connect(
(store) => {
return {
token: store.login.token,
loggedin: store.login.fetched,
error: store.albums.error,
albums: store.albums.albums,
gotAlbums: store.albums.fetched,
users: store.users.users,
gotUsers: store.users.fetched,
};
}
)(Albums);
---------------------------------------------
//react/redux/actions/userAction
import axios from 'axios';
export function getUsers(token) {
return {
type: "fetch_users",
payload: axios({
method: 'get',
url: 'http://127.0.0.1:8000/api/user_list/',
headers: {
Authorization: 'Token ' + token
},
})
}
}
----------------------------------------
//react/redux/reducers/index
import { combineReducers } from 'redux';
import login from "./loginReduder";
import albums from "./albumReducer";
import users from "./userReducer";
export default combineReducers(
{
login, albums, users
})
-------------------------------
//react/redux/reducers/userReducer
export default function reducer(
state = {
users: [],
fetching: false,
fetched: false,
error: ''
},
action
) {
switch (action.type) {
case "fetch_users_PENDING": {
return { ...state, fetching: true, fetched: false }
}
case "fetch_users_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
users: action.payload.data,
error: ''
}
}
case "fetch_users_REJECTED": {
return {
...state,
fetching: false,
error: action.payload.toString()
}
}
default:
break;
}
return state;
}
----------------------------------------
//react/redux/store
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import promise from 'redux-promise-middleware';
import reducer from "./reducers";
const middleware = applyMiddleware(promise, thunk, logger);
export default createStore(reducer, middleware);
----------------------------------------------------
//react/partials/header
import React, { Component } from 'react';
import '../App.css';
import { Menu, Input } from 'antd';
import { Link } from 'react-router-dom'
import { FaCompactDisc, FaPlus, FaEdit, FaBarcode, FaUser, FaSearch } from "react-icons/fa";
import { GiLoveSong } from "react-icons/gi";
import { IoIosLogOut, IoIosLogIn } from "react-icons/io";
import { connect } from 'react-redux';
import { reset } from '../redux/actions/logoutAction'
class Header extends Component {
constructor(props) {
super(props);
this.state = {
menu_mode: null,
current: 'albums',
};
}
componentWillMount() {
//change menu mode based on screen width
const mode = window.innerWidth > 434 ? "horizontal" : "inline";
this.setState({ menu_mode: mode });
}
handleClick = (e) => {
this.setState({
current: e.key,
});
}
logout = () => {
this.props.dispatch(reset())
}
render() {
return (
<div style={{ position: 'sticky', top: 0, width: '100%', zIndex: 2 }}>
<Menu
style={{ backgroundColor: '#fcefe5' }}
onClick={this.handleClick}
selectedKeys={[this.state.current]}
mode={this.state.menu_mode}
>
<Menu.Item key="home">
<Link to='/'><span className='Nav-Brand'>Django</span></Link>
</Menu.Item>
<Menu.Item key="albums">
<Link to='/'> <FaCompactDisc /> Albums</Link>
</Menu.Item>
<Menu.Item key="songs">
<Link to='/'> <GiLoveSong /> Songs</Link>
</Menu.Item>
<Menu.Item key="search">
<Input type='text' size='small'
suffix={<FaSearch style={{ color: 'rgba(0,0,0,.25)', cursor: 'text' }} />}
placeholder='Search'
style={{ width: 150 }}></Input>
</Menu.Item>
{
this.props.loggedin ?
<Menu.SubMenu title={<span><FaUser /> {this.props.username}</span>} style={{ float: 'right' }}>
<Menu.Item key="0" onClick={() => this.logout()}>
<Link to='/logout'> <IoIosLogOut /> Logout</Link>
</Menu.Item>
<Menu.Item key="1">
<Link to='/'> <FaEdit /> Update Profile</Link>
</Menu.Item>
<Menu.Item key="2">
<Link to='/'> <FaBarcode /> Change Password</Link>
</Menu.Item>
</Menu.SubMenu>
:
<Menu.Item key="login" style={{ float: 'right' }}>
<Link to='/login'> <IoIosLogIn /> Login</Link>
</Menu.Item>
}
<Menu.Item key="add" style={{ float: 'right' }}>
<Link to='/'> <FaPlus /> Add Album</Link>
</Menu.Item>
</Menu>
</div>
);
}
}
export default connect(
(store) => {
return {
username: store.login.username,
token: store.login.token,
loggingin: store.login.fetching,
loggedin: store.login.fetched,
error: store.login.error
};
}
)(Header);
reference:
https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559
https://www.w3schools.com/jsref/met_win_setinterval.asp
http://chuanshuoge2.blogspot.com/2019/06/django-42-react-redux-fetch-data-with.html
http://chuanshuoge2.blogspot.com/2019/05/django-38-rest-framework-authentication.html
This comment has been removed by the author.
ReplyDelete