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