Monday 24 June 2019

django 52 rest framework change user password

send put request to http://127.0.0.1:8000/api/update_password/ with old and new password
new password failed validation

old password not match

pass

login

success
#api/serializer

from rest_framework import serializers
from django.contrib.auth.password_validation import validate_password

class PasswordSerializer(serializers.Serializer):
    old_password = serializers.CharField(required=True)
    new_password = serializers.CharField(required=True)

    def validate_new_password(self, value):
        validate_password(value)
        return value

-----------------------------------------
#api/apiview

from music.api.serializers import PasswordSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404

class UpdatePassword(APIView):
    def get_object(self, queryset=None):
        return self.request.user

    def put(self, request, *args, **kwargs):
        currentUser = self.get_object()
        serializer = PasswordSerializer(data=request.data)

        if serializer.is_valid():
            # Check old password
            old_password = serializer.data.get("old_password")
            if not currentUser.check_password(old_password):
                return Response({"old_password": ["Wrong password."]},
                                status=status.HTTP_400_BAD_REQUEST)
            # set_password also hashes the password that the user will get
            currentUser.set_password(serializer.data.get("new_password"))
            currentUser.save()
            return Response(status=status.HTTP_204_NO_CONTENT)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

--------------------------------
#api/urls

from django.urls import path
from music.api import apiview
from rest_framework.authtoken import views

app_name = 'musicAPI'

urlpatterns = [
    path('api-token-auth/', views.obtain_auth_token, name='AuthToken'),
    path('update_password/', apiview.UpdatePassword.as_view(), name='UpdatePassword'),
]


reference:
https://stackoverflow.com/questions/38845051/how-to-update-user-password-in-django-rest-framework

Sunday 23 June 2019

django 51 react pagination

initial page

change to 8 posts/page

go to page 3

go to last page
//pages/album

import React, { Component } from 'react';
import '../App.css';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import FlippyCard from '../partials/flippyCard';
import { Row, Col } from 'reactstrap'
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';
import { getSongs } from '../redux/actions/getSongs';
import { deleteAlbum } from '../redux/actions/deleteAlbum';
import { message } from 'antd';
import { Button } from 'reactstrap';

class Albums extends Component {
    constructor(props) {
        super(props);

        this.state = {
            AlbumAuthorData: [],
            AlbumAuthorIntegrated: false,
            postsPerPage: 4,
            currentPage: 1,
        };
    }

    componentDidMount() {

        //start fetching database once logged in
        if (this.props.loggedin) {
            if (!this.props.gotUsers) {
                this.props.dispatch(getUsers(this.props.token));
            }
            if (!this.props.gotAlbums) {
                this.props.dispatch(getAlbums(this.props.token));
            }
            if (!this.props.gotSongs) {
                this.props.dispatch(getSongs(this.props.token));
            }
        }

        this.RefreshAlbum();
    }

