├── cypress.json ├── myproject ├── __init__.py ├── core │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── create_data.py │ ├── admin.py │ ├── apps.py │ ├── urls.py │ ├── static │ │ ├── css │ │ │ └── style.css │ │ └── js │ │ │ └── app.js │ ├── templates │ │ ├── index.html │ │ ├── includes │ │ │ └── nav.html │ │ └── base.html │ └── views.py ├── shopping │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── apps.py │ ├── urls.py │ ├── views.py │ ├── admin.py │ ├── templates │ │ ├── cart_items.html │ │ └── shopping.html │ └── models.py ├── urls.py ├── wsgi.py └── settings.py ├── mockup.png ├── tabelas.png ├── .gitignore ├── requirements.txt ├── cypress ├── fixtures │ └── example.json ├── support │ ├── index.js │ └── commands.js ├── integration │ ├── input-form3.spec.js │ ├── input-form.spec.js │ └── input-form2.spec.js └── plugins │ └── index.js ├── fix └── produtos.csv ├── manage.py ├── contrib └── env_gen.py ├── README.md └── passo-a-passo.md /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /myproject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/shopping/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/core/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/shopping/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /myproject/core/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/carrinho-de-compras-django-vuejs/main/mockup.png -------------------------------------------------------------------------------- /myproject/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tabelas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/carrinho-de-compras-django-vuejs/main/tabelas.png -------------------------------------------------------------------------------- /myproject/shopping/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /myproject/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /myproject/shopping/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ShoppingConfig(AppConfig): 5 | name = 'shopping' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *.sqlite3 4 | *.env 5 | *.DS_Store 6 | .venv/ 7 | staticfiles/ 8 | .ipynb_checkpoints/ 9 | node_modules/ 10 | package* -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.5.0 2 | Django==2.2.19 3 | django-extensions==3.0.9 4 | django-widget-tweaks==1.4.8 5 | python-decouple==3.3 6 | pytz==2020.4 7 | sqlparse==0.4.1 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /myproject/shopping/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from myproject.shopping import views as v 3 | 4 | 5 | app_name = 'shopping' 6 | 7 | 8 | urlpatterns = [ 9 | path('shopping/', v.shopping, name='shopping'), 10 | path('cart-items//', v.cart_items, name='cart_items'), 11 | ] 12 | -------------------------------------------------------------------------------- /myproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.contrib import admin 3 | 4 | 5 | urlpatterns = [ 6 | path('', include('myproject.core.urls', namespace='core')), 7 | path('shopping/', include('myproject.shopping.urls', namespace='shopping')), 8 | path('admin/', admin.site.urls), 9 | ] 10 | -------------------------------------------------------------------------------- /fix/produtos.csv: -------------------------------------------------------------------------------- 1 | produto,preco 2 | Apontador,5.54 3 | Caderno 100 folhas,6.52 4 | Caderno capa dura 200 folhas,24.46 5 | Caneta esferográfica azul,1.42 6 | Durex,7.43 7 | Giz de cera 12 cores,9.66 8 | Lapiseira 0.5 mm,8.71 9 | Lápis de cor 24 cores,13.8 10 | Papel sulfite A4 pacote 100 folhas,18.26 11 | Tesoura,9.68 12 | -------------------------------------------------------------------------------- /myproject/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from myproject.core import views as v 3 | 4 | 5 | app_name = 'core' 6 | 7 | 8 | urlpatterns = [ 9 | path('', v.index, name='index'), 10 | path('api/products/', v.api_product, name='api_product'), 11 | path('api/shopping-items/add/', v.api_shopping_items_add, name='api_shopping_items_add'), 12 | ] 13 | -------------------------------------------------------------------------------- /myproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for myproject project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /myproject/core/static/css/style.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin-top: 0; 3 | margin-bottom: 0; 4 | } 5 | .mm-2 { 6 | margin-top: 0.5rem; 7 | margin-bottom: 0.5rem; 8 | } 9 | .table { 10 | margin-bottom: 0; 11 | } 12 | .table td { 13 | padding: 0 .75rem; 14 | vertical-align: middle; 15 | } 16 | .mytable tr td { 17 | padding-top: 0; 18 | padding-bottom: 0; 19 | } 20 | .w15 { 21 | width: 15%; 22 | } 23 | .w40 { 24 | width: 40%; 25 | } 26 | .close { 27 | color: red; 28 | } 29 | -------------------------------------------------------------------------------- /myproject/shopping/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from .models import Cart 3 | 4 | 5 | def shopping(request): 6 | template_name = 'shopping.html' 7 | return render(request, template_name) 8 | 9 | 10 | def cart_items(request, pk): 11 | template_name = 'cart_items.html' 12 | carts = Cart.objects.filter(shop=pk) 13 | 14 | qs = carts.values_list('price', 'quantity') or 0 15 | total = sum(map(lambda q: q[0] * q[1], qs)) 16 | 17 | context = {'object_list': carts, 'total': total} 18 | return render(request, template_name, context) 19 | -------------------------------------------------------------------------------- /myproject/core/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |

Este é um projeto feito com Django e VueJS.

7 |

8 | Veja no GitHub 9 |

