Tuesday 11 June 2019

django 44 react redux chain actions

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


1 comment: