|
Wednesday, 24 July 2019
中年妇女优雅炫富的境界
django 61 adding price tag
shopping cart has price tags, default price is $10.
login as bob, change stages price to $8
stages is now $8
Nick's cart, stages price changed, total changed.
change stages qty to 7, total changed.
delete stages, total updated.
#django/modals
class Album(models.Model):
artist = models.CharField(max_length=50)
album_title = models.CharField(max_length=50)
genre = models.CharField(max_length=50)
album_logo = models.FileField()
date_posted = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
price = models.DecimalField(decimal_places=2,max_digits=5, default=10)
-------------------------------
#django/api/serializers
class MusicSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ('id', 'artist', 'album_title', 'genre', 'album_logo', 'date_posted', 'author', 'price')
--------------------------------
//react/pages/shoppingCart
import React, { Component } from 'react';
import '../App.css';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { getShoppingItems } from '../redux/actions/getShoppingIItems';
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';
import { putShoppingItem } from '../redux/actions/putShoppingItem';
import { deleteShoppingItem } from '../redux/actions/deleteShoppingItem'
import { Input, message, Tag } from 'antd';
import { Button } from 'reactstrap';
import { MdExposureNeg1, MdExposurePlus1 } from "react-icons/md";
class ShoppingCart extends Component {
constructor(props) {
super(props);
this.state = {
qty: [],
qty_input: [],
qty_loaded: false,
};
}
componentDidMount() {
//start fetching database once logged in
if (this.props.loggedin) {
if (!this.props.gotAlbums) {
this.props.dispatch(getAlbums(this.props.token));
}
if (!this.props.gotUsers) {
this.props.dispatch(getUsers(this.props.token));
}
if (!this.props.gotShoppingItems) {
this.props.dispatch(getShoppingItems(this.props.token));
}
//wait for 5sec, check every 0.1s to see if shoppingItems are fetched
let i = 0;
const waitShoppingItem = setInterval(async () => {
if (this.props.gotShoppingItems) {
await this.setState({ qty: [], qty_input: [], qty_loaded: false })
this.props.shoppingItems.map(async (item, index) => {
await this.setState(prevState => {
return {
qty: prevState.qty.concat({ album_id: item.album, qty: item.quantity }),
qty_input: prevState.qty_input.concat({ album_id: item.album, qty: null })
}
})
if (this.state.qty.length === this.props.shoppingItems.length) { this.setState({ qty_loaded: true }) }
})
clearInterval(waitShoppingItem);
}
if (i == 50) {
message.error('fetching shopping items timed out.')
clearInterval(waitShoppingItem);
}
i++;
}, 100)
}
}
add = async (album_id) => {
await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty + 1 }) } })
}
subtract = async (album_id) => {
await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
if (this.state.qty.find(item => item.album_id === album_id).qty > 1) {
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty - 1 }) } })
}
}
inputChange = (e, album_id) => {
if (e.target.value > 0) {
const newQty = parseInt(e.target.value)
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
} else {
message.error('entered invalid value')
}
}
deleteItem = (id) => {
this.props.dispatch(deleteShoppingItem(this.props.token, id));
//wait for 5sec, check every sec to see if delete successful
let i = 0;
const waitDelete = setInterval(() => {
if (this.props.shoppingItemDeleted) {
clearInterval(waitDelete);
}
if (i == 50) {
message.error('connection timed out.')
clearInterval(waitDelete);
}
i++;
}, 100)
}
updateCart = () => {
this.state.qty.map((item, index) => {
//only update items with qty changed
const oldItem = this.props.shoppingItems.find(i => i.album === item.album_id)
if (item.qty !== oldItem.quantity) {
const formData = new FormData();
formData.set('shopper', this.props.users.find(user => user.username === this.props.username).id);
formData.set('album', item.album_id);
formData.set('quantity', item.qty);
this.props.dispatch(putShoppingItem(this.props.token, oldItem.id, formData));
//wait for 5sec, check every sec to see if updateShoppingItem successful
let i = 0;
const waitUpdate = setInterval(() => {
const newItem = this.props.shoppingItems.find(i => i.album === item.album_id)
if (newItem.quantity === item.qty) {
message.success('Updated quantity of ' + this.props.albums.filter(album => album.id === item.album_id)[0].album_title + ' to ' + item.qty)
clearInterval(waitUpdate);
}
if (i == 50) {
message.error('connection timed out.')
clearInterval(waitUpdate);
}
i++;
}, 100)
}
})
}
render() {
if (!this.props.loggedin) {
return <Redirect to='/login' />
}
let total = 0
return (
<div style={{ padding: '10px', marginTop: '10px' }}>
<legend>Shopping Cart</legend>
<hr />
<div style={{ color: 'red' }}>{this.props.errorShoppingItem} {this.props.errorAlbum}</div>
{
this.state.qty_loaded ?
this.props.shoppingItems
.sort((a, b) => { return this.props.albums.find(album => album.id === a.album).album_title.toUpperCase().localeCompare(this.props.albums.find(album => album.id === b.album).album_title.toUpperCase()) })
.map((item, index) => {
const album = this.props.albums.find(album => album.id === item.album)
total = total + album.price * item.quantity
return <div key={index}>
<div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontStyle: 'italic' }}>{album.album_title} <Tag color='geekblue'>${album.price}</Tag></span>
<Input
type='number'
onChange={(e) => { this.inputChange(e, item.album) }}
style={{ width: '150px' }}
addonBefore={<MdExposureNeg1 onClick={() => this.subtract(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
addonAfter={<MdExposurePlus1 onClick={() => this.add(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
value={this.state.qty_input.find(i => i.album_id === item.album).qty || this.state.qty.find(i => i.album_id === item.album).qty}
/>
</div>
<b style={{ textAlign: 'right' }} onClick={() => this.deleteItem(item.id)} style={{ cursor: 'pointer' }}>X</b>
</div>
<hr />
</div>
})
: null
}
<div style={{ width: '80%', display: 'flex', justifyContent: 'space-between' }}>
<Button color='success' size='sm' onClick={() => this.updateCart()}>Update</Button>
<span>Total: <Tag color='geekblue'>${total}</Tag></span>
</div>
</div>
);
}
}
export default connect(
(store) => {
return {
loggedin: store.login.fetched,
gotShoppingItems: store.shoppingItems.fetched,
shoppingItems: store.shoppingItems.shoppingItems,
errorShoppingItem: store.shoppingItems.error,
shoppingItemUpdated: store.shoppingItems.updated,
shoppingItemDeleted: store.shoppingItems.deleted,
albums: store.albums.albums,
gotAlbums: store.albums.fetched,
errorAlbum: store.albums.error,
users: store.users.users,
gotUsers: store.users.fetched,
errorUser: store.users.error,
username: store.login.username,
token: store.login.token,
loggedin: store.login.fetched,
};
}
)(ShoppingCart);
Tuesday, 23 July 2019
Sunday, 21 July 2019
django 60 shopping cart detail update
login as nick, has 2 items in cart
click cart, display detail
postgres shoppingCart table, 2 item belong to nick, 1 belongs to andy.
add 1 stage album
cart has 3 items
add 2 great expanse
the qty of great expanse in cart is 3
change qty of stages to 7, qty of the balance to 5, click update
#django/apiview
class ShoppingItemDetail(APIView):
def get_object(self, pk):
try:
return ShoppingItem.objects.get(pk=pk)
except ShoppingItem.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
shoppingItem = self.get_object(pk)
serializer = ShoppingItemsSerializer(shoppingItem)
return Response(serializer.data)
def put(self, request, pk, format=None):
shoppintItem = self.get_object(pk)
#only owner can edit
if shoppintItem.shopper != request.user:
return Response({"detail": "You do not have permission to perform this action."},
status= status.HTTP_403_FORBIDDEN)
serializer = ShoppingItemsSerializer(shoppintItem, 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):
shoppingItem = self.get_object(pk)
# only owner can delete
if shoppingItem.shopper != request.user:
return Response({"detail": "You do not have permission to perform this action."},
status=status.HTTP_403_FORBIDDEN)
shoppingItem.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
--------------------------------------
#django/urls
path('shoppingItem/<int:pk>/', apiview.ShoppingItemDetail.as_view(), name='ShoppingItemDetail'),
------------------------------------
//pages/shoppingCart
import React, { Component } from 'react';
import '../App.css';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { getShoppingItems } from '../redux/actions/getShoppingIItems';
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';
import { putShoppingItem } from '../redux/actions/putShoppingItem';
import { Input, message } from 'antd';
import { Button } from 'reactstrap';
import { MdExposureNeg1, MdExposurePlus1 } from "react-icons/md";
class ShoppingCart extends Component {
constructor(props) {
super(props);
this.state = {
qty: [],
qty_input: [],
qty_loaded: false,
};
}
componentDidMount() {
//start fetching database once logged in
if (this.props.loggedin) {
if (!this.props.gotAlbums) {
this.props.dispatch(getAlbums(this.props.token));
}
if (!this.props.gotUsers) {
this.props.dispatch(getUsers(this.props.token));
}
if (!this.props.gotShoppingItems) {
this.props.dispatch(getShoppingItems(this.props.token));
}
//wait for 5sec, check every 0.1s to see if shoppingItems are fetched
let i = 0;
const waitShoppingItem = setInterval(async () => {
if (this.props.gotShoppingItems) {
await this.setState({ qty: [], qty_input: [], qty_loaded: false })
//load qty from database, json format {album_id: xx, qty: xx}
this.props.shoppingItems.map(async (item, index) => {
await this.setState(prevState => {
return {
qty: prevState.qty.concat({ album_id: item.album, qty: item.quantity }),
qty_input: prevState.qty_input.concat({ album_id: item.album, qty: null })
}
})
if (this.state.qty.length === this.props.shoppingItems.length) { this.setState({ qty_loaded: true }) }
})
clearInterval(waitShoppingItem);
}
if (i == 50) {
message.error('fetching shopping items timed out.')
clearInterval(waitShoppingItem);
}
i++;
}, 100)
}
}
add = async (album_id) => {
await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty + 1 }) } })
}
subtract = async (album_id) => {
await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
if (this.state.qty.find(item => item.album_id === album_id).qty > 1) {
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty - 1 }) } })
}
}
inputChange = (e, album_id) => {
if (e.target.value > 0) {
const newQty = parseInt(e.target.value)
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
} else {
message.error('entered invalid value')
}
}
updateCart = () => {
this.state.qty.map((item, index) => {
//only update items with qty changed
const oldItem = this.props.shoppingItems.find(i => i.album === item.album_id)
if (item.qty !== oldItem.quantity) {
const formData = new FormData();
formData.set('shopper', this.props.users.find(user => user.username === this.props.username).id);
formData.set('album', item.album_id);
formData.set('quantity', item.qty);
this.props.dispatch(putShoppingItem(this.props.token, oldItem.id, formData));
//wait for 5sec, check every sec to see if updateShoppingItem successful
let i = 0;
const waitAdd = setInterval(() => {
const newItem = this.props.shoppingItems.find(i => i.album === item.album_id)
if (newItem.quantity === item.qty) {
message.success('Updated quantity of ' + this.props.albums.filter(album => album.id === item.album_id)[0].album_title + ' to ' + item.qty)
clearInterval(waitAdd);
}
if (i == 50) {
message.error('connection timed out.')
clearInterval(waitAdd);
}
i++;
}, 100)
}
})
}
render() {
if (!this.props.loggedin) {
return <Redirect to='/login' />
}
return (
<div style={{ padding: '10px', marginTop: '10px' }}>
<legend>Shopping Cart</legend>
<hr />
<div style={{ color: 'red' }}>{this.props.errorShoppingItem} {this.props.errorAlbum}</div>
{
this.state.qty_loaded ?
this.props.shoppingItems
.sort((a, b) => { return this.props.albums.find(album => album.id === a.album).album_title.toUpperCase().localeCompare(this.props.albums.find(album => album.id === b.album).album_title.toUpperCase()) })
.map((item, index) => {
return <div key={index}>
<div style={{ width: '80%', display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontStyle: 'italic' }}>{this.props.albums.find(album => album.id === item.album).album_title}</span>
<Input
type='number'
onChange={(e) => { this.inputChange(e, item.album) }}
style={{ width: '150px' }}
addonBefore={<MdExposureNeg1 onClick={() => this.subtract(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
addonAfter={<MdExposurePlus1 onClick={() => this.add(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
value={this.state.qty_input.find(i => i.album_id === item.album).qty || this.state.qty.find(i => i.album_id === item.album).qty}
/>
</div>
<hr />
</div>
})
: null
}
<Button color='success' size='sm' onClick={() => this.updateCart()}>Update</Button>
</div>
);
}
}
export default connect(
(store) => {
return {
loggedin: store.login.fetched,
gotShoppingItems: store.shoppingItems.fetched,
shoppingItems: store.shoppingItems.shoppingItems,
errorShoppingItem: store.shoppingItems.error,
shoppingItemUpdated: store.shoppingItems.updated,
albums: store.albums.albums,
gotAlbums: store.albums.fetched,
errorAlbum: store.albums.error,
users: store.users.users,
gotUsers: store.users.fetched,
errorUser: store.users.error,
username: store.login.username,
token: store.login.token,
loggedin: store.login.fetched,
};
}
)(ShoppingCart);
---------------------------------------------------
//redux/actions/putShoppingItem
import axios from 'axios';
export function putShoppingItem(token, id, data) {
return {
type: "update_shoppingItem",
payload: axios({
method: 'put',
url: 'http://127.0.0.1:8000/api/shoppingItem/' + id.toString() + '/',
headers: {
Authorization: 'Token ' + token,
},
data: data,
})
}
}
--------------------------------------------
//redux/reducers/shoppingItemReducer
export default function reducer(
state = {
shoppingItems: [],
fetching: false,
fetched: false,
deleting: false,
deleted: false,
updating: false,
updated: false,
adding: false,
added: false,
error: ''
},
action
) {
switch (action.type) {
case "fetch_shoppingItems_PENDING": {
return { ...state, fetching: true, fetched: false, error: '' }
}
case "fetch_shoppingItems_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
shoppingItems: action.payload.data,
error: ''
}
}
case "fetch_shoppingItems_REJECTED": {
return {
...state,
fetching: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "delete_shoppingItem_PENDING": {
return { ...state, deleting: true, deleted: false, error: '' }
}
case "delete_shoppingItem_FULFILLED": {
const deleteId = action.payload.config.data;
return {
...state,
deleting: false,
deleted: true,
shoppingItems: [...state.shoppingItems].filter(item => item.id != deleteId),
error: ''
}
}
case "delete_shoppingItem_REJECTED": {
return {
...state,
deleting: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "add_shoppingItem_PENDING": {
return { ...state, adding: true, added: false, error: '' }
}
case "add_shoppingItem_FULFILLED": {
const data = action.payload.data;
//check if shopper has item in cart
const itemInCart = [...state.shoppingItems].filter(item => item.shopper == data.shopper && item.album == data.album)
//not in cart, create new
if (itemInCart.length === 0) {
return {
...state,
adding: false,
added: true,
shoppingItems: [...state.shoppingItems].concat(data),
error: ''
}
}
//in cart, update
else {
const repurchasedItem = {
shopper: data.shopper,
album: data.album,
quantity: data.quantity,
}
return {
...state,
adding: false,
added: true,
shoppingItems: [...state.shoppingItems].filter(item => item.album != data.album || item.shopper != data.shopper).concat(repurchasedItem),
error: ''
}
}
}
case "add_shoppingItem_REJECTED": {
return {
...state,
adding: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "update_shoppingItem_PENDING": {
return { ...state, updating: true, updated: false, error: '' }
}
case "update_shoppingItem_FULFILLED": {
const data = action.payload.data;
return {
...state,
updating: false,
updated: true,
shoppingItems: [...state.shoppingItems].filter(item => item.album != data.album || item.shopper != data.shopper).concat(data),
error: ''
}
}
case "update_shoppingItem_REJECTED": {
return {
...state,
updating: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "reset": {
return {
...state,
shoppingItems: [],
fetching: false,
fetched: false,
deleting: false,
deleted: false,
updating: false,
updated: false,
error: ''
}
}
default:
break;
}
return state;
}
reference:
http://chuanshuoge2.blogspot.com/2019/07/django-59-create-shopping-cart.html
Saturday, 13 July 2019
django 59 create shopping cart
login as Nick, cart empty
interested in the first album
click add to cart, qty 1
1 album in cart
added more albums in cart
django admin
add another first album to cart for friends
same # of albums in cart
the qty of first album increased by 1
Andy logged in and added an album to cart
database has Andy's selection as well
postgres shoppingItems table
Nick logged in again, found same # of albums in Cart as used to be.
He did not see albums in Andy's cart
#django modelfrom django.db import models
from django.urls import reverse
from django.contrib.auth.models import User
class Album(models.Model):
artist = models.CharField(max_length=50)
album_title = models.CharField(max_length=50)
genre = models.CharField(max_length=50)
album_logo = models.FileField()
date_posted = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
#form submitted without action redirect to detail
def get_absolute_url(self):
return reverse('music:detail', kwargs={'pk': self.pk})
#query album.objects.get(pk=1)
def __str__(self):
return self.album_title + ' - ' + self.artist
class ShoppingItem(models.Model):
shopper = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
album = models.ForeignKey(Album, on_delete=models.CASCADE, blank=True, default=1)
quantity = models.PositiveIntegerField(blank=True, default=0)
def __str__(self):
return 'shopper ' + str(self.shopper) + ' - album ' + str(self.album) + ' - quantity '+str(self.quantity)
---------------------------------------------
#django/api/serializers
class ShoppingItemsSerializer(serializers.ModelSerializer):
class Meta:
model = ShoppingItem
fields = ('id', 'shopper', 'album', 'quantity')
------------------------------------
#django/api/urls
urlpatterns = [
path('shoppingItems/', apiview.ShoppingItemsList.as_view(), name='ShoppingItems'),
]
-------------------------------------
#django/api/apiview
from music.models import Album, Song, ShoppingItem
from music.api.serializers import MusicSerializer, UserSerializer, SongSerializer, PasswordSerializer, ShoppingItemsSerializer
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
from rest_framework.decorators import authentication_classes, permission_classes
from django.contrib.auth.password_validation import validate_password
from django.core import exceptions
from django.forms.models import model_to_dict
class ShoppingItemsList(APIView):
def get(self, request, format=None):
#for privacy, only return shoppers items
shoppingItems = ShoppingItem.objects.filter(shopper=request.user)
serializer = ShoppingItemsSerializer(shoppingItems, many=True)
return Response(serializer.data)
def post(self, request, format=None):
#customer has item in cart
try:
revisitedItem = ShoppingItem.objects.get(shopper=request.user, album=request.data.get('album'))
revisitedItem.quantity= int(request.data.get('quantity')) + revisitedItem.quantity
serializer = ShoppingItemsSerializer(data=model_to_dict( revisitedItem ))
if serializer.is_valid():
# update database
revisitedItem.save()
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#customer select new item
except exceptions.ObjectDoesNotExist:
serializer = ShoppingItemsSerializer(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)
------------------------------------
//partials/flippyCard
import React, { Component } from 'react';
import '../App.css';
import { FaPenNib } from "react-icons/fa";
import { Button } from 'reactstrap';
import { MdEdit, MdDelete, MdFlip, MdAddShoppingCart, MdExposureNeg1, MdExposurePlus1 } from "react-icons/md";
import { Link } from 'react-router-dom';
import { Popconfirm, Popover, Input } from 'antd';
export default class FlippyCard extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
qty: 0,
qty_input: null,
};
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);
}
hide = () => {
this.setState({
visible: false,
});
};
handleVisibleChange = visible => {
this.setState({ visible, qty_input: null, qty: 0 });
this.flipClick();
};
add = async () => {
await this.setState({ qty_input: null });
this.setState(prevState => { return { qty: prevState.qty + 1 } })
}
subtract = async () => {
await this.setState({ qty_input: null });
if (this.state.qty > 0) {
this.setState(prevState => { return { qty: prevState.qty - 1 } })
}
}
inputChange = (e) => {
this.setState({ qty_input: parseInt(e.target.value), qty: parseInt(e.target.value) })
}
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()} />
<Popover
content={
<div style={{ display: 'flex', justifyContent: 'space-around', backgroundColor: '#D5F5E3' }}>
<a onClick={() => this.props.addShoppingItem(this.props.data.id, this.state.qty)}>Add to Cart</a>
<span>|</span>
<a onClick={this.hide}>Cancel</a>
</div>
}
title={
<Input
type='number'
onChange={(e) => { this.inputChange(e) }}
style={{ width: '150px' }}
addonBefore={<MdExposureNeg1 onClick={() => this.subtract()} style={{ cursor: 'pointer', fontSize: '20px' }} />}
addonAfter={<MdExposurePlus1 onClick={() => this.add()} style={{ cursor: 'pointer', fontSize: '20px' }} />}
value={this.state.qty_input || this.state.qty}
/>
}
trigger="click"
visible={this.state.visible}
onVisibleChange={this.handleVisibleChange}
>
<MdAddShoppingCart className='shopping-cart' />
</Popover>
</div>
</div>
</div>
);
}
}
//pages/albums
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 { postShoppingItem } from '../redux/actions/postShoppingItem';
import { getShoppingItems } from '../redux/actions/getShoppingIItems';
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));
}
if (!this.props.gotShoppingItems) {
this.props.dispatch(getShoppingItems(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 })
}
addShoppingItem = (album_id, qty) => {
if (qty === 0) { message.error('quantity entered 0'); return }
const formData = new FormData();
formData.set('shopper', this.props.users.filter(user => user.username === this.props.username)[0].id);
formData.set('album', album_id);
formData.set('quantity', qty);
this.props.dispatch(postShoppingItem(this.props.token, formData));
//wait for 5sec, check every sec to see if addShoppingItem successful
let i = 0;
const waitAdd = setInterval(() => {
if (this.props.shoppingItemAdded) {
message.success('Added ' + qty + ' ' + this.props.albums.filter(album => album.id === album_id)[0].album_title)
clearInterval(waitAdd);
}
if (i == 50) {
message.error('connection timed out.')
clearInterval(waitAdd);
}
i++;
}, 100)
}
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)}
addShoppingItem={(id, qty) => this.addShoppingItem(id, qty)}></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.props.errorShoppingItem}
{
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,
username: store.login.username,
errorShoppingItem: store.shoppingItems.error,
shoppingItemAdded: store.shoppingItems.updated,
gotShoppingItems: store.shoppingItems.fetched,
shoppingItems: store.shoppingItems.shoppingItems,
};
}
)(Albums);
------------------------------------
//partials/header
import React, { Component } from 'react';
import '../App.css';
import { Menu, Input, Badge } from 'antd';
import { Link } from 'react-router-dom'
import { FaCompactDisc, FaPlus, FaEdit, FaBarcode, FaUser, FaSearch, FaShoppingCart } from "react-icons/fa";
import { GiLoveSong } from "react-icons/gi";
import { IoIosLogOut, IoIosLogIn } from "react-icons/io";
import { connect } from 'react-redux';
import { reset } from '../redux/actions/logoutAction';
class Header extends Component {
constructor(props) {
super(props);
this.state = {
menu_mode: null,
current: 'albums',
};
}
componentWillMount() {
//change menu mode based on screen width
const mode = window.innerWidth > 434 ? "horizontal" : "inline";
this.setState({ menu_mode: mode });
}
handleClick = (e) => {
this.setState({
current: e.key,
});
}
logout = () => {
this.props.dispatch(reset())
}
render() {
return (
<div style={{ position: 'sticky', top: 0, width: '100%', zIndex: 2 }}>
<Menu
style={{ backgroundColor: '#fcefe5' }}
onClick={this.handleClick}
selectedKeys={[this.state.current]}
mode={this.state.menu_mode}
>
<Menu.Item key="home">
<Link to='/'><span className='Nav-Brand'>Django</span></Link>
</Menu.Item>
<Menu.SubMenu title={<span><FaCompactDisc /> Albums</span>}>
<Menu.Item key="albums">
<Link to='/'> <FaCompactDisc /> Get Albums</Link>
</Menu.Item>
<Menu.Item key="add">
<Link to='/addAlbum'> <FaPlus /> Add Album</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.Item key="songs">
<Link to='/'> <GiLoveSong /> Songs</Link>
</Menu.Item>
<Menu.Item key="search">
<Input type='text' size='small'
suffix={<FaSearch style={{ color: 'rgba(0,0,0,.25)', cursor: 'text' }} />}
placeholder='Search'
style={{ width: 150 }}></Input>
</Menu.Item>
{
this.props.loggedin ?
<Menu.Item key="shoppingCart" style={{ float: 'right' }}>
<Link to='/shoppingCart'>
<span style={{ fontSize: '20px' }}><FaShoppingCart /></span>
<Badge count={this.props.shoppingItems.length}></Badge></Link>
</Menu.Item>
:
null
}
{
this.props.loggedin ?
<Menu.SubMenu title={<span><FaUser /> {this.props.username}</span>} style={{ float: 'right' }}>
<Menu.Item key="0" onClick={() => this.logout()}>
<Link to='/logout'> <IoIosLogOut /> Logout</Link>
</Menu.Item>
<Menu.Item key="1">
<Link to='/'> <FaEdit /> Update Profile</Link>
</Menu.Item>
<Menu.Item key="2">
<Link to='/changePassword/'> <FaBarcode /> Change Password</Link>
</Menu.Item>
</Menu.SubMenu>
:
<Menu.Item key="login" style={{ float: 'right' }}>
<Link to='/login'> <IoIosLogIn /> Login</Link>
</Menu.Item>
}
</Menu>
</div>
);
}
}
export default connect(
(store) => {
return {
username: store.login.username,
token: store.login.token,
loggingin: store.login.fetching,
loggedin: store.login.fetched,
shoppingItems: store.shoppingItems.shoppingItems,
};
}
)(Header);
-------------------------------------
//redux/actions/getShoppingItems
import axios from 'axios';
export function getShoppingItems(token) {
return {
type: "fetch_shoppingItems",
payload: axios({
method: 'get',
url: 'http://127.0.0.1:8000/api/shoppingItems/',
headers: {
Authorization: 'Token ' + token
},
})
}
}
---------------------------------
//redux/postShoppingItem
import axios from 'axios';
export function postShoppingItem(token, data) {
return {
type: "add_update_shoppingItem",
payload: axios({
method: 'post',
url: 'http://127.0.0.1:8000/api/shoppingItems/',
headers: {
Authorization: 'Token ' + token,
},
data: data,
})
}
}
------------------------------------------
//redux/reducers/shoppingItemReducer
export default function reducer(
state = {
shoppingItems: [],
fetching: false,
fetched: false,
deleting: false,
deleted: false,
updating: false,
updated: false,
error: ''
},
action
) {
switch (action.type) {
case "fetch_shoppingItems_PENDING": {
return { ...state, fetching: true, fetched: false, error: '' }
}
case "fetch_shoppingItems_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
shoppingItems: action.payload.data,
error: ''
}
}
case "fetch_shoppingItems_REJECTED": {
return {
...state,
fetching: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "delete_shoppingItem_PENDING": {
return { ...state, deleting: true, deleted: false, error: '' }
}
case "delete_shoppingItem_FULFILLED": {
const deleteId = action.payload.config.data;
return {
...state,
deleting: false,
deleted: true,
shoppingItems: [...state.shoppingItems].filter(item => item.id != deleteId),
error: ''
}
}
case "delete_shoppingItem_REJECTED": {
return {
...state,
deleting: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "add_update_shoppingItem_PENDING": {
return { ...state, updating: true, updated: false, error: '' }
}
case "add_update_shoppingItem_FULFILLED": {
const data = action.payload.data;
//check if shopper has item in cart
const itemInCart = [...state.shoppingItems].filter(item => item.shopper == data.shopper && item.album == data.album)
//not in cart, create new
if (itemInCart.length === 0) {
return {
...state,
updating: false,
updated: true,
shoppingItems: [...state.shoppingItems].concat(data),
error: ''
}
}
//in cart, update
else {
const repurchasedItem = {
shopper: data.shopper,
album: data.album,
quantity: itemInCart[0].quantity + data.quantity,
}
return {
...state,
updating: false,
updated: true,
shoppingItems: [...state.shoppingItems].filter(item => item.album != data.album || item.shopper != data.shopper).concat(repurchasedItem),
error: ''
}
}
}
case "add_update_shoppingItem_REJECTED": {
return {
...state,
updating: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "reset": {
return {
...state,
shoppingItems: [],
fetching: false,
fetched: false,
deleting: false,
deleted: false,
updating: false,
updated: false,
error: ''
}
}
default:
break;
}
return state;
}
reference:
QuerySet
https://docs.djangoproject.com/en/2.2/ref/models/querysets/
QueryDict
https://kite.com/python/docs/django.http.QueryDict
https://stackoverflow.com/questions/769843/how-do-i-use-and-in-a-django-filter
https://stackoverflow.com/questions/21925671/convert-django-model-object-to-dict-with-all-of-the-fields-intact
https://stackoverflow.com/questions/26672077/django-model-vs-model-objects-create
https://stackoverflow.com/questions/3805958/how-to-delete-a-record-in-django-models
https://stackoverflow.com/questions/21128899/how-do-i-make-an-auto-increment-integer-field-in-django
https://stackoverflow.com/questions/2712682/django-update-object
https://stackoverflow.com/questions/757022/how-do-you-serialize-a-model-instance-in-django
Subscribe to:
Posts (Atom)