    RefreshAlbum = () => {
        //check every 1 second to see if albums are fetched. If so, start integrating Albums and Authors
        const waitGetAlbums = setInterval(() => {
            if (this.props.gotAlbums && this.props.gotUsers) {
                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,
                        'album_logo': album.album_logo
                    }

                    newAlbums.push(newAlbum);
                });

                this.setState({ AlbumAuthorData: newAlbums, AlbumAuthorIntegrated: true });
                clearInterval(waitGetAlbums);
            }
        }, 100)
    }

    confirmDelete = (id) => {
        this.props.dispatch(deleteAlbum(this.props.token, id));

        //wait for 5sec, check every sec to see if delete successful
        let i = 0;
        const waitDelete = setInterval(() => {
            if (this.props.albumDeleted) {
                message.success('Album deleted.')
                //refresh database
                this.RefreshAlbum();

                clearInterval(waitDelete);
            }
            if (i == 50) {
                message.error(this.props.errorAlbum + ' timed out.')
                clearInterval(waitDelete);
            }
            i++;
        }, 100)
    }

    changePostPerPage = (e) => {
        this.setState({ currentPage: 1, postsPerPage: parseInt(e.target.value) });
    }

    previousClick = () => {
        this.setState((prevState) => {
            const { currentPage } = prevState;
            return { currentPage: currentPage <= 1 ? 1 : currentPage - 1 }
        })
    }

    nextClick = () => {
        this.setState((prevState) => {
            const { currentPage, AlbumAuthorData, postsPerPage } = prevState;
            return {
                currentPage: currentPage * postsPerPage >= AlbumAuthorData.length ? currentPage : currentPage + 1
            }
        })
    }

    firstClick = () => {
        this.setState({ currentPage: 1 })
    }

    lastClick = () => {
        const { AlbumAuthorData, postsPerPage } = this.state;
        const lastPage = parseInt(AlbumAuthorData.length / postsPerPage) + 1;
        this.setState({ currentPage: lastPage })
    }

    pageClick = (page) => {
        this.setState({ currentPage: page })
    }

    render() {
        if (!this.props.loggedin) {
            return <Redirect to='/login' />
        }

        const cards = [];

        if (this.state.AlbumAuthorIntegrated) {
            this.state.AlbumAuthorData.map((item, index) => {
                const card = <Col key={index}>
                    <FlippyCard data={item} confirmDelete={(id) => this.confirmDelete(id)}></FlippyCard>
                </Col>

                cards.push(card);
            })
        }

        const { currentPage, postsPerPage, AlbumAuthorData } = this.state;
        const arrayStart = (currentPage - 1) * postsPerPage;
        const arrayEnd = arrayStart + postsPerPage < cards.length ? arrayStart + postsPerPage : cards.length;
        const cardsPagination = cards.slice(arrayStart, arrayEnd);

        return (
            <div style={{ marginTop: '20px' }}>
                {this.props.errorAlbum} {this.props.errorSong} {this.props.errorUser}

                {
                    this.state.AlbumAuthorIntegrated ?
                        <Row>
                            {cardsPagination}
                        </Row>
                        : <p>integrating authors and albums...</p>
                }

                <div style={{ display: 'flex', justifyContent: 'center', marginTop: '10px' }}>
                    <Button onClick={() => this.firstClick()} color='secondary' style={{ marginRight: '5px' }} size='sm'>{'<<'}</Button>
                    <Button onClick={() => this.previousClick()} color='secondary' style={{ marginRight: '5px' }} size='sm'>{'<'}</Button>
                    {currentPage >= 3 ? <Button onClick={() => this.pageClick(currentPage - 2)} color='secondary' style={{ marginRight: '5px' }} size='sm'>{this.state.currentPage - 2}</Button> : null}
                    {currentPage >= 2 ? <Button onClick={() => this.pageClick(currentPage - 1)} color='secondary' style={{ marginRight: '5px' }} size='sm'>{this.state.currentPage - 1}</Button> : null}
                    <Button color='primary' style={{ marginRight: '5px' }} size='sm'>{this.state.currentPage}</Button>
                    {currentPage * postsPerPage < AlbumAuthorData.length ? <Button onClick={() => this.pageClick(currentPage + 1)} color='secondary' style={{ marginRight: '5px' }} size='sm'>{this.state.currentPage + 1}</Button> : null}
                    {(currentPage + 1) * postsPerPage < AlbumAuthorData.length ? <Button onClick={() => this.pageClick(currentPage + 2)} color='secondary' style={{ marginRight: '5px' }} size='sm'>{this.state.currentPage + 2}</Button> : null}
                    <Button onClick={() => this.nextClick()} color='secondary' style={{ marginRight: '5px' }} size='sm'>{'>'}</Button>
                    <Button onClick={() => this.lastClick()} color='secondary' style={{ marginRight: '5px' }} size='sm'>{'>>'}</Button>

                    <div style={{ display: 'flex' }}>
                        <select style={{ marginRight: '5px' }} onChange={(e) => this.changePostPerPage(e)}>
                            <option value={2}>2 Posts/Page</option>
                            <option value={3}>3 Posts/Page</option>
                            <option value={4} selected>4 Posts/Page</option>
                            <option value={6}>6 Posts/Page</option>
                            <option value={8}>8 Posts/Page</option>
                            <option value={10}>10 Posts/Page</option>
                        </select>
                    </div>
                </div>
            </div>
        );
    }
}

