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:

No comments:

Post a Comment