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

No comments:

Post a Comment