export default connect(
    (store) => {
        return {
            token: store.login.token,
            loggedin: store.login.fetched,
            errorAlbum: store.albums.error,
            errorSong: store.songs.error,
            errorUser: store.users.error,
            albums: store.albums.albums,
            gotAlbums: store.albums.fetched,
            songs: store.songs.songs,
            gotSongs: store.songs.fetched,
            users: store.users.users,
            gotUsers: store.users.fetched,
            albumDeleted: store.albums.deleted,
        };
    }
)(Albums);

reference:
http://chuanshuoge2.blogspot.com/2019/06/django-45-react-flippy-card-with-image.html

Saturday 22 June 2019

django 50 react redux update album

initial page

click edit button

 redirected to update page, album info are auto filled except file attached

change second text input, and attach picture, submit

album title updated

log in as bob. try to update chuanshuo
#django/api/apiview

from music.models import Album, Song
from music.api.serializers import MusicSerializer, UserSerializer, SongSerializer
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)

class SongList(APIView):
    def get(self, request, format=None):
        songs = Song.objects.all()
        serializer = SongSerializer(songs, many=True)
        return Response(serializer.data)

--------------------------------------
#django/api/urls

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from music.api import apiview, mixins
from rest_framework.authtoken import views

app_name = 'musicAPI'

urlpatterns = [
    path('album_list/', apiview.AlbumList.as_view(), name='AlbumList'),
    path('user_list/', apiview.UserList.as_view(), name='UserList'),
    path('song_list/', apiview.SongList.as_view(), name='SongList'),
    path('album_detail/<int:pk>/', apiview.AlbumDetail.as_view(), name='AlbumDetail'),
    path('api-token-auth/', views.obtain_auth_token, name='AuthToken'),
]


//partials/body

import React, { Component } from 'react';
import '../App.css';
import { Switch, Route } from 'react-router-dom';
import Albums from '../pages/albums';
import Login from '../pages/login';
import Logout from '../pages/logout';
import AlbumDetail from '../pages/albumDetail';
import AlbumForm from '../pages/albumForm';

export default class Body extends Component {

    render() {
        return (
            <main className='Body-Background'>
                <Switch>
                    <Route exact path='/' component={Albums} />
                    <Route path='/login/' component={Login} />
                    <Route path='/logout/' component={Logout} />
                    <Route path='/albumDetail/' component={AlbumDetail} />
                    <Route path='/addAlbum/' component={AlbumForm} />
                    <Route path='/updateAlbum/' component={AlbumForm} />
                </Switch>
            </main>
        );
    }
}

-----------------------------
//partials/flippyCard

import React, { Component } from 'react';
import '../App.css';
import { FaPenNib } from "react-icons/fa";
import { Button } from 'reactstrap';
import { MdEdit, MdDelete, MdFlip } from "react-icons/md";
import { Link } from 'react-router-dom';
import { Popconfirm } from 'antd';

