Wednesday, 26 June 2019
Tuesday, 25 June 2019
django Access Tokens for Sending Unique Email Links
reference:
http://blog.appliedinformaticsinc.com/using-django-access-tokens-for-sending-unique-email-links/
https://www.django-rest-framework.org/api-guide/authentication/
https://django-rest-auth.readthedocs.io/en/latest/installation.html
https://django-rest-auth.readthedocs.io/en/latest/api_endpoints.html
https://stackoverflow.com/questions/40769136/django-rest-auth-reset-password-uid-and-token
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.
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:
Subscribe to:
Posts (Atom)