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 model

from 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

No comments:

Post a Comment