export default class FlippyCard extends Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }

    flipClick = () => {
        this.myRef.current.classList.toggle('manu-flip');
        console.log('Album: ', this.props.data.album_title, ' DOM class: ', this.myRef.current.classList);
    }

    render() {
        const imgSrc = 'http://127.0.0.1:8000' + this.props.data.album_logo;
        const detailLink = '/albumDetail/' + this.props.data.id;
        const updateLink = '/updateAlbum/' + this.props.data.id;

        return (
            <div className="flip-container" ref={this.myRef} onTouchStart={() => this.flipClick()} style={{ margin: '10px' }}>
                <div className="flipper">
                    <div className="front" style={{ backgroundColor: 'white' }}>
                        <img src={imgSrc}
                            style={{ height: '150px', width: '100%' }}></img>
                        <div style={{ height: '60px' }}>
                            <h6 style={{ textAlign: 'center', marginTop: '10px' }}>{this.props.data.album_title}</h6>
                        </div>
                        <div style={{ backgroundColor: '#F4F6F6', height: '30px', paddingLeft: '5px' }}>
                            <FaPenNib /> <span>{this.props.data.author}</span>
                        </div>
                        <MdFlip className='flip-button' onClick={() => this.flipClick()} />
                    </div>
                    <div className="back" style={{ backgroundColor: 'white' }}>
                        <div style={{ textAlign: 'center', height: '220px', paddingTop: '10px' }}>
                            <p>Artist: {this.props.data.artist}</p>
                            <p>Genre: {this.props.data.genre}</p>
                            <div style={{ display: 'flex', justifyContent: 'space-around', marginLeft: '5px', marginRight: '5px' }}>
                                <Link to={detailLink}><Button color='primary' size='sm'>View Detail</Button></Link>
                                <Link to={updateLink}><Button outline size='sm'><MdEdit /></Button></Link>
                                <Popconfirm
                                    title="Are you sure to delete?"
                                    onConfirm={() => this.props.confirmDelete(this.props.data.id)}
                                    okText="Yes"
                                    cancelText="No"
                                >
                                    <Button outline size='sm'><MdDelete /></Button>
                                </Popconfirm>
                            </div>
                        </div>
                        <div style={{ backgroundColor: '#F4F6F6', height: '30px', paddingLeft: '5px' }}>
                            <FaPenNib /> <span>{this.props.data.date_posted.split('.')[0].replace('T', ' ')}</span>
                        </div>
                        <MdFlip className='flip-button' onClick={() => this.flipClick()} />
                    </div>
                </div>
            </div>
        );
    }
}

----------------------------------
//pages/albumForm

import React, { Component } from 'react';
import '../App.css';
import { Input } from 'antd';
import { FaOpencart, FaLyft, FaSignature } from "react-icons/fa";
import { Button } from 'reactstrap';
import { connect } from 'react-redux';
import { postAlbum } from '../redux/actions/postAlbum';
import { putAlbum } from '../redux/actions/putAlbum';
import { Redirect } from 'react-router-dom';
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';

class albumForm extends Component {
    constructor(props) {
        super(props);

        this.state = {
            artist: '',
            album_title: '',
            genre: '',
            album_logo: null,
            message: <p></p>,
            mode: 'add',
        };
    }

    componentDidMount() {
        if (this.props.loggedin) {
            if (!this.props.gotUsers) {
                this.props.dispatch(getUsers(this.props.token));
            }
            if (!this.props.gotAlbums) {
                this.props.dispatch(getAlbums(this.props.token));
            }
        }

        //detecting if add or update
        const pathName = window.location.pathname.split('/');
        const albumId = pathName[pathName.length - 1];

        if (albumId == 'addAlbum') {
            //add
            this.setState({ mode: 'add' })
        } else {
            //update
            const album_id = parseInt(albumId);

            const waitGetAlbums = setInterval(() => {
                if (this.props.gotAlbums) {
                    const album = this.props.albums.filter(album => album.id == album_id)[0];

                    this.setState({
                        mode: 'update',
                        artist: album.artist,
                        album_title: album.album_title,
                        genre: album.genre,
                        album_logo: album.album_logo,
                    })
                    clearInterval(waitGetAlbums);
                }
            }, 100)
        }
    }

    inputChange = (e, p) => {
        this.setState({ [p]: e.target.value });
    }

    fileChange = (e) => {
        console.log(e.target.files);
        this.setState({ album_logo: e.target.files[0] })
    }

