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

No comments:

Post a Comment