10 | Fazer uma compra 11 |
12 | {% endblock content %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /myproject/shopping/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Shop, Product, Cart 3 | 4 | 5 | class CartInline(admin.TabularInline): 6 | model = Cart 7 | extra = 0 8 | 9 | 10 | @admin.register(Shop) 11 | class ShopAdmin(admin.ModelAdmin): 12 | inlines = (CartInline,) 13 | list_display = ('__str__', 'created') 14 | search_fields = ('customer',) 15 | 16 | 17 | @admin.register(Product) 18 | class ProductAdmin(admin.ModelAdmin): 19 | list_display = ('__str__', 'price') 20 | search_fields = ('name',) 21 | 22 | 23 | @admin.register(Cart) 24 | class CartAdmin(admin.ModelAdmin): 25 | list_display = ('__str__', 'shop', 'quantity', 'price') 26 | search_fields = ('shop__customer', 'product__name') 27 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/integration/input-form3.spec.js: -------------------------------------------------------------------------------- 1 | describe('Input form', () => { 2 | it('focuses input on load', () => { 3 | cy.visit('http://localhost:8000/shopping/shopping/') 4 | 5 | cy.get('#customer').type('Regis') 6 | for (var i = 0; i < 5; i++) { 7 | let randName = Math.floor(Math.random() * 10) + 1 8 | let randQuantity = Math.floor(Math.random() * 20) 9 | let randPrice = Math.floor(Math.random() * 10) 10 | cy.get('#productName').select(randName.toString()) 11 | cy.get('#productQuantity').clear() 12 | cy.get('#productQuantity').type(randQuantity) 13 | cy.get('#productPrice').clear() 14 | cy.get('#productPrice').type(randPrice) 15 | cy.wait(500) 16 | cy.get('#btnSubmit').click() 17 | } 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /cypress/integration/input-form.spec.js: -------------------------------------------------------------------------------- 1 | let name = ['Apontador', 'Borracha', 'Caneta azul', 'Lápis', 'Tesoura'] 2 | 3 | describe('Input form', () => { 4 | it('focuses input on load', () => { 5 | cy.visit('http://localhost:8000/shopping/shopping/') 6 | 7 | for (var i = 0; i < 5; i++) { 8 | var randName = name[Math.floor(Math.random() * name.length)] 9 | let randQuantity = Math.floor(Math.random() * 20) 10 | let randPrice = Math.floor(Math.random() * 10) 11 | cy.get('#productName').type(randName) 12 | cy.get('#productQuantity').clear() 13 | cy.get('#productQuantity').type(randQuantity) 14 | cy.get('#productPrice').clear() 15 | cy.get('#productPrice').type(randPrice) 16 | cy.wait(500) 17 | cy.get('#btnSubmit').click() 18 | } 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /cypress/integration/input-form2.spec.js: -------------------------------------------------------------------------------- 1 | let name = ['Apontador', 'Borracha', 'Caneta azul', 'Lápis', 'Tesoura'] 2 | 3 | describe('Input form', () => { 4 | it('focuses input on load', () => { 5 | cy.visit('http://localhost:8000/shopping/shopping/') 6 | 7 | for (var i = 0; i < 5; i++) { 8 | cy.get('#btnAddLine').click() 9 | var randName = name[Math.floor(Math.random() * name.length)] 10 | let randQuantity = Math.floor(Math.random() * 20) 11 | let randPrice = Math.floor(Math.random() * 10) 12 | cy.get('#name'+i).type(randName) 13 | cy.get('#quantity'+i).clear() 14 | cy.get('#quantity'+i).type(randQuantity) 15 | cy.get('#price'+i).clear() 16 | cy.get('#price'+i).type(randPrice) 17 | cy.wait(500) 18 | // cy.get('#btnSubmit').click() 19 | } 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /myproject/core/templates/includes/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /contrib/env_gen.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()?" 4 | size = 50 5 | secret_key = "".join(random.sample(chars, size)) 6 | 7 | chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ@#$%_" 8 | size = 20 9 | password = "".join(random.sample(chars, size)) 10 | 11 | CONFIG_STRING = """ 12 | DEBUG=True 13 | SECRET_KEY=%s 14 | ALLOWED_HOSTS=127.0.0.1, .localhost 15 | 16 | #DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME 17 | #DB_NAME= 18 | #DB_USER= 19 | #DB_PASSWORD=%s 20 | #DB_HOST=localhost 21 | 22 | #DEFAULT_FROM_EMAIL= 23 | #EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 24 | #EMAIL_HOST= 25 | #EMAIL_PORT= 26 | #EMAIL_USE_TLS= 27 | #EMAIL_HOST_USER= 28 | #EMAIL_HOST_PASSWORD= 29 | """.strip() % (secret_key, password) 30 | 31 | # Writing our configuration file to '.env' 32 | with open('.env', 'w') as configfile: 33 | configfile.write(CONFIG_STRING) 34 | 35 | print('Success!') 36 | print('Type: cat .env') 37 | -------------------------------------------------------------------------------- /myproject/core/management/commands/create_data.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from django.core.management.base import BaseCommand 3 | from myproject.shopping.models import Product 4 | 5 | 6 | def csv_to_list(filename: str) -> list: 7 | ''' 8 | Lê um csv e retorna um OrderedDict. 9 | Créditos para Rafael Henrique 10 | https://bit.ly/2FLDHsH 11 | ''' 12 | with open(filename) as csv_file: 13 | reader = csv.DictReader(csv_file, delimiter=',') 14 | csv_data = [line for line in reader] 15 | return csv_data 16 | 17 | 18 | def save_data(data): 19 | ''' 20 | Salva os dados no banco. 21 | ''' 22 | aux = [] 23 | for item in data: 24 | name = item.get('produto') 25 | price = item.get('preco') 26 | obj = Product( 27 | name=name, 28 | price=price, 29 | ) 30 | aux.append(obj) 31 | Product.objects.bulk_create(aux) 32 | 33 | 34 | class Command(BaseCommand): 35 | help = 'Importa os produtos.' 36 | 37 | def handle(self, *args, **options): 38 | data = csv_to_list('fix/produtos.csv') 39 | save_data(data) 40 | -------------------------------------------------------------------------------- /myproject/core/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.http import JsonResponse 3 | from django.shortcuts import render 4 | from django.views.decorators.csrf import csrf_exempt 5 | from myproject.shopping.models import Product, Shop, Cart 6 | 7 | 8 | def index(request): 9 | return render(request, 'index.html') 10 | 11 | 12 | def api_product(request): 13 | products = Product.objects.all() 14 | data = [item.to_dict() for item in products] 15 | response = {'data': data} 16 | return JsonResponse(response) 17 | 18 | 19 | @csrf_exempt 20 | def api_shopping_items_add(request): 21 | request = request.POST 22 | customer = json.loads(request.get('customer')) 23 | products = json.loads(request.get('products')) 24 | 25 | shop = Shop.objects.create(customer=customer) 26 | 27 | for product in products: 28 | product_obj = Product.objects.get(pk=product['pk']) 29 | quantity = product['quantity'] 30 | price = product['price'] 31 | 32 | Cart.objects.create( 33 | shop=shop, 34 | product=product_obj, 35 | quantity=quantity, 36 | price=price 37 | ) 38 | response = {'data': shop.pk} 39 | return JsonResponse(response) 40 | -------------------------------------------------------------------------------- /myproject/core/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Carrinho de compras 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | {% include "includes/nav.html" %} 32 | 33 |
34 | {% block content %}{% endblock content %} 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% block js %}{% endblock js %} 47 | 48 | 49 | -------------------------------------------------------------------------------- /myproject/shopping/templates/cart_items.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |
9 |
10 |

Confirmação de compra

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for object in object_list %} 22 | 23 | 24 | 25 | 31 | 37 | 38 | {% endfor %} 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 |
ProdutoQuantidadePreçoSubtotal
{{ object.product.name }}{{ object.quantity }} 26 |
27 |
R$
28 |
{{ object.price }}
29 |
30 |
32 |
33 |
R$
34 |
{{ object.get_subtotal }}
35 |
36 |
Total 44 |
45 |
R$
46 |
{{ total }}
47 |
48 |
52 |
53 |
54 |
55 |
56 | 57 | {% endblock content %} 58 | -------------------------------------------------------------------------------- /myproject/shopping/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Shop(models.Model): 5 | customer = models.CharField('cliente', max_length=100) 6 | created = models.DateTimeField('criado em', auto_now_add=True, auto_now=False) 7 | 8 | class Meta: 9 | ordering = ('-pk',) 10 | verbose_name = 'compra' 11 | verbose_name_plural = 'compras' 12 | 13 | def __str__(self): 14 | return self.customer 15 | 16 | 17 | class Product(models.Model): 18 | name = models.CharField('nome', max_length=100, unique=True) 19 | price = models.DecimalField('preço', max_digits=6, decimal_places=2) 20 | 21 | class Meta: 22 | ordering = ('name',) 23 | verbose_name = 'produto' 24 | verbose_name_plural = 'produtos' 25 | 26 | def __str__(self): 27 | return self.name 28 | 29 | def to_dict(self): 30 | return { 31 | 'value': self.pk, 32 | 'text': self.name, 33 | 'price': self.price 34 | } 35 | 36 | 37 | class Cart(models.Model): 38 | shop = models.ForeignKey( 39 | Shop, 40 | related_name='compras', 41 | on_delete=models.CASCADE 42 | ) 43 | product = models.ForeignKey( 44 | Product, 45 | related_name='products', 46 | on_delete=models.SET_NULL, 47 | null=True, 48 | blank=True 49 | ) 50 | quantity = models.PositiveIntegerField('quantidade') 51 | price = models.DecimalField('preço', max_digits=6, decimal_places=2) 52 | 53 | class Meta: 54 | ordering = ('-pk',) 55 | verbose_name = 'carrinho' 56 | verbose_name_plural = 'carrinhos' 57 | 58 | def __str__(self): 59 | if self.shop: 60 | return f'{self.shop.pk}-{self.pk}-{self.product}' 61 | return str(self.pk) 62 | 63 | def get_subtotal(self): 64 | return self.price * (self.quantity or 0) 65 | -------------------------------------------------------------------------------- /myproject/shopping/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2021-01-03 06:03 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Product', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=100, unique=True, verbose_name='nome')), 20 | ('price', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='preço')), 21 | ], 22 | options={ 23 | 'verbose_name': 'produto', 24 | 'verbose_name_plural': 'produtos', 25 | 'ordering': ('name',), 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='Shop', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('customer', models.CharField(max_length=100, verbose_name='cliente')), 33 | ('created', models.DateTimeField(auto_now_add=True, verbose_name='criado em')), 34 | ], 35 | options={ 36 | 'verbose_name': 'compra', 37 | 'verbose_name_plural': 'compras', 38 | 'ordering': ('-pk',), 39 | }, 40 | ), 41 | migrations.CreateModel( 42 | name='Cart', 43 | fields=[ 44 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 45 | ('quantity', models.PositiveIntegerField(verbose_name='quantidade')), 46 | ('price', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='preço')), 47 | ('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='shopping.Product')), 48 | ('shop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='compras', to='shopping.Shop')), 49 | ], 50 | options={ 51 | 'verbose_name': 'carrinho', 52 | 'verbose_name_plural': 'carrinhos', 53 | 'ordering': ('-pk',), 54 | }, 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # carrinho-de-compras-django-vuejs 2 | 3 | Este projeto tem por objetivo experimentar o uso de [VueJS](https://vuejs.org/) junto com [Django](https://www.djangoproject.com/) para simular um carrinho de compras com a intenção de eliminar o uso de [inlineformset](https://docs.djangoproject.com/en/3.1/ref/forms/models/#inlineformset-factory). 4 | 5 | Este projeto é similar ao de [vendas](https://github.com/rg3915/vendas) onde temos essa modelagem como base. 6 | 7 | ![tables](https://raw.githubusercontent.com/rg3915/vendas/master/modelling/tables.jpg) 8 | 9 | Mas vamos usar nosso próprio modelo simplificado. 10 | 11 | ![tabelas](tabelas.png) 12 | 13 | ## Objetivo 14 | 15 | Nosso objetivo é desenhar essa tela: 16 | 17 | ![mockup](mockup.png) 18 | 19 | ## Técnica 20 | 21 | Usaremos o [VueJS](https://vuejs.org/) via [CDN](https://cdn.jsdelivr.net/npm/vue/dist/vue.js) utilizando a técnica descrita no video [Django e VueJS #01 - arquivos estáticos via cdn](https://www.youtube.com/watch?v=KOMER5MhBlY), onde o VueJS será usado como arquivo estático dentro da pasta `/static/js/`. 22 | 23 | Neste caso teremos conflito de delimitadores. Para resolver isso usamos o seguinte delimitador no VueJS: 24 | 25 | ``` 26 | ${ } 27 | ``` 28 | 29 | Os estáticos do VueJS via CDN estão declarados em `base.html`. 30 | 31 | 32 | ## Este projeto foi feito com: 33 | 34 | * [Python 3.8.2](https://www.python.org/) 35 | * [Django 2.2.19](https://www.djangoproject.com/) 36 | * [Bootstrap 4.0](https://getbootstrap.com/) 37 | * [VueJS 2.6.11](https://vuejs.org/) 38 | * [vue-toast-notification 0.6.1](https://github.com/ankurk91/vue-toast-notification) 39 | 40 | ## Como rodar o projeto? 41 | 42 | * Clone esse repositório. 43 | * Crie um virtualenv com Python 3. 44 | * Ative o virtualenv. 45 | * Instale as dependências. 46 | * Rode as migrações. 47 | 48 | ``` 49 | git clone https://github.com/rg3915/carrinho-de-compras-django-vuejs.git 50 | cd carrinho-de-compras-django-vuejs 51 | python3 -m venv .venv 52 | source .venv/bin/activate 53 | pip install -r requirements.txt 54 | python contrib/env_gen.py 55 | python manage.py migrate 56 | python manage.py create_data 57 | python manage.py createsuperuser --username="admin" --email="" 58 | python manage.py runserver 59 | ``` 60 | 61 | 62 | ## Apresentação 63 | 64 | ``` 65 | git clone --depth 1 --branch 0.5 https://github.com/rg3915/carrinho-de-compras-django-vuejs.git 66 | cd carrinho-de-compras-django-vuejs 67 | python3 -m venv .venv 68 | source .venv/bin/activate 69 | pip install -r requirements.txt 70 | python contrib/env_gen.py 71 | python manage.py migrate 72 | python manage.py create_data 73 | python manage.py createsuperuser --username="admin" --email="" 74 | python manage.py runserver 75 | ``` 76 | 77 | Ver o [commit/c2f498e](https://github.com/rg3915/carrinho-de-compras-django-vuejs/commit/c2f498e275afdea0a750a80fec680c96e48abb5c). 78 | 79 | 80 | 81 | ## Passo a passo 82 | 83 | Leia o [passo a passo](passo-a-passo.md) detalhado. -------------------------------------------------------------------------------- /myproject/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from decouple import config, Csv 3 | from dj_database_url import parse as dburl 4 | 5 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | 9 | # Quick-start development settings - unsuitable for production 10 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 11 | 12 | # SECURITY WARNING: keep the secret key used in production secret! 13 | SECRET_KEY = config('SECRET_KEY') 14 | 15 | # SECURITY WARNING: don't run with debug turned on in production! 16 | DEBUG = config('DEBUG', default=False, cast=bool) 17 | 18 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) 19 | 20 | 21 | # Application definition 22 | 23 | INSTALLED_APPS = [ 24 | 'django.contrib.admin', 25 | 'django.contrib.auth', 26 | 'django.contrib.contenttypes', 27 | 'django.contrib.sessions', 28 | 'django.contrib.messages', 29 | 'django.contrib.staticfiles', 30 | 'django_extensions', 31 | 'myproject.core', 32 | 'myproject.shopping', 33 | ] 34 | 35 | MIDDLEWARE = [ 36 | 'django.middleware.security.SecurityMiddleware', 37 | 'django.contrib.sessions.middleware.SessionMiddleware', 38 | 'django.middleware.common.CommonMiddleware', 39 | 'django.middleware.csrf.CsrfViewMiddleware', 40 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 41 | 'django.contrib.messages.middleware.MessageMiddleware', 42 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 43 | ] 44 | 45 | ROOT_URLCONF = 'myproject.urls' 46 | 47 | TEMPLATES = [ 48 | { 49 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 50 | 'DIRS': [], 51 | 'APP_DIRS': True, 52 | 'OPTIONS': { 53 | 'context_processors': [ 54 | 'django.template.context_processors.debug', 55 | 'django.template.context_processors.request', 56 | 'django.contrib.auth.context_processors.auth', 57 | 'django.contrib.messages.context_processors.messages', 58 | ], 59 | }, 60 | }, 61 | ] 62 | 63 | WSGI_APPLICATION = 'myproject.wsgi.application' 64 | 65 | 66 | # Database 67 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 68 | 69 | default_dburl = 'sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3') 70 | DATABASES = { 71 | 'default': config('DATABASE_URL', default=default_dburl, cast=dburl), 72 | } 73 | 74 | 75 | # Password validation 76 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 77 | 78 | AUTH_PASSWORD_VALIDATORS = [ 79 | { 80 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 81 | }, 82 | { 83 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 84 | }, 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 87 | }, 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 90 | }, 91 | ] 92 | 93 | 94 | # Internationalization 95 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 96 | 97 | LANGUAGE_CODE = 'pt-br' 98 | 99 | TIME_ZONE = 'America/Sao_Paulo' 100 | 101 | USE_I18N = True 102 | 103 | USE_L10N = True 104 | 105 | USE_TZ = True 106 | 107 | 108 | # Static files (CSS, JavaScript, Images) 109 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 110 | 111 | STATIC_URL = '/static/' 112 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 113 | -------------------------------------------------------------------------------- /myproject/core/static/js/app.js: -------------------------------------------------------------------------------- 1 | axios.defaults.xsrfHeaderName = 'X-CSRFToken' 2 | axios.defaults.xsrfCookieName = 'csrftoken' 3 | 4 | const endpoint = 'http://localhost:8000/' 5 | 6 | Vue.filter("formatPrice", value => (value / 1).toFixed(2).replace('.', ',').toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".")) 7 | 8 | Vue.use(VueToast, { 9 | position: 'top-right' 10 | }) 11 | 12 | var app = new Vue({ 13 | el: '#app', 14 | delimiters: ['${', '}'], 15 | data: { 16 | form: { 17 | customer: null 18 | }, 19 | cartItems: [], 20 | currentProduct: { 21 | pk: null, 22 | quantity: 0, 23 | price: 0.0 24 | }, 25 | products: [] 26 | }, 27 | mounted() { 28 | axios.get(endpoint + 'api/products/') 29 | .then(response => { 30 | this.products = response.data.data; 31 | }) 32 | }, 33 | computed: { 34 | cartValue() { 35 | return this.cartItems.reduce((prev, curr) => { 36 | return prev + (curr.price * curr.quantity) 37 | }, 0).toFixed(2) 38 | } 39 | }, 40 | methods: { 41 | validateForm() { 42 | if (this.cartItems.length == 0) { 43 | Vue.$toast.error('O carrinho está vazio.') 44 | return false 45 | } 46 | if (this.cartItems.length == 1 & this.cartItems[0].pk === null) { 47 | Vue.$toast.error('Favor escolher um produto.') 48 | return false 49 | } 50 | if (this.cartItems.length == 1 & this.cartItems[0].quantity == 0) { 51 | Vue.$toast.error('Quantidade deve ser maior que zero.') 52 | return false 53 | } 54 | if (this.cartItems.length == 1 & this.cartItems[0].price == 0) { 55 | Vue.$toast.error('Preço deve ser maior que zero.') 56 | return false 57 | } 58 | if (!this.form.customer) { 59 | Vue.$toast.error('Favor digitar o nome do cliente.') 60 | return false 61 | } 62 | return true 63 | }, 64 | onProductChange(cart, e) { 65 | if (cart) { 66 | const pk = e.target.value; 67 | const price = this.products.find(p => p.value == pk).price; 68 | cart.price = price; 69 | return; 70 | } 71 | const price = this.products.find(p => p.value == this.currentProduct.pk).price; 72 | this.currentProduct.price = price; 73 | }, 74 | addProduct() { 75 | this.cartItems.push(this.currentProduct) 76 | this.currentProduct = { 77 | pk: null, 78 | quantity: 0, 79 | price: 0.0 80 | } 81 | }, 82 | addLine() { 83 | this.cartItems.push( 84 | { 85 | pk: null, 86 | quantity: 0, 87 | price: 0.0 88 | } 89 | ) 90 | }, 91 | deleteProduct(item) { 92 | var idx = this.cartItems.indexOf(item) 93 | this.cartItems.splice(idx, 1) 94 | }, 95 | submitForm() { 96 | if (!this.validateForm()) return 97 | 98 | let bodyFormData = new FormData(); 99 | 100 | bodyFormData.append('products', JSON.stringify(this.cartItems)); 101 | bodyFormData.append('customer', JSON.stringify(this.form.customer)); 102 | 103 | axios.post('/api/shopping-items/add/', bodyFormData) 104 | .then((res) => { 105 | location.href = endpoint + 'shopping/cart-items/' + res.data.data 106 | }) 107 | }, 108 | resetForm() { 109 | this.form = { 110 | customer: null 111 | } 112 | this.cartItems = [] 113 | this.currentProduct = { 114 | pk: null, 115 | quantity: 0, 116 | price: 0.0 117 | } 118 | } 119 | } 120 | }) -------------------------------------------------------------------------------- /myproject/shopping/templates/shopping.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |
9 |
10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 35 | 43 | 49 | 54 | 55 | 56 |
ProdutoQuantidadePreçoSubtotal
27 | 31 | 33 | 34 | 36 |
37 |
38 | R$ 39 |
40 | 41 |
42 |
44 |
45 |
R$
46 |
${ (currentProduct.price * currentProduct.quantity) | formatPrice }
47 |
48 |
50 | 53 |
57 | 58 |
59 | 60 | 61 | 62 | 63 | 69 | 72 | 80 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 105 | 110 | 115 | 116 | 117 |
64 | 68 | 70 | 71 | 73 |
74 |
75 | R$ 76 |
77 | 78 |
79 |
81 |
82 |
R$
83 |
${ (cart.price * cart.quantity) | formatPrice }
84 |
85 |
87 | 90 |
TotalR$ ${ cartValue | formatPrice }
101 | 104 | 106 | 109 | 111 | 114 |
118 |
119 | 120 |
121 |
122 |
123 | 124 | {% endblock content %} 125 | 126 | {% block js %} 127 | 128 | {% endblock js %} -------------------------------------------------------------------------------- /passo-a-passo.md: -------------------------------------------------------------------------------- 1 | ### Criar o projeto inicial 2 | 3 | ``` 4 | git clone https://gist.github.com/b363f5c4a998f42901705b23ccf4b8e8.git /tmp/boilerplatesimple 5 | ls /tmp/boilerplatesimple 6 | cp /tmp/boilerplatesimple/boilerplatesimple.sh . 7 | source boilerplatesimple.sh 8 | # Após terminar de instalar delete o arquivo boilerplatesimple.sh 9 | rm -f boilerplatesimple.sh 10 | ``` 11 | 12 | ### Criar app shopping 13 | 14 | ``` 15 | cd myproject 16 | python ../manage.py startapp shopping 17 | ``` 18 | 19 | E em `settings.py` faça 20 | 21 | ```python 22 | INSTALLED_APPS = [ 23 | ... 24 | 'myproject.core', 25 | 'myproject.shopping', 26 | ] 27 | ``` 28 | 29 | 30 | #### Criar modelo 31 | 32 | ```python 33 | # shopping/models.py 34 | from django.db import models 35 | 36 | 37 | class Shop(models.Model): 38 | customer = models.CharField('cliente', max_length=100) 39 | created = models.DateTimeField('criado em', auto_now_add=True, auto_now=False) 40 | 41 | class Meta: 42 | ordering = ('-pk',) 43 | verbose_name = 'compra' 44 | verbose_name_plural = 'compras' 45 | 46 | def __str__(self): 47 | return self.customer 48 | 49 | 50 | class Product(models.Model): 51 | name = models.CharField('nome', max_length=100, unique=True) 52 | price = models.DecimalField('preço', max_digits=6, decimal_places=2) 53 | 54 | class Meta: 55 | ordering = ('name',) 56 | verbose_name = 'produto' 57 | verbose_name_plural = 'produtos' 58 | 59 | def __str__(self): 60 | return self.name 61 | 62 | def to_dict(self): 63 | return { 64 | 'value': self.pk, 65 | 'text': self.name, 66 | 'price': self.price 67 | } 68 | 69 | 70 | class Cart(models.Model): 71 | shop = models.ForeignKey( 72 | Shop, 73 | related_name='compras', 74 | on_delete=models.CASCADE 75 | ) 76 | product = models.ForeignKey( 77 | Product, 78 | related_name='products', 79 | on_delete=models.SET_NULL, 80 | null=True, 81 | blank=True 82 | ) 83 | quantity = models.PositiveIntegerField('quantidade') 84 | price = models.DecimalField('preço', max_digits=6, decimal_places=2) 85 | 86 | class Meta: 87 | ordering = ('-pk',) 88 | verbose_name = 'carrinho' 89 | verbose_name_plural = 'carrinhos' 90 | 91 | def __str__(self): 92 | if self.shop: 93 | return f'{self.shop.pk}-{self.pk}-{self.product}' 94 | return str(self.pk) 95 | 96 | def get_subtotal(self): 97 | return self.price * (self.quantity or 0) 98 | ``` 99 | 100 | ### Criar url e views 101 | 102 | ```python 103 | # urls.py 104 | from django.urls import include, path 105 | from django.contrib import admin 106 | 107 | 108 | urlpatterns = [ 109 | path('', include('myproject.core.urls', namespace='core')), 110 | # path('shopping/', include('myproject.shopping.urls', namespace='shopping')), 111 | path('admin/', admin.site.urls), 112 | ] 113 | ``` 114 | 115 | ```python 116 | # core/urls.py 117 | from django.urls import path 118 | from myproject.core import views as v 119 | 120 | 121 | app_name = 'core' 122 | 123 | 124 | urlpatterns = [ 125 | path('', v.index, name='index'), 126 | ] 127 | ``` 128 | 129 | ```python 130 | # core/views.py 131 | from django.shortcuts import render 132 | 133 | 134 | def index(request): 135 | return render(request, 'index.html') 136 | ``` 137 | 138 | ### Criar os templates 139 | 140 | * Criar `nav.html`, `base.html` e `index.html` 141 | 142 | ``` 143 | cd .. 144 | mkdir -p myproject/core/templates/includes 145 | touch myproject/core/templates/includes/nav.html 146 | touch myproject/core/templates/{base,index}.html 147 | ``` 148 | 149 | Editar `nav.html` 150 | 151 | ```html 152 | 153 | 154 | 155 | 156 | 170 | ``` 171 | 172 | Editar `base.html` 173 | 174 | ```html 175 | 176 | {% load static %} 177 | 178 | 179 | 180 | 181 | 182 | 183 | Carrinho de compras 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 198 | 199 | 200 | 201 | 202 | {% include "includes/nav.html" %} 203 | 204 |
205 | {% block content %}{% endblock content %} 206 |
207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | {% block js %}{% endblock js %} 215 | 216 | 217 | 218 | ``` 219 | 220 | Editar `index.html` 221 | 222 | ```html 223 | 224 | {% extends "base.html" %} 225 | {% load static %} 226 | 227 | {% block content %} 228 |
229 |

Este é um projeto feito com Django e VueJS.

230 |

231 | Veja no GitHub 232 |

233 | Fazer uma compra 234 |
235 | {% endblock content %} 236 | ``` 237 | 238 | Rodar a aplicação até aqui. 239 | 240 | > Talvez você tenha que tirar a url de index temporariamente. 241 | 242 | 243 | * Criar visualização para `shopping.html` 244 | 245 | ```python 246 | # shopping/views.py 247 | from django.shortcuts import render 248 | 249 | 250 | def shopping(request): 251 | template_name = 'shopping.html' 252 | return render(request, template_name) 253 | ``` 254 | 255 | ``` 256 | touch shopping/urls.py 257 | ``` 258 | 259 | ```python 260 | # shopping/urls.py 261 | from django.urls import path 262 | from myproject.shopping import views as v 263 | 264 | 265 | app_name = 'shopping' 266 | 267 | 268 | urlpatterns = [ 269 | path('shopping/', v.shopping, name='shopping'), 270 | ] 271 | ``` 272 | 273 | Acrescente em `urls.py` 274 | 275 | ```python 276 | # urls.py 277 | ... 278 | path('shopping/', include('myproject.shopping.urls', namespace='shopping')), 279 | ... 280 | ``` 281 | 282 | ``` 283 | mkdir -p myproject/shopping/templates 284 | touch myproject/shopping/templates/shopping.html 285 | ``` 286 | 287 | ```html 288 | 289 | {% extends "base.html" %} 290 | 291 | {% block content %} 292 | 293 |
294 |
295 |
296 |
297 | 298 |
299 | 300 |
301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 316 | 319 | 327 | 335 | 340 | 341 | 342 |
ProdutoQuantidadePreçoSubtotal
314 | 315 | 317 | 318 | 320 |
321 |
322 | R$ 323 |
324 | 325 |
326 |
328 |
329 |
330 | R$ 331 |
332 | 333 |
334 |
336 | 339 |
343 | 344 |
345 | 346 | 347 | 348 | 349 | 352 | 355 | 363 | 371 | 376 | 377 | 378 | 379 | 382 | 385 | 393 | 401 | 406 | 407 | 408 | 409 | 412 | 415 | 423 | 431 | 436 | 437 | 438 | 439 | 442 | 445 | 453 | 461 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 |
350 | 351 | 353 | 354 | 356 |
357 |
358 | R$ 359 |
360 | 361 |
362 |
364 |
365 |
366 | R$ 367 |
368 | 369 |
370 |
372 | 375 |
380 | 381 | 383 | 384 | 386 |
387 |
388 | R$ 389 |
390 | 391 |
392 |
394 |
395 |
396 | R$ 397 |
398 | 399 |
400 |
402 | 405 |
410 | 411 | 413 | 414 | 416 |
417 |
418 | R$ 419 |
420 | 421 |
422 |
424 |
425 |
426 | R$ 427 |
428 | 429 |
430 |
432 | 435 |
440 | 441 | 443 | 444 | 446 |
447 |
448 | R$ 449 |
450 | 451 |
452 |
454 |
455 |
456 | R$ 457 |
458 | 459 |
460 |
462 | 465 |
TotalR$ 125,38
475 |
476 | 477 |
478 |
479 |
480 | 481 | {% endblock content %} 482 | ``` 483 | 484 | ``` 485 | mkdir -p myproject/core/static/{css,js} 486 | touch myproject/core/static/css/style.css 487 | touch myproject/core/static/js/app.js 488 | ``` 489 | 490 | Editar `style.css` 491 | 492 | ```css 493 | /* style.css */ 494 | hr { 495 | margin-top: 0; 496 | margin-bottom: 0; 497 | } 498 | .mm-2 { 499 | margin-top: 0.5rem; 500 | margin-bottom: 0.5rem; 501 | } 502 | .table { 503 | margin-bottom: 0; 504 | } 505 | .table td { 506 | padding: 0 .75rem; 507 | vertical-align: middle; 508 | } 509 | .mytable tr td { 510 | padding-top: 0; 511 | padding-bottom: 0; 512 | } 513 | .w15 { 514 | width: 15%; 515 | } 516 | .w40 { 517 | width: 40%; 518 | } 519 | .close { 520 | color: red; 521 | } 522 | ``` 523 | 524 | Versão com ajustes de CSS: 525 | 526 | ```html 527 | 528 | {% extends "base.html" %} 529 | {% load static %} 530 | 531 | {% block content %} 532 | 533 |
534 |
535 |
536 |
537 | 538 |
539 | 540 |
541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 556 | 559 | 567 | 573 | 578 | 579 | 580 |
ProdutoQuantidadePreçoSubtotal
554 | 555 | 557 | 558 | 560 |
561 |
562 | R$ 563 |
564 | 565 |
566 |
568 |
569 |
R$
570 |
32,80
571 |
572 |
574 | 577 |
581 | 582 |
583 | 584 | 585 | 586 | 587 | 590 | 593 | 601 | 607 | 612 | 613 | 614 | 615 | 618 | 621 | 629 | 635 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 |
588 | 589 | 591 | 592 | 594 |
595 |
596 | R$ 597 |
598 | 599 |
600 |
602 |
603 |
R$
604 |
${ (cart.price * cart.quantity) | formatPrice }
605 |
606 |
608 | 611 |
616 | 617 | 619 | 620 | 622 |
623 |
624 | R$ 625 |
626 | 627 |
628 |
630 |
631 |
R$
632 |
32,80
633 |
634 |
636 | 639 |
TotalR$ ${ cartValue | formatPrice }
649 |
650 | 651 |
652 |
653 |
654 | 655 | {% endblock content %} 656 | 657 | {% block js %} 658 | 659 | {% endblock js %} 660 | ``` 661 | 662 | Mostrar 663 | 664 | ``` 665 | ${ (cart.price * cart.quantity) | formatPrice } 666 | 667 | ${ cartValue | formatPrice } 668 | ``` 669 | 670 | > Mostrar a aplicação rodando. 671 | 672 | Versão final 673 | 674 | ```html 675 | 676 | {% extends "base.html" %} 677 | {% load static %} 678 | 679 | {% block content %} 680 | 681 |
682 |
683 |
684 |
685 | 686 |
687 | 688 |
689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 707 | 710 | 718 | 724 | 729 | 730 | 731 |
ProdutoQuantidadePreçoSubtotal
702 | 706 | 708 | 709 | 711 |
712 |
713 | R$ 714 |
715 | 716 |
717 |
719 |
720 |
R$
721 |
${ (currentProduct.price * currentProduct.quantity) | formatPrice }
722 |
723 |
725 | 728 |
732 | 733 |
734 | 735 | 736 | 737 | 738 | 744 | 747 | 755 | 761 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 780 | 785 | 790 | 791 | 792 |
739 | 743 | 745 | 746 | 748 |
749 |
750 | R$ 751 |
752 | 753 |
754 |
756 |
757 |
R$
758 |
${ (cart.price * cart.quantity) | formatPrice }
759 |
760 |
762 | 765 |
TotalR$ ${ cartValue | formatPrice }
776 | 779 | 781 | 784 | 786 | 789 |
793 |
794 | 795 |
796 |
797 |
798 | 799 | {% endblock content %} 800 | 801 | {% block js %} 802 | 803 | {% endblock js %} 804 | ``` 805 | 806 | 807 | Adicione uma nova rota em `core/urls.py` 808 | 809 | ```python 810 | path('api/products/', v.api_product, name='api_product'), 811 | ``` 812 | 813 | Em `core/views.py` 814 | 815 | ```python 816 | from django.shortcuts import render 817 | from django.http import JsonResponse 818 | from myproject.shopping.models import Product 819 | 820 | 821 | def index(request): 822 | return render(request, 'index.html') 823 | 824 | 825 | def api_product(request): 826 | products = Product.objects.all() 827 | data = [item.to_dict() for item in products] 828 | response = {'data': data} 829 | return JsonResponse(response) 830 | ``` 831 | 832 | Editar `shopping/admin.py` 833 | 834 | ```python 835 | # shopping/admin.py 836 | from django.contrib import admin 837 | from .models import Shop, Product, Cart 838 | 839 | 840 | class CartInline(admin.TabularInline): 841 | model = Cart 842 | extra = 0 843 | 844 | 845 | @admin.register(Shop) 846 | class ShopAdmin(admin.ModelAdmin): 847 | inlines = (CartInline,) 848 | list_display = ('__str__', 'created') 849 | search_fields = ('customer',) 850 | 851 | 852 | @admin.register(Product) 853 | class ProductAdmin(admin.ModelAdmin): 854 | list_display = ('__str__', 'price') 855 | search_fields = ('name',) 856 | 857 | 858 | @admin.register(Cart) 859 | class CartAdmin(admin.ModelAdmin): 860 | list_display = ('__str__', 'shop', 'quantity', 'price') 861 | search_fields = ('shop__customer', 'product__name') 862 | ``` 863 | 864 | Mostrar a url `/api/products/`. 865 | 866 | Cadastrar alguns produtos em admin. 867 | 868 | Para isso eu criei um [CSV](fix/produtos.csv) e um comando [create_data](myproject/core/management/commands/create_data.py) pra criar os dados. 869 | 870 | Em `core/static/js/app.js` 871 | 872 | ```js 873 | axios.defaults.xsrfHeaderName = 'X-CSRFToken' 874 | axios.defaults.xsrfCookieName = 'csrftoken' 875 | 876 | const endpoint = 'http://localhost:8000/' 877 | 878 | Vue.filter("formatPrice", value => (value / 1).toFixed(2).replace('.', ',').toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".")) 879 | 880 | var app = new Vue({ 881 | el: '#app', 882 | delimiters: ['${', '}'], 883 | data: { 884 | form: { 885 | customer: null 886 | }, 887 | cartItems: [], 888 | currentProduct: { 889 | pk: null, 890 | quantity: 0, 891 | price: 0.0 892 | }, 893 | products: [] 894 | }, 895 | mounted() { 896 | axios.get(endpoint + 'api/products/') 897 | .then(response => { 898 | this.products = response.data.data; 899 | }) 900 | }, 901 | computed: { 902 | cartValue() { 903 | return this.cartItems.reduce((prev, curr) => { 904 | return prev + (curr.price * curr.quantity) 905 | }, 0).toFixed(2) 906 | } 907 | }, 908 | methods: { 909 | submitForm() { 910 | let bodyFormData = new FormData(); 911 | 912 | bodyFormData.append('products', JSON.stringify(this.cartItems)); 913 | bodyFormData.append('customer', JSON.stringify(this.form.customer)); 914 | 915 | axios.post('/api/shopping-items/add/', bodyFormData) 916 | .then((res) => { 917 | location.href = endpoint + 'shopping/cart-items/' + res.data.data 918 | }) 919 | }, 920 | addProduct() { 921 | this.cartItems.push(this.currentProduct) 922 | this.currentProduct = { 923 | pk: null, 924 | quantity: 0, 925 | price: 0.0 926 | } 927 | }, 928 | addLine() { 929 | this.cartItems.push( 930 | { 931 | pk: null, 932 | quantity: 0, 933 | price: 0.0 934 | } 935 | ) 936 | }, 937 | deleteProduct(item) { 938 | var idx = this.cartItems.indexOf(item) 939 | this.cartItems.splice(idx, 1) 940 | }, 941 | resetForm() { 942 | this.form = { 943 | customer: null 944 | } 945 | this.cartItems = [] 946 | this.currentProduct = { 947 | pk: null, 948 | quantity: 0, 949 | price: 0.0 950 | } 951 | } 952 | } 953 | }) 954 | ``` 955 | 956 | 957 | 958 | Em `core/urls.py` 959 | 960 | ```python 961 | path('api/shopping-items/add/', v.api_shopping_items_add, name='api_shopping_items_add'), 962 | ``` 963 | 964 | Em `core/views.py` 965 | 966 | ```python 967 | import json 968 | from django.http import JsonResponse 969 | from django.shortcuts import render 970 | from django.views.decorators.csrf import csrf_exempt 971 | from myproject.shopping.models import Product, Shop, Cart 972 | 973 | 974 | @csrf_exempt 975 | def api_shopping_items_add(request): 976 | request = request.POST 977 | customer = json.loads(request.get('customer')) 978 | products = json.loads(request.get('products')) 979 | 980 | shop = Shop.objects.create(customer=customer) 981 | 982 | for product in products: 983 | product_obj = Product.objects.get(pk=product['pk']) 984 | quantity = product['quantity'] 985 | price = product['price'] 986 | 987 | Cart.objects.create( 988 | shop=shop, 989 | product=product_obj, 990 | quantity=quantity, 991 | price=price 992 | ) 993 | response = {'data': shop.pk} 994 | return JsonResponse(response) 995 | ``` 996 | 997 | Em `shopping/urls.py` 998 | 999 | ```python 1000 | path('cart-items//', v.cart_items, name='cart_items'), 1001 | ``` 1002 | 1003 | Em `shopping/views.py` 1004 | 1005 | ```python 1006 | def cart_items(request, pk): 1007 | template_name = 'cart_items.html' 1008 | carts = Cart.objects.filter(shop=pk) 1009 | 1010 | qs = carts.values_list('price', 'quantity') or 0 1011 | total = sum(map(lambda q: q[0] * q[1], qs)) 1012 | 1013 | context = {'object_list': carts, 'total': total} 1014 | return render(request, template_name, context) 1015 | ``` 1016 | 1017 | E crie `cart_items.html` 1018 | 1019 | ``` 1020 | touch myproject/shopping/templates/cart_items.html 1021 | ``` 1022 | 1023 | ```html 1024 | 1025 | {% extends "base.html" %} 1026 | {% load static %} 1027 | 1028 | {% block content %} 1029 | 1030 |
1031 |
1032 |
1033 |
1034 |

Confirmação de compra

1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | {% for object in object_list %} 1046 | 1047 | 1048 | 1049 | 1055 | 1061 | 1062 | {% endfor %} 1063 | 1064 | 1065 | 1066 | 1067 | 1073 | 1074 | 1075 |
ProdutoQuantidadePreçoSubtotal
{{ object.product.name }}{{ object.quantity }} 1050 |
1051 |
R$
1052 |
{{ object.price }}
1053 |
1054 |
1056 |
1057 |
R$
1058 |
{{ object.get_subtotal }}
1059 |
1060 |
Total 1068 |
1069 |
R$
1070 |
{{ total }}
1071 |
1072 |
1076 |
1077 |
1078 |
1079 |
1080 | 1081 | {% endblock content %} 1082 | ``` 1083 | --------------------------------------------------------------------------------