    formSubmit = (e) => {
        e.preventDefault();

        const formData = new FormData();
        formData.set('artist', this.state.artist);
        formData.set('album_title', this.state.album_title);
        formData.set('genre', this.state.genre);
        formData.set('author', this.props.users.filter(user => user.username == this.props.username)[0].id);
        formData.append('album_logo', this.state.album_logo);

        if (this.state.mode == 'add') {
            this.props.dispatch(postAlbum(this.props.token, formData))

            //wait for 5sec, check every sec to see if post successful
            let i = 0;
            const waitPost = setInterval(() => {
                if (this.props.added) {
                    this.setState({ message: <p style={{ color: 'green' }}>Post successful</p> })
                    clearInterval(waitPost);
                }
                if (i == 50) {
                    this.setState({ message: <p style={{ color: 'red' }}>Post timeout</p> })
                    clearInterval(waitPost);
                }
                i++;
            }, 100)
        }
        else {
            //update
            const pathName = window.location.pathname.split('/');
            const albumId = parseInt(pathName[pathName.length - 1]);

            this.props.dispatch(putAlbum(this.props.token, albumId, formData));

            let i = 0;
            const waitPost = setInterval(() => {
                if (this.props.updated) {
                    this.setState({ message: <p style={{ color: 'green' }}>Update successful</p> })
                    clearInterval(waitPost);
                }
                if (i == 50) {
                    this.setState({ message: <p style={{ color: 'red' }}>Update timeout</p> })
                    clearInterval(waitPost);
                }
                i++;
            }, 100)
        }
    }

    render() {
        if (!this.props.loggedin) {
            return <Redirect to='/login' />
        }

        return (
            <form style={{ marginLeft: '10px', width: '300px' }}
                onSubmit={(e) => this.formSubmit(e)}>
                <legend>Album Form</legend>
                <hr />
                <p style={{ color: 'red' }}>{this.props.error}</p>
                {this.state.message}

                <Input placeholder={this.state.mode == 'add' ? "artist" : this.state.artist}
                    required={this.state.mode == 'add' ? true : false}
                    prefix={<FaOpencart style={{ color: 'rgba(0,0,0,.25)' }} />}
                    onChange={(e) => this.inputChange(e, 'artist')}
                    style={{ marginTop: '5px' }}
                />
                <Input placeholder={this.state.mode == 'add' ? "album title" : this.state.album_title}
                    required={this.state.mode == 'add' ? true : false}
                    prefix={<FaSignature style={{ color: 'rgba(0,0,0,.25)' }} />}
                    onChange={(e) => this.inputChange(e, 'album_title')}
                    style={{ marginTop: '15px' }}
                />
                <Input placeholder={this.state.mode == 'add' ? "genre" : this.state.genre}
                    required={this.state.mode == 'add' ? true : false}
                    prefix={<FaLyft style={{ color: 'rgba(0,0,0,.25)' }} />}
                    onChange={(e) => this.inputChange(e, 'genre')}
                    style={{ marginTop: '15px' }}
                />
                <div style={{ marginTop: '15px' }}>
                    Album Logo:
                    <input type='file' required='required'
                        onChange={(e) => this.fileChange(e)} />
                </div>
                <Button color="success" type='submit' size='sm'
                    style={{ marginTop: '15px' }}
                >Submit</Button>
            </form>
        );
    }
}

export default connect(
    (store) => {
        return {
            token: store.login.token,
            loggedin: store.login.fetched,
            added: store.albums.added,
            gotAlbums: store.albums.fetched,
            albums: store.albums.albums,
            gotUsers: store.users.fetched,
            users: store.users.users,
            username: store.login.username,
            error: store.albums.error,
            updated: store.albums.updated,
        };
    }
)(albumForm);

------------------------------------
//redux/actions/putAlbum

import axios from 'axios';

export function putAlbum(token, id, data) {
    return {
        type: "update_album",
        payload: axios({
            method: 'put',
            url: 'http://127.0.0.1:8000/api/album_detail/' + id.toString() + '/',
            headers: {
                Authorization: 'Token ' + token,
            },
            data: data,
        })
    }
}

--------------------------------
//redux/reducers/albumReducer

export default function reducer(
    state = {
        albums: [],
        fetching: false,
        fetched: false,
        adding: false,
        added: false,
        deleting: false,
        deleted: false,
        updating: false,
        updated: false,
        error: ''
    },
    action
) {
    switch (action.type) {
        case "fetch_albums_PENDING": {
            return { ...state, fetching: true, fetched: false, error: '' }
        }

        case "fetch_albums_FULFILLED": {
            return {
                ...state,
                fetching: false,
                fetched: true,
                albums: action.payload.data,
                error: ''
            }
        }

        case "fetch_albums_REJECTED": {
            return {
                ...state,
                fetching: false,
                error: action.payload.toString()
            }
        }

        case "add_album_PENDING": {
            return { ...state, adding: true, added: false, error: '' }
        }

        case "add_album_FULFILLED": {
            return {
                ...state,
                adding: false,
                added: true,
                albums: [...state.albums].concat(action.payload.data),
                error: ''
            }
        }

        case "add_album_REJECTED": {
            return {
                ...state,
                adding: false,
                error: action.payload.toString()
            }
        }

        case "delete_album_PENDING": {
            return { ...state, deleting: true, deleted: false, error: '' }
        }

        case "delete_album_FULFILLED": {
            const deleteId = action.payload.config.data;

            return {
                ...state,
                deleting: false,
                deleted: true,
                albums: [...state.albums].filter(album => album.id != deleteId),
                error: ''
            }
        }

        case "delete_album_REJECTED": {
            return {
                ...state,
                deleting: false,
                error: action.payload.toString()
            }
        }

        case "update_album_PENDING": {
            return { ...state, updating: true, updated: false, error: '' }
        }

        case "update_album_FULFILLED": {
            const data = action.payload.data;

            return {
                ...state,
                updating: false,
                updated: true,
                albums: [...state.albums].filter(album => album.id != data.id).concat(data),
                error: ''
            }
        }

        case "update_album_REJECTED": {
            return {
                ...state,
                updating: false,
                error: action.payload.toString()
            }
        }

        default:
            break;
    }
    return state;
}

reference:
https://chuanshuoge2.blogspot.com/2019/05/django-36-rest-framework-class-based.html
http://chuanshuoge2.blogspot.com/2019/06/django-49-react-redux-delete-album.html

Friday 21 June 2019

django 49 react redux delete album

initial page

click delete

confirm

receive success message

album deleted after auto refresh is done

login as bob. try to delete chuanshuo's post, denied.

#django/api/apiview

from music.models import Album, Song
from music.api.serializers import MusicSerializer, UserSerializer, SongSerializer
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)

class SongList(APIView):
    def get(self, request, format=None):
        songs = Song.objects.all()
        serializer = SongSerializer(songs, many=True)
        return Response(serializer.data)

--------------------------------
# django/api/urls

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from music.api import apiview, mixins
from rest_framework.authtoken import views

app_name = 'musicAPI'

urlpatterns = [
    path('album_list/', apiview.AlbumList.as_view(), name='AlbumList'),
    path('user_list/', apiview.UserList.as_view(), name='UserList'),
    path('song_list/', apiview.SongList.as_view(), name='SongList'),
    path('album_detail/<int:pk>/', apiview.AlbumDetail.as_view(), name='AlbumDetail'),
    path('api-token-auth/', views.obtain_auth_token, name='AuthToken'),
]



//partials/flippyCard

import React, { Component } from 'react';
import '../App.css';
import { FaPenNib } from "react-icons/fa";
import { Button } from 'reactstrap';
import { MdEdit, MdDelete, MdFlip } from "react-icons/md";
import { Link } from 'react-router-dom';
import { Popconfirm } from 'antd';

export default class FlippyCard extends Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }

    flipClick = () => {
        this.myRef.current.classList.toggle('manu-flip');
        console.log('Album: ', this.props.data.album_title, ' DOM class: ', this.myRef.current.classList);
    }

    render() {
        const imgSrc = 'http://127.0.0.1:8000' + this.props.data.album_logo;
        const detailLink = '/albumDetail/' + this.props.data.id;

        return (
            <div className="flip-container" ref={this.myRef} onTouchStart={() => this.flipClick()} style={{ margin: '10px' }}>
                <div className="flipper">
                    <div className="front" style={{ backgroundColor: 'white' }}>
                        <img src={imgSrc}
                            style={{ height: '150px', width: '100%' }}></img>
                        <div style={{ height: '60px' }}>
                            <h6 style={{ textAlign: 'center', marginTop: '10px' }}>{this.props.data.album_title}</h6>
                        </div>
                        <div style={{ backgroundColor: '#F4F6F6', height: '30px', paddingLeft: '5px' }}>
                            <FaPenNib /> <span>{this.props.data.author}</span>
                        </div>
                        <MdFlip className='flip-button' onClick={() => this.flipClick()} />
                    </div>
                    <div className="back" style={{ backgroundColor: 'white' }}>
                        <div style={{ textAlign: 'center', height: '220px', paddingTop: '10px' }}>
                            <p>Artist: {this.props.data.artist}</p>
                            <p>Genre: {this.props.data.genre}</p>
                            <div style={{ display: 'flex', justifyContent: 'space-around', marginLeft: '5px', marginRight: '5px' }}>
                                <Button color='primary' size='sm'><Link to={detailLink} style={{ color: 'white' }}>View Detail</Link></Button>
                                <Button outline size='sm'><MdEdit /></Button>
                                <Popconfirm
                                    title="Are you sure to delete?"
                                    onConfirm={() => this.props.confirmDelete(this.props.data.id)}
                                    okText="Yes"
                                    cancelText="No"
                                >
                                    <Button outline size='sm'><MdDelete /></Button>
                                </Popconfirm>
                            </div>
                        </div>
                        <div style={{ backgroundColor: '#F4F6F6', height: '30px', paddingLeft: '5px' }}>
                            <FaPenNib /> <span>{this.props.data.date_posted.split('.')[0].replace('T', ' ')}</span>
                        </div>
                        <MdFlip className='flip-button' onClick={() => this.flipClick()} />
                    </div>
                </div>
            </div>
        );
    }
}

--------------------------------------------
//pages/album

import React, { Component } from 'react';
import '../App.css';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import FlippyCard from '../partials/flippyCard';
import { Row, Col } from 'reactstrap'
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';
import { getSongs } from '../redux/actions/getSongs';
import { deleteAlbum } from '../redux/actions/deleteAlbum';
import { message } from 'antd';

class Albums extends Component {
    constructor(props) {
        super(props);

        this.state = {
            AlbumAuthorData: [],
            AlbumAuthorIntegrated: false,
        };
    }

    componentDidMount() {

        //start fetching database once logged in
        if (this.props.loggedin) {
            if (!this.props.gotUsers) {
                this.props.dispatch(getUsers(this.props.token));
            }
            if (!this.props.gotAlbums) {
                this.props.dispatch(getAlbums(this.props.token));
            }
            if (!this.props.gotSongs) {
                this.props.dispatch(getSongs(this.props.token));
            }
        }

        this.RefreshAlbum();
    }

    RefreshAlbum = () => {
        //check every 1 second to see if albums are fetched. If so, start integrating Albums and Authors
        const waitGetAlbums = setInterval(() => {
            if (this.props.gotAlbums && this.props.gotUsers) {
                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,
                        'album_logo': album.album_logo
                    }

                    newAlbums.push(newAlbum);
                });

                this.setState({ AlbumAuthorData: newAlbums, AlbumAuthorIntegrated: true });
                clearInterval(waitGetAlbums);
            }
        }, 1000)
    }

    confirmDelete = (id) => {
        this.props.dispatch(deleteAlbum(this.props.token, id));

        //wait for 5sec, check every sec to see if delete successful
        let i = 0;
        const waitDelete = setInterval(() => {
            if (this.props.albumDeleted) {
                message.success('Album deleted.')
                //refresh database
                this.RefreshAlbum();

                clearInterval(waitDelete);
            }
            if (i == 5) {
                message.error(this.props.errorAlbum + ' timed out.')
                clearInterval(waitDelete);
            }
            i++;
        }, 1000)
    }

    render() {
        if (!this.props.loggedin) {
            return <Redirect to='/login' />
        }

        const cards = [];

        if (this.state.AlbumAuthorIntegrated) {
            this.state.AlbumAuthorData.map((item, index) => {
                const card = <Col key={index}>
                    <FlippyCard data={item} confirmDelete={(id) => this.confirmDelete(id)}></FlippyCard>
                </Col>

                cards.push(card);
            })
        }

        return (
            <div style={{ marginTop: '20px' }}>
                {this.props.errorAlbum} {this.props.errorSong} {this.props.errorUser}

                {
                    this.state.AlbumAuthorIntegrated ?
                        <Row>
                            {cards}
                        </Row>
                        : <p>integrating authors and albums...</p>
                }
            </div>
        );
    }
}

export default connect(
    (store) => {
        return {
            token: store.login.token,
            loggedin: store.login.fetched,
            errorAlbum: store.albums.error,
            errorSong: store.songs.error,
            errorUser: store.users.error,
            albums: store.albums.albums,
            gotAlbums: store.albums.fetched,
            songs: store.songs.songs,
            gotSongs: store.songs.fetched,
            users: store.users.users,
            gotUsers: store.users.fetched,
            albumDeleted: store.albums.deleted,
        };
    }
)(Albums);

-------------------------------------
//redux/actions/deleteAlbum

import axios from 'axios';

export function deleteAlbum(token, id) {
    return {
        type: "delete_album",
        payload: axios({
            method: 'delete',
            url: 'http://127.0.0.1:8000/api/album_detail/' + id.toString() + '/',
            headers: {
                Authorization: 'Token ' + token
            },
            data: id,
        })
    }
}

------------------------------------
//redux/reducers/albumReducer

export default function reducer(
    state = {
        albums: [],
        fetching: false,
        fetched: false,
        adding: false,
        added: false,
        deleting: false,
        deleted: false,
        error: ''
    },
    action
) {
    switch (action.type) {
        case "fetch_albums_PENDING": {
            return { ...state, fetching: true, fetched: false, error: '' }
        }

        case "fetch_albums_FULFILLED": {
            return {
                ...state,
                fetching: false,
                fetched: true,
                albums: action.payload.data,
                error: ''
            }
        }

        case "fetch_albums_REJECTED": {
            return {
                ...state,
                fetching: false,
                error: action.payload.toString()
            }
        }

        case "add_album_PENDING": {
            return { ...state, adding: true, added: false, error: '' }
        }

        case "add_album_FULFILLED": {
            return {
                ...state,
                adding: false,
                added: true,
                albums: [...state.albums].concat(action.payload.data),
                error: ''
            }
        }

        case "add_album_REJECTED": {
            return {
                ...state,
                adding: false,
                error: action.payload.toString()
            }
        }

        case "delete_album_PENDING": {
            return { ...state, deleting: true, deleted: false, error: '' }
        }

        case "delete_album_FULFILLED": {
            const deleteId = action.payload.config.data;

            return {
                ...state,
                deleting: false,
                deleted: true,
                albums: [...state.albums].filter(album => album.id != deleteId),
                error: ''
            }
        }

        case "delete_album_REJECTED": {
            return {
                ...state,
                deleting: false,
                error: action.payload.toString()
            }
        }

        default:
            break;
    }
    return state;
